Simplify column headers (#27557)
This commit is contained in:
		
							parent
							
								
									1f5187e2e2
								
							
						
					
					
						commit
						13d310e64d
					
				| @ -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 />', () => { | ||||
|  | ||||
| @ -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> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| @ -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, | ||||
|  | ||||
| @ -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} | ||||
|  | ||||
| @ -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} | ||||
|  | ||||
| @ -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} | ||||
|  | ||||
| @ -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} | ||||
|  | ||||
| @ -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' | ||||
|  | ||||
| @ -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')); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
| @ -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 | ||||
|  | ||||
| @ -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> | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
| } | ||||
							
								
								
									
										62
									
								
								app/javascript/mastodon/test_helpers.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								app/javascript/mastodon/test_helpers.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -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 }; | ||||
| @ -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; | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user