From 06f3d50cbddc63afa474672cda39f40ddbcf0389 Mon Sep 17 00:00:00 2001 From: Oleg Isonen Date: Thu, 27 Oct 2022 20:39:49 +0200 Subject: [PATCH] fix focus management --- .../shared/fonts-manager/fonts-manager.tsx | 44 ++++++++++++------- .../shared/fonts-manager/item-menu.tsx | 15 +++++-- .../designer/shared/fonts-manager/listbox.tsx | 5 ++- .../design-system/src/components/combobox.tsx | 14 +++--- 4 files changed, 51 insertions(+), 27 deletions(-) diff --git a/apps/designer/app/designer/shared/fonts-manager/fonts-manager.tsx b/apps/designer/app/designer/shared/fonts-manager/fonts-manager.tsx index da47bd2027..29a17089e1 100644 --- a/apps/designer/app/designer/shared/fonts-manager/fonts-manager.tsx +++ b/apps/designer/app/designer/shared/fonts-manager/fonts-manager.tsx @@ -13,10 +13,10 @@ import { Listbox, ListboxItem } from "./listbox"; type Item = { label: string; - type: "uploaded" | "system" | "category"; + type: "uploaded" | "system"; }; -const getItems = (assets: Array): Array => { +const toItems = (assets: Array): Array => { 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(); + 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); + }} /> @@ -163,19 +161,27 @@ export const FontsManager = ({ value, onChange }: FontsManagerProps) => { {...getItemProps({ item, index, - highlighted: item === openMenuItem ? true : undefined, })} key={index} suffix={ { - 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} @@ -188,13 +194,17 @@ export const FontsManager = ({ value, onChange }: FontsManagerProps) => { )} {systemItems.map((item, index) => { + const globalIndex = uploadedItems.length + index; return ( { + setHighlightedIndex(globalIndex); + }} > {item.label} diff --git a/apps/designer/app/designer/shared/fonts-manager/item-menu.tsx b/apps/designer/app/designer/shared/fonts-manager/item-menu.tsx index 08e025e365..5d3abc0ea4 100644 --- a/apps/designer/app/designer/shared/fonts-manager/item-menu.tsx +++ b/apps/designer/app/designer/shared/fonts-manager/item-menu.tsx @@ -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 ( { }} > - + diff --git a/apps/designer/app/designer/shared/fonts-manager/listbox.tsx b/apps/designer/app/designer/shared/fonts-manager/listbox.tsx index bf0605f886..78acce1ee8 100644 --- a/apps/designer/app/designer/shared/fonts-manager/listbox.tsx +++ b/apps/designer/app/designer/shared/fonts-manager/listbox.tsx @@ -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", }, diff --git a/packages/design-system/src/components/combobox.tsx b/packages/design-system/src/components/combobox.tsx index ff147e2311..697c8c4743 100644 --- a/packages/design-system/src/components/combobox.tsx +++ b/packages/design-system/src/components/combobox.tsx @@ -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 = ({ }; }; -type UseComboboxProps = { +type UseComboboxProps = UseDownshiftComboboxProps & { items: Array; 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 = ({ onItemHighlight, stateReducer = (state, { changes }) => changes, match, + ...rest }: UseComboboxProps) => { const { filteredItems, filter, resetFilter } = useFilter({ items, @@ -159,7 +161,9 @@ export const useCombobox = ({ }); 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 = ({ return { ...getMenuProps(options), state: isOpen ? "open" : "closed", - isEmpty: filteredItems.length === 0, + empty: filteredItems.length === 0, }; }, [getMenuProps, isOpen, filteredItems.length]