diff --git a/app/javascript/mastodon/components/form_fields/combobox.module.scss b/app/javascript/mastodon/components/form_fields/combobox.module.scss
index ef230a77b8..c14bac0cb6 100644
--- a/app/javascript/mastodon/components/form_fields/combobox.module.scss
+++ b/app/javascript/mastodon/components/form_fields/combobox.module.scss
@@ -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);
}
diff --git a/app/javascript/mastodon/components/form_fields/combobox_field.stories.tsx b/app/javascript/mastodon/components/form_fields/combobox_field.stories.tsx
index 32fc736423..795cba727a 100644
--- a/app/javascript/mastodon/components/form_fields/combobox_field.stories.tsx
+++ b/app/javascript/mastodon/components/form_fields/combobox_field.stories.tsx
@@ -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) => {fruit.name},
+ (fruit: Fruit) => {fruit.name},
[],
);
@@ -93,6 +93,7 @@ const ComboboxDemo: React.FC<{ withGroups?: boolean }> = ({ withGroups }) => {
const meta = {
title: 'Components/Form Fields/ComboboxField',
component: ComboboxField,
+ subcomponents: { ComboboxMenuItem },
render: () => ,
} satisfies Meta;
diff --git a/app/javascript/mastodon/components/form_fields/combobox_field.tsx b/app/javascript/mastodon/components/form_fields/combobox_field.tsx
index 64cbb3ce8a..40199d47ed 100644
--- a/app/javascript/mastodon/components/form_fields/combobox_field.tsx
+++ b/app/javascript/mastodon/components/form_fields/combobox_field.tsx
@@ -1,6 +1,8 @@
import {
+ createContext,
forwardRef,
useCallback,
+ useContext,
useId,
useMemo,
useRef,
@@ -109,6 +111,58 @@ interface ComboboxProps<
interface Props-
extends ComboboxProps
- , CommonFieldWrapperProps {}
+interface ComboboxItemPropsContext {
+ role: 'option';
+ 'data-highlighted': boolean;
+ 'aria-selected': boolean;
+ 'aria-disabled': boolean;
+ 'data-item-id': string;
+ onMouseEnter: React.MouseEventHandler;
+ onClick: React.MouseEventHandler;
+}
+
+const ComboboxItemPropsContext = createContext(
+ 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 (
+
+ {children}
+
+ );
+};
+
+export const ComboboxMenuGroupTitle: React.FC<
+ React.ComponentPropsWithoutRef<'li'>
+> = ({ className, children, ...otherProps }) => {
+ return (
+
+ {children}
+
+ );
+};
+
/**
* 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 =
- (
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 =
- (
? getIsItemSelected(item)
: isHighlighted;
return (
- // eslint-disable-next-line jsx-a11y/click-events-have-key-events
-
{renderItem(item, {
isSelected,
- isDisabled: isDisabled ?? false,
+ isDisabled,
})}
-
+
);
});
@@ -498,16 +552,12 @@ const ComboboxWithRef =
- (
role='group'
aria-labelledby={hasTitle ? groupTitleId : undefined}
>
- {hasTitle && (
-
- {customGroupTitle ?? (
- {groupKey}
- )}
-
- )}
+ {hasTitle &&
+ (customGroupTitle ?? (
+
+ {groupKey}
+
+ ))}
{renderItems(groupItems)}
);
diff --git a/app/javascript/mastodon/features/account_edit/components/tag_search.tsx b/app/javascript/mastodon/features/account_edit/components/tag_search.tsx
index 2dd17c2a72..04befb4690 100644
--- a/app/javascript/mastodon/features/account_edit/components/tag_search.tsx
+++ b/app/javascript/mastodon/features/account_edit/components/tag_search.tsx
@@ -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) => (
+ {item.label ?? `#${item.name}`}
+);
diff --git a/app/javascript/mastodon/features/collections/editor/accounts.tsx b/app/javascript/mastodon/features/collections/editor/accounts.tsx
index a51f58c236..dddd0242e0 100644
--- a/app/javascript/mastodon/features/collections/editor/accounts.tsx
+++ b/app/javascript/mastodon/features/collections/editor/accounts.tsx
@@ -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 (
- }
- >
-
-
-
-
+
+ }>
+
+
+
+
+
);
};
@@ -158,11 +159,13 @@ const renderGroupTitle = (groupKey: GroupKey, titleId: string) => {
}
return (
-
-
- {title}
-
-
+
+
+
+ {title}
+
+
+
);
};
diff --git a/app/javascript/mastodon/features/collections/editor/details.tsx b/app/javascript/mastodon/features/collections/editor/details.tsx
index ae944ba41f..42ef8db970 100644
--- a/app/javascript/mastodon/features/collections/editor/details.tsx
+++ b/app/javascript/mastodon/features/collections/editor/details.tsx
@@ -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) => (
+ {item.label ?? `#${item.name}`}
+);
const LanguageField: React.FC = () => {
const dispatch = useAppDispatch();
diff --git a/app/javascript/mastodon/features/collections/editor/styles.module.scss b/app/javascript/mastodon/features/collections/editor/styles.module.scss
index 1befc06cc1..d4b1d2d7f3 100644
--- a/app/javascript/mastodon/features/collections/editor/styles.module.scss
+++ b/app/javascript/mastodon/features/collections/editor/styles.module.scss
@@ -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;
}