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;