Implement final design for collection editor account dropdown menu (#38767)
This commit is contained in:
parent
57c5d1c8dd
commit
a706fce678
@ -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>
|
||||
);
|
||||
|
||||
@ -4,6 +4,7 @@ import { useCallback, useId, useState } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import { Combobox } from '@/mastodon/components/form_fields';
|
||||
import { ComboboxMenuItem } from '@/mastodon/components/form_fields/combobox_field';
|
||||
import { useSearchTags } from '@/mastodon/hooks/useSearchTags';
|
||||
import type { TagSearchResult } from '@/mastodon/hooks/useSearchTags';
|
||||
import { addFeaturedTags } from '@/mastodon/reducers/slices/profile_edit';
|
||||
@ -77,4 +78,6 @@ export const AccountEditTagSearch: FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const renderItem = (item: TagSearchResult) => item.label ?? `#${item.name}`;
|
||||
const renderItem = (item: TagSearchResult) => (
|
||||
<ComboboxMenuItem>{item.label ?? `#${item.name}`}</ComboboxMenuItem>
|
||||
);
|
||||
|
||||
@ -6,6 +6,7 @@ import { useHistory } from 'react-router-dom';
|
||||
|
||||
import type { Map as ImmutableMap } from 'immutable';
|
||||
|
||||
import { useComboboxItemProps } from '@/mastodon/components/form_fields/combobox_field';
|
||||
import type { ApiMutedAccountJSON } from 'mastodon/api_types/accounts';
|
||||
import type { ApiCollectionJSON } from 'mastodon/api_types/collections';
|
||||
import { AccountListItem } from 'mastodon/components/account_list_item';
|
||||
@ -67,18 +68,18 @@ const AddedAccountItem: React.FC<{
|
||||
const SuggestedAccountItem: React.FC<{ id: string }> = ({ id }) => {
|
||||
const account = useAccount(id);
|
||||
const handle = useAccountHandle(account, domain);
|
||||
const comboboxItemProps = useComboboxItemProps();
|
||||
|
||||
if (!account) return null;
|
||||
|
||||
return (
|
||||
<ListItemWrapper
|
||||
className={classes.suggestion}
|
||||
icon={<Avatar account={account} size={40} />}
|
||||
>
|
||||
<ListItemContent subtitle={handle}>
|
||||
<DisplayName account={account} variant='simple' />
|
||||
</ListItemContent>
|
||||
</ListItemWrapper>
|
||||
<li {...comboboxItemProps} className={classes.suggestion}>
|
||||
<ListItemWrapper icon={<Avatar account={account} size={40} />}>
|
||||
<ListItemContent subtitle={handle}>
|
||||
<DisplayName account={account} variant='simple' />
|
||||
</ListItemContent>
|
||||
</ListItemWrapper>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
@ -158,11 +159,13 @@ const renderGroupTitle = (groupKey: GroupKey, titleId: string) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<ListItemWrapper className={classes.suggestionGroup}>
|
||||
<ListItemContent id={titleId} subtitle={description}>
|
||||
{title}
|
||||
</ListItemContent>
|
||||
</ListItemWrapper>
|
||||
<li role='presentation'>
|
||||
<ListItemWrapper className={classes.suggestionGroup}>
|
||||
<ListItemContent id={titleId} subtitle={description}>
|
||||
{title}
|
||||
</ListItemContent>
|
||||
</ListItemWrapper>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -6,6 +6,7 @@ import { useHistory } from 'react-router-dom';
|
||||
|
||||
import { isFulfilled } from '@reduxjs/toolkit';
|
||||
|
||||
import { ComboboxMenuItem } from '@/mastodon/components/form_fields/combobox_field';
|
||||
import { languages } from '@/mastodon/initial_state';
|
||||
import {
|
||||
hasSpecialCharacters,
|
||||
@ -373,7 +374,9 @@ const TopicField: React.FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const renderTagItem = (item: TagSearchResult) => item.label ?? `#${item.name}`;
|
||||
const renderTagItem = (item: TagSearchResult) => (
|
||||
<ComboboxMenuItem>{item.label ?? `#${item.name}`}</ComboboxMenuItem>
|
||||
);
|
||||
|
||||
const LanguageField: React.FC = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
@ -60,17 +60,20 @@
|
||||
}
|
||||
|
||||
.suggestion {
|
||||
padding: 4px 0;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
border-bottom: 1px solid var(--color-border-primary);
|
||||
|
||||
[aria-disabled='true'] & {
|
||||
&[data-highlighted='true'] {
|
||||
background: var(--color-bg-overlay-highlight);
|
||||
}
|
||||
|
||||
&[aria-disabled='true'] {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.suggestionGroup {
|
||||
padding: 4px 0;
|
||||
|
||||
// Undo default group styles:
|
||||
font-weight: 400;
|
||||
text-transform: none;
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user