From 5441005efd00943632211202f393088c2e44913b Mon Sep 17 00:00:00 2001 From: Mitchell Austin Date: Thu, 28 Sep 2023 19:26:33 -0700 Subject: [PATCH 1/3] Add unit tests for body class name effects --- packages/components/src/modal/test/index.tsx | 91 +++++++++++++++++++- 1 file changed, 90 insertions(+), 1 deletion(-) diff --git a/packages/components/src/modal/test/index.tsx b/packages/components/src/modal/test/index.tsx index 9073735e94dbe..99f68345eec90 100644 --- a/packages/components/src/modal/test/index.tsx +++ b/packages/components/src/modal/test/index.tsx @@ -7,7 +7,7 @@ import userEvent from '@testing-library/user-event'; /** * WordPress dependencies */ -import { useState } from '@wordpress/element'; +import { useEffect, useState } from '@wordpress/element'; /** * Internal dependencies @@ -388,4 +388,93 @@ describe( 'Modal', () => { expect( opener ).toHaveFocus(); } ); } ); + + describe( 'Body class name', () => { + const overrideClass = 'is-any-open'; + const BodyClassDemo = () => { + const [ isAShown, setIsAShown ] = useState( false ); + const [ isA1Shown, setIsA1Shown ] = useState( false ); + const [ isBShown, setIsBShown ] = useState( false ); + const [ isClassOverriden, setIsClassOverriden ] = useState( false ); + useEffect( () => { + const toggles: ( e: KeyboardEvent ) => void = ( { + key, + metaKey, + } ) => { + if ( key === 'a' ) { + if ( metaKey ) return setIsA1Shown( ( v ) => ! v ); + return setIsAShown( ( v ) => ! v ); + } + if ( key === 'b' ) return setIsBShown( ( v ) => ! v ); + if ( key === 'c' ) + return setIsClassOverriden( ( v ) => ! v ); + }; + document.addEventListener( 'keydown', toggles ); + return () => + void document.removeEventListener( 'keydown', toggles ); + }, [] ); + return ( + <> + { isAShown && ( + setIsAShown( false ) } + > +

Modal A contents

+ { isA1Shown && ( + + setIsA1Shown( false ) + } + > +

Modal A1 contents

+
+ ) } +
+ ) } + { isBShown && ( + setIsBShown( false ) } + > +

Modal B contents

+
+ ) } + + ); + }; + + it( 'is added and removed when modal opens and closes including when closed due to another modal opening', async () => { + const user = userEvent.setup(); + + const { baseElement } = render( ); + + await user.keyboard( 'a' ); // Opens modal A. + expect( baseElement ).toHaveClass( 'is-A-open' ); + + await user.keyboard( 'b' ); // Opens modal B > closes modal A. + expect( baseElement ).toHaveClass( 'is-B-open' ); + expect( baseElement ).not.toHaveClass( 'is-A-open' ); + + await user.keyboard( 'b' ); // Closes modal B. + expect( baseElement ).not.toHaveClass( 'is-B-open' ); + } ); + + it( 'is removed even when prop changes while nested modal is open', async () => { + const user = userEvent.setup(); + + const { baseElement } = render( ); + + await user.keyboard( 'a' ); // Opens modal A. + await user.keyboard( '{Meta>}a{/Meta}' ); // Opens nested modal. + await user.keyboard( 'c' ); // Changes `bodyOpenClassName`. + await user.keyboard( 'a' ); // Closes modal A. + expect( baseElement ).not.toHaveClass( 'is-A-open' ); + } ); + } ); } ); From 3da763705600a4f3668f3fbe441195b463ae5c51 Mon Sep 17 00:00:00 2001 From: Mitchell Austin Date: Thu, 21 Sep 2023 22:04:20 -0700 Subject: [PATCH 2/3] Fix and enhance body class attribute effect --- packages/components/src/modal/index.tsx | 32 +++++++++++++------------ 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/packages/components/src/modal/index.tsx b/packages/components/src/modal/index.tsx index 041c592166ab7..b1bee51805f78 100644 --- a/packages/components/src/modal/index.tsx +++ b/packages/components/src/modal/index.tsx @@ -43,12 +43,12 @@ import StyleProvider from '../style-provider'; import type { ModalProps } from './types'; // Used to track and dismiss the prior modal when another opens unless nested. -const level0Dismissers: MutableRefObject< - ModalProps[ 'onRequestClose' ] | undefined ->[] = []; -const ModalContext = createContext( level0Dismissers ); +const ModalContext = createContext< + MutableRefObject< ModalProps[ 'onRequestClose' ] | undefined >[] +>( [] ); -let isBodyOpenClassActive = false; +// Used to track body class names applied while modals are open. +const bodyOpenClasses = new Map< string, number >(); function UnforwardedModal( props: ModalProps, @@ -146,7 +146,7 @@ function UnforwardedModal( // one should remain open at a time and the list enables closing prior ones. const dismissers = useContext( ModalContext ); // Used for the tracking and dismissing any nested modals. - const nestedDismissers = useRef< typeof level0Dismissers >( [] ); + const nestedDismissers = useRef< typeof dismissers >( [] ); // Updates the stack tracking open modals at this level and calls // onRequestClose for any prior and/or nested modals as applicable. @@ -162,20 +162,22 @@ function UnforwardedModal( }; }, [ dismissers ] ); - const isLevel0 = dismissers === level0Dismissers; // Adds/removes the value of bodyOpenClassName to body element. useEffect( () => { - if ( ! isBodyOpenClassActive ) { - isBodyOpenClassActive = true; - document.body.classList.add( bodyOpenClassName ); - } + const theClass = bodyOpenClassName; + const oneMore = 1 + ( bodyOpenClasses.get( theClass ) ?? 0 ); + bodyOpenClasses.set( theClass, oneMore ); + document.body.classList.add( bodyOpenClassName ); return () => { - if ( isLevel0 && dismissers.length === 0 ) { - document.body.classList.remove( bodyOpenClassName ); - isBodyOpenClassActive = false; + const oneLess = bodyOpenClasses.get( theClass )! - 1; + if ( oneLess === 0 ) { + document.body.classList.remove( theClass ); + bodyOpenClasses.delete( theClass ); + } else { + bodyOpenClasses.set( theClass, oneLess ); } }; - }, [ bodyOpenClassName, dismissers, isLevel0 ] ); + }, [ bodyOpenClassName ] ); // Calls the isContentScrollable callback when the Modal children container resizes. useLayoutEffect( () => { From f4b2112f3f05be466f9101958b45ad4ea7b39f29 Mon Sep 17 00:00:00 2001 From: Mitchell Austin Date: Tue, 19 Dec 2023 17:35:30 -0800 Subject: [PATCH 3/3] Add changelog entry --- packages/components/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index eccd9e639e5f2..ecf4d3628c083 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -14,6 +14,7 @@ ### Enhancements - `DateTimePicker`: Adjustment of the dot position on DayButton and expansion of the button area. ([#55502](https://github.com/WordPress/gutenberg/pull/55502)). +- `Modal`: Improve application of body class names ([#55430](https://github.com/WordPress/gutenberg/pull/55430)). ### Experimental