diff --git a/app/javascript/mastodon/components/column_back_button.jsx b/app/javascript/mastodon/components/column_back_button.jsx deleted file mode 100644 index 3e914ecadd..0000000000 --- a/app/javascript/mastodon/components/column_back_button.jsx +++ /dev/null @@ -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 = ( - - ); - - 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); diff --git a/app/javascript/mastodon/components/column_back_button.tsx b/app/javascript/mastodon/components/column_back_button.tsx new file mode 100644 index 0000000000..965edc8dcd --- /dev/null +++ b/app/javascript/mastodon/components/column_back_button.tsx @@ -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 = ( + + ); + + return {component}; +}; + +export const ColumnBackButtonSlim: React.FC<{ onClick: OnClickCallback }> = ({ + onClick, +}) => { + const handleClick = useHandleClick(onClick); + + return ( +
+ {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events */} +
+ + +
+
+ ); +}; diff --git a/app/javascript/mastodon/components/column_back_button_slim.jsx b/app/javascript/mastodon/components/column_back_button_slim.jsx deleted file mode 100644 index 397e6c6a77..0000000000 --- a/app/javascript/mastodon/components/column_back_button_slim.jsx +++ /dev/null @@ -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 ( -
-
- - -
-
- ); - } -} diff --git a/app/javascript/mastodon/components/column_header.jsx b/app/javascript/mastodon/components/column_header.jsx index f60b17d9b8..c3709f0b71 100644 --- a/app/javascript/mastodon/components/column_header.jsx +++ b/app/javascript/mastodon/components/column_header.jsx @@ -1,6 +1,5 @@ import PropTypes from 'prop-types'; import { PureComponent } from 'react'; -import { createPortal } from 'react-dom'; import { FormattedMessage, injectIntl, defineMessages } from 'react-intl'; @@ -15,6 +14,7 @@ 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 { WithRouterPropTypes } from 'mastodon/utils/react_router'; const messages = defineMessages({ @@ -203,22 +203,12 @@ class ColumnHeader extends PureComponent { ); - if (multiColumn || placeholder) { + if (placeholder) { 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); - } + return ( + {component} + ); } } diff --git a/app/javascript/mastodon/components/router.tsx b/app/javascript/mastodon/components/router.tsx index e381571676..fe50fc2ba9 100644 --- a/app/javascript/mastodon/components/router.tsx +++ b/app/javascript/mastodon/components/router.tsx @@ -1,7 +1,7 @@ import type { PropsWithChildren } from 'react'; import React from 'react'; -import { Router as OriginalRouter } from 'react-router'; +import { Router as OriginalRouter, useHistory } from 'react-router'; import type { LocationDescriptor, @@ -16,18 +16,23 @@ interface MastodonLocationState { fromMastodon?: boolean; mastodonModalKey?: string; } -type HistoryPath = Path | LocationDescriptor; -const browserHistory = createBrowserHistory< - MastodonLocationState | undefined ->(); +type LocationState = MastodonLocationState | null | undefined; + +type HistoryPath = Path | LocationDescriptor; + +const browserHistory = createBrowserHistory(); const originalPush = browserHistory.push.bind(browserHistory); const originalReplace = browserHistory.replace.bind(browserHistory); +export function useAppHistory() { + return useHistory(); +} + function normalizePath( path: HistoryPath, - state?: MastodonLocationState, -): LocationDescriptorObject { + state?: LocationState, +): LocationDescriptorObject { const location = typeof path === 'string' ? { pathname: path } : { ...path }; if (location.state === undefined && state !== undefined) { diff --git a/app/javascript/mastodon/features/account_gallery/index.jsx b/app/javascript/mastodon/features/account_gallery/index.jsx index 19c10fa9f8..6a1d0b322f 100644 --- a/app/javascript/mastodon/features/account_gallery/index.jsx +++ b/app/javascript/mastodon/features/account_gallery/index.jsx @@ -8,7 +8,7 @@ import { connect } from 'react-redux'; import { lookupAccount, fetchAccount } from 'mastodon/actions/accounts'; import { openModal } from 'mastodon/actions/modal'; -import ColumnBackButton from 'mastodon/components/column_back_button'; +import { ColumnBackButton } from 'mastodon/components/column_back_button'; import { LoadMore } from 'mastodon/components/load_more'; import { LoadingIndicator } from 'mastodon/components/loading_indicator'; import ScrollContainer from 'mastodon/containers/scroll_container'; @@ -203,7 +203,7 @@ class AccountGallery extends ImmutablePureComponent { return ( - +
diff --git a/app/javascript/mastodon/features/account_timeline/index.jsx b/app/javascript/mastodon/features/account_timeline/index.jsx index 0f18c043b8..5dae66b463 100644 --- a/app/javascript/mastodon/features/account_timeline/index.jsx +++ b/app/javascript/mastodon/features/account_timeline/index.jsx @@ -16,7 +16,7 @@ import { getAccountHidden } from 'mastodon/selectors'; import { lookupAccount, fetchAccount } from '../../actions/accounts'; import { fetchFeaturedTags } from '../../actions/featured_tags'; import { expandAccountFeaturedTimeline, expandAccountTimeline, connectTimeline, disconnectTimeline } from '../../actions/timelines'; -import ColumnBackButton from '../../components/column_back_button'; +import { ColumnBackButton } from '../../components/column_back_button'; import { LoadingIndicator } from '../../components/loading_indicator'; import StatusList from '../../components/status_list'; import Column from '../ui/components/column'; @@ -184,7 +184,7 @@ class AccountTimeline extends ImmutablePureComponent { return ( - + } diff --git a/app/javascript/mastodon/features/blocks/index.jsx b/app/javascript/mastodon/features/blocks/index.jsx index 210260c811..21b7a263f1 100644 --- a/app/javascript/mastodon/features/blocks/index.jsx +++ b/app/javascript/mastodon/features/blocks/index.jsx @@ -10,7 +10,7 @@ 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_slim'; +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'; diff --git a/app/javascript/mastodon/features/domain_blocks/index.jsx b/app/javascript/mastodon/features/domain_blocks/index.jsx index 5ac1d2a71e..958083d588 100644 --- a/app/javascript/mastodon/features/domain_blocks/index.jsx +++ b/app/javascript/mastodon/features/domain_blocks/index.jsx @@ -12,7 +12,7 @@ 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_slim'; +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'; diff --git a/app/javascript/mastodon/features/follow_requests/index.jsx b/app/javascript/mastodon/features/follow_requests/index.jsx index 8e17607fd9..7d8785e052 100644 --- a/app/javascript/mastodon/features/follow_requests/index.jsx +++ b/app/javascript/mastodon/features/follow_requests/index.jsx @@ -12,7 +12,7 @@ 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_slim'; +import { ColumnBackButtonSlim } from '../../components/column_back_button'; import ScrollableList from '../../components/scrollable_list'; import { me } from '../../initial_state'; import Column from '../ui/components/column'; diff --git a/app/javascript/mastodon/features/followers/index.jsx b/app/javascript/mastodon/features/followers/index.jsx index ec39395518..fc0ce8ab30 100644 --- a/app/javascript/mastodon/features/followers/index.jsx +++ b/app/javascript/mastodon/features/followers/index.jsx @@ -19,7 +19,7 @@ import { fetchFollowers, expandFollowers, } from '../../actions/accounts'; -import ColumnBackButton from '../../components/column_back_button'; +import { ColumnBackButton } from '../../components/column_back_button'; import { LoadingIndicator } from '../../components/loading_indicator'; import ScrollableList from '../../components/scrollable_list'; import AccountContainer from '../../containers/account_container'; @@ -147,7 +147,7 @@ class Followers extends ImmutablePureComponent { return ( - + - + - +
diff --git a/app/javascript/mastodon/features/onboarding/index.jsx b/app/javascript/mastodon/features/onboarding/index.jsx index 0be1512ad8..7b8a41faa5 100644 --- a/app/javascript/mastodon/features/onboarding/index.jsx +++ b/app/javascript/mastodon/features/onboarding/index.jsx @@ -47,7 +47,6 @@ class Onboarding extends ImmutablePureComponent { static propTypes = { dispatch: PropTypes.func.isRequired, account: ImmutablePropTypes.map, - multiColumn: PropTypes.bool, ...WithRouterPropTypes, }; @@ -100,14 +99,14 @@ class Onboarding extends ImmutablePureComponent { } render () { - const { account, multiColumn } = this.props; + const { account } = this.props; const { step, shareClicked } = this.state; switch(step) { case 'follows': - return ; + return ; case 'share': - return ; + return ; } return ( diff --git a/app/javascript/mastodon/features/onboarding/share.jsx b/app/javascript/mastodon/features/onboarding/share.jsx index c9d58c4e65..8e01701eb3 100644 --- a/app/javascript/mastodon/features/onboarding/share.jsx +++ b/app/javascript/mastodon/features/onboarding/share.jsx @@ -14,7 +14,7 @@ import { ReactComponent as ContentCopyIcon } from '@material-symbols/svg-600/out import SwipeableViews from 'react-swipeable-views'; import Column from 'mastodon/components/column'; -import ColumnBackButton from 'mastodon/components/column_back_button'; +import { ColumnBackButton } from 'mastodon/components/column_back_button'; import { Icon } from 'mastodon/components/icon'; import { me, domain } from 'mastodon/initial_state'; @@ -146,18 +146,17 @@ class Share extends PureComponent { static propTypes = { onBack: PropTypes.func, account: ImmutablePropTypes.map, - multiColumn: PropTypes.bool, intl: PropTypes.object, }; render () { - const { onBack, account, multiColumn, intl } = this.props; + const { onBack, account, intl } = this.props; const url = (new URL(`/@${account.get('username')}`, document.baseURI)).href; return ( - +
diff --git a/app/javascript/mastodon/features/pinned_statuses/index.jsx b/app/javascript/mastodon/features/pinned_statuses/index.jsx index 57d9768bed..e8206d704c 100644 --- a/app/javascript/mastodon/features/pinned_statuses/index.jsx +++ b/app/javascript/mastodon/features/pinned_statuses/index.jsx @@ -13,7 +13,7 @@ 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_slim'; +import { ColumnBackButtonSlim } from '../../components/column_back_button'; import StatusList from '../../components/status_list'; import Column from '../ui/components/column'; diff --git a/app/javascript/mastodon/features/ui/components/columns_area.jsx b/app/javascript/mastodon/features/ui/components/columns_area.jsx index 9cf6136652..19c2f40ac6 100644 --- a/app/javascript/mastodon/features/ui/components/columns_area.jsx +++ b/app/javascript/mastodon/features/ui/components/columns_area.jsx @@ -1,5 +1,5 @@ import PropTypes from 'prop-types'; -import { Children, cloneElement } from 'react'; +import { Children, cloneElement, useCallback } from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; @@ -21,6 +21,7 @@ import { ListTimeline, Directory, } from '../util/async-components'; +import { useColumnsContext } from '../util/columns_context'; import BundleColumnError from './bundle_column_error'; import { ColumnLoading } from './column_loading'; @@ -43,6 +44,17 @@ const componentMap = { 'DIRECTORY': Directory, }; +const TabsBarPortal = () => { + const {setTabsBarElement} = useColumnsContext(); + + const setRef = useCallback((element) => { + if(element) + setTabsBarElement(element); + }, [setTabsBarElement]); + + return
; +}; + export default class ColumnsArea extends ImmutablePureComponent { static propTypes = { columns: ImmutablePropTypes.list.isRequired, @@ -146,7 +158,7 @@ export default class ColumnsArea extends ImmutablePureComponent {
-
+
{children}
diff --git a/app/javascript/mastodon/features/ui/index.jsx b/app/javascript/mastodon/features/ui/index.jsx index f836dace7f..02c69cbbaf 100644 --- a/app/javascript/mastodon/features/ui/index.jsx +++ b/app/javascript/mastodon/features/ui/index.jsx @@ -64,8 +64,8 @@ import { About, PrivacyPolicy, } from './util/async-components'; +import { ColumnsContextProvider } from './util/columns_context'; import { WrappedSwitch, WrappedRoute } from './util/react_router_helpers'; - // Dummy import, to make sure that ends up in the application bundle. // Without this it ends up in ~8 very commonly used bundles. import '../../components/status'; @@ -179,68 +179,70 @@ class SwitchingColumnsArea extends PureComponent { } return ( - - - {redirect} + + + + {redirect} - {singleColumn ? : null} - {singleColumn && pathName.startsWith('/deck/') ? : null} - {/* Redirect old bookmarks (without /deck) with home-like routes to the advanced interface */} - {!singleColumn && pathName === '/getting-started' ? : null} - {!singleColumn && pathName === '/home' ? : null} + {singleColumn ? : null} + {singleColumn && pathName.startsWith('/deck/') ? : null} + {/* Redirect old bookmarks (without /deck) with home-like routes to the advanced interface */} + {!singleColumn && pathName === '/getting-started' ? : null} + {!singleColumn && pathName === '/home' ? : null} - - - - + + + + - - - - - - - - - - - + + + + + + + + + + + - - + + - - - - + + + + - - - - - - - - - + + + + + + + + + - {/* Legacy routes, cannot be easily factored with other routes because they share a param name */} - - - - - + {/* Legacy routes, cannot be easily factored with other routes because they share a param name */} + + + + + - - - - - - + + + + + + - - - + + + + ); } diff --git a/app/javascript/mastodon/features/ui/util/columns_context.tsx b/app/javascript/mastodon/features/ui/util/columns_context.tsx new file mode 100644 index 0000000000..e02918deb0 --- /dev/null +++ b/app/javascript/mastodon/features/ui/util/columns_context.tsx @@ -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; + +export const ColumnsContextProvider: React.FC< + React.PropsWithChildren<{ multiColumn: boolean }> +> = ({ multiColumn, children }) => { + const [tabsBarElement, setTabsBarElement] = + useState(null); + + const contextValue = useMemo( + () => ({ multiColumn, tabsBarElement, setTabsBarElement }), + [multiColumn, tabsBarElement], + ); + + return ( + + {children} + + ); +};