Add MenuItemRadio and MenuItemCheckbox components (#40215)

This commit is contained in:
diondiondion
2026-08-19 15:53:38 +00:00
committed by GitHub
parent 87db34be63
commit 07f91058a0
12 changed files with 370 additions and 349 deletions

View File

@@ -73,8 +73,7 @@ input.input {
}
}
// Use double class to override original styles.
.toggle.toggle {
.toggle > span {
background: var(--color-text-tertiary);
&::before {
@@ -87,9 +86,11 @@ input.input {
--padding: 2px;
--diameter: 14px;
width: 32px;
}
> span {
width: 32px;
}
input:checked + .toggle.toggleSmall::before {
transform: translateX(var(--diameter));
> input:checked + span::before {
transform: translateX(var(--diameter));
}
}

View File

@@ -61,6 +61,10 @@
.input:checked + .toggle::before {
transform: translateX(calc(var(--diameter) - (var(--padding) * 2)));
:global([dir='rtl']) & {
transform: translateX(calc(-1 * (var(--diameter) - (var(--padding) * 2))));
}
}
.input:disabled {
@@ -70,7 +74,3 @@
.input:disabled + .toggle {
opacity: 0.6;
}
:global([dir='rtl']) .input:checked + .toggle::before {
transform: translateX(calc(-1 * (var(--diameter) - (var(--padding) * 2))));
}

View File

@@ -38,7 +38,7 @@ ToggleField.displayName = 'ToggleField';
export const Toggle = forwardRef<HTMLInputElement, Props>(
({ className, size, ...otherProps }, ref) => (
<span
className={classes.wrapper}
className={classNames(classes.wrapper, className)}
style={{ '--diameter': size ? `${size}px` : undefined } as CSSProperties}
>
<input
@@ -47,7 +47,7 @@ export const Toggle = forwardRef<HTMLInputElement, Props>(
className={classes.input}
ref={ref}
/>
<span className={classNames(classes.toggle, className)} hidden />
<span className={classes.toggle} hidden />
</span>
),
);

View File

@@ -32,7 +32,7 @@ export const MenuCard = <As extends React.ElementType>({
return (
<Component
{...props}
className={classNames(className, classes.menuCard)}
className={classNames(className, classes.card)}
data-elevation={elevation}
style={{
maxWidth: typeof maxWidth === 'number' ? `${maxWidth}px` : maxWidth,
@@ -83,7 +83,7 @@ export const PopoverMenuCard = <As extends React.ElementType>({
{...props}
className={classNames(
className,
props.maxWidth && classes.popoverMenuCard,
props.maxWidth && classes.popoverCard,
)}
>
{children}

View File

@@ -8,19 +8,24 @@ import {
useState,
} from 'react';
import classNames from 'classnames';
import type { Merge } from 'type-fest';
import { Button } from '../button/redesign';
import { Icon } from '../icon';
import type { IconProp } from '../icon';
import { PopoverMenuCard } from './card';
import type { PopoverMenuCardProps } from './card';
import classes from './styles.module.scss';
export const menuItemClass = classes.menuItem;
export const menuItemClass = classes.item;
export {
MenuItemDivider,
MenuItemGroup,
MenuItemBase,
MenuItem,
MenuItemRadio,
MenuItemCheckbox,
} from './items';
interface PopoverState {
isMenuOpen: boolean;
@@ -71,7 +76,7 @@ export function useMenuContext(): MenuState {
function getAllMenuItems(menuListElement: HTMLDivElement) {
return Array.from(
menuListElement.querySelectorAll<HTMLElement>(
':scope [data-menu-item]:not([disabled], [aria-disabled])',
':scope [data-menu-item]:not([disabled])',
),
);
}
@@ -258,79 +263,3 @@ export const MenuList = <As extends React.ElementType>({
</PopoverMenuCard>
);
};
type MenuItemProps<As extends React.ElementType> = Merge<
{
as?: As;
children?: React.ReactNode;
className?: string;
active?: boolean;
disabled?: boolean;
leadingIcon?: IconProp;
trailingIcon?: IconProp;
iconClassName?: string;
},
React.ComponentPropsWithoutRef<As>
>;
export const MenuItemBase = <As extends React.ElementType>({
active,
disabled,
as: AsComp,
children,
className,
leadingIcon,
trailingIcon,
iconClassName,
...props
}: MenuItemProps<As>) => {
const Component = AsComp ?? 'div';
return (
<Component
{...props}
data-menu-item
className={classNames(
className,
classes.menuItem,
active && classes.menuItemActive,
)}
aria-disabled={disabled}
>
{leadingIcon && (
<Icon
id='menu'
icon={leadingIcon}
className={classNames(iconClassName, classes.menuItemIcon)}
/>
)}
{children}
{trailingIcon && (
<Icon
id='menu'
icon={trailingIcon}
className={classNames(iconClassName, classes.menuItemIcon)}
/>
)}
</Component>
);
};
export const MenuItem: React.FC<Omit<MenuItemProps<'button'>, 'as'>> = ({
children,
className,
...props
}) => {
return (
<MenuItemBase
as='button'
type='button'
role='menuitem'
{...props}
className={className}
>
{children}
</MenuItemBase>
);
};

View File

@@ -0,0 +1,163 @@
import { useCallback, useId } from 'react';
import classNames from 'classnames';
import { CheckIcon } from '@phosphor-icons/react';
import { Toggle } from '../form_fields/redesign';
import { Icon } from '../icon';
import type { IconProp } from '../icon';
import classes from './styles.module.scss';
interface MenuItemGroupProps extends React.ComponentProps<'div'> {
label: React.ReactNode;
}
export const MenuItemGroup: React.FC<MenuItemGroupProps> = ({
label,
children,
}) => {
const uniqueId = useId();
return (
<div aria-labelledby={uniqueId} role='group'>
<div id={uniqueId} className={classes.itemGroupLabel}>
{label}
</div>
{children}
</div>
);
};
export const MenuItemDivider: React.FC = () => {
return <hr className={classes.itemDivider} />;
};
type MenuItemProps<As extends React.ElementType> =
React.ComponentPropsWithoutRef<As> & {
as?: As;
children?: React.ReactNode;
className?: string;
active?: boolean;
disabled?: boolean;
icon?: IconProp | 'reserve-space';
trailingContent?: React.ReactNode;
iconClassName?: string;
};
export const MenuItemBase = <As extends React.ElementType>({
active,
disabled,
as: AsComp,
children,
className,
icon,
trailingContent,
iconClassName,
...props
}: MenuItemProps<As>) => {
const Component = AsComp ?? 'div';
return (
<Component
{...props}
data-menu-item
className={classNames(
className,
classes.item,
active && classes.itemActive,
)}
aria-disabled={disabled}
>
{icon && icon !== 'reserve-space' && (
<Icon
id='menu'
icon={icon}
className={classNames(iconClassName, classes.itemIcon)}
/>
)}
{icon === 'reserve-space' && <div className={classes.itemIcon} />}
{children}
<span className={classes.itemTrailingContent}>{trailingContent}</span>
</Component>
);
};
export const MenuItem: React.FC<Omit<MenuItemProps<'button'>, 'as'>> = ({
children,
...props
}) => {
return (
<MenuItemBase as='button' type='button' role='menuitem' {...props}>
{children}
</MenuItemBase>
);
};
interface MenuItemRadioProps extends Omit<
MenuItemProps<'button'>,
'as' | 'onChange' | 'icon'
> {
checked: boolean;
value: string;
onChange: (change: { value: string }) => void;
}
export const MenuItemRadio: React.FC<MenuItemRadioProps> = ({
children,
checked,
value,
onChange,
...props
}) => {
const handleChange = useCallback(() => {
onChange({ value });
}, [value, onChange]);
return (
<MenuItemBase
as='button'
{...props}
role='menuitemradio'
aria-checked={checked}
onClick={handleChange}
icon={checked ? CheckIcon : 'reserve-space'}
>
{children}
</MenuItemBase>
);
};
interface MenuItemCheckboxProps extends Omit<MenuItemRadioProps, 'onChange'> {
icon?: IconProp;
onChange: (change: { value: string; checked: boolean }) => void;
}
export const MenuItemCheckbox: React.FC<MenuItemCheckboxProps> = ({
children,
checked,
value,
onChange,
...props
}) => {
const handleChange = useCallback(() => {
onChange({ value, checked: !checked });
}, [onChange, value, checked]);
return (
<MenuItemBase
as='button'
{...props}
role='menuitemcheckbox'
aria-checked={checked}
onClick={handleChange}
trailingContent={
<Toggle aria-hidden='true' tabIndex={-1} size='sm' checked={checked} />
}
>
{children}
</MenuItemBase>
);
};

View File

@@ -1,3 +1,5 @@
import { useCallback, useState } from 'react';
import {
MoonIcon,
NumberCircleOneIcon,
@@ -9,9 +11,16 @@ import { action } from 'storybook/actions';
import { useToggle } from '@/mastodon/hooks/useToggle';
import { ToggleField } from '../form_fields/redesign';
import { Menu, MenuButton, MenuList, MenuItemBase, MenuItem } from '.';
import {
Menu,
MenuButton,
MenuList,
MenuItem,
MenuItemDivider,
MenuItemCheckbox,
MenuItemGroup,
MenuItemRadio,
} from '.';
import type { MenuCardProps } from './card';
import { MenuCard } from './card';
@@ -38,16 +47,10 @@ export const Simple: Story = {
render(args) {
return (
<MenuCard {...args}>
<MenuItem
leadingIcon={NumberCircleOneIcon}
onClick={handleMenuItemClick}
>
<MenuItem icon={NumberCircleOneIcon} onClick={handleMenuItemClick}>
First item
</MenuItem>
<MenuItem
leadingIcon={NumberCircleTwoIcon}
onClick={handleMenuItemClick}
>
<MenuItem icon={NumberCircleTwoIcon} onClick={handleMenuItemClick}>
Second item
</MenuItem>
</MenuCard>
@@ -63,16 +66,10 @@ export const Popover: Story = {
<MenuButton>Click to show dropdown</MenuButton>
<MenuList {...args}>
<MenuItem
leadingIcon={NumberCircleOneIcon}
onClick={handleMenuItemClick}
>
<MenuItem icon={NumberCircleOneIcon} onClick={handleMenuItemClick}>
First item
</MenuItem>
<MenuItem
leadingIcon={NumberCircleTwoIcon}
onClick={handleMenuItemClick}
>
<MenuItem icon={NumberCircleTwoIcon} onClick={handleMenuItemClick}>
Second item
</MenuItem>
</MenuList>
@@ -82,18 +79,53 @@ export const Popover: Story = {
},
};
export const Controls: Story = {
export const Complex: Story = {
render(args) {
const [sun, { onToggle }] = useToggle();
const [precip, setPrecip] = useState('none');
const handlePrecipChange = useCallback(({ value }: { value: string }) => {
setPrecip(value);
}, []);
return (
<MenuCard {...args}>
<MenuItem onClick={handleMenuItemClick}>First item</MenuItem>
<hr />
<MenuItemDivider />
<MenuItemBase leadingIcon={sun ? SunIcon : MoonIcon}>
<ToggleField label='Daytime toggle' size='sm' onChange={onToggle} />
</MenuItemBase>
<MenuItemCheckbox
icon={sun ? SunIcon : MoonIcon}
checked={sun}
onChange={onToggle}
value='daytime'
>
Daytime toggle
</MenuItemCheckbox>
<MenuItemDivider />
<MenuItemGroup label='Precipitation'>
<MenuItemRadio
value='none'
checked={precip === 'none'}
onChange={handlePrecipChange}
>
None
</MenuItemRadio>
<MenuItemRadio
value='rain'
checked={precip === 'rain'}
onChange={handlePrecipChange}
>
Rain
</MenuItemRadio>
<MenuItemRadio
value='snow'
checked={precip === 'snow'}
onChange={handlePrecipChange}
>
Snow
</MenuItemRadio>
</MenuItemGroup>
</MenuCard>
);
},

View File

@@ -1,6 +1,6 @@
@use '@/styles/mastodon/mixins';
.menuCard {
.card {
@include mixins.elevation-1;
display: flex;
@@ -14,20 +14,27 @@
&[data-elevation='2'] {
@include mixins.elevation-2;
}
> hr {
height: 1px;
background-color: var(--color-border-primary);
margin: var(--space-2xs) var(--space-sm);
border: none;
}
}
.popoverMenuCard {
.popoverCard {
width: 100%;
}
.menuItem {
.itemGroupLabel {
@include mixins.type-label-sm;
color: var(--color-text-tertiary);
padding: var(--space-xs) var(--space-sm) 0;
}
.itemDivider {
height: 1px;
background-color: var(--color-border-primary);
margin: var(--space-2xs) var(--space-sm);
border: none;
}
.item {
@include mixins.type-body-compact;
--cursor: pointer;
@@ -47,20 +54,20 @@
background: none;
border: none;
width: 100%;
}
label {
cursor: inherit;
font-weight: inherit;
font-size: var(--fs-sm);
text-align: start;
}
&:hover:not([aria-disabled='true']) {
background-color: var(--color-bg-highlight);
}
&:focus-visible {
outline: var(--outline-focus-default);
outline-offset: -2px;
}
&:active:not([aria-disabled='true']),
.menuItemActive {
.itemActive {
background-color: var(--color-bg-inverted);
color: var(--color-text-inverted);
@@ -70,22 +77,18 @@
}
&:has(:disabled),
[aria-disabled='true'] {
&[aria-disabled='true'] {
--cursor: not-allowed;
opacity: 0.5;
}
}
.menuItemIcon {
.itemIcon {
width: var(--space-lg);
height: var(--space-lg);
}
.menu > hr {
height: 1px;
background-color: var(--color-border-primary);
margin: var(--space-2xs) var(--space-sm);
border: none;
width: calc(100% - (2 * var(--space-sm)));
.itemTrailingContent {
margin-inline-start: auto;
}

View File

@@ -62,14 +62,6 @@
}
}
.dropdownItemControl {
@include mixins.type-body-compact;
color: inherit;
width: 100%;
user-select: none;
}
.editorWrapper {
--fade-size: var(--space-xl);
@@ -149,25 +141,6 @@
color: var(--color-text-error);
}
.visibilityFieldset {
display: block;
[data-label] {
@include mixins.type-label-sm;
color: var(--color-text-tertiary);
padding: var(--space-xs) var(--space-sm) 0;
}
[data-fields] {
display: block;
}
[role='status'] {
margin-top: 0;
}
}
.languageMenu {
padding: var(--space-xs) var(--space-sm);

View File

@@ -85,18 +85,14 @@ export const ComposeRedesignButton: React.FC = () => {
</MenuButton>
<MenuList maxWidth={180} placement='top-end'>
<MenuItem
name='post'
onClick={handleComposerOpen}
leadingIcon={NewspaperIcon}
>
<MenuItem name='post' onClick={handleComposerOpen} icon={NewspaperIcon}>
<FormattedMessage id='compose.new.post' defaultMessage='Post' />
</MenuItem>
<MenuItem
name='message'
onClick={handleComposerOpen}
leadingIcon={ChatCircleIcon}
icon={ChatCircleIcon}
>
<FormattedMessage
id='compose.new.message'

View File

@@ -12,7 +12,11 @@ import { openModal } from '@/mastodon/actions/modal';
import type { ApiAudioAttachmentJSON } from '@/mastodon/api_types/media_attachments';
import { Blurhash } from '@/mastodon/components/blurhash';
import { IconButton } from '@/mastodon/components/button/redesign';
import { MenuItem, MenuList } from '@/mastodon/components/menu';
import {
MenuItem,
MenuItemDivider,
MenuList,
} from '@/mastodon/components/menu';
import { useToggle } from '@/mastodon/hooks/useToggle';
import { useAppDispatch, useAppSelector } from '@/mastodon/store';
@@ -126,17 +130,17 @@ export const ComposeUpload: React.FC<{
<MenuItem onClick={handleRearrange}>
<FormattedMessage
id='compose.upload.menu.rearrange'
defaultMessage='Rearrange&hellip;'
defaultMessage='Rearrange'
/>
</MenuItem>
)}
<hr />
<MenuItemDivider />
<MenuItem
className={classes.mediaMenuDelete}
onClick={handleDelete}
leadingIcon={TrashIcon}
icon={TrashIcon}
>
<FormattedMessage
id='compose.upload.menu.delete'

View File

@@ -1,11 +1,10 @@
import type React from 'react';
import { useCallback, useRef } from 'react';
import { useCallback } from 'react';
import { FormattedMessage } from 'react-intl';
import {
ChatCircleIcon,
CheckIcon,
MagnifyingGlassIcon,
QuotesIcon,
} from '@phosphor-icons/react';
@@ -16,23 +15,19 @@ import {
} from '@/mastodon/actions/compose_typed';
import type { ApiQuotePolicy } from '@/mastodon/api_types/quotes';
import type { StatusVisibility } from '@/mastodon/api_types/statuses';
import { Fieldset } from '@/mastodon/components/form_fields';
import {
ToggleField,
RadioButtonField,
} from '@/mastodon/components/form_fields/redesign';
import type { IconProp } from '@/mastodon/components/icon';
import {
Menu,
MenuList,
MenuButton,
MenuItemBase,
MenuItemDivider,
MenuItemGroup,
MenuItem,
MenuItemRadio,
MenuItemCheckbox,
} from '@/mastodon/components/menu';
import { useAppDispatch, useAppSelector } from '@/mastodon/store';
import { selectComposeMentions, selectComposePrivacy } from './selectors';
import classes from './styles.module.scss';
export const ComposeVisibility: React.FC = () => {
const privacy = useAppSelector(selectComposePrivacy);
@@ -83,53 +78,47 @@ const ComposeVisibilityMenu: React.FC = () => {
const quotePolicy = currentQuotePolicy ?? defaultQuotePolicy;
const dispatch = useAppDispatch();
const handlePrivacyChange: React.ChangeEventHandler<HTMLInputElement> =
useCallback(
(event) => {
const { value } = event.target;
if (value === 'private' && privacy !== 'private') {
dispatch(changeComposeVisibility(value));
} else if (value === 'public' && privacy === 'private') {
dispatch(
changeComposeVisibility(
defaultPrivacy === 'unlisted' ? 'unlisted' : 'public',
),
);
} else if (value === 'unlisted' && privacy !== 'private') {
dispatch(
changeComposeVisibility(
privacy === 'public' ? 'unlisted' : 'public',
),
);
}
},
[defaultPrivacy, dispatch, privacy],
);
const handleQuotePolicyChange: React.ChangeEventHandler<HTMLInputElement> =
useCallback(
(event) => {
const { value, checked } = event.target;
let newQuotePolicy: ApiQuotePolicy = 'nobody';
switch (value) {
case 'public':
newQuotePolicy = 'public';
break;
case 'followers':
newQuotePolicy = 'followers';
break;
case 'others':
// If it's not checked, then it's nobody.
if (checked) {
// Only use the default if it's not nobody, as then it'll never be enabled.
newQuotePolicy =
defaultQuotePolicy !== 'nobody' ? defaultQuotePolicy : 'public';
}
break;
}
dispatch(setComposeQuotePolicy(newQuotePolicy));
},
[defaultQuotePolicy, dispatch],
);
const handlePrivacyChange = useCallback(
({ value }: { value: string }) => {
if (value === 'private' && privacy !== 'private') {
dispatch(changeComposeVisibility(value));
} else if (value === 'public' && privacy === 'private') {
dispatch(
changeComposeVisibility(
defaultPrivacy === 'unlisted' ? 'unlisted' : 'public',
),
);
} else if (value === 'unlisted' && privacy !== 'private') {
dispatch(
changeComposeVisibility(privacy === 'public' ? 'unlisted' : 'public'),
);
}
},
[defaultPrivacy, dispatch, privacy],
);
const handleQuotePolicyChange = useCallback(
({ value, checked }: { value: string; checked?: boolean }) => {
let newQuotePolicy: ApiQuotePolicy = 'nobody';
switch (value) {
case 'public':
newQuotePolicy = 'public';
break;
case 'followers':
newQuotePolicy = 'followers';
break;
case 'others':
// If it's not checked, then it's nobody.
if (checked) {
// Only use the default if it's not nobody, as then it'll never be enabled.
newQuotePolicy =
defaultQuotePolicy !== 'nobody' ? defaultQuotePolicy : 'public';
}
break;
}
dispatch(setComposeQuotePolicy(newQuotePolicy));
},
[defaultQuotePolicy, dispatch],
);
const handleSwitchToMessage: React.MouseEventHandler<HTMLButtonElement> =
useCallback(() => {
dispatch(changeComposeVisibility('direct'));
@@ -137,26 +126,24 @@ const ComposeVisibilityMenu: React.FC = () => {
return (
<MenuList placement='bottom-start' offset={4} maxWidth={280}>
<Fieldset
name='visibility'
legend={
<MenuItemGroup
label={
<FormattedMessage
id='compose.visibility.title'
defaultMessage='Visibility'
/>
}
className={classes.visibilityFieldset}
>
<DropdownRadioCheckField
<MenuItemRadio
name='visibility'
value='public'
checked={privacy === 'public' || privacy === 'unlisted'}
onChange={handlePrivacyChange}
>
<FormattedMessage id='privacy.public.short' defaultMessage='Public' />
</DropdownRadioCheckField>
</MenuItemRadio>
<DropdownRadioCheckField
<MenuItemRadio
name='visibility'
value='private'
checked={privacy === 'private'}
@@ -166,49 +153,47 @@ const ComposeVisibilityMenu: React.FC = () => {
id='privacy.private.short'
defaultMessage='Followers'
/>
</DropdownRadioCheckField>
</Fieldset>
</MenuItemRadio>
<hr />
<MenuItemDivider />
<DropdownToggleField
value='unlisted'
disabled={privacy === 'private'}
checked={privacy === 'public'}
onChange={handlePrivacyChange}
icon={MagnifyingGlassIcon}
>
<FormattedMessage
id='compose.discoverable'
defaultMessage='Discoverable in public feeds & search results'
/>
</DropdownToggleField>
<MenuItemCheckbox
value='unlisted'
disabled={privacy === 'private'}
checked={privacy === 'public'}
onChange={handlePrivacyChange}
icon={MagnifyingGlassIcon}
>
<FormattedMessage
id='compose.discoverable'
defaultMessage='Discoverable in public feeds & search results'
/>
</MenuItemCheckbox>
<DropdownToggleField
value='others'
disabled={privacy === 'private'}
checked={quotePolicy !== 'nobody' && privacy !== 'private'}
onChange={handleQuotePolicyChange}
icon={QuotesIcon}
>
<FormattedMessage
id='compose.quotable'
defaultMessage='Allow others to quote'
/>
</DropdownToggleField>
<MenuItemCheckbox
value='others'
disabled={privacy === 'private'}
checked={quotePolicy !== 'nobody' && privacy !== 'private'}
onChange={handleQuotePolicyChange}
icon={QuotesIcon}
>
<FormattedMessage
id='compose.quotable'
defaultMessage='Allow others to quote'
/>
</MenuItemCheckbox>
</MenuItemGroup>
{quotePolicy !== 'nobody' && privacy !== 'private' && (
<Fieldset
name='quote_policy'
legend={
<MenuItemGroup
label={
<FormattedMessage
id='compose.visibility.quote_policy'
defaultMessage='Who can quote'
/>
}
className={classes.visibilityFieldset}
>
<DropdownRadioCheckField
<MenuItemRadio
name='quote_policy'
value='public'
checked={quotePolicy === 'public'}
@@ -218,9 +203,9 @@ const ComposeVisibilityMenu: React.FC = () => {
id='compose.visibility.quote_policy.anyone'
defaultMessage='Anyone'
/>
</DropdownRadioCheckField>
</MenuItemRadio>
<DropdownRadioCheckField
<MenuItemRadio
name='quote_policy'
value='followers'
checked={quotePolicy === 'followers'}
@@ -230,13 +215,13 @@ const ComposeVisibilityMenu: React.FC = () => {
id='compose.visibility.quote_policy.followers'
defaultMessage='Followers'
/>
</DropdownRadioCheckField>
</Fieldset>
</MenuItemRadio>
</MenuItemGroup>
)}
<hr />
<MenuItemDivider />
<MenuItem leadingIcon={ChatCircleIcon} onClick={handleSwitchToMessage}>
<MenuItem icon={ChatCircleIcon} onClick={handleSwitchToMessage}>
<FormattedMessage
id='compose.post.to_message'
defaultMessage='Compose a message instead'
@@ -246,68 +231,3 @@ const ComposeVisibilityMenu: React.FC = () => {
</MenuList>
);
};
const DropdownRadioCheckField: React.FC<
Omit<
React.ComponentProps<typeof RadioButtonField>,
'label' | 'icon' | 'iconClassName'
> & {
children: React.ReactNode;
}
> = ({ children, onClick, disabled, ...props }) => {
const { ref, onWrapperClick } = useDropdownControl();
return (
<MenuItemBase onClick={onWrapperClick} disabled={disabled}>
<RadioButtonField
{...props}
ref={ref}
label={children}
icon={CheckIcon}
disabled={disabled}
wrapperClassName={classes.dropdownItemControl}
/>
</MenuItemBase>
);
};
const DropdownToggleField: React.FC<
Omit<React.ComponentProps<typeof ToggleField>, 'label'> & {
children: React.ReactNode;
icon?: IconProp;
}
> = ({ children, icon, disabled, ...props }) => {
const { ref, onWrapperClick } = useDropdownControl();
return (
<MenuItemBase
onClick={onWrapperClick}
leadingIcon={icon}
disabled={disabled}
>
<ToggleField
size='sm'
{...props}
disabled={disabled}
ref={ref}
label={children}
wrapperClassName={classes.dropdownItemControl}
/>
</MenuItemBase>
);
};
function useDropdownControl() {
const ref = useRef<HTMLInputElement | null>(null);
const onWrapperClick: React.MouseEventHandler = useCallback((event) => {
const { target } = event;
if (
target instanceof HTMLLabelElement ||
target instanceof HTMLInputElement
) {
return;
}
ref.current?.click();
}, []);
return { ref, onWrapperClick };
}