mirror of https://github.com/mastodon/mastodon
Use a context to propagate column-related Props, and remove `forceUpdate` usage (#27548)
parent
3ca974e101
commit
537442853f
@ -1,65 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import { PureComponent } from 'react';
|
|
||||||
import { createPortal } from 'react-dom';
|
|
||||||
|
|
||||||
import { FormattedMessage } from 'react-intl';
|
|
||||||
|
|
||||||
import { withRouter } from 'react-router-dom';
|
|
||||||
|
|
||||||
import { ReactComponent as ArrowBackIcon } from '@material-symbols/svg-600/outlined/arrow_back.svg';
|
|
||||||
|
|
||||||
import { Icon } from 'mastodon/components/icon';
|
|
||||||
import { WithRouterPropTypes } from 'mastodon/utils/react_router';
|
|
||||||
|
|
||||||
export class ColumnBackButton extends PureComponent {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
multiColumn: PropTypes.bool,
|
|
||||||
onClick: PropTypes.func,
|
|
||||||
...WithRouterPropTypes,
|
|
||||||
};
|
|
||||||
|
|
||||||
handleClick = () => {
|
|
||||||
const { onClick, history } = this.props;
|
|
||||||
|
|
||||||
if (onClick) {
|
|
||||||
onClick();
|
|
||||||
} else if (history.location?.state?.fromMastodon) {
|
|
||||||
history.goBack();
|
|
||||||
} else {
|
|
||||||
history.push('/');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
render () {
|
|
||||||
const { multiColumn } = this.props;
|
|
||||||
|
|
||||||
const component = (
|
|
||||||
<button onClick={this.handleClick} className='column-back-button'>
|
|
||||||
<Icon id='chevron-left' icon={ArrowBackIcon} className='column-back-button__icon' />
|
|
||||||
<FormattedMessage id='column_back_button.label' defaultMessage='Back' />
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
|
|
||||||
if (multiColumn) {
|
|
||||||
return component;
|
|
||||||
} else {
|
|
||||||
// The portal container and the component may be rendered to the DOM in
|
|
||||||
// the same React render pass, so the container might not be available at
|
|
||||||
// the time `render()` is called.
|
|
||||||
const container = document.getElementById('tabs-bar__portal');
|
|
||||||
if (container === null) {
|
|
||||||
// The container wasn't available, force a re-render so that the
|
|
||||||
// component can eventually be inserted in the container and not scroll
|
|
||||||
// with the rest of the area.
|
|
||||||
this.forceUpdate();
|
|
||||||
return component;
|
|
||||||
} else {
|
|
||||||
return createPortal(component, container);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export default withRouter(ColumnBackButton);
|
|
@ -0,0 +1,70 @@
|
|||||||
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import { ReactComponent as ArrowBackIcon } from '@material-symbols/svg-600/outlined/arrow_back.svg';
|
||||||
|
|
||||||
|
import { Icon } from 'mastodon/components/icon';
|
||||||
|
import { ButtonInTabsBar } from 'mastodon/features/ui/util/columns_context';
|
||||||
|
|
||||||
|
import { useAppHistory } from './router';
|
||||||
|
|
||||||
|
type OnClickCallback = () => void;
|
||||||
|
|
||||||
|
function useHandleClick(onClick?: OnClickCallback) {
|
||||||
|
const history = useAppHistory();
|
||||||
|
|
||||||
|
return useCallback(() => {
|
||||||
|
if (onClick) {
|
||||||
|
onClick();
|
||||||
|
} else if (history.location.state?.fromMastodon) {
|
||||||
|
history.goBack();
|
||||||
|
} else {
|
||||||
|
history.push('/');
|
||||||
|
}
|
||||||
|
}, [history, onClick]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ColumnBackButton: React.FC<{ onClick: OnClickCallback }> = ({
|
||||||
|
onClick,
|
||||||
|
}) => {
|
||||||
|
const handleClick = useHandleClick(onClick);
|
||||||
|
|
||||||
|
const component = (
|
||||||
|
<button onClick={handleClick} className='column-back-button'>
|
||||||
|
<Icon
|
||||||
|
id='chevron-left'
|
||||||
|
icon={ArrowBackIcon}
|
||||||
|
className='column-back-button__icon'
|
||||||
|
/>
|
||||||
|
<FormattedMessage id='column_back_button.label' defaultMessage='Back' />
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
|
||||||
|
return <ButtonInTabsBar>{component}</ButtonInTabsBar>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ColumnBackButtonSlim: React.FC<{ onClick: OnClickCallback }> = ({
|
||||||
|
onClick,
|
||||||
|
}) => {
|
||||||
|
const handleClick = useHandleClick(onClick);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='column-back-button--slim'>
|
||||||
|
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events */}
|
||||||
|
<div
|
||||||
|
role='button'
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={handleClick}
|
||||||
|
className='column-back-button column-back-button--slim-button'
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
id='chevron-left'
|
||||||
|
icon={ArrowBackIcon}
|
||||||
|
className='column-back-button__icon'
|
||||||
|
/>
|
||||||
|
<FormattedMessage id='column_back_button.label' defaultMessage='Back' />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -1,20 +0,0 @@
|
|||||||
import { FormattedMessage } from 'react-intl';
|
|
||||||
|
|
||||||
import { ReactComponent as ArrowBackIcon } from '@material-symbols/svg-600/outlined/arrow_back.svg';
|
|
||||||
|
|
||||||
import { Icon } from 'mastodon/components/icon';
|
|
||||||
|
|
||||||
import { ColumnBackButton } from './column_back_button';
|
|
||||||
|
|
||||||
export default class ColumnBackButtonSlim extends ColumnBackButton {
|
|
||||||
render () {
|
|
||||||
return (
|
|
||||||
<div className='column-back-button--slim'>
|
|
||||||
<div role='button' tabIndex={0} onClick={this.handleClick} className='column-back-button column-back-button--slim-button'>
|
|
||||||
<Icon id='chevron-left' icon={ArrowBackIcon} className='column-back-button__icon' />
|
|
||||||
<FormattedMessage id='column_back_button.label' defaultMessage='Back' />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,51 @@
|
|||||||
|
import type { ReactElement } from 'react';
|
||||||
|
import { createContext, useContext, useMemo, useState } from 'react';
|
||||||
|
import { createPortal } from 'react-dom';
|
||||||
|
|
||||||
|
export const ColumnsContext = createContext<{
|
||||||
|
tabsBarElement: HTMLElement | null;
|
||||||
|
setTabsBarElement: (element: HTMLElement) => void;
|
||||||
|
multiColumn: boolean;
|
||||||
|
}>({
|
||||||
|
tabsBarElement: null,
|
||||||
|
multiColumn: false,
|
||||||
|
setTabsBarElement: () => undefined, // no-op
|
||||||
|
});
|
||||||
|
|
||||||
|
export function useColumnsContext() {
|
||||||
|
return useContext(ColumnsContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ButtonInTabsBar: React.FC<{
|
||||||
|
children: ReactElement | string | undefined;
|
||||||
|
}> = ({ children }) => {
|
||||||
|
const { multiColumn, tabsBarElement } = useColumnsContext();
|
||||||
|
|
||||||
|
if (multiColumn) {
|
||||||
|
return children;
|
||||||
|
} else if (!tabsBarElement) {
|
||||||
|
return children;
|
||||||
|
} else {
|
||||||
|
return createPortal(children, tabsBarElement);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
type ContextValue = React.ContextType<typeof ColumnsContext>;
|
||||||
|
|
||||||
|
export const ColumnsContextProvider: React.FC<
|
||||||
|
React.PropsWithChildren<{ multiColumn: boolean }>
|
||||||
|
> = ({ multiColumn, children }) => {
|
||||||
|
const [tabsBarElement, setTabsBarElement] =
|
||||||
|
useState<ContextValue['tabsBarElement']>(null);
|
||||||
|
|
||||||
|
const contextValue = useMemo<ContextValue>(
|
||||||
|
() => ({ multiColumn, tabsBarElement, setTabsBarElement }),
|
||||||
|
[multiColumn, tabsBarElement],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ColumnsContext.Provider value={contextValue}>
|
||||||
|
{children}
|
||||||
|
</ColumnsContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
Loading…
Reference in New Issue