Implement final design for collection editor account dropdown menu (#38767)

This commit is contained in:
diondiondion authored and GitHub committed 2026-04-21 14:12:44 +00:00
1 parent 57c5d1c8dd
commit a706fce678
7 files changed
+121 -55

No files matched your search

@@ -26,7 +26,6 @@
z-index: 9999;
box-sizing: border-box;
max-height: min(320px, 50dvh);
padding: 4px;
border-radius: 4px;
color: var(--color-text-primary);
background: var(--color-bg-primary);
@@ -38,22 +37,18 @@
overscroll-behavior-y: contain;
}
.groupLabel {
padding-block: 12px 4px;
padding-inline: 12px;
.groupTitle {
padding: 8px 16px 4px;
font-size: 13px;
font-weight: bold;
text-transform: uppercase;
ul:first-child > & {
padding-top: 4px;
}
}
.menuItem {
display: flex;
align-items: center;
padding: 8px 12px;
margin-inline: 4px;
gap: 12px;
font-size: 14px;
line-height: 20px;
@@ -62,6 +57,14 @@
cursor: pointer;
user-select: none;
&:first-child {
margin-top: 4px;
}
&:last-child {
margin-bottom: 4px;
}
&[data-highlighted='true'] {
background: var(--color-bg-overlay-highlight);
}
@@ -2,7 +2,7 @@ import { useCallback, useState } from 'react';
import type { Meta, StoryObj } from '@storybook/react-vite';
import { ComboboxField } from './combobox_field';
import { ComboboxField, ComboboxMenuItem } from './combobox_field';
interface Fruit {
id: string;
@@ -60,7 +60,7 @@ const ComboboxDemo: React.FC<{ withGroups?: boolean }> = ({ withGroups }) => {
}, []);
const renderItem = useCallback(
(fruit: Fruit) => <span>{fruit.name}</span>,
(fruit: Fruit) => <ComboboxMenuItem>{fruit.name}</ComboboxMenuItem>,
[],
);
@@ -93,6 +93,7 @@ const ComboboxDemo: React.FC<{ withGroups?: boolean }> = ({ withGroups }) => {
const meta = {
title: 'Components/Form Fields/ComboboxField',
component: ComboboxField,
subcomponents: { ComboboxMenuItem },
render: () => <ComboboxDemo />,
} satisfies Meta<typeof ComboboxField>;
@@ -1,6 +1,8 @@
import {
createContext,
forwardRef,
useCallback,
useContext,
useId,
useMemo,
useRef,
@@ -109,6 +111,58 @@ interface ComboboxProps<
interface Props<Item extends ComboboxItem, GroupKey extends string>
extends ComboboxProps<Item, GroupKey>, CommonFieldWrapperProps {}
interface ComboboxItemPropsContext {
role: 'option';
'data-highlighted': boolean;
'aria-selected': boolean;
'aria-disabled': boolean;
'data-item-id': string;
onMouseEnter: React.MouseEventHandler<HTMLLIElement>;
onClick: React.MouseEventHandler<HTMLLIElement>;
}
const ComboboxItemPropsContext = createContext<ComboboxItemPropsContext | null>(
null,
);
export function useComboboxItemProps() {
const context = useContext(ComboboxItemPropsContext);
if (context === null) {
throw new Error(
'useComboboxItemProps must be used within a Combobox component',
);
}
return context;
}
export const ComboboxMenuItem: React.FC<{
className?: string;
children: React.ReactNode;
}> = ({ className, children }) => {
const props = useComboboxItemProps();
return (
<li className={classNames(className, classes.menuItem)} {...props}>
{children}
</li>
);
};
export const ComboboxMenuGroupTitle: React.FC<
React.ComponentPropsWithoutRef<'li'>
> = ({ className, children, ...otherProps }) => {
return (
<li
{...otherProps}
role='presentation'
className={classNames(className, classes.groupTitle)}
>
{children}
</li>
);
};
/**
* The combobox field allows users to select one or more items
* by searching or filtering a large or dynamic list of options.
@@ -371,7 +425,7 @@ const ComboboxWithRef = <Item extends ComboboxItem, GroupKey extends string>(
const renderItems = (items: Item[]) =>
items.map((item) => {
const id = getItemId(item);
const isDisabled = getIsItemDisabled?.(item);
const isDisabled = getIsItemDisabled?.(item) ?? false;
const isHighlighted = id === highlightedItemId;
// If `getIsItemSelected` is defined, we assume 'multi-select'
// behaviour and don't set `aria-selected` based on highlight,
@@ -380,23 +434,23 @@ const ComboboxWithRef = <Item extends ComboboxItem, GroupKey extends string>(
? getIsItemSelected(item)
: isHighlighted;
return (
// eslint-disable-next-line jsx-a11y/click-events-have-key-events
<li
<ComboboxItemPropsContext.Provider
key={id}
role='option'
className={classes.menuItem}
data-highlighted={isHighlighted}
aria-selected={isSelected}
aria-disabled={isDisabled}
data-item-id={id}
onMouseEnter={handleItemMouseEnter}
onClick={handleSelectItem}
value={{
role: 'option',
'data-highlighted': isHighlighted,
'aria-selected': isSelected,
'aria-disabled': isDisabled,
'data-item-id': id,
onMouseEnter: handleItemMouseEnter,
onClick: handleSelectItem,
}}
>
{renderItem(item, {
isSelected,
isDisabled: isDisabled ?? false,
isDisabled,
})}
</li>
</ComboboxItemPropsContext.Provider>
);
});
@@ -498,16 +552,12 @@ const ComboboxWithRef = <Item extends ComboboxItem, GroupKey extends string>(
role='group'
aria-labelledby={hasTitle ? groupTitleId : undefined}
>
{hasTitle && (
<li
role='presentation'
className={classes.groupLabel}
>
{customGroupTitle ?? (
<span id={groupTitleId}>{groupKey}</span>
)}
</li>
)}
{hasTitle &&
(customGroupTitle ?? (
<ComboboxMenuGroupTitle id={groupTitleId}>
{groupKey}
</ComboboxMenuGroupTitle>
))}
{renderItems(groupItems)}
</ul>
);