Redesign: Menu and modal fixes (#40220)

This commit is contained in:
Echo
2026-08-20 12:04:30 +00:00
committed by GitHub
parent d3585fff02
commit ceb9be7dc5
7 changed files with 127 additions and 132 deletions

View File

@@ -57,7 +57,11 @@ export const ModalTitle: React.FC<
const Header = `h${level}` as const;
if (!noFocus) {
return (
<NavigationFocusTarget as={Header} {...props}>
<NavigationFocusTarget
as={Header}
{...props}
className={classNames(className, classes.title)}
>
{children}
</NavigationFocusTarget>
);

View File

@@ -59,12 +59,13 @@ export const ComposeHints = () => {
defaultMessage="You're replying to a followers-only post. People not following {user} might see your reply without the context of what youre replying to."
values={{ user: `@${replyFollowersHandle}` }}
/>,
'followers-reply',
),
);
}
if (isDifferentLanguage) {
messages.push(<LanguageHint guess={guess} />);
messages.push(<LanguageHint guess={guess} key='different-language' />);
}
if (missingAlt && count === 1) {
@@ -74,6 +75,7 @@ export const ComposeHints = () => {
id='compose.hints.missing-alt'
defaultMessage='Your attachment is missing alt text.'
/>,
'missing-alt',
),
);
} else if (missingAlt) {
@@ -83,6 +85,7 @@ export const ComposeHints = () => {
id='compose.hints.missing-alt-many'
defaultMessage='One or more of your attachments are missing alt text.'
/>,
'missing-alts',
),
);
}
@@ -94,8 +97,10 @@ export const ComposeHints = () => {
return <div>{messages}</div>;
};
const defaultWrapper = (children: React.ReactNode) => (
<Callout icon={WarningIcon}>{children}</Callout>
const defaultWrapper = (children: React.ReactNode, key: string) => (
<Callout icon={WarningIcon} key={key}>
{children}
</Callout>
);
const LanguageHint: React.FC<{ guess: string }> = ({ guess }) => {

View File

@@ -4,6 +4,11 @@ import { FormattedMessage } from 'react-intl';
import { closeModal } from '@/mastodon/actions/modal';
import { Button } from '@/mastodon/components/button/redesign';
import {
ModalActions,
ModalShell,
ModalTitle,
} from '@/mastodon/components/modal_shell/redesign';
import {
focusComposerTextarea,
openNewComposer,
@@ -11,8 +16,6 @@ import {
} from '@/mastodon/reducers/slices/composer';
import { useAppDispatch } from '@/mastodon/store';
import classes from './modals.module.scss';
const ComposerModalCancelConfirm: React.FC<{ openNew?: boolean }> = ({
openNew,
}) => {
@@ -37,20 +40,20 @@ const ComposerModalCancelConfirm: React.FC<{ openNew?: boolean }> = ({
}, [dispatch]);
return (
<div className={classes.root}>
<h2 className={classes.title}>
<ModalShell>
<ModalTitle>
<FormattedMessage
id='compose.cancel_modal.title'
defaultMessage='Discard draft'
/>
</h2>
</ModalTitle>
<FormattedMessage
id='compose.cancel_modal.body'
defaultMessage='You have a draft already in progress. What would you like to do?'
/>
<div className={classes.footer}>
<ModalActions>
<Button color='destructive' onClick={handleDelete}>
<FormattedMessage
id='compose.cancel_modal.delete'
@@ -63,8 +66,8 @@ const ComposerModalCancelConfirm: React.FC<{ openNew?: boolean }> = ({
defaultMessage='Continue draft'
/>
</Button>
</div>
</div>
</ModalActions>
</ModalShell>
);
};

View File

@@ -10,6 +10,11 @@ import { DotsSixVerticalIcon } from '@phosphor-icons/react';
import { rearrangeComposeAttachments } from '@/mastodon/actions/compose_typed';
import { Button, IconButton } from '@/mastodon/components/button/redesign';
import {
ModalActions,
ModalShell,
ModalTitle,
} from '@/mastodon/components/modal_shell/redesign';
import {
SortableList,
SortableListItem,
@@ -28,26 +33,26 @@ const messages = defineMessages({
},
onDragStart: {
id: 'compose.rearrange_modal.drag_start',
defaultMessage: 'Picked up attachment at index {index, number}.',
defaultMessage: 'Picked up attachment at index {item, number}.',
},
onDragMove: {
id: 'compose.rearrange_modal.drag_move',
defaultMessage: 'Attachment index {index, number} was moved.',
defaultMessage: 'Attachment index {item, number} was moved.',
},
onDragMoveOver: {
id: 'compose.rearrange_modal.drag_over',
defaultMessage:
'Attachment index {index, number} was moved over index {over, number}.',
'Attachment index {item, number} was moved over index {over, number}.',
},
onDragEnd: {
id: 'compose.rearrange_modal.drag_end',
defaultMessage:
'Attachment index {index, number} was moved to index {newIndex, number}.',
'Attachment index {item, number} was moved to index {over, number}.',
},
onDragCancel: {
id: 'compose.rearrange_modal.drag_cancel',
defaultMessage:
'Dragging was cancelled. Attachment index {index, number} was dropped.',
'Dragging was cancelled. Attachment index {item, number} was dropped.',
},
});
@@ -80,16 +85,13 @@ const ComposerModalRearrange: React.FC<{ onClose: () => void }> = ({
);
return (
<div
className={classNames(classes.root, classes.attachmentRoot)}
onKeyUpCapture={onModalExit}
>
<h2 className={classes.title}>
<ModalShell className={classes.attachmentRoot} onKeyUpCapture={onModalExit}>
<ModalTitle className={classes.title}>
<FormattedMessage
id='compose.rearrange_modal.title'
defaultMessage='Rearrange media'
/>
</h2>
</ModalTitle>
<SortableList
ids={attachmentIds}
@@ -117,7 +119,7 @@ const ComposerModalRearrange: React.FC<{ onClose: () => void }> = ({
))}
</SortableList>
<div className={classes.footer}>
<ModalActions className={classes.footer}>
<Button onClick={onClose}>
<FormattedMessage
id='compose.rearrange_modal.cancel'
@@ -131,8 +133,8 @@ const ComposerModalRearrange: React.FC<{ onClose: () => void }> = ({
defaultMessage='Save'
/>
</Button>
</div>
</div>
</ModalActions>
</ModalShell>
);
};

View File

@@ -1,29 +1,5 @@
@use '@/styles/mastodon/mixins';
.root {
@include mixins.elevation-2;
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;
max-width: 100vw;
}
.title {
@include mixins.type-heading-sm;
}
.footer {
display: flex;
align-items: center;
justify-content: space-between;
}
// Attachment rearrange modal
.attachmentRoot {

View File

@@ -1,5 +1,5 @@
import type React from 'react';
import { useCallback, useState } from 'react';
import { useCallback } from 'react';
import { FormattedMessage } from 'react-intl';
@@ -13,11 +13,13 @@ import type { ApiAudioAttachmentJSON } from '@/mastodon/api_types/media_attachme
import { Blurhash } from '@/mastodon/components/blurhash';
import { IconButton } from '@/mastodon/components/button/redesign';
import {
Menu,
MenuButton,
MenuItem,
MenuItemDivider,
MenuList,
useMenuContext,
} from '@/mastodon/components/menu';
import { useToggle } from '@/mastodon/hooks/useToggle';
import { useAppDispatch, useAppSelector } from '@/mastodon/store';
import classes from './attachments.module.scss';
@@ -33,26 +35,6 @@ export const ComposeUpload: React.FC<{
selectComposeAttachment(state, id),
);
const sensitive = useAppSelector((state) => !!state.compose.get('spoiler'));
const [open, { onToggle, onFalse }] = useToggle();
const [target, setTarget] = useState<HTMLButtonElement | null>(null);
const dispatch = useAppDispatch();
const handleEdit = useCallback(() => {
if (id) {
dispatch(
openModal({ modalType: 'FOCAL_POINT', modalProps: { mediaId: id } }),
);
}
}, [dispatch, id]);
const handleRearrange = useCallback(() => {
onFalse();
dispatch(openModal({ modalType: 'COMPOSER_REARRANGE', modalProps: {} }));
}, [dispatch, onFalse]);
const handleDelete = useCallback(() => {
if (id) {
dispatch(undoUploadCompose(id));
}
}, [dispatch, id]);
if (!attachment || attachment.type === 'unknown') {
return <div className={classNames(classes.mediaUpload, className)} />;
@@ -90,64 +72,22 @@ export const ComposeUpload: React.FC<{
<Blurhash hash={attachment.blurhash} className={classes.blurHash} />
)}
<IconButton
icon={DotsThreeIcon}
size='sm'
color='neutral'
className={classes.mediaMenuButton}
onClick={onToggle}
ref={setTarget}
>
<FormattedMessage
id='compose.upload.menu'
defaultMessage='Add alt text or remove the image'
/>
</IconButton>
<MenuList
isOpen={open}
onClose={onFalse}
reference={target}
placement='bottom-end'
offset={4}
maxWidth={170}
>
<MenuItem onClick={handleEdit}>
{attachment.description ? (
<FormattedMessage
id='compose.upload.menu.edit_alt'
defaultMessage='Edit alt text'
/>
) : (
<FormattedMessage
id='compose.upload.menu.add_alt'
defaultMessage='Add alt text'
/>
)}
</MenuItem>
{!single && (
<MenuItem onClick={handleRearrange}>
<FormattedMessage
id='compose.upload.menu.rearrange'
defaultMessage='Rearrange…'
/>
</MenuItem>
)}
<MenuItemDivider />
<MenuItem
className={classes.mediaMenuDelete}
onClick={handleDelete}
icon={TrashIcon}
<Menu>
<MenuButton
as={IconButton}
icon={DotsThreeIcon}
size='sm'
color='neutral'
className={classes.mediaMenuButton}
>
<FormattedMessage
id='compose.upload.menu.delete'
defaultMessage='Remove image'
id='compose.upload.menu'
defaultMessage='Add alt text or remove the image'
/>
</MenuItem>
</MenuList>
</MenuButton>
<ComposeUploadMenu attachment={attachment} single={single} />
</Menu>
{attachment.description && (
<span className={classes.mediaAlt}>
@@ -158,6 +98,71 @@ export const ComposeUpload: React.FC<{
);
};
const ComposeUploadMenu: React.FC<{
attachment: ComposeAttachment;
single?: boolean;
}> = ({ attachment, single }) => {
const dispatch = useAppDispatch();
const id = attachment.id;
const { popover } = useMenuContext();
const handleEdit = useCallback(() => {
popover.closeMenu();
dispatch(
openModal({ modalType: 'FOCAL_POINT', modalProps: { mediaId: id } }),
);
}, [dispatch, id, popover]);
const handleRearrange = useCallback(() => {
popover.closeMenu();
dispatch(openModal({ modalType: 'COMPOSER_REARRANGE', modalProps: {} }));
}, [dispatch, popover]);
const handleDelete = useCallback(() => {
popover.closeMenu();
dispatch(undoUploadCompose(id));
}, [dispatch, id, popover]);
return (
<MenuList placement='bottom-end' offset={4} maxWidth={170}>
<MenuItem onClick={handleEdit}>
{attachment.description ? (
<FormattedMessage
id='compose.upload.menu.edit_alt'
defaultMessage='Edit alt text'
/>
) : (
<FormattedMessage
id='compose.upload.menu.add_alt'
defaultMessage='Add alt text'
/>
)}
</MenuItem>
{!single && (
<MenuItem onClick={handleRearrange}>
<FormattedMessage
id='compose.upload.menu.rearrange'
defaultMessage='Rearrange…'
/>
</MenuItem>
)}
<MenuItemDivider />
<MenuItem
className={classes.mediaMenuDelete}
onClick={handleDelete}
icon={TrashIcon}
>
<FormattedMessage
id='compose.upload.menu.delete'
defaultMessage='Remove image'
/>
</MenuItem>
</MenuList>
);
};
const ComposeAudioUpload: React.FC<{
attachment: ComposeAttachment<ApiAudioAttachmentJSON>;
}> = ({ attachment }) => {

View File

@@ -524,12 +524,12 @@
"compose.published.open": "Open",
"compose.quotable": "Allow others to quote",
"compose.rearrange_modal.cancel": "Cancel",
"compose.rearrange_modal.drag_cancel": "Dragging was cancelled. Attachment index {index, number} was dropped.",
"compose.rearrange_modal.drag_end": "Attachment index {index, number} was moved to index {newIndex, number}.",
"compose.rearrange_modal.drag_cancel": "Dragging was cancelled. Attachment index {item, number} was dropped.",
"compose.rearrange_modal.drag_end": "Attachment index {item, number} was moved to index {over, number}.",
"compose.rearrange_modal.drag_instructions": "To rearrange attachments, press space or enter. While dragging, use the arrow keys to move the attachment up or down. Press space or enter again to drop the attachment in its new position, or press escape to cancel.",
"compose.rearrange_modal.drag_move": "Attachment index {index, number} was moved.",
"compose.rearrange_modal.drag_over": "Attachment index {index, number} was moved over index {over, number}.",
"compose.rearrange_modal.drag_start": "Picked up attachment at index {index, number}.",
"compose.rearrange_modal.drag_move": "Attachment index {item, number} was moved.",
"compose.rearrange_modal.drag_over": "Attachment index {item, number} was moved over index {over, number}.",
"compose.rearrange_modal.drag_start": "Picked up attachment at index {item, number}.",
"compose.rearrange_modal.handle": "Drag attachment at position {index, number}",
"compose.rearrange_modal.save": "Save",
"compose.rearrange_modal.title": "Rearrange media",