From 13d310e64df9aee293d434eb68d541b9002ec2e9 Mon Sep 17 00:00:00 2001 From: Renaud Chaput <renchap@gmail.com> Date: Fri, 27 Oct 2023 15:21:07 +0200 Subject: [PATCH] Simplify column headers (#27557) --- .../components/__tests__/button-test.jsx | 3 +- .../components/column_back_button.tsx | 25 -------- .../mastodon/components/column_header.jsx | 55 +++++++++------- .../mastodon/features/blocks/index.jsx | 4 +- .../mastodon/features/domain_blocks/index.jsx | 5 +- .../features/follow_requests/index.jsx | 4 +- .../mastodon/features/mutes/index.jsx | 4 +- .../features/pinned_statuses/index.jsx | 4 +- .../ui/components/__tests__/column-test.jsx | 8 ++- .../features/ui/components/column.jsx | 9 +-- .../features/ui/components/column_header.jsx | 41 ------------ app/javascript/mastodon/test_helpers.tsx | 62 +++++++++++++++++++ .../styles/mastodon/components.scss | 14 ----- 13 files changed, 113 insertions(+), 125 deletions(-) delete mode 100644 app/javascript/mastodon/features/ui/components/column_header.jsx create mode 100644 app/javascript/mastodon/test_helpers.tsx diff --git a/app/javascript/mastodon/components/__tests__/button-test.jsx b/app/javascript/mastodon/components/__tests__/button-test.jsx index ad7a0c49ca..f38ff6a7dd 100644 --- a/app/javascript/mastodon/components/__tests__/button-test.jsx +++ b/app/javascript/mastodon/components/__tests__/button-test.jsx @@ -1,6 +1,7 @@ -import { render, fireEvent, screen } from '@testing-library/react'; import renderer from 'react-test-renderer'; +import { render, fireEvent, screen } from 'mastodon/test_helpers'; + import { Button } from '../button'; describe('<Button />', () => { diff --git a/app/javascript/mastodon/components/column_back_button.tsx b/app/javascript/mastodon/components/column_back_button.tsx index 965edc8dcd..b835e9e6ad 100644 --- a/app/javascript/mastodon/components/column_back_button.tsx +++ b/app/javascript/mastodon/components/column_back_button.tsx @@ -43,28 +43,3 @@ export const ColumnBackButton: React.FC<{ onClick: OnClickCallback }> = ({ 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> - ); -}; diff --git a/app/javascript/mastodon/components/column_header.jsx b/app/javascript/mastodon/components/column_header.jsx index c3709f0b71..b78bd9a8ef 100644 --- a/app/javascript/mastodon/components/column_header.jsx +++ b/app/javascript/mastodon/components/column_header.jsx @@ -1,5 +1,5 @@ import PropTypes from 'prop-types'; -import { PureComponent } from 'react'; +import { PureComponent, useCallback } from 'react'; import { FormattedMessage, injectIntl, defineMessages } from 'react-intl'; @@ -14,9 +14,11 @@ import { ReactComponent as CloseIcon } from '@material-symbols/svg-600/outlined/ import { ReactComponent as TuneIcon } from '@material-symbols/svg-600/outlined/tune.svg'; import { Icon } from 'mastodon/components/icon'; -import { ButtonInTabsBar } from 'mastodon/features/ui/util/columns_context'; +import { ButtonInTabsBar, useColumnsContext } from 'mastodon/features/ui/util/columns_context'; import { WithRouterPropTypes } from 'mastodon/utils/react_router'; +import { useAppHistory } from './router'; + const messages = defineMessages({ show: { id: 'column_header.show_settings', defaultMessage: 'Show settings' }, hide: { id: 'column_header.hide_settings', defaultMessage: 'Hide settings' }, @@ -24,6 +26,34 @@ const messages = defineMessages({ moveRight: { id: 'column_header.moveRight_settings', defaultMessage: 'Move column to the right' }, }); +const BackButton = ({ pinned, show }) => { + const history = useAppHistory(); + const { multiColumn } = useColumnsContext(); + + const handleBackClick = useCallback(() => { + if (history.location?.state?.fromMastodon) { + history.goBack(); + } else { + history.push('/'); + } + }, [history]); + + const showButton = history && !pinned && ((multiColumn && history.location?.state?.fromMastodon) || show); + + if(!showButton) return null; + + return (<button onClick={handleBackClick} className='column-header__back-button'> + <Icon id='chevron-left' icon={ArrowBackIcon} className='column-back-button__icon' /> + <FormattedMessage id='column_back_button.label' defaultMessage='Back' /> + </button>); + +}; + +BackButton.propTypes = { + pinned: PropTypes.bool, + show: PropTypes.bool, +}; + class ColumnHeader extends PureComponent { static contextTypes = { @@ -72,16 +102,6 @@ class ColumnHeader extends PureComponent { this.props.onMove(1); }; - handleBackClick = () => { - const { history } = this.props; - - if (history.location?.state?.fromMastodon) { - history.goBack(); - } else { - history.push('/'); - } - }; - handleTransitionEnd = () => { this.setState({ animating: false }); }; @@ -95,7 +115,7 @@ class ColumnHeader extends PureComponent { }; render () { - const { title, icon, iconComponent, active, children, pinned, multiColumn, extraButton, showBackButton, intl: { formatMessage }, placeholder, appendContent, collapseIssues, history } = this.props; + const { title, icon, iconComponent, active, children, pinned, multiColumn, extraButton, showBackButton, intl: { formatMessage }, placeholder, appendContent, collapseIssues } = this.props; const { collapsed, animating } = this.state; const wrapperClassName = classNames('column-header__wrapper', { @@ -138,14 +158,7 @@ class ColumnHeader extends PureComponent { pinButton = <button key='pin-button' className='text-btn column-header__setting-btn' onClick={this.handlePin}><Icon id='plus' icon={AddIcon} /> <FormattedMessage id='column_header.pin' defaultMessage='Pin' /></button>; } - if (!pinned && ((multiColumn && history.location?.state?.fromMastodon) || showBackButton)) { - backButton = ( - <button onClick={this.handleBackClick} className='column-header__back-button'> - <Icon id='chevron-left' icon={ArrowBackIcon} className='column-back-button__icon' /> - <FormattedMessage id='column_back_button.label' defaultMessage='Back' /> - </button> - ); - } + backButton = <BackButton pinned={pinned} show={showBackButton} />; const collapsedContent = [ extraContent, diff --git a/app/javascript/mastodon/features/blocks/index.jsx b/app/javascript/mastodon/features/blocks/index.jsx index 21b7a263f1..615e4c8be2 100644 --- a/app/javascript/mastodon/features/blocks/index.jsx +++ b/app/javascript/mastodon/features/blocks/index.jsx @@ -10,7 +10,6 @@ import { ReactComponent as BlockIcon } from '@material-symbols/svg-600/outlined/ import { debounce } from 'lodash'; import { fetchBlocks, expandBlocks } from '../../actions/blocks'; -import { ColumnBackButtonSlim } from '../../components/column_back_button'; import { LoadingIndicator } from '../../components/loading_indicator'; import ScrollableList from '../../components/scrollable_list'; import AccountContainer from '../../containers/account_container'; @@ -60,8 +59,7 @@ class Blocks extends ImmutablePureComponent { const emptyMessage = <FormattedMessage id='empty_column.blocks' defaultMessage="You haven't blocked any users yet." />; return ( - <Column bindToDocument={!multiColumn} icon='ban' iconComponent={BlockIcon} heading={intl.formatMessage(messages.heading)}> - <ColumnBackButtonSlim /> + <Column bindToDocument={!multiColumn} icon='ban' iconComponent={BlockIcon} heading={intl.formatMessage(messages.heading)} alwaysShowBackButton> <ScrollableList scrollKey='blocks' onLoadMore={this.handleLoadMore} diff --git a/app/javascript/mastodon/features/domain_blocks/index.jsx b/app/javascript/mastodon/features/domain_blocks/index.jsx index 958083d588..142f14bf71 100644 --- a/app/javascript/mastodon/features/domain_blocks/index.jsx +++ b/app/javascript/mastodon/features/domain_blocks/index.jsx @@ -12,7 +12,6 @@ import { ReactComponent as BlockIcon } from '@material-symbols/svg-600/outlined/ import { debounce } from 'lodash'; import { fetchDomainBlocks, expandDomainBlocks } from '../../actions/domain_blocks'; -import { ColumnBackButtonSlim } from '../../components/column_back_button'; import { LoadingIndicator } from '../../components/loading_indicator'; import ScrollableList from '../../components/scrollable_list'; import DomainContainer from '../../containers/domain_container'; @@ -61,9 +60,7 @@ class Blocks extends ImmutablePureComponent { const emptyMessage = <FormattedMessage id='empty_column.domain_blocks' defaultMessage='There are no blocked domains yet.' />; return ( - <Column bindToDocument={!multiColumn} icon='ban' iconComponent={BlockIcon} heading={intl.formatMessage(messages.heading)}> - <ColumnBackButtonSlim /> - + <Column bindToDocument={!multiColumn} icon='ban' iconComponent={BlockIcon} heading={intl.formatMessage(messages.heading)} alwaysShowBackButton> <ScrollableList scrollKey='domain_blocks' onLoadMore={this.handleLoadMore} diff --git a/app/javascript/mastodon/features/follow_requests/index.jsx b/app/javascript/mastodon/features/follow_requests/index.jsx index 7d8785e052..3b98791926 100644 --- a/app/javascript/mastodon/features/follow_requests/index.jsx +++ b/app/javascript/mastodon/features/follow_requests/index.jsx @@ -12,7 +12,6 @@ import { ReactComponent as PersonAddIcon } from '@material-symbols/svg-600/outli import { debounce } from 'lodash'; import { fetchFollowRequests, expandFollowRequests } from '../../actions/accounts'; -import { ColumnBackButtonSlim } from '../../components/column_back_button'; import ScrollableList from '../../components/scrollable_list'; import { me } from '../../initial_state'; import Column from '../ui/components/column'; @@ -68,8 +67,7 @@ class FollowRequests extends ImmutablePureComponent { ); return ( - <Column bindToDocument={!multiColumn} icon='user-plus' iconComponent={PersonAddIcon} heading={intl.formatMessage(messages.heading)}> - <ColumnBackButtonSlim /> + <Column bindToDocument={!multiColumn} icon='user-plus' iconComponent={PersonAddIcon} heading={intl.formatMessage(messages.heading)} alwaysShowBackButton> <ScrollableList scrollKey='follow_requests' onLoadMore={this.handleLoadMore} diff --git a/app/javascript/mastodon/features/mutes/index.jsx b/app/javascript/mastodon/features/mutes/index.jsx index afecd72d60..7f66edc03d 100644 --- a/app/javascript/mastodon/features/mutes/index.jsx +++ b/app/javascript/mastodon/features/mutes/index.jsx @@ -12,7 +12,6 @@ import { ReactComponent as VolumeOffIcon } from '@material-symbols/svg-600/outli import { debounce } from 'lodash'; import { fetchMutes, expandMutes } from '../../actions/mutes'; -import { ColumnBackButtonSlim } from '../../components/column_back_button'; import { LoadingIndicator } from '../../components/loading_indicator'; import ScrollableList from '../../components/scrollable_list'; import AccountContainer from '../../containers/account_container'; @@ -62,8 +61,7 @@ class Mutes extends ImmutablePureComponent { const emptyMessage = <FormattedMessage id='empty_column.mutes' defaultMessage="You haven't muted any users yet." />; return ( - <Column bindToDocument={!multiColumn} icon='volume-off' iconComponent={VolumeOffIcon} heading={intl.formatMessage(messages.heading)}> - <ColumnBackButtonSlim /> + <Column bindToDocument={!multiColumn} icon='volume-off' iconComponent={VolumeOffIcon} heading={intl.formatMessage(messages.heading)} alwaysShowBackButton> <ScrollableList scrollKey='mutes' onLoadMore={this.handleLoadMore} diff --git a/app/javascript/mastodon/features/pinned_statuses/index.jsx b/app/javascript/mastodon/features/pinned_statuses/index.jsx index e8206d704c..82398ccda9 100644 --- a/app/javascript/mastodon/features/pinned_statuses/index.jsx +++ b/app/javascript/mastodon/features/pinned_statuses/index.jsx @@ -13,7 +13,6 @@ import { ReactComponent as PushPinIcon } from '@material-symbols/svg-600/outline import { getStatusList } from 'mastodon/selectors'; import { fetchPinnedStatuses } from '../../actions/pin_statuses'; -import { ColumnBackButtonSlim } from '../../components/column_back_button'; import StatusList from '../../components/status_list'; import Column from '../ui/components/column'; @@ -52,8 +51,7 @@ class PinnedStatuses extends ImmutablePureComponent { const { intl, statusIds, hasMore, multiColumn } = this.props; return ( - <Column bindToDocument={!multiColumn} icon='thumb-tack' iconComponent={PushPinIcon} heading={intl.formatMessage(messages.heading)} ref={this.setRef}> - <ColumnBackButtonSlim /> + <Column bindToDocument={!multiColumn} icon='thumb-tack' iconComponent={PushPinIcon} heading={intl.formatMessage(messages.heading)} ref={this.setRef} alwaysShowBackButton> <StatusList statusIds={statusIds} scrollKey='pinned_statuses' diff --git a/app/javascript/mastodon/features/ui/components/__tests__/column-test.jsx b/app/javascript/mastodon/features/ui/components/__tests__/column-test.jsx index 0482942426..ca74fa2dc4 100644 --- a/app/javascript/mastodon/features/ui/components/__tests__/column-test.jsx +++ b/app/javascript/mastodon/features/ui/components/__tests__/column-test.jsx @@ -1,13 +1,15 @@ -import { render, fireEvent, screen } from '@testing-library/react'; +import { render, fireEvent, screen } from 'mastodon/test_helpers'; import Column from '../column'; +const fakeIcon = () => <span />; + describe('<Column />', () => { describe('<ColumnHeader /> click handler', () => { it('runs the scroll animation if the column contains scrollable content', () => { const scrollToMock = jest.fn(); const { container } = render( - <Column heading='notifications'> + <Column heading='notifications' icon='notifications' iconComponent={fakeIcon}> <div className='scrollable' /> </Column>, ); @@ -17,7 +19,7 @@ describe('<Column />', () => { }); it('does not try to scroll if there is no scrollable content', () => { - render(<Column heading='notifications' />); + render(<Column heading='notifications' icon='notifications' iconComponent={fakeIcon} />); fireEvent.click(screen.getByText('notifications')); }); }); diff --git a/app/javascript/mastodon/features/ui/components/column.jsx b/app/javascript/mastodon/features/ui/components/column.jsx index d667f42d99..b6c09b62cd 100644 --- a/app/javascript/mastodon/features/ui/components/column.jsx +++ b/app/javascript/mastodon/features/ui/components/column.jsx @@ -3,15 +3,15 @@ import { PureComponent } from 'react'; import { debounce } from 'lodash'; +import ColumnHeader from '../../../components/column_header'; import { isMobile } from '../../../is_mobile'; import { scrollTop } from '../../../scroll'; -import ColumnHeader from './column_header'; - export default class Column extends PureComponent { static propTypes = { heading: PropTypes.string, + alwaysShowBackButton: PropTypes.bool, icon: PropTypes.string, iconComponent: PropTypes.func, children: PropTypes.node, @@ -51,13 +51,14 @@ export default class Column extends PureComponent { }; render () { - const { heading, icon, iconComponent, children, active, hideHeadingOnMobile } = this.props; + const { heading, icon, iconComponent, children, active, hideHeadingOnMobile, alwaysShowBackButton } = this.props; const showHeading = heading && (!hideHeadingOnMobile || (hideHeadingOnMobile && !isMobile(window.innerWidth))); const columnHeaderId = showHeading && heading.replace(/ /g, '-'); + const header = showHeading && ( - <ColumnHeader icon={icon} iconComponent={iconComponent} active={active} type={heading} onClick={this.handleHeaderClick} columnHeaderId={columnHeaderId} /> + <ColumnHeader icon={icon} iconComponent={iconComponent} active={active} title={heading} onClick={this.handleHeaderClick} columnHeaderId={columnHeaderId} showBackButton={alwaysShowBackButton} /> ); return ( <div diff --git a/app/javascript/mastodon/features/ui/components/column_header.jsx b/app/javascript/mastodon/features/ui/components/column_header.jsx deleted file mode 100644 index 5478e8a411..0000000000 --- a/app/javascript/mastodon/features/ui/components/column_header.jsx +++ /dev/null @@ -1,41 +0,0 @@ -import PropTypes from 'prop-types'; -import { PureComponent } from 'react'; - -import classNames from 'classnames'; - -import { Icon } from 'mastodon/components/icon'; - -export default class ColumnHeader extends PureComponent { - - static propTypes = { - icon: PropTypes.string, - iconComponent: PropTypes.func, - type: PropTypes.string, - active: PropTypes.bool, - onClick: PropTypes.func, - columnHeaderId: PropTypes.string, - }; - - handleClick = () => { - this.props.onClick(); - }; - - render () { - const { icon, iconComponent, type, active, columnHeaderId } = this.props; - let iconElement = ''; - - if (icon) { - iconElement = <Icon id={icon} icon={iconComponent} className='column-header__icon' />; - } - - return ( - <h1 className={classNames('column-header', { active })} id={columnHeaderId || null}> - <button onClick={this.handleClick}> - {iconElement} - {type} - </button> - </h1> - ); - } - -} diff --git a/app/javascript/mastodon/test_helpers.tsx b/app/javascript/mastodon/test_helpers.tsx new file mode 100644 index 0000000000..6895895569 --- /dev/null +++ b/app/javascript/mastodon/test_helpers.tsx @@ -0,0 +1,62 @@ +import PropTypes from 'prop-types'; +import type { PropsWithChildren } from 'react'; +import { Component } from 'react'; + +import { IntlProvider } from 'react-intl'; + +import { MemoryRouter } from 'react-router'; + +// eslint-disable-next-line import/no-extraneous-dependencies +import { render as rtlRender } from '@testing-library/react'; + +class FakeIdentityWrapper extends Component< + PropsWithChildren<{ signedIn: boolean }> +> { + static childContextTypes = { + identity: PropTypes.shape({ + signedIn: PropTypes.bool.isRequired, + accountId: PropTypes.string, + disabledAccountId: PropTypes.string, + accessToken: PropTypes.string, + }).isRequired, + }; + + getChildContext() { + return { + identity: { + signedIn: this.props.signedIn, + accountId: '123', + accessToken: 'test-access-token', + }, + }; + } + + render() { + return this.props.children; + } +} + +function render( + ui: React.ReactElement, + { locale = 'en', signedIn = true, ...renderOptions } = {}, +) { + const Wrapper = (props: { children: React.ReactElement }) => { + return ( + <MemoryRouter> + <IntlProvider locale={locale}> + <FakeIdentityWrapper signedIn={signedIn}> + {props.children} + </FakeIdentityWrapper> + </IntlProvider> + </MemoryRouter> + ); + }; + return rtlRender(ui, { wrapper: Wrapper, ...renderOptions }); +} + +// re-export everything +// eslint-disable-next-line import/no-extraneous-dependencies +export * from '@testing-library/react'; + +// override render method +export { render }; diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index b8e6af0aaa..1e341680e0 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -3137,20 +3137,6 @@ $ui-header-height: 55px; margin-inline-end: 5px; } -.column-back-button--slim { - position: relative; -} - -.column-back-button--slim-button { - cursor: pointer; - flex: 0 0 auto; - font-size: 16px; - padding: 15px; - position: absolute; - inset-inline-end: 0; - top: -50px; -} - .react-toggle { display: inline-block; position: relative;