diff --git a/app/javascript/mastodon/components/form_fields/combobox.module.scss b/app/javascript/mastodon/components/form_fields/combobox.module.scss index ca7bc2020b..ef230a77b8 100644 --- a/app/javascript/mastodon/components/form_fields/combobox.module.scss +++ b/app/javascript/mastodon/components/form_fields/combobox.module.scss @@ -25,7 +25,7 @@ .popover { z-index: 9999; box-sizing: border-box; - max-height: max(200px, 30dvh); + max-height: min(320px, 50dvh); padding: 4px; border-radius: 4px; color: var(--color-text-primary); @@ -63,13 +63,7 @@ user-select: none; &[data-highlighted='true'] { - color: var(--color-text-on-brand-base); - background: var(--color-bg-brand-base); - - &[aria-disabled='true'] { - color: var(--color-text-on-disabled); - background: var(--color-bg-disabled); - } + background: var(--color-bg-overlay-highlight); } &[aria-disabled='true'] { diff --git a/app/javascript/mastodon/components/form_fields/combobox_field.tsx b/app/javascript/mastodon/components/form_fields/combobox_field.tsx index 6cc68b90bf..64cbb3ce8a 100644 --- a/app/javascript/mastodon/components/form_fields/combobox_field.tsx +++ b/app/javascript/mastodon/components/form_fields/combobox_field.tsx @@ -82,11 +82,12 @@ interface ComboboxProps< * Customise the rendering of group titles. * The `titleId` must be attached to the element that provides the * accessible name for the group. + * Return `null` to omit rendering the group title. */ renderGroupTitle?: ( groupKey: GroupKey, titleId: string, - ) => React.ReactElement | string; + ) => React.ReactElement | null; /** * The main selection handler, called when an option is selected or deselected. */ @@ -182,7 +183,12 @@ const ComboboxWithRef = ( const hasGroups = !Array.isArray(items); const flatItems = useMemo( - () => (hasGroups ? (Object.values(items).flat() as Item[]) : items), + () => + hasGroups + ? (Object.values(items) + .flat() + .filter((i) => !!i) as Item[]) + : items, [hasGroups, items], ); @@ -478,10 +484,11 @@ const ComboboxWithRef = ( {(Object.keys(items) as GroupKey[]).map((groupKey) => { const groupItems = items[groupKey]; const groupTitleId = `${listId}-group-${groupKey}`; - const groupTitle = renderGroupTitle?.( + const customGroupTitle = renderGroupTitle?.( groupKey, groupTitleId, - ) ?? {groupKey}; + ); + const hasTitle = customGroupTitle !== null; if (!groupItems?.length) return null; @@ -489,11 +496,18 @@ const ComboboxWithRef = (
    -
  • - {groupTitle} -
  • + {hasTitle && ( +
  • + {customGroupTitle ?? ( + {groupKey} + )} +
  • + )} {renderItems(groupItems)}
); diff --git a/app/javascript/mastodon/components/list_item/index.tsx b/app/javascript/mastodon/components/list_item/index.tsx index 66758bd4de..263e206fb9 100644 --- a/app/javascript/mastodon/components/list_item/index.tsx +++ b/app/javascript/mastodon/components/list_item/index.tsx @@ -14,8 +14,9 @@ interface WrapperProps extends Omit< /** * A basic list item component that can be used as a base for more bespoke list items. * - * Depending on functionality, use `ListItemButton` or `ListItemLink` as a child of the - * wrapper component. + * Choose the child of the wrapper component based on needed interactivity: + * `ListItemContent` for a non-interactive item, `ListItemButton` or `ListItemLink` + * for interactive items. */ export const ListItemWrapper: React.FC = ({ icon, @@ -33,10 +34,31 @@ export const ListItemWrapper: React.FC = ({ ); }; -interface LinkProps extends React.ComponentPropsWithoutRef { +interface WithSubtitle { subtitle?: React.ReactNode; } +interface ContentProps + extends React.ComponentPropsWithoutRef<'h3'>, WithSubtitle {} + +export const ListItemContent: React.FC = ({ + subtitle, + children, + ...otherProps +}) => { + return ( + <> +

+ {children} +

+ {subtitle &&
{subtitle}
} + + ); +}; + +interface LinkProps + extends React.ComponentPropsWithoutRef, WithSubtitle {} + export const ListItemLink: React.FC = ({ subtitle, children, @@ -44,20 +66,16 @@ export const ListItemLink: React.FC = ({ ...otherProps }) => { return ( - <> -

- - {children} - -

- {subtitle &&
{subtitle}
} - + + + {children} + + ); }; -interface ButtonProps extends React.ComponentPropsWithoutRef<'button'> { - subtitle?: React.ReactNode; -} +interface ButtonProps + extends React.ComponentPropsWithoutRef<'button'>, WithSubtitle {} export const ListItemButton: React.FC = ({ subtitle, @@ -66,17 +84,14 @@ export const ListItemButton: React.FC = ({ ...otherProps }) => { return ( - <> -

- -

- {subtitle &&
{subtitle}
} - + + + ); }; diff --git a/app/javascript/mastodon/components/list_item/list_item.stories.tsx b/app/javascript/mastodon/components/list_item/list_item.stories.tsx index d46102c1c3..dd066683c9 100644 --- a/app/javascript/mastodon/components/list_item/list_item.stories.tsx +++ b/app/javascript/mastodon/components/list_item/list_item.stories.tsx @@ -7,18 +7,31 @@ import VisibilityOffIcon from '@/material-icons/400-24px/visibility_off.svg?reac import { Icon } from '../icon'; -import { ListItemWrapper, ListItemButton, ListItemLink } from './index'; +import { + ListItemWrapper, + ListItemContent, + ListItemButton, + ListItemLink, +} from './index'; const meta = { title: 'Components/ListItem', component: ListItemWrapper, - subcomponents: { ListItemButton, ListItemLink }, + subcomponents: { ListItemContent, ListItemButton, ListItemLink }, } satisfies Meta; export default meta; type Story = StoryObj; +export const NonInteractive: Story = { + render: () => ( + }> + View more + + ), +}; + export const WithButton: Story = { render: () => ( ; }; -const SuggestedAccountItem: React.FC<{ account: ApiMutedAccountJSON }> = ( - props, -) => { - const account = useAccount(props.account.id); +const SuggestedAccountItem: React.FC<{ id: string }> = ({ id }) => { + const account = useAccount(id); + const handle = useAccountHandle(account, domain); if (!account) return null; return ( - <> - - - + } + > + + + + ); }; const renderAccountItem = (account: ApiMutedAccountJSON) => ( - + ); +type GroupKey = 'available' | 'mustFollow' | 'disabled'; + +function groupSuggestions(accounts: ApiMutedAccountJSON[]) { + const { available, disabled } = Object.groupBy(accounts, (account) => { + if (getIsItemDisabled(account)) { + return 'disabled'; + } + // if (account.locked && !relationship?.following) { + // return 'mustFollow'; + // } + return 'available'; + }); + + // Returning a new object ensures a fixed property order + return { available, disabled }; +} + +const renderGroupTitle = (groupKey: GroupKey, titleId: string) => { + if (groupKey === 'available') { + return null; + } + + let title: React.ReactElement; + let description: React.ReactElement; + + if (groupKey === 'mustFollow') { + title = ( + + ); + description = ( + + ); + } else { + title = ( + + ); + description = ( + + ); + } + + return ( + + + {title} + + + ); +}; + const getItemId = (account: ApiMutedAccountJSON) => account.id; // Disable accounts who can't be added to a collection @@ -259,10 +330,11 @@ export const CollectionAccounts: React.FC<{ onKeyDown={handleSearchKeyDown} disabled={hasMaxAccounts} isLoading={isLoadingSuggestions} - items={suggestedAccounts} + items={groupSuggestions(suggestedAccounts)} getItemId={getItemId} getIsItemDisabled={getIsItemDisabled} renderItem={renderAccountItem} + renderGroupTitle={renderGroupTitle} onSelectItem={handleSelectItem} status={ hasMaxAccounts diff --git a/app/javascript/mastodon/features/collections/editor/details.tsx b/app/javascript/mastodon/features/collections/editor/details.tsx index f4e561e6e3..ae944ba41f 100644 --- a/app/javascript/mastodon/features/collections/editor/details.tsx +++ b/app/javascript/mastodon/features/collections/editor/details.tsx @@ -276,18 +276,16 @@ export const CollectionDetails: React.FC = () => {
-
- -
+
); diff --git a/app/javascript/mastodon/features/collections/editor/styles.module.scss b/app/javascript/mastodon/features/collections/editor/styles.module.scss index 577a6d7d12..1befc06cc1 100644 --- a/app/javascript/mastodon/features/collections/editor/styles.module.scss +++ b/app/javascript/mastodon/features/collections/editor/styles.module.scss @@ -58,3 +58,19 @@ .scrollableWrapper { margin-inline: -16px; } + +.suggestion { + padding: 4px 0; + + [aria-disabled='true'] & { + opacity: 0.6; + } +} + +.suggestionGroup { + padding: 4px 0; + + // Undo default group styles: + font-weight: 400; + text-transform: none; +} diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index 449679aab5..601293fb22 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -410,6 +410,10 @@ "collections.search_accounts_max_reached": "You have added the maximum number of accounts", "collections.sensitive": "Sensitive", "collections.share_short": "Share", + "collections.suggestions.can_not_add": "Can’t be added", + "collections.suggestions.can_not_add_desc": "These accounts may have opted out of discovery, or they might be on a server that doesn’t support collections.", + "collections.suggestions.must_follow": "Must follow first", + "collections.suggestions.must_follow_desc": "These accounts review all follow requests. Followers can add them to collections.", "collections.topic_hint": "Add a hashtag that helps others understand the main topic of this collection.", "collections.topic_special_chars_hint": "Special characters will be removed when saving", "collections.unlisted_collections_description": "These don’t appear on your profile to others. Anyone with the link can discover them.",