Refactor <ActionsModal> to TypeScript (#34559)

This commit is contained in:
Eugen Rochko 2025-04-28 13:43:42 +02:00 committed by GitHub
parent 17e4345eb2
commit 926c67c648
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 127 additions and 135 deletions

View File

@ -26,11 +26,12 @@ import {
import { openModal, closeModal } from 'mastodon/actions/modal'; import { openModal, closeModal } from 'mastodon/actions/modal';
import { CircularProgress } from 'mastodon/components/circular_progress'; import { CircularProgress } from 'mastodon/components/circular_progress';
import { isUserTouching } from 'mastodon/is_mobile'; import { isUserTouching } from 'mastodon/is_mobile';
import type { import {
MenuItem, isMenuItem,
ActionMenuItem, isActionItem,
ExternalLinkMenuItem, isExternalLinkItem,
} from 'mastodon/models/dropdown_menu'; } from 'mastodon/models/dropdown_menu';
import type { MenuItem } from 'mastodon/models/dropdown_menu';
import { useAppDispatch, useAppSelector } from 'mastodon/store'; import { useAppDispatch, useAppSelector } from 'mastodon/store';
import type { IconProp } from './icon'; import type { IconProp } from './icon';
@ -38,30 +39,6 @@ import { IconButton } from './icon_button';
let id = 0; let id = 0;
const isMenuItem = (item: unknown): item is MenuItem => {
if (item === null) {
return true;
}
return typeof item === 'object' && 'text' in item;
};
const isActionItem = (item: unknown): item is ActionMenuItem => {
if (!item || !isMenuItem(item)) {
return false;
}
return 'action' in item;
};
const isExternalLinkItem = (item: unknown): item is ExternalLinkMenuItem => {
if (!item || !isMenuItem(item)) {
return false;
}
return 'href' in item;
};
type RenderItemFn<Item = MenuItem> = ( type RenderItemFn<Item = MenuItem> = (
item: Item, item: Item,
index: number, index: number,
@ -354,6 +331,9 @@ export const Dropdown = <Item = MenuItem,>({
const open = currentId === openDropdownId; const open = currentId === openDropdownId;
const activeElement = useRef<HTMLElement | null>(null); const activeElement = useRef<HTMLElement | null>(null);
const targetRef = useRef<HTMLButtonElement | null>(null); const targetRef = useRef<HTMLButtonElement | null>(null);
const prefetchAccountId = status
? status.getIn(['account', 'id'])
: undefined;
const handleClose = useCallback(() => { const handleClose = useCallback(() => {
if (activeElement.current) { if (activeElement.current) {
@ -402,8 +382,8 @@ export const Dropdown = <Item = MenuItem,>({
} else { } else {
onOpen?.(); onOpen?.();
if (status) { if (prefetchAccountId) {
dispatch(fetchRelationships([status.getIn(['account', 'id'])])); dispatch(fetchRelationships([prefetchAccountId]));
} }
if (isUserTouching()) { if (isUserTouching()) {
@ -411,7 +391,6 @@ export const Dropdown = <Item = MenuItem,>({
openModal({ openModal({
modalType: 'ACTIONS', modalType: 'ACTIONS',
modalProps: { modalProps: {
status,
actions: items, actions: items,
onClick: handleItemClick, onClick: handleItemClick,
}, },
@ -431,11 +410,11 @@ export const Dropdown = <Item = MenuItem,>({
[ [
dispatch, dispatch,
currentId, currentId,
prefetchAccountId,
scrollKey, scrollKey,
onOpen, onOpen,
handleItemClick, handleItemClick,
open, open,
status,
items, items,
handleClose, handleClose,
], ],

View File

@ -30,9 +30,6 @@ const messages = defineMessages({
class PrivacyDropdown extends PureComponent { class PrivacyDropdown extends PureComponent {
static propTypes = { static propTypes = {
isUserTouching: PropTypes.func,
onModalOpen: PropTypes.func,
onModalClose: PropTypes.func,
value: PropTypes.string.isRequired, value: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired,
noDirect: PropTypes.bool, noDirect: PropTypes.bool,

View File

@ -15,16 +15,6 @@ const mapDispatchToProps = dispatch => ({
dispatch(changeComposeVisibility(value)); dispatch(changeComposeVisibility(value));
}, },
isUserTouching,
onModalOpen: props => dispatch(openModal({
modalType: 'ACTIONS',
modalProps: props,
})),
onModalClose: () => dispatch(closeModal({
modalType: undefined,
ignoreFocus: false,
})),
}); });
export default connect(mapStateToProps, mapDispatchToProps)(PrivacyDropdown); export default connect(mapStateToProps, mapDispatchToProps)(PrivacyDropdown);

View File

@ -1,48 +0,0 @@
import PropTypes from 'prop-types';
import classNames from 'classnames';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { IconButton } from '../../../components/icon_button';
export default class ActionsModal extends ImmutablePureComponent {
static propTypes = {
status: ImmutablePropTypes.map,
actions: PropTypes.array,
onClick: PropTypes.func,
};
renderAction = (action, i) => {
if (action === null) {
return <li key={`sep-${i}`} className='dropdown-menu__separator' />;
}
const { icon = null, iconComponent = null, text, meta = null, active = false, href = '#' } = action;
return (
<li key={`${text}-${i}`}>
<a href={href} target='_blank' rel='noopener' onClick={this.props.onClick} data-index={i} className={classNames({ active })}>
{icon && <IconButton title={text} icon={icon} iconComponent={iconComponent} role='presentation' tabIndex={-1} inverted />}
<div>
<div className={classNames({ 'actions-modal__item-label': !!meta })}>{text}</div>
<div>{meta}</div>
</div>
</a>
</li>
);
};
render () {
return (
<div className='modal-root__modal actions-modal'>
<ul className={classNames({ 'with-status': !!status })}>
{this.props.actions.map(this.renderAction)}
</ul>
</div>
);
}
}

View File

@ -0,0 +1,65 @@
import classNames from 'classnames';
import { Link } from 'react-router-dom';
import type { MenuItem } from 'mastodon/models/dropdown_menu';
import {
isActionItem,
isExternalLinkItem,
} from 'mastodon/models/dropdown_menu';
export const ActionsModal: React.FC<{
actions: MenuItem[];
onClick: React.MouseEventHandler;
}> = ({ actions, onClick }) => (
<div className='modal-root__modal actions-modal'>
<ul>
{actions.map((option, i: number) => {
if (option === null) {
return <li key={`sep-${i}`} className='dropdown-menu__separator' />;
}
const { text, dangerous } = option;
let element: React.ReactElement;
if (isActionItem(option)) {
element = (
<button onClick={onClick} data-index={i}>
{text}
</button>
);
} else if (isExternalLinkItem(option)) {
element = (
<a
href={option.href}
target={option.target ?? '_target'}
data-method={option.method}
rel='noopener'
onClick={onClick}
data-index={i}
>
{text}
</a>
);
} else {
element = (
<Link to={option.to} onClick={onClick} data-index={i}>
{text}
</Link>
);
}
return (
<li
className={classNames({
'dropdown-menu__item--dangerous': dangerous,
})}
key={`${text}-${i}`}
>
{element}
</li>
);
})}
</ul>
</div>
);

View File

@ -24,7 +24,7 @@ import { getScrollbarWidth } from 'mastodon/utils/scrollbar';
import BundleContainer from '../containers/bundle_container'; import BundleContainer from '../containers/bundle_container';
import ActionsModal from './actions_modal'; import { ActionsModal } from './actions_modal';
import AudioModal from './audio_modal'; import AudioModal from './audio_modal';
import { BoostModal } from './boost_modal'; import { BoostModal } from './boost_modal';
import { import {

View File

@ -22,3 +22,29 @@ export type MenuItem =
| LinkMenuItem | LinkMenuItem
| ExternalLinkMenuItem | ExternalLinkMenuItem
| null; | null;
export const isMenuItem = (item: unknown): item is MenuItem => {
if (item === null) {
return true;
}
return typeof item === 'object' && 'text' in item;
};
export const isActionItem = (item: unknown): item is ActionMenuItem => {
if (!item || !isMenuItem(item)) {
return false;
}
return 'action' in item;
};
export const isExternalLinkItem = (
item: unknown,
): item is ExternalLinkMenuItem => {
if (!item || !isMenuItem(item)) {
return false;
}
return 'href' in item;
};

View File

@ -6484,55 +6484,38 @@ a.status-card {
} }
.actions-modal { .actions-modal {
border-radius: 8px 8px 0 0;
background: var(--dropdown-background-color);
backdrop-filter: var(--background-filter);
border-color: var(--dropdown-border-color);
box-shadow: var(--dropdown-shadow);
max-height: 80vh; max-height: 80vh;
max-width: 80vw; max-width: 80vw;
.actions-modal__item-label {
font-weight: 500;
}
ul { ul {
overflow-y: auto; overflow-y: auto;
flex-shrink: 0; flex-shrink: 0;
max-height: 80vh; padding-bottom: 8px;
}
&.with-status { a,
max-height: calc(80vh - 75px); button {
} color: inherit;
display: flex;
padding: 16px;
font-size: 15px;
line-height: 21px;
background: transparent;
border: none;
align-items: center;
text-decoration: none;
width: 100%;
box-sizing: border-box;
li:empty { &:hover,
margin: 0; &:active,
} &:focus {
background: var(--dropdown-border-color);
li:not(:empty) {
a {
color: $primary-text-color;
display: flex;
padding: 12px 16px;
font-size: 15px;
align-items: center;
text-decoration: none;
&,
button {
transition: none;
}
&.active,
&:hover,
&:active,
&:focus {
&,
button {
background: $ui-highlight-color;
color: $primary-text-color;
}
}
button:first-child {
margin-inline-end: 10px;
}
}
} }
} }
} }