mirror of
https://github.com/mastodon/mastodon.git
synced 2026-09-13 01:26:14 -05:00
Redesign: Modals (#40191)
This commit is contained in:
@@ -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;
|
||||
}
|
||||
@@ -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 = {};
|
||||
101
app/javascript/mastodon/components/modal_shell/redesign.tsx
Normal file
101
app/javascript/mastodon/components/modal_shell/redesign.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user