mirror of
https://github.com/mastodon/mastodon.git
synced 2026-09-13 02:36:36 -05:00
Redesign dropdown (#39991)
This commit is contained in:
@@ -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)));
|
||||
}
|
||||
123
app/javascript/mastodon/components/dropdown/redesign.stories.tsx
Normal file
123
app/javascript/mastodon/components/dropdown/redesign.stories.tsx
Normal 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>
|
||||
);
|
||||
},
|
||||
};
|
||||
125
app/javascript/mastodon/components/dropdown/redesign.tsx
Normal file
125
app/javascript/mastodon/components/dropdown/redesign.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user