mirror of
https://github.com/mastodon/mastodon.git
synced 2026-09-29 16:33:27 -05:00
Redesign: Prevent scrolling behind BottomSheet component (#40558)
This commit is contained in:
@@ -3,6 +3,7 @@ import { useLayoutEffect, useRef } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { useBodyScrollLock } from '@/mastodon/hooks/useBodyScrollLock';
|
||||
import { useMergedRefs } from '@/mastodon/hooks/useMergedRefs';
|
||||
import { useOnClickOutside } from '@/mastodon/hooks/useOnClickOutside';
|
||||
import { useScrollSensor } from '@/mastodon/hooks/useScrollSensor';
|
||||
@@ -25,6 +26,8 @@ export const BottomSheet: React.FC<BottomSheetProps> = ({
|
||||
const dialogRef = useRef<HTMLDialogElement>(null);
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useBodyScrollLock();
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const dialog = dialogRef.current;
|
||||
if (!dialog) return;
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Provider as ReduxProvider } from 'react-redux';
|
||||
|
||||
import { hydrateStore } from 'mastodon/actions/store';
|
||||
import { connectUserStream } from 'mastodon/actions/streaming';
|
||||
import { BodyScrollLock } from '@/mastodon/hooks/useBodyScrollLock';
|
||||
import ErrorBoundary from 'mastodon/components/error_boundary';
|
||||
import { FocusTargetProvider } from '@/mastodon/components/navigation_focus_target';
|
||||
import { Router } from 'mastodon/components/router';
|
||||
@@ -16,7 +17,6 @@ import { initialState, title as siteTitle } from 'mastodon/initial_state';
|
||||
import { IntlProvider } from 'mastodon/locales';
|
||||
import { store } from 'mastodon/store';
|
||||
import { isProduction } from 'mastodon/utils/environment';
|
||||
import { BodyScrollLock } from 'mastodon/features/ui/components/body_scroll_lock';
|
||||
|
||||
import { ScrollContext } from './scroll_container/scroll_context';
|
||||
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
import { useLayoutEffect } from 'react';
|
||||
|
||||
import { isRedesignEnabled } from '@/mastodon/utils/environment';
|
||||
import { createAppSelector, useAppSelector } from 'mastodon/store';
|
||||
|
||||
const getShouldLockBodyScroll = createAppSelector(
|
||||
[
|
||||
(state) => state.navigation.open,
|
||||
(state) => state.modal.get('stack').size > 0,
|
||||
(state) =>
|
||||
isRedesignEnabled() &&
|
||||
state.composer.displayState === 'showing' &&
|
||||
state.meta.get('layout') === 'mobile',
|
||||
],
|
||||
(
|
||||
isMobileMenuOpen: boolean,
|
||||
isModalOpen: boolean,
|
||||
isRedesignComposerOpen: boolean,
|
||||
) => isMobileMenuOpen || isModalOpen || isRedesignComposerOpen,
|
||||
);
|
||||
|
||||
/**
|
||||
* This component locks scrolling on the body when
|
||||
* `getShouldLockBodyScroll` returns true.
|
||||
*/
|
||||
|
||||
export const BodyScrollLock: React.FC = () => {
|
||||
const shouldLockBodyScroll = useAppSelector(getShouldLockBodyScroll);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
document.documentElement.classList.toggle(
|
||||
'has-modal',
|
||||
shouldLockBodyScroll,
|
||||
);
|
||||
}, [shouldLockBodyScroll]);
|
||||
|
||||
return null;
|
||||
};
|
||||
74
app/javascript/mastodon/hooks/useBodyScrollLock.tsx
Normal file
74
app/javascript/mastodon/hooks/useBodyScrollLock.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import { useId, useLayoutEffect } from 'react';
|
||||
|
||||
import {
|
||||
addToScrollLockStack,
|
||||
removeFromScrollLockStack,
|
||||
} from '@/mastodon/reducers/slices/scrollLockStack';
|
||||
import { isRedesignEnabled } from '@/mastodon/utils/environment';
|
||||
import {
|
||||
createAppSelector,
|
||||
useAppDispatch,
|
||||
useAppSelector,
|
||||
} from 'mastodon/store';
|
||||
|
||||
const getShouldLockBodyScroll = createAppSelector(
|
||||
[
|
||||
(state) => state.scrollLockStack.stack.length > 0,
|
||||
(state) => state.navigation.open,
|
||||
(state) => state.modal.get('stack').size > 0,
|
||||
(state) =>
|
||||
isRedesignEnabled() &&
|
||||
state.composer.displayState === 'showing' &&
|
||||
state.meta.get('layout') === 'mobile',
|
||||
],
|
||||
(
|
||||
hasScrollLockStackItems: boolean,
|
||||
isMobileMenuOpen: boolean,
|
||||
isModalOpen: boolean,
|
||||
isRedesignComposerOpen: boolean,
|
||||
) =>
|
||||
hasScrollLockStackItems ||
|
||||
isMobileMenuOpen ||
|
||||
isModalOpen ||
|
||||
isRedesignComposerOpen,
|
||||
);
|
||||
|
||||
/**
|
||||
* This component locks scrolling on the body when
|
||||
* `getShouldLockBodyScroll` returns true.
|
||||
* Should only be used once per app.
|
||||
*/
|
||||
|
||||
export const BodyScrollLock: React.FC = () => {
|
||||
const shouldLockBodyScroll = useAppSelector(getShouldLockBodyScroll);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
document.documentElement.classList.toggle(
|
||||
'has-modal',
|
||||
shouldLockBodyScroll,
|
||||
);
|
||||
}, [shouldLockBodyScroll]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Utility hook for engaging the body scroll lock for a component
|
||||
* on mount & disabling it on unmount.
|
||||
*/
|
||||
export function useBodyScrollLock(active = true) {
|
||||
const dispatch = useAppDispatch();
|
||||
const id = useId();
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (active) {
|
||||
dispatch(addToScrollLockStack(id));
|
||||
} else {
|
||||
dispatch(removeFromScrollLockStack(id));
|
||||
}
|
||||
|
||||
return () => {
|
||||
dispatch(removeFromScrollLockStack(id));
|
||||
};
|
||||
}, [id, dispatch, active]);
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { collections } from './collections';
|
||||
import { composer } from './composer';
|
||||
import { emojis } from './emojis';
|
||||
import { profileEdit } from './profile_edit';
|
||||
import { scrollLockStack } from './scrollLockStack';
|
||||
|
||||
export const sliceReducers = {
|
||||
annualReport,
|
||||
@@ -10,4 +11,5 @@ export const sliceReducers = {
|
||||
composer,
|
||||
emojis,
|
||||
profileEdit,
|
||||
scrollLockStack,
|
||||
};
|
||||
|
||||
38
app/javascript/mastodon/reducers/slices/scrollLockStack.ts
Normal file
38
app/javascript/mastodon/reducers/slices/scrollLockStack.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
|
||||
/**
|
||||
* This state tracks the open/closed state of modal dialogs that were not opened
|
||||
* via our existing `openModal` actions, for example the `BottomSheet` component
|
||||
* that can be rendered standalone.
|
||||
*
|
||||
* Modals rendered like this should add an id to this state and remove it when
|
||||
* they're closed. The presence of any id in this state is used by the
|
||||
* `BodyScrollLock` component to prevent the page from scrolling behind modals.
|
||||
*/
|
||||
interface ScrollLockStackState {
|
||||
stack: string[];
|
||||
}
|
||||
|
||||
const initialState: ScrollLockStackState = {
|
||||
stack: [],
|
||||
};
|
||||
|
||||
const scrollLockStackSlice = createSlice({
|
||||
name: 'scrollLockStack',
|
||||
initialState,
|
||||
reducers: {
|
||||
addToScrollLockStack(state, action: PayloadAction<string>) {
|
||||
if (!state.stack.includes(action.payload)) {
|
||||
state.stack.push(action.payload);
|
||||
}
|
||||
},
|
||||
removeFromScrollLockStack(state, action: PayloadAction<string>) {
|
||||
state.stack = state.stack.filter((id) => id !== action.payload);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const scrollLockStack = scrollLockStackSlice.reducer;
|
||||
export const { addToScrollLockStack, removeFromScrollLockStack } =
|
||||
scrollLockStackSlice.actions;
|
||||
Reference in New Issue
Block a user