Redesign dropdown (#39991)

This commit is contained in:
Echo
2026-07-30 12:15:10 +02:00
committed by GitHub
parent 6e40737d7a
commit c47bf15bd9
5 changed files with 339 additions and 0 deletions

View File

@@ -0,0 +1,79 @@
@use '@/styles/mastodon/mixins';
.menu {
@include mixins.elevation-1;
display: flex;
flex-direction: column;
align-items: flex-start;
padding: var(--space-2xs) 0;
border-radius: var(--radius-md);
background: var(--color-bg-primary);
overflow: hidden;
&[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;
}
}
.menuItem {
@include mixins.type-body-compact;
--cursor: pointer;
display: flex;
align-items: center;
gap: var(--space-xs);
padding: var(--space-xs) var(--space-sm);
cursor: var(--cursor);
transition:
background 200ms,
color 200ms;
label {
cursor: inherit;
font-weight: inherit;
}
&:hover {
background-color: color-mix(
in srgb,
var(--color-bg-primary) 90%,
var(--color-bg-inverted)
);
}
&:active,
.menuItemActive {
background-color: var(--color-bg-inverted);
color: var(--color-text-inverted);
}
&:has(:disabled) {
--cursor: not-allowed;
background-color: var(--color-bg-primary);
}
}
.menuItemButton {
appearance: none;
background: none;
border: none;
width: 100%;
}
.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)));
}

View File

@@ -0,0 +1,123 @@
import { useState } from 'react';
import {
MoonIcon,
NumberCircleOneIcon,
NumberCircleTwoIcon,
SunIcon,
} from '@phosphor-icons/react';
import type { Meta, StoryObj } from '@storybook/react-vite';
import { action } from 'storybook/actions';
import { useToggle } from '@/mastodon/hooks/useToggle';
import { Button } from '../button/redesign';
import { ToggleField } from '../form_fields/redesign';
import { Icon } from '../icon';
import type { DropdownProps } from './redesign';
import {
Dropdown,
DropdownItem,
DropdownItemButton,
DropdownPopover,
} from './redesign';
const meta = {
title: 'Redesign/Dropdown',
args: {
elevation: 1,
},
argTypes: {
elevation: {
control: 'inline-radio',
options: [1, 2],
},
},
} satisfies Meta<Omit<DropdownProps<'div'>, 'children'>>;
export default meta;
type Story = StoryObj<typeof meta>;
const handleMenuItemClick = action('menu item click');
export const Simple: Story = {
render(args) {
return (
<Dropdown {...args}>
<DropdownItemButton
icon={NumberCircleOneIcon}
onClick={handleMenuItemClick}
>
First item
</DropdownItemButton>
<DropdownItemButton
icon={NumberCircleTwoIcon}
onClick={handleMenuItemClick}
>
Second item
</DropdownItemButton>
</Dropdown>
);
},
};
export const Popover: Story = {
render(args) {
const [ref, setRef] = useState<HTMLButtonElement | null>(null);
const [open, { onToggle, onFalse }] = useToggle();
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
<Button ref={setRef} onClick={onToggle}>
Click to show dropdown
</Button>
<DropdownPopover
reference={ref}
isOpen={open}
onClose={onFalse}
{...args}
>
<DropdownItemButton
icon={NumberCircleOneIcon}
onClick={handleMenuItemClick}
>
First item
</DropdownItemButton>
<DropdownItemButton
icon={NumberCircleTwoIcon}
onClick={handleMenuItemClick}
>
Second item
</DropdownItemButton>
</DropdownPopover>
</div>
);
},
};
export const Controls: Story = {
render(args) {
const [sun, { onToggle }] = useToggle();
return (
<Dropdown {...args}>
<DropdownItemButton onClick={handleMenuItemClick}>
First item
</DropdownItemButton>
<hr />
<DropdownItem>
{sun ? (
<Icon id='sun' icon={SunIcon} />
) : (
<Icon id='moon' icon={MoonIcon} />
)}
<ToggleField label='Daytime toggle' size='sm' onChange={onToggle} />
</DropdownItem>
</Dropdown>
);
},
};

View File

@@ -0,0 +1,125 @@
import type React from 'react';
import classNames from 'classnames';
import type { Merge } from 'type-fest';
import { Icon } from '../icon';
import type { IconProp } from '../icon';
import type { PopoverProps } from '../popover';
import { Popover } from '../popover';
import classes from './redesign.module.scss';
export const menuItemClass = classes.menuItem;
export type DropdownProps<As extends React.ElementType> = Merge<
{
as?: As;
children: React.ReactNode;
className?: string;
elevation?: 1 | 2;
},
React.ComponentProps<As>
>;
export const DropdownPopover = <As extends React.ElementType = 'div'>({
isOpen,
onClose,
reference,
popoverElement,
container,
placement,
offset = 4,
flip,
strategy,
matchReferenceWidth,
closeOnClickOutside,
children,
...props
}: DropdownProps<As> & Omit<PopoverProps, 'children'>) => {
const popoverProps = {
isOpen,
onClose,
reference,
popoverElement,
container,
placement,
offset,
flip,
strategy,
matchReferenceWidth,
closeOnClickOutside,
};
return (
<Popover {...popoverProps}>
{({ props: popoverChildProps }) => (
<Dropdown {...props} {...popoverChildProps}>
{children}
</Dropdown>
)}
</Popover>
);
};
export const Dropdown = <As extends React.ElementType = 'div'>({
as: asComp,
children,
className,
elevation = 1,
...props
}: DropdownProps<As>) => {
const Component = asComp ?? 'div';
return (
<Component
{...props}
className={classNames(className, classes.menu)}
data-elevation={elevation}
>
{children}
</Component>
);
};
type DropdownItemProps<As extends React.ElementType> = Merge<
{ as?: As; children: React.ReactNode; className?: string; active?: boolean },
React.ComponentPropsWithoutRef<As>
>;
export const DropdownItem = <As extends React.ElementType = 'div'>({
active,
as: AsComp,
children,
className,
...props
}: DropdownItemProps<As>) => {
const Component = AsComp ?? 'div';
return (
<Component
{...props}
className={classNames(
className,
classes.menuItem,
active && classes.menuItemActive,
)}
>
{children}
</Component>
);
};
export const DropdownItemButton: React.FC<
{ icon?: IconProp } & React.ComponentProps<'button'>
> = ({ icon, children, className, ...props }) => {
return (
<DropdownItem
type='button'
{...props}
as='button'
className={classNames(className, classes.menuItemButton)}
>
{icon && <Icon id='menu' icon={icon} />}
{children}
</DropdownItem>
);
};