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;
|
z-index: 9999;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
max-height: min(320px, 50dvh);
|
max-height: min(320px, 50dvh);
|
||||||
padding: 4px;
|
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
color: var(--color-text-primary);
|
color: var(--color-text-primary);
|
||||||
background: var(--color-bg-primary);
|
background: var(--color-bg-primary);
|
||||||
@ -38,22 +37,18 @@
|
|||||||
overscroll-behavior-y: contain;
|
overscroll-behavior-y: contain;
|
||||||
}
|
}
|
||||||
|
|
||||||
.groupLabel {
|
.groupTitle {
|
||||||
padding-block: 12px 4px;
|
padding: 8px 16px 4px;
|
||||||
padding-inline: 12px;
|
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
|
|
||||||
ul:first-child > & {
|
|
||||||
padding-top: 4px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.menuItem {
|
.menuItem {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
|
margin-inline: 4px;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
@ -62,6 +57,14 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
&[data-highlighted='true'] {
|
&[data-highlighted='true'] {
|
||||||
background: var(--color-bg-overlay-highlight);
|
background: var(--color-bg-overlay-highlight);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { useCallback, useState } from 'react';
|
|||||||
|
|
||||||
import type { Meta, StoryObj } from '@storybook/react-vite';
|
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||||
|
|
||||||
import { ComboboxField } from './combobox_field';
|
import { ComboboxField, ComboboxMenuItem } from './combobox_field';
|
||||||
|
|
||||||
interface Fruit {
|
interface Fruit {
|
||||||
id: string;
|
id: string;
|
||||||
@ -60,7 +60,7 @@ const ComboboxDemo: React.FC<{ withGroups?: boolean }> = ({ withGroups }) => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const renderItem = useCallback(
|
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 = {
|
const meta = {
|
||||||
title: 'Components/Form Fields/ComboboxField',
|
title: 'Components/Form Fields/ComboboxField',
|
||||||
component: ComboboxField,
|
component: ComboboxField,
|
||||||
|
subcomponents: { ComboboxMenuItem },
|
||||||
render: () => <ComboboxDemo />,
|
render: () => <ComboboxDemo />,
|
||||||
} satisfies Meta<typeof ComboboxField>;
|
} satisfies Meta<typeof ComboboxField>;
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
import {
|
import {
|
||||||
|
createContext,
|
||||||
forwardRef,
|
forwardRef,
|
||||||
useCallback,
|
useCallback,
|
||||||
|
useContext,
|
||||||
useId,
|
useId,
|
||||||
useMemo,
|
useMemo,
|
||||||
useRef,
|
useRef,
|
||||||
@ -109,6 +111,58 @@ interface ComboboxProps<
|
|||||||
interface Props<Item extends ComboboxItem, GroupKey extends string>
|
interface Props<Item extends ComboboxItem, GroupKey extends string>
|
||||||
extends ComboboxProps<Item, GroupKey>, CommonFieldWrapperProps {}
|
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
|
* The combobox field allows users to select one or more items
|
||||||
* by searching or filtering a large or dynamic list of options.
|
* 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[]) =>
|
const renderItems = (items: Item[]) =>
|
||||||
items.map((item) => {
|
items.map((item) => {
|
||||||
const id = getItemId(item);
|
const id = getItemId(item);
|
||||||
const isDisabled = getIsItemDisabled?.(item);
|
const isDisabled = getIsItemDisabled?.(item) ?? false;
|
||||||
const isHighlighted = id === highlightedItemId;
|
const isHighlighted = id === highlightedItemId;
|
||||||
// If `getIsItemSelected` is defined, we assume 'multi-select'
|
// If `getIsItemSelected` is defined, we assume 'multi-select'
|
||||||
// behaviour and don't set `aria-selected` based on highlight,
|
// behaviour and don't set `aria-selected` based on highlight,
|
||||||
@ -380,23 +434,23 @@ const ComboboxWithRef = <Item extends ComboboxItem, GroupKey extends string>(
|
|||||||
? getIsItemSelected(item)
|
? getIsItemSelected(item)
|
||||||
: isHighlighted;
|
: isHighlighted;
|
||||||
return (
|
return (
|
||||||
// eslint-disable-next-line jsx-a11y/click-events-have-key-events
|
<ComboboxItemPropsContext.Provider
|
||||||
<li
|
|
||||||
key={id}
|
key={id}
|
||||||
role='option'
|
value={{
|
||||||
className={classes.menuItem}
|
role: 'option',
|
||||||
data-highlighted={isHighlighted}
|
'data-highlighted': isHighlighted,
|
||||||
aria-selected={isSelected}
|
'aria-selected': isSelected,
|
||||||
aria-disabled={isDisabled}
|
'aria-disabled': isDisabled,
|
||||||
data-item-id={id}
|
'data-item-id': id,
|
||||||
onMouseEnter={handleItemMouseEnter}
|
onMouseEnter: handleItemMouseEnter,
|
||||||
onClick={handleSelectItem}
|
onClick: handleSelectItem,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{renderItem(item, {
|
{renderItem(item, {
|
||||||
isSelected,
|
isSelected,
|
||||||
isDisabled: isDisabled ?? false,
|
isDisabled,
|
||||||
})}
|
})}
|
||||||
</li>
|
</ComboboxItemPropsContext.Provider>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -498,16 +552,12 @@ const ComboboxWithRef = <Item extends ComboboxItem, GroupKey extends string>(
|
|||||||
role='group'
|
role='group'
|
||||||
aria-labelledby={hasTitle ? groupTitleId : undefined}
|
aria-labelledby={hasTitle ? groupTitleId : undefined}
|
||||||
>
|
>
|
||||||
{hasTitle && (
|
{hasTitle &&
|
||||||
<li
|
(customGroupTitle ?? (
|
||||||
role='presentation'
|
<ComboboxMenuGroupTitle id={groupTitleId}>
|
||||||
className={classes.groupLabel}
|
{groupKey}
|
||||||
>
|
</ComboboxMenuGroupTitle>
|
||||||
{customGroupTitle ?? (
|
))}
|
||||||
<span id={groupTitleId}>{groupKey}</span>
|
|
||||||
)}
|
|
||||||
</li>
|
|
||||||
)}
|
|
||||||
{renderItems(groupItems)}
|
{renderItems(groupItems)}
|
||||||
</ul>
|
</ul>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { useCallback, useId, useState } from 'react';
|
|||||||
import { defineMessages, useIntl } from 'react-intl';
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
|
|
||||||
import { Combobox } from '@/mastodon/components/form_fields';
|
import { Combobox } from '@/mastodon/components/form_fields';
|
||||||
|
import { ComboboxMenuItem } from '@/mastodon/components/form_fields/combobox_field';
|
||||||
import { useSearchTags } from '@/mastodon/hooks/useSearchTags';
|
import { useSearchTags } from '@/mastodon/hooks/useSearchTags';
|
||||||
import type { TagSearchResult } from '@/mastodon/hooks/useSearchTags';
|
import type { TagSearchResult } from '@/mastodon/hooks/useSearchTags';
|
||||||
import { addFeaturedTags } from '@/mastodon/reducers/slices/profile_edit';
|
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 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 { ApiMutedAccountJSON } from 'mastodon/api_types/accounts';
|
||||||
import type { ApiCollectionJSON } from 'mastodon/api_types/collections';
|
import type { ApiCollectionJSON } from 'mastodon/api_types/collections';
|
||||||
import { AccountListItem } from 'mastodon/components/account_list_item';
|
import { AccountListItem } from 'mastodon/components/account_list_item';
|
||||||
@ -67,18 +68,18 @@ const AddedAccountItem: React.FC<{
|
|||||||
const SuggestedAccountItem: React.FC<{ id: string }> = ({ id }) => {
|
const SuggestedAccountItem: React.FC<{ id: string }> = ({ id }) => {
|
||||||
const account = useAccount(id);
|
const account = useAccount(id);
|
||||||
const handle = useAccountHandle(account, domain);
|
const handle = useAccountHandle(account, domain);
|
||||||
|
const comboboxItemProps = useComboboxItemProps();
|
||||||
|
|
||||||
if (!account) return null;
|
if (!account) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ListItemWrapper
|
<li {...comboboxItemProps} className={classes.suggestion}>
|
||||||
className={classes.suggestion}
|
<ListItemWrapper icon={<Avatar account={account} size={40} />}>
|
||||||
icon={<Avatar account={account} size={40} />}
|
<ListItemContent subtitle={handle}>
|
||||||
>
|
<DisplayName account={account} variant='simple' />
|
||||||
<ListItemContent subtitle={handle}>
|
</ListItemContent>
|
||||||
<DisplayName account={account} variant='simple' />
|
</ListItemWrapper>
|
||||||
</ListItemContent>
|
</li>
|
||||||
</ListItemWrapper>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -158,11 +159,13 @@ const renderGroupTitle = (groupKey: GroupKey, titleId: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ListItemWrapper className={classes.suggestionGroup}>
|
<li role='presentation'>
|
||||||
<ListItemContent id={titleId} subtitle={description}>
|
<ListItemWrapper className={classes.suggestionGroup}>
|
||||||
{title}
|
<ListItemContent id={titleId} subtitle={description}>
|
||||||
</ListItemContent>
|
{title}
|
||||||
</ListItemWrapper>
|
</ListItemContent>
|
||||||
|
</ListItemWrapper>
|
||||||
|
</li>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import { useHistory } from 'react-router-dom';
|
|||||||
|
|
||||||
import { isFulfilled } from '@reduxjs/toolkit';
|
import { isFulfilled } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
|
import { ComboboxMenuItem } from '@/mastodon/components/form_fields/combobox_field';
|
||||||
import { languages } from '@/mastodon/initial_state';
|
import { languages } from '@/mastodon/initial_state';
|
||||||
import {
|
import {
|
||||||
hasSpecialCharacters,
|
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 LanguageField: React.FC = () => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|||||||
@ -60,17 +60,20 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.suggestion {
|
.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;
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.suggestionGroup {
|
.suggestionGroup {
|
||||||
padding: 4px 0;
|
padding-bottom: 4px;
|
||||||
|
|
||||||
// Undo default group styles:
|
|
||||||
font-weight: 400;
|
|
||||||
text-transform: none;
|
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user