Redesign: Modals (#40191)

This commit is contained in:
Echo
2026-08-19 12:33:42 +00:00
committed by GitHub
parent c2d8dbbc1b
commit cb82dfddff
4 changed files with 212 additions and 4 deletions

View File

@@ -0,0 +1,38 @@
@use '@/styles/mastodon/mixins';
.modal {
@include mixins.elevation-1;
border-radius: var(--radius-xl);
background: var(--color-bg-primary);
display: flex;
flex-direction: column;
gap: var(--space-md);
padding: var(--space-md);
pointer-events: auto;
user-select: text;
overflow: hidden;
}
.modalElevated {
@include mixins.elevation-2;
}
.title {
@include mixins.type-heading-sm;
}
.actions {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-md);
}
.actionsLeft {
justify-content: flex-start;
}
.actionsRight {
justify-content: flex-end;
}

View File

@@ -0,0 +1,57 @@
import type { Meta, StoryObj } from '@storybook/react-vite';
import { Button } from '../button/redesign';
import type { ModalShellProps } from './redesign';
import { ModalShell, ModalActions, ModalTitle } from './redesign';
const meta = {
title: 'Redesign/Modal',
args: {
actionAlign: 'stretch',
children: 'This is a modal.',
elevation: 2,
maxWidth: 0,
},
argTypes: {
actionAlign: {
control: 'inline-radio',
options: ['left', 'right', 'stretch'],
},
elevation: {
control: 'inline-radio',
options: [1, 2],
},
},
render({ actionAlign, children, ...props }) {
return (
<ModalShell {...props}>
<ModalTitle>Modal Example</ModalTitle>
{children
.split('\n')
.filter((line) => !!line.trim())
.map((line, index) => (
<p key={index}>{line}</p>
))}
<ModalActions align={actionAlign}>
<Button>Cancel</Button>
<Button color='neutral'>Save</Button>
</ModalActions>
</ModalShell>
);
},
} satisfies Meta<
Pick<ModalShellProps, 'elevation' | 'maxWidth'> & {
children: string;
actionAlign?: 'left' | 'right' | 'stretch';
}
>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {};

View File

@@ -0,0 +1,101 @@
import type React from 'react';
import classNames from 'classnames';
import type { PolymorphicProps } from '@/types/polymorphic';
import { NavigationFocusTarget } from '../navigation_focus_target';
import classes from './redesign.module.scss';
export interface ModalShellProps {
children: React.ReactNode;
className?: string;
elevation?: 1 | 2;
maxWidth?: number;
style?: React.CSSProperties;
}
export const ModalShell = <As extends React.ElementType>({
as: asComp,
elevation,
className,
children,
maxWidth,
style,
...props
}: PolymorphicProps<ModalShellProps, As>) => {
const Comp = asComp ?? 'div';
return (
<Comp
{...props}
className={classNames(
className,
classes.modal,
elevation === 2 && classes.modalElevated,
)}
style={{
maxWidth: maxWidth ? `${maxWidth}px` : undefined,
...style,
}}
>
{children}
</Comp>
);
};
type HeadingLevels = 1 | 2 | 3 | 4 | 5 | 6;
type ModalTitleProps = { children: React.ReactNode; level?: HeadingLevels } & (
| { noFocus: true }
| { noFocus?: false; focusTargetName?: string }
);
export const ModalTitle: React.FC<
ModalTitleProps & React.ComponentPropsWithRef<`h${HeadingLevels}`>
> = ({ level = 1, children, className, noFocus, ...props }) => {
const Header = `h${level}` as const;
if (!noFocus) {
return (
<NavigationFocusTarget as={Header} {...props}>
{children}
</NavigationFocusTarget>
);
}
return (
<Header {...props} className={classNames(className, classes.title)}>
{children}
</Header>
);
};
export const ModalActions = <As extends React.ElementType>({
align,
as: asComp,
children,
className,
...props
}: PolymorphicProps<
{
align?: 'left' | 'right' | 'stretch';
children?: React.ReactNode;
className?: string;
},
As
>) => {
const Comp = asComp ?? 'div';
return (
<Comp
{...props}
className={classNames(
className,
classes.actions,
align === 'left' && classes.actionsLeft,
align === 'right' && classes.actionsRight,
)}
>
{children}
</Comp>
);
};

View File

@@ -38,7 +38,7 @@ type ElementRef<As extends ElementType> =
* Merge additional props with intrinsic/element props for `as`.
* Additional props win on conflicts.
*/
type PolymorphicProps<
type OldPolymorphicProps<
As extends ElementType,
AdditionalProps extends object = object,
> = AdditionalProps &
@@ -52,7 +52,9 @@ type PolymorphicWithRef<
DefaultAs extends ElementType,
AdditionalProps extends object = object,
> = <As extends ElementType = DefaultAs>(
props: PolymorphicProps<As, AdditionalProps> & { ref?: Ref<ElementRef<As>> },
props: OldPolymorphicProps<As, AdditionalProps> & {
ref?: Ref<ElementRef<As>>;
},
) => ReactElement | null;
/**
@@ -64,12 +66,22 @@ type PolyRefFunction = <
>(
render: ForwardRefRenderFunction<
ElementRef<DefaultAs>,
PolymorphicProps<DefaultAs, AdditionalProps>
OldPolymorphicProps<DefaultAs, AdditionalProps>
>,
) => PolymorphicWithRef<DefaultAs, AdditionalProps> &
ForwardRefExoticComponent<PolymorphicProps<DefaultAs, AdditionalProps>>;
ForwardRefExoticComponent<OldPolymorphicProps<DefaultAs, AdditionalProps>>;
/**
* Polymorphic `forwardRef` function.
*/
export const polymorphicForwardRef = forwardRef as PolyRefFunction;
// Use this for modern React 19 props
export type PolymorphicProps<
AdditionalProps extends object,
As extends React.ElementType,
> = {
as?: As;
} & Omit<React.ComponentPropsWithoutRef<As>, keyof AdditionalProps | 'as'> &
AdditionalProps;