fix focus management

This commit is contained in:
Oleg Isonen committed 2022-10-27 20:39:59 +02:00
1 parent 532c69c3c4
commit 06f3d50cbd
4 files changed
+51 -27

No files matched your search

@@ -13,10 +13,10 @@ import { Listbox, ListboxItem } from "./listbox";
type Item = {
label: string;
type: "uploaded" | "system" | "category";
type: "uploaded" | "system";
};
const getItems = (assets: Array<Asset | PreviewAsset>): Array<Item> => {
const toItems = (assets: Array<Asset | PreviewAsset>): Array<Item> => {
const system = Array.from(SYSTEM_FONTS.keys()).map((label) => ({
label,
type: "system",
@@ -65,25 +65,23 @@ const useLogic = ({
handleDelete(ids);
};
const fontItems = useMemo(() => getItems(assets), [assets]);
const [openMenuItem, setOpenMenuItem] = useState<Item>();
const fontItems = useMemo(() => toItems(assets), [assets]);
const selectedItem =
// After deletion the selected item may not be in the list anymore.
fontItems.find((item) => item.label === value) ??
fontItems.find((item) => item.type !== "category") ??
null;
fontItems.find((item) => item.label === value) ?? null;
const {
items: filteredItems,
resetFilter,
highlightedIndex,
...comboboxProps
} = useCombobox({
isOpen: true,
items: fontItems,
value: selectedItem,
itemToString(item) {
return item?.type === "category" ? "" : item?.label ?? "";
return item?.label ?? "";
},
onItemSelect(value) {
if (value !== null) {
@@ -106,8 +104,6 @@ const useLogic = ({
filteredItems,
uploadedItems,
systemItems,
openMenuItem,
setOpenMenuItem,
handleDelete: handleDeleteByLabel,
...comboboxProps,
};
@@ -128,8 +124,7 @@ export const FontsManager = ({ value, onChange }: FontsManagerProps) => {
getComboboxProps,
getMenuProps,
getItemProps,
openMenuItem,
setOpenMenuItem,
setHighlightedIndex,
} = useLogic({ value, onChange });
return (
@@ -141,6 +136,9 @@ export const FontsManager = ({ value, onChange }: FontsManagerProps) => {
autoFocus
placeholder="Search"
onBlur={undefined}
onFocus={() => {
setHighlightedIndex(-1);
}}
/>
</Flex>
<Separator css={{ my: "$1" }} />
@@ -163,19 +161,27 @@ export const FontsManager = ({ value, onChange }: FontsManagerProps) => {
{...getItemProps({
item,
index,
highlighted: item === openMenuItem ? true : undefined,
})}
key={index}
suffix={
<ItemMenu
onOpenChange={(isOpen) => {
setOpenMenuItem(isOpen ? item : undefined);
onOpenChange={() => {
setHighlightedIndex(index);
}}
onDelete={() => {
handleDelete(item.label);
}}
onFocusTrigger={() => {
setHighlightedIndex(-1);
}}
/>
}
onFocus={(event) => {
// We need to ignore focus on a menu button inside
if (event.target === event.currentTarget) {
setHighlightedIndex(index);
}
}}
>
{item.label}
</ListboxItem>
@@ -188,13 +194,17 @@ export const FontsManager = ({ value, onChange }: FontsManagerProps) => {
</>
)}
{systemItems.map((item, index) => {
const globalIndex = uploadedItems.length + index;
return (
<ListboxItem
{...getItemProps({
item,
index: uploadedItems.length + index,
index: globalIndex,
})}
key={index}
onFocus={() => {
setHighlightedIndex(globalIndex);
}}
>
{item.label}
</ListboxItem>
@@ -10,7 +10,7 @@ import {
} from "@webstudio-is/design-system";
import { DotsHorizontalIcon } from "@webstudio-is/icons";
import { cssVars } from "@webstudio-is/css-vars";
import { useState } from "react";
import { type FocusEventHandler, useState } from "react";
const visibilityVar = cssVars.define("visibility");
@@ -31,9 +31,14 @@ const MenuButton = styled(IconButton, {
type ItemMenuProps = {
onDelete: () => void;
onOpenChange: (open: boolean) => void;
onFocusTrigger?: FocusEventHandler;
};
export const ItemMenu = ({ onDelete, onOpenChange }: ItemMenuProps) => {
export const ItemMenu = ({
onDelete,
onOpenChange,
onFocusTrigger,
}: ItemMenuProps) => {
const [isOpen, setIsOpen] = useState(false);
return (
<DropdownMenu
@@ -44,7 +49,11 @@ export const ItemMenu = ({ onDelete, onOpenChange }: ItemMenuProps) => {
}}
>
<DropdownMenuTrigger asChild>
<MenuButton aria-label="Font menu">
<MenuButton
aria-label="Font menu"
onFocus={onFocusTrigger}
tabIndex={0}
>
<DotsHorizontalIcon />
</MenuButton>
</DropdownMenuTrigger>
@@ -9,7 +9,7 @@ export const Listbox = styled("ul", {
margin: 0,
padding: 0,
variants: {
isEmpty: {
empty: {
true: {
display: "none",
},
@@ -27,10 +27,11 @@ const ListboxItemBase = styled("li", {
listStyle: "none",
borderRadius: "$1",
outline: 0,
"&:focus-within, &[data-highlighted], &[aria-selected=true]": {
"&[data-highlighted]": {
boxShadow: "0px 0px 0px 2px $colors$blue10, 0px 0px 0px 2px $colors$blue10",
...getItemMenuVars("visible"),
},
"&:focus-within": getItemMenuVars("visible"),
"&[disabled]": {
pointerEvents: "none",
},
@@ -10,8 +10,9 @@ import React, {
import { CheckIcon, ChevronDownIcon } from "@webstudio-is/icons";
import { Popper, PopperContent, PopperAnchor } from "@radix-ui/react-popper";
import {
DownshiftState,
UseComboboxStateChangeOptions,
type DownshiftState,
type UseComboboxStateChangeOptions,
type UseComboboxProps as UseDownshiftComboboxProps,
useCombobox as useDownshiftCombobox,
} from "downshift";
import { matchSorter } from "match-sorter";
@@ -37,7 +38,7 @@ const Listbox = styled("ul", panelStyles, {
display: "none",
},
},
isEmpty: {
empty: {
true: {
display: "none",
},
@@ -128,7 +129,7 @@ const useFilter = <Item,>({
};
};
type UseComboboxProps<Item> = {
type UseComboboxProps<Item> = UseDownshiftComboboxProps<Item> & {
items: Array<Item>;
itemToString: (item: Item | null) => string;
value: Item | null; // This is to prevent: "downshift: A component has changed the uncontrolled prop "selectedItem" to be controlled."
@@ -151,6 +152,7 @@ export const useCombobox = <Item,>({
onItemHighlight,
stateReducer = (state, { changes }) => changes,
match,
...rest
}: UseComboboxProps<Item>) => {
const { filteredItems, filter, resetFilter } = useFilter<Item>({
items,
@@ -159,7 +161,9 @@ export const useCombobox = <Item,>({
});
const downshiftProps = useDownshiftCombobox({
...rest,
items: filteredItems,
defaultHighlightedIndex: -1,
selectedItem: value, // Prevent downshift warning about switching controlled mode
stateReducer,
itemToString,
@@ -203,7 +207,7 @@ export const useCombobox = <Item,>({
return {
...getMenuProps(options),
state: isOpen ? "open" : "closed",
isEmpty: filteredItems.length === 0,
empty: filteredItems.length === 0,
};
},
[getMenuProps, isOpen, filteredItems.length]