* Improve ActionButtonDropdown selection * center rectangle icon fixed * ignore Esc key when displaying hotkeys * add ability to escape 3 point circle tool * remove focus from ActionButton, ActionButtonDropdown * remove focus outline from buttons * remember lastly selected multi action item * Add tests for toolbar buttons * fix sketch-tests by turning toolbar dropdown arrays into an object with an id - this got broken because dropdown now remember the last selected option so we cant rely on cant reference the first option in tests * update other tests with open menu click
This commit is contained in:
106
src/Toolbar.tsx
106
src/Toolbar.tsx
@ -17,12 +17,14 @@ import { isDesktop } from '@src/lib/isDesktop'
|
||||
import { openExternalBrowserIfDesktop } from '@src/lib/openWindow'
|
||||
import { editorManager, kclManager } from '@src/lib/singletons'
|
||||
import type {
|
||||
ToolbarDropdown,
|
||||
ToolbarItem,
|
||||
ToolbarItemCallbackProps,
|
||||
ToolbarItemResolved,
|
||||
ToolbarItemResolvedDropdown,
|
||||
ToolbarModeName,
|
||||
} from '@src/lib/toolbar'
|
||||
import { toolbarConfig } from '@src/lib/toolbar'
|
||||
import { isToolbarItemResolvedDropdown, toolbarConfig } from '@src/lib/toolbar'
|
||||
import { isArray } from '@src/lib/utils'
|
||||
import { commandBarActor } from '@src/machines/commandBarMachine'
|
||||
|
||||
@ -131,21 +133,27 @@ export function Toolbar({
|
||||
*/
|
||||
const currentModeItems: (
|
||||
| ToolbarItemResolved
|
||||
| ToolbarItemResolved[]
|
||||
| ToolbarItemResolvedDropdown
|
||||
| 'break'
|
||||
)[] = useMemo(() => {
|
||||
return toolbarConfig[currentMode].items.map((maybeIconConfig) => {
|
||||
if (maybeIconConfig === 'break') {
|
||||
return 'break'
|
||||
} else if (isArray(maybeIconConfig)) {
|
||||
return maybeIconConfig.map(resolveItemConfig)
|
||||
} else if (isToolbarDropdown(maybeIconConfig)) {
|
||||
return {
|
||||
id: maybeIconConfig.id,
|
||||
array: maybeIconConfig.array.map((item) =>
|
||||
resolveItemConfig(item, maybeIconConfig.id)
|
||||
),
|
||||
}
|
||||
} else {
|
||||
return resolveItemConfig(maybeIconConfig)
|
||||
}
|
||||
})
|
||||
|
||||
function resolveItemConfig(
|
||||
maybeIconConfig: ToolbarItem
|
||||
maybeIconConfig: ToolbarItem,
|
||||
dropdownId?: string
|
||||
): ToolbarItemResolved {
|
||||
const isDisabled =
|
||||
disableAllButtons ||
|
||||
@ -176,6 +184,14 @@ export function Toolbar({
|
||||
}
|
||||
}, [currentMode, disableAllButtons, configCallbackProps])
|
||||
|
||||
// To remember the last selected item in an ActionButtonDropdown
|
||||
const [lastSelectedMultiActionItem, _] = useState(
|
||||
new Map<
|
||||
number /* index in currentModeItems */,
|
||||
number /* index in maybeIconConfig */
|
||||
>()
|
||||
)
|
||||
|
||||
return (
|
||||
<menu
|
||||
data-current-mode={currentMode}
|
||||
@ -199,24 +215,33 @@ export function Toolbar({
|
||||
className="h-5 w-[1px] block bg-chalkboard-30 dark:bg-chalkboard-80"
|
||||
/>
|
||||
)
|
||||
} else if (isArray(maybeIconConfig)) {
|
||||
} else if (isToolbarItemResolvedDropdown(maybeIconConfig)) {
|
||||
// A button with a dropdown
|
||||
const selectedIcon =
|
||||
maybeIconConfig.array.find((c) => c.isActive) ||
|
||||
maybeIconConfig.array[lastSelectedMultiActionItem.get(i) ?? 0]
|
||||
|
||||
// Save the last selected item in the dropdown
|
||||
lastSelectedMultiActionItem.set(
|
||||
i,
|
||||
maybeIconConfig.array.indexOf(selectedIcon)
|
||||
)
|
||||
return (
|
||||
<ActionButtonDropdown
|
||||
Element="button"
|
||||
key={maybeIconConfig[0].id}
|
||||
data-testid={maybeIconConfig[0].id + '-dropdown'}
|
||||
id={maybeIconConfig[0].id + '-dropdown'}
|
||||
name={maybeIconConfig[0].title}
|
||||
key={selectedIcon.id}
|
||||
data-testid={selectedIcon.id + '-dropdown'}
|
||||
id={selectedIcon.id + '-dropdown'}
|
||||
name={maybeIconConfig.id}
|
||||
className={
|
||||
(maybeIconConfig[0].alwaysDark
|
||||
(maybeIconConfig.array[0].alwaysDark
|
||||
? 'dark bg-chalkboard-90 '
|
||||
: '!bg-transparent ') +
|
||||
'group/wrapper ' +
|
||||
buttonBorderClassName +
|
||||
' relative group !gap-0'
|
||||
}
|
||||
splitMenuItems={maybeIconConfig.map((itemConfig) => ({
|
||||
splitMenuItems={maybeIconConfig.array.map((itemConfig) => ({
|
||||
id: itemConfig.id,
|
||||
label: itemConfig.title,
|
||||
hotkey: itemConfig.hotkey,
|
||||
@ -236,11 +261,11 @@ export function Toolbar({
|
||||
>
|
||||
<ActionButton
|
||||
Element="button"
|
||||
id={maybeIconConfig[0].id}
|
||||
data-testid={maybeIconConfig[0].id}
|
||||
id={selectedIcon.id}
|
||||
data-testid={selectedIcon.id}
|
||||
iconStart={{
|
||||
icon: maybeIconConfig[0].icon,
|
||||
iconColor: maybeIconConfig[0].iconColor,
|
||||
icon: selectedIcon.icon,
|
||||
iconColor: selectedIcon.iconColor,
|
||||
className: iconClassName,
|
||||
bgClassName: bgClassName,
|
||||
}}
|
||||
@ -248,40 +273,36 @@ export function Toolbar({
|
||||
'!border-transparent !px-0 pressed:!text-chalkboard-10 pressed:enabled:hovered:!text-chalkboard-10 ' +
|
||||
buttonBgClassName
|
||||
}
|
||||
aria-pressed={maybeIconConfig[0].isActive}
|
||||
aria-pressed={selectedIcon.isActive}
|
||||
disabled={
|
||||
disableAllButtons ||
|
||||
maybeIconConfig[0].status !== 'available' ||
|
||||
maybeIconConfig[0].disabled
|
||||
selectedIcon.status !== 'available' ||
|
||||
selectedIcon.disabled
|
||||
}
|
||||
name={maybeIconConfig[0].title}
|
||||
name={selectedIcon.title}
|
||||
// aria-description is still in ARIA 1.3 draft.
|
||||
// eslint-disable-next-line jsx-a11y/aria-props
|
||||
aria-description={maybeIconConfig[0].description}
|
||||
onClick={() =>
|
||||
maybeIconConfig[0].onClick(configCallbackProps)
|
||||
}
|
||||
aria-description={selectedIcon.description}
|
||||
onClick={() => selectedIcon.onClick(configCallbackProps)}
|
||||
>
|
||||
<span
|
||||
className={!maybeIconConfig[0].showTitle ? 'sr-only' : ''}
|
||||
>
|
||||
{maybeIconConfig[0].title}
|
||||
<span className={!selectedIcon.showTitle ? 'sr-only' : ''}>
|
||||
{selectedIcon.title}
|
||||
</span>
|
||||
<ToolbarItemTooltip
|
||||
itemConfig={maybeIconConfig[0]}
|
||||
itemConfig={selectedIcon}
|
||||
configCallbackProps={configCallbackProps}
|
||||
wrapperClassName="ui-open:!hidden"
|
||||
contentClassName={tooltipContentClassName}
|
||||
>
|
||||
{showRichContent ? (
|
||||
<ToolbarItemTooltipRichContent
|
||||
itemConfig={maybeIconConfig[0]}
|
||||
itemConfig={selectedIcon}
|
||||
/>
|
||||
) : (
|
||||
<ToolbarItemTooltipShortContent
|
||||
status={maybeIconConfig[0].status}
|
||||
title={maybeIconConfig[0].title}
|
||||
hotkey={maybeIconConfig[0].hotkey}
|
||||
status={selectedIcon.status}
|
||||
title={selectedIcon.title}
|
||||
hotkey={selectedIcon.hotkey}
|
||||
/>
|
||||
)}
|
||||
</ToolbarItemTooltip>
|
||||
@ -430,7 +451,9 @@ const ToolbarItemTooltipShortContent = ({
|
||||
>
|
||||
{title}
|
||||
{hotkey && (
|
||||
<kbd className="inline-block ml-2 flex-none hotkey">{hotkey}</kbd>
|
||||
<kbd className="inline-block ml-2 flex-none hotkey">
|
||||
{displayHotkeys(hotkey)}
|
||||
</kbd>
|
||||
)}
|
||||
</span>
|
||||
)
|
||||
@ -461,7 +484,9 @@ const ToolbarItemTooltipRichContent = ({
|
||||
{itemConfig.title}
|
||||
</span>
|
||||
{itemConfig.status === 'available' && itemConfig.hotkey ? (
|
||||
<kbd className="flex-none hotkey">{itemConfig.hotkey}</kbd>
|
||||
<kbd className="flex-none hotkey">
|
||||
{displayHotkeys(itemConfig.hotkey)}
|
||||
</kbd>
|
||||
) : itemConfig.status === 'kcl-only' ? (
|
||||
<>
|
||||
<span className="text-wrap font-sans flex-0 text-chalkboard-70 dark:text-chalkboard-40">
|
||||
@ -522,3 +547,14 @@ const ToolbarItemTooltipRichContent = ({
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
// We don't want to display Esc hotkeys to avoid confusion in the Toolbar UI (eg. "EscR")
|
||||
function displayHotkeys(hotkey: string | string[]) {
|
||||
return (isArray(hotkey) ? hotkey : [hotkey]).filter((h) => h !== 'Esc')
|
||||
}
|
||||
|
||||
function isToolbarDropdown(
|
||||
item: ToolbarItem | ToolbarDropdown
|
||||
): item is ToolbarDropdown {
|
||||
return 'array' in item
|
||||
}
|
||||
|
@ -71,6 +71,7 @@ export const ActionButton = forwardRef((props: ActionButtonProps, ref) => {
|
||||
<button
|
||||
ref={ref as ForwardedRef<HTMLButtonElement>}
|
||||
className={classNames}
|
||||
tabIndex={-1}
|
||||
{...rest}
|
||||
>
|
||||
{iconStart && <ActionIcon {...iconStart} />}
|
||||
|
@ -69,6 +69,7 @@ export function ActionButtonDropdown({
|
||||
close()
|
||||
}}
|
||||
className="group/button flex items-center gap-6 px-3 py-1 font-sans text-xs hover:bg-primary/10 dark:hover:bg-chalkboard-80 border-0 m-0 w-full rounded-none text-left disabled:!bg-transparent dark:disabled:text-chalkboard-60"
|
||||
tabIndex={-1}
|
||||
disabled={item.disabled}
|
||||
data-testid={'dropdown-' + item.id}
|
||||
>
|
||||
|
@ -86,7 +86,7 @@ textarea,
|
||||
|
||||
button {
|
||||
@apply border border-chalkboard-30 m-0.5 px-3 rounded text-xs;
|
||||
@apply focus-visible:outline-chalkboard-100;
|
||||
@apply focus-visible:outline-none;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
@ -94,7 +94,7 @@ button:hover {
|
||||
}
|
||||
|
||||
.dark button {
|
||||
@apply border-chalkboard-70 focus-visible:outline-chalkboard-10;
|
||||
@apply border-chalkboard-70;
|
||||
}
|
||||
|
||||
.dark button:hover {
|
||||
|
1163
src/lib/toolbar.ts
1163
src/lib/toolbar.ts
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user