[Glitch] Use a context to propagate column-related Props, and remove forceUpdate
usage
Port 537442853f
to glitch-soc
Signed-off-by: Claire <claire.github-309c@sitedethib.com>
This commit is contained in:
parent
04f786abd0
commit
ca17c2d35a
18 changed files with 225 additions and 205 deletions
|
@ -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 'flavours/glitch/components/icon';
|
||||
import { WithRouterPropTypes } from 'flavours/glitch/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 'flavours/glitch/components/icon';
|
||||
import { ButtonInTabsBar } from 'flavours/glitch/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,42 +0,0 @@
|
|||
import { PureComponent } from 'react';
|
||||
|
||||
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 'flavours/glitch/components/icon';
|
||||
import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router';
|
||||
|
||||
class ColumnBackButtonSlim extends PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
...WithRouterPropTypes,
|
||||
};
|
||||
|
||||
handleClick = () => {
|
||||
const { location, history } = this.props;
|
||||
|
||||
// Check if there is a previous page in the app to go back to per https://stackoverflow.com/a/70532858/9703201
|
||||
// When upgrading to V6, check `location.key !== 'default'` instead per https://github.com/remix-run/history/blob/main/docs/api-reference.md#location
|
||||
if (location.key) {
|
||||
history.goBack();
|
||||
} else {
|
||||
history.push('/');
|
||||
}
|
||||
};
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withRouter(ColumnBackButtonSlim);
|
|
@ -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 'flavours/glitch/components/icon';
|
||||
import { ButtonInTabsBar } from 'flavours/glitch/features/ui/util/columns_context';
|
||||
import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router';
|
||||
|
||||
const messages = defineMessages({
|
||||
|
@ -203,22 +203,12 @@ class ColumnHeader extends PureComponent {
|
|||
</div>
|
||||
);
|
||||
|
||||
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 (<ButtonInTabsBar>
|
||||
{component}
|
||||
</ButtonInTabsBar>);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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<MastodonLocationState>;
|
||||
|
||||
const browserHistory = createBrowserHistory<
|
||||
MastodonLocationState | undefined
|
||||
>();
|
||||
type LocationState = MastodonLocationState | null | undefined;
|
||||
|
||||
type HistoryPath = Path | LocationDescriptor<LocationState>;
|
||||
|
||||
const browserHistory = createBrowserHistory<LocationState>();
|
||||
const originalPush = browserHistory.push.bind(browserHistory);
|
||||
const originalReplace = browserHistory.replace.bind(browserHistory);
|
||||
|
||||
export function useAppHistory() {
|
||||
return useHistory<LocationState>();
|
||||
}
|
||||
|
||||
function normalizePath(
|
||||
path: HistoryPath,
|
||||
state?: MastodonLocationState,
|
||||
): LocationDescriptorObject<MastodonLocationState> {
|
||||
state?: LocationState,
|
||||
): LocationDescriptorObject<LocationState> {
|
||||
const location = typeof path === 'string' ? { pathname: path } : { ...path };
|
||||
|
||||
if (location.state === undefined && state !== undefined) {
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -13,7 +13,7 @@ import { ReactComponent as StarIcon } from '@material-symbols/svg-600/outlined/s
|
|||
import { ReactComponent as VolumeOffIcon } from '@material-symbols/svg-600/outlined/volume_off.svg';
|
||||
|
||||
import { openModal } from 'flavours/glitch/actions/modal';
|
||||
import ColumnBackButtonSlim from 'flavours/glitch/components/column_back_button_slim';
|
||||
import { ColumnBackButtonSlim } from 'flavours/glitch/components/column_back_button';
|
||||
import Column from 'flavours/glitch/features/ui/components/column';
|
||||
import ColumnLink from 'flavours/glitch/features/ui/components/column_link';
|
||||
import ColumnSubheading from 'flavours/glitch/features/ui/components/column_subheading';
|
||||
|
|
|
@ -12,7 +12,7 @@ import { connect } from 'react-redux';
|
|||
import { ReactComponent as ListAltIcon } from '@material-symbols/svg-600/outlined/list_alt.svg';
|
||||
|
||||
import { fetchLists } from 'flavours/glitch/actions/lists';
|
||||
import ColumnBackButtonSlim from 'flavours/glitch/components/column_back_button_slim';
|
||||
import { ColumnBackButtonSlim } from 'flavours/glitch/components/column_back_button';
|
||||
import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator';
|
||||
import ScrollableList from 'flavours/glitch/components/scrollable_list';
|
||||
import Column from 'flavours/glitch/features/ui/components/column';
|
||||
|
|
|
@ -12,7 +12,7 @@ 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_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';
|
||||
|
|
|
@ -9,7 +9,7 @@ import { connect } from 'react-redux';
|
|||
import { fetchSuggestions } from 'flavours/glitch/actions/suggestions';
|
||||
import { markAsPartial } from 'flavours/glitch/actions/timelines';
|
||||
import Column from 'flavours/glitch/components/column';
|
||||
import ColumnBackButton from 'flavours/glitch/components/column_back_button';
|
||||
import { ColumnBackButton } from 'flavours/glitch/components/column_back_button';
|
||||
import { EmptyAccount } from 'flavours/glitch/components/empty_account';
|
||||
import Account from 'flavours/glitch/containers/account_container';
|
||||
|
||||
|
@ -25,7 +25,6 @@ class Follows extends PureComponent {
|
|||
dispatch: PropTypes.func.isRequired,
|
||||
suggestions: ImmutablePropTypes.list,
|
||||
isLoading: PropTypes.bool,
|
||||
multiColumn: PropTypes.bool,
|
||||
};
|
||||
|
||||
componentDidMount () {
|
||||
|
@ -39,7 +38,7 @@ class Follows extends PureComponent {
|
|||
}
|
||||
|
||||
render () {
|
||||
const { onBack, isLoading, suggestions, multiColumn } = this.props;
|
||||
const { onBack, isLoading, suggestions } = this.props;
|
||||
|
||||
let loadedContent;
|
||||
|
||||
|
@ -53,7 +52,7 @@ class Follows extends PureComponent {
|
|||
|
||||
return (
|
||||
<Column>
|
||||
<ColumnBackButton multiColumn={multiColumn} onClick={onBack} />
|
||||
<ColumnBackButton onClick={onBack} />
|
||||
|
||||
<div className='scrollable privacy-policy'>
|
||||
<div className='column-title'>
|
||||
|
|
|
@ -48,7 +48,6 @@ class Onboarding extends ImmutablePureComponent {
|
|||
static propTypes = {
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
account: ImmutablePropTypes.record,
|
||||
multiColumn: PropTypes.bool,
|
||||
...WithRouterPropTypes,
|
||||
};
|
||||
|
||||
|
@ -101,14 +100,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 <Follows onBack={this.handleBackClick} multiColumn={multiColumn} />;
|
||||
return <Follows onBack={this.handleBackClick} />;
|
||||
case 'share':
|
||||
return <Share onBack={this.handleBackClick} multiColumn={multiColumn} />;
|
||||
return <Share onBack={this.handleBackClick} />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
@ -14,7 +14,7 @@ import { ReactComponent as ContentCopyIcon } from '@material-symbols/svg-600/out
|
|||
import SwipeableViews from 'react-swipeable-views';
|
||||
|
||||
import Column from 'flavours/glitch/components/column';
|
||||
import ColumnBackButton from 'flavours/glitch/components/column_back_button';
|
||||
import { ColumnBackButton } from 'flavours/glitch/components/column_back_button';
|
||||
import { Icon } from 'flavours/glitch/components/icon';
|
||||
import { me, domain } from 'flavours/glitch/initial_state';
|
||||
|
||||
|
@ -146,18 +146,17 @@ class Share extends PureComponent {
|
|||
static propTypes = {
|
||||
onBack: PropTypes.func,
|
||||
account: ImmutablePropTypes.record,
|
||||
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 (
|
||||
<Column>
|
||||
<ColumnBackButton multiColumn={multiColumn} onClick={onBack} />
|
||||
<ColumnBackButton onClick={onBack} />
|
||||
|
||||
<div className='scrollable privacy-policy'>
|
||||
<div className='column-title'>
|
||||
|
|
|
@ -13,7 +13,7 @@ import { ReactComponent as PushPinIcon } from '@material-symbols/svg-600/outline
|
|||
import { getStatusList } from 'flavours/glitch/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';
|
||||
|
||||
|
|
|
@ -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 <div id='tabs-bar__portal' ref={setRef} />;
|
||||
};
|
||||
|
||||
export default class ColumnsArea extends ImmutablePureComponent {
|
||||
static propTypes = {
|
||||
columns: ImmutablePropTypes.list.isRequired,
|
||||
|
@ -146,7 +158,7 @@ export default class ColumnsArea extends ImmutablePureComponent {
|
|||
</div>
|
||||
|
||||
<div className='columns-area__panels__main'>
|
||||
<div className='tabs-bar__wrapper'><div id='tabs-bar__portal' /></div>
|
||||
<div className='tabs-bar__wrapper'><TabsBarPortal /></div>
|
||||
<div className='columns-area columns-area--mobile'>{children}</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -67,8 +67,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 <Status /> ends up in the application bundle.
|
||||
// Without this it ends up in ~8 very commonly used bundles.
|
||||
import '../../components/status';
|
||||
|
@ -188,6 +188,7 @@ class SwitchingColumnsArea extends PureComponent {
|
|||
}
|
||||
|
||||
return (
|
||||
<ColumnsContextProvider multiColumn={!singleColumn}>
|
||||
<ColumnsAreaContainer ref={this.setRef} singleColumn={singleColumn}>
|
||||
<WrappedSwitch>
|
||||
{redirect}
|
||||
|
@ -251,6 +252,7 @@ class SwitchingColumnsArea extends PureComponent {
|
|||
<Route component={BundleColumnError} />
|
||||
</WrappedSwitch>
|
||||
</ColumnsAreaContainer>
|
||||
</ColumnsContextProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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 a new issue