mirror of
https://github.com/webstudio-is/webstudio.git
synced 2026-11-08 21:15:18 +09:00
fix focus management
This commit is contained in:
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]
|
||||
|
||||
Reference in new issue
Block a user