From d9cb39208706283588d37def643233fa2301201c Mon Sep 17 00:00:00 2001 From: Tobias Date: Sat, 12 Mar 2022 10:56:56 +0100 Subject: [PATCH] feat(Modal): add "bypass_invalidation_selectors" property to omit element invalidation Co-authored-by: Anders --- .../docs/uilib/components/modal/prop-table.md | 67 +++++++++-------- .../__snapshots__/Dialog.test.tsx.snap | 2 + .../__snapshots__/Drawer.test.tsx.snap | 2 + .../src/components/modal/Modal.tsx | 2 + .../src/components/modal/ModalContent.tsx | 3 +- .../components/modal/__tests__/Modal.test.tsx | 75 +++++++++++++++---- .../__snapshots__/Modal.test.tsx.snap | 5 ++ .../dnb-eufemia/src/components/modal/types.ts | 5 ++ 8 files changed, 111 insertions(+), 50 deletions(-) diff --git a/packages/dnb-design-system-portal/src/docs/uilib/components/modal/prop-table.md b/packages/dnb-design-system-portal/src/docs/uilib/components/modal/prop-table.md index 7150c88c785..e5d2a20fafe 100644 --- a/packages/dnb-design-system-portal/src/docs/uilib/components/modal/prop-table.md +++ b/packages/dnb-design-system-portal/src/docs/uilib/components/modal/prop-table.md @@ -1,36 +1,37 @@ --- --- -| Properties | Description | -| ------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `id` | _(optional)_ The id used internal for the trigger button and Modal component. | -| `rootId` / `root_id` | _(optional)_ The id used internal in the modal root element. Defaults to `root`, so the element id will be `dnb-modal-root`. | -| `contentId` / `content_id` | _(optional)_ Defines an unique identifier to a modal. Use it in case you have to refer in some way to the modal content. | -| `labelledBy` / `labelled_by` | _(optional)_ The ID of the trigger component, describing the modal content. Defaults to the internal `trigger`, so make sure You define the `title` in `triggerAttributes`. | -| `children` | _(optional)_ the content which will appear when triggering open the modal. Make sure you set `mode="custom"` to enable custom modal content. If mode is not set, the children will be sent to the [Dialog](/uilib/components/dialog) component. | -| `fullscreen` | _(optional)_ If set to `true` then the modal content will be shown as fullscreen, without showing the original content behind. Can be set to `false` to omit the auto fullscreen. Defaults to `auto`. | -| `openState` / `open_state` | _(optional)_ use this prop to control the open/close state by setting either: `opened` / `closed` or `true` / `false`. | -| `openDelay` / `open_delay` | _(optional)_ forces the modal to delay the opening. The delay is given in `ms`. | -| `disabled` | _(optional)_ Will disable the trigger button | -| `noAnimation` / `no_animation` | _(optional)_ if set to `true`, no open/close animation will be shown. Defaults to false. | -| `noAnimationOnMobile` / `no_animation_on_mobile` | _(optional)_ same as `noAnimation`, but gets triggered only if the viewport width is less than `40em`. Defaults to false. | -| `animationDuration` / `animation_duration` | _(optional)_ Duration of animation open/close in ms. Defaults to 300ms. | -| `preventClose` / `prevent_close` | _(optional)_ if set to `true` (boolean or string), then the user can't close the modal. | -| `preventOverlayClose` / `prevent_overlay_close` | _(optional)_ Disable clicking the background overlay to close the modal. PS! Pressing `esc` key will still close the modal. | -| `openModal` / `open_modal` | _(optional)_ set a function to call the callback function, once the modal should open: `open_modal={(open) => open()}` | -| `closeModal` / `close_modal` | _(optional)_ set a function to call the callback function, once the modal should close: `close_modal={(close) => close()}` | -| `focusSelector` / `focus_selector` | _(optional)_ The Modal handles the first focus – automatically. However, you can define a custom focus selector the will be used instead `focusSelector=".css-selector"`. | -| `overlayClass` / `overlay_class` | _(optional)_ give the page overlay a custom class name (maps to `dnb-modal__overlay`). | -| `contentClass` / `content_class` | _(optional)_ give the content wrapper a custom class name (maps to `dnb-modal__content`). | -| `omitTriggerButton` / `omit_trigger_button` | _(optional)_ omits default showing trigger button. | -| `trigger` | _(optional)_ provide a custom trigger component. Like `trigger={}`. It will set the focus on it when the modal gets closed. | -| `triggerAttributes` / `trigger_attributes` | _(optional)_ send along with custom HTML attributes or properties to the trigger button. | -| `dialogTitle` / `dialog_title` | _(optional)_ The aria label of the dialog when no labelled_by and no title is given. Defaults to `Vindu`. | -| `directDomReturn` / `direct_dom_return` | _(optional)_ If truthy, the modal will not open in a new DOM but directly in current DOM. Defaults to `false`. Be aware of the side effects of setting this property to `true`. | -| [Space](/uilib/components/space/properties) | _(optional)_ spacing properties like `top` or `bottom` are supported. | -| `mode` | _(deprecated/optional)_ the modal mode. Can be set to `dialog`, `drawer` or `custom`. Defaults to `dialog`. | -| `spacing` | _(deprecated/optional)_ if set to `false` then the modal content will be shown without any spacing. Defaults to `true`. | -| `closeTitle` / `close_title` | _(deprecated/optional)_ the title of the close button. Defaults to _Lukk_. | -| `hideCloseButton` | _(deprecated/optional)_ if truthy, the close button will not be shown. | -| `closeButtonAttributes` | _(deprecated/optional)_ define any valid Eufemia Button property or HTML attribute inside an object. | -| `class` or `className` | _(deprecated/optional)_ give the inner content wrapper a class name (maps to `dnb-modal__content__inner`). | +| Properties | Description | +| --------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `id` | _(optional)_ The id used internal for the trigger button and Modal component. | +| `rootId` / `root_id` | _(optional)_ The id used internal in the modal root element. Defaults to `root`, so the element id will be `dnb-modal-root`. | +| `contentId` / `content_id` | _(optional)_ Defines an unique identifier to a modal. Use it in case you have to refer in some way to the modal content. | +| `labelledBy` / `labelled_by` | _(optional)_ The ID of the trigger component, describing the modal content. Defaults to the internal `trigger`, so make sure You define the `title` in `triggerAttributes`. | +| `children` | _(optional)_ the content which will appear when triggering open the modal. Make sure you set `mode="custom"` to enable custom modal content. If mode is not set, the children will be sent to the [Dialog](/uilib/components/dialog) component. | +| `fullscreen` | _(optional)_ If set to `true` then the modal content will be shown as fullscreen, without showing the original content behind. Can be set to `false` to omit the auto fullscreen. Defaults to `auto`. | +| `openState` / `open_state` | _(optional)_ use this prop to control the open/close state by setting either: `opened` / `closed` or `true` / `false`. | +| `openDelay` / `open_delay` | _(optional)_ forces the modal to delay the opening. The delay is given in `ms`. | +| `disabled` | _(optional)_ Will disable the trigger button | +| `noAnimation` / `no_animation` | _(optional)_ if set to `true`, no open/close animation will be shown. Defaults to false. | +| `noAnimationOnMobile` / `no_animation_on_mobile` | _(optional)_ same as `noAnimation`, but gets triggered only if the viewport width is less than `40em`. Defaults to false. | +| `animationDuration` / `animation_duration` | _(optional)_ Duration of animation open/close in ms. Defaults to 300ms. | +| `preventClose` / `prevent_close` | _(optional)_ if set to `true` (boolean or string), then the user can't close the modal. | +| `preventOverlayClose` / `prevent_overlay_close` | _(optional)_ Disable clicking the background overlay to close the modal. PS! Pressing `esc` key will still close the modal. | +| `openModal` / `open_modal` | _(optional)_ set a function to call the callback function, once the modal should open: `open_modal={(open) => open()}` | +| `closeModal` / `close_modal` | _(optional)_ set a function to call the callback function, once the modal should close: `close_modal={(close) => close()}` | +| `focusSelector` / `focus_selector` | _(optional)_ The Modal handles the first focus – automatically. However, you can define a custom focus selector the will be used instead `focusSelector=".css-selector"`. | +| `overlayClass` / `overlay_class` | _(optional)_ give the page overlay a custom class name (maps to `dnb-modal__overlay`). | +| `contentClass` / `content_class` | _(optional)_ give the content wrapper a custom class name (maps to `dnb-modal__content`). | +| `omitTriggerButton` / `omit_trigger_button` | _(optional)_ omits default showing trigger button. | +| `trigger` | _(optional)_ provide a custom trigger component. Like `trigger={}`. It will set the focus on it when the modal gets closed. | +| `triggerAttributes` / `trigger_attributes` | _(optional)_ send along with custom HTML attributes or properties to the trigger button. | +| `dialogTitle` / `dialog_title` | _(optional)_ The aria label of the dialog when no labelled_by and no title is given. Defaults to `Vindu`. | +| `directDomReturn` / `direct_dom_return` | _(optional)_ If truthy, the modal will not open in a new DOM but directly in current DOM. Defaults to `false`. Be aware of the side effects of setting this property to `true`. | +| `bypassInvalidationSelectors` / `bypass_invalidation_selectors` | _(optional)_ Define an array with HTML class selectors (`['.element-selector']`) which should not get invalidated when the modal opens/closes. Use this in order to let some parts of your site still be accessible by screen readers. | +| [Space](/uilib/components/space/properties) | _(optional)_ spacing properties like `top` or `bottom` are supported. | +| `mode` | _(deprecated/optional)_ the modal mode. Can be set to `dialog`, `drawer` or `custom`. Defaults to `dialog`. | +| `spacing` | _(deprecated/optional)_ if set to `false` then the modal content will be shown without any spacing. Defaults to `true`. | +| `closeTitle` / `close_title` | _(deprecated/optional)_ the title of the close button. Defaults to _Lukk_. | +| `hideCloseButton` | _(deprecated/optional)_ if truthy, the close button will not be shown. | +| `closeButtonAttributes` | _(deprecated/optional)_ define any valid Eufemia Button property or HTML attribute inside an object. | +| `class` or `className` | _(deprecated/optional)_ give the inner content wrapper a class name (maps to `dnb-modal__content__inner`). | diff --git a/packages/dnb-eufemia/src/components/dialog/__tests__/__snapshots__/Dialog.test.tsx.snap b/packages/dnb-eufemia/src/components/dialog/__tests__/__snapshots__/Dialog.test.tsx.snap index 35a1d38b983..147b67737c7 100644 --- a/packages/dnb-eufemia/src/components/dialog/__tests__/__snapshots__/Dialog.test.tsx.snap +++ b/packages/dnb-eufemia/src/components/dialog/__tests__/__snapshots__/Dialog.test.tsx.snap @@ -704,6 +704,7 @@ exports[`Dialog component snapshot should match component snapshot 1`] = ` align_content="left" animation_duration={300} bar_content={null} + bypass_invalidation_selectors={null} class={null} className={null} class_name={null} @@ -759,6 +760,7 @@ exports[`Dialog component snapshot should match component snapshot 1`] = ` align_content="left" animation_duration={300} bar_content={null} + bypass_invalidation_selectors={null} class={null} className={null} class_name={null} diff --git a/packages/dnb-eufemia/src/components/drawer/__tests__/__snapshots__/Drawer.test.tsx.snap b/packages/dnb-eufemia/src/components/drawer/__tests__/__snapshots__/Drawer.test.tsx.snap index 494096c2914..2584a3a3748 100644 --- a/packages/dnb-eufemia/src/components/drawer/__tests__/__snapshots__/Drawer.test.tsx.snap +++ b/packages/dnb-eufemia/src/components/drawer/__tests__/__snapshots__/Drawer.test.tsx.snap @@ -702,6 +702,7 @@ exports[`Drawer component snapshot should match component snapshot 1`] = ` align_content="left" animation_duration={300} bar_content={null} + bypass_invalidation_selectors={null} class={null} className={null} class_name={null} @@ -756,6 +757,7 @@ exports[`Drawer component snapshot should match component snapshot 1`] = ` align_content="left" animation_duration={300} bar_content={null} + bypass_invalidation_selectors={null} class={null} className={null} class_name={null} diff --git a/packages/dnb-eufemia/src/components/modal/Modal.tsx b/packages/dnb-eufemia/src/components/modal/Modal.tsx index 412c5f613d9..e65bdf7ec98 100644 --- a/packages/dnb-eufemia/src/components/modal/Modal.tsx +++ b/packages/dnb-eufemia/src/components/modal/Modal.tsx @@ -420,6 +420,7 @@ class Modal extends React.PureComponent< focus_selector = null, header_content = null, bar_content = null, + bypass_invalidation_selectors = null, id, // eslint-disable-line open_state, // eslint-disable-line @@ -515,6 +516,7 @@ class Modal extends React.PureComponent< modal_content={modal_content} header_content={header_content} bar_content={bar_content} + bypass_invalidation_selectors={bypass_invalidation_selectors} close={this.close} hide={hide} title={rest.title || fallbackTitle} diff --git a/packages/dnb-eufemia/src/components/modal/ModalContent.tsx b/packages/dnb-eufemia/src/components/modal/ModalContent.tsx index d704efb2efc..c8b779ee8c3 100644 --- a/packages/dnb-eufemia/src/components/modal/ModalContent.tsx +++ b/packages/dnb-eufemia/src/components/modal/ModalContent.tsx @@ -127,7 +127,8 @@ export default class ModalContent extends React.PureComponent< // TODO: Eventually in future, make it possible to bypass invalidation from outside // '.dnb-modal--bypass_invalidation', // '.dnb-modal--bypass_invalidation_deep *', - // this.props.bypass_invalidation_selectors, + + ...(this.props?.bypass_invalidation_selectors || []), ].filter(Boolean) ) this._ii.activate() diff --git a/packages/dnb-eufemia/src/components/modal/__tests__/Modal.test.tsx b/packages/dnb-eufemia/src/components/modal/__tests__/Modal.test.tsx index 8aab013863b..5cf58625cc6 100644 --- a/packages/dnb-eufemia/src/components/modal/__tests__/Modal.test.tsx +++ b/packages/dnb-eufemia/src/components/modal/__tests__/Modal.test.tsx @@ -73,24 +73,29 @@ describe('Modal component', () => { it('should have aria-hidden and tabindex on other elements', () => { const Comp = mount( - - - , + <> + + + + + , { attachTo: attachToBody() } ) - // Check the global button Comp.find('Modal').find('button.dnb-modal__trigger').simulate('click') - expect(document.querySelector('button') instanceof HTMLElement).toBe( - true - ) + + // Check the global button expect( - document.querySelector('button').hasAttribute('aria-hidden') + document.querySelector('button.bypass-me') instanceof HTMLElement ).toBe(true) - expect(document.querySelector('button').getAttribute('tabindex')).toBe( - '-1' - ) - Comp.update() + expect( + document + .querySelector('button.bypass-me') + .hasAttribute('aria-hidden') + ).toBe(true) + expect( + document.querySelector('button.bypass-me').getAttribute('tabindex') + ).toBe('-1') expect( Comp.find('.dnb-modal__content') .instance() @@ -98,7 +103,7 @@ describe('Modal component', () => { ).toBe(false) expect( Comp.find('.dnb-modal__content') - .find('button') + .find('button.but-not-me') .instance() .hasAttribute('aria-hidden') ).toBe(false) @@ -106,11 +111,49 @@ describe('Modal component', () => { // And close it again Comp.find('button.dnb-modal__close-button').simulate('click') expect( - document.querySelector('button').hasAttribute('aria-hidden') + document + .querySelector('button.bypass-me') + .hasAttribute('aria-hidden') + ).toBe(false) + expect( + document.querySelector('button.bypass-me').hasAttribute('tabindex') ).toBe(false) - expect(document.querySelector('button').hasAttribute('tabindex')).toBe( - false + }) + + it('should bypass elements defined in bypass_invalidation_selectors', () => { + const Comp = mount( + <> + + + + content + + , + { attachTo: attachToBody() } ) + + Comp.find('Modal').find('button.dnb-modal__trigger').simulate('click') + + expect( + document + .querySelector('button.bypass-me') + .hasAttribute('aria-hidden') + ).toBe(false) + expect( + document.querySelector('button.bypass-me').hasAttribute('tabindex') + ).toBe(false) + + expect( + document + .querySelector('button.but-not-me') + .getAttribute('aria-hidden') + ).toBe('true') + expect( + document.querySelector('button.but-not-me').getAttribute('tabindex') + ).toBe('-1') }) it('has to have the correct title', () => { diff --git a/packages/dnb-eufemia/src/components/modal/__tests__/__snapshots__/Modal.test.tsx.snap b/packages/dnb-eufemia/src/components/modal/__tests__/__snapshots__/Modal.test.tsx.snap index d2a21efdab1..bb0004ffdb9 100644 --- a/packages/dnb-eufemia/src/components/modal/__tests__/__snapshots__/Modal.test.tsx.snap +++ b/packages/dnb-eufemia/src/components/modal/__tests__/__snapshots__/Modal.test.tsx.snap @@ -699,6 +699,7 @@ exports[`Modal component have to match snapshot 1`] = ` align_content="left" animation_duration={300} bar_content={null} + bypass_invalidation_selectors={null} class={null} className={null} class_name={null} @@ -745,6 +746,7 @@ exports[`Modal component have to match snapshot 1`] = ` align_content="left" animation_duration={300} bar_content={null} + bypass_invalidation_selectors={null} class={null} className={null} class_name={null} @@ -798,6 +800,7 @@ exports[`Modal component have to match snapshot 1`] = ` } } alignContent="left" + bypass_invalidation_selectors={null} class={null} className={null} class_name={null} @@ -830,6 +833,7 @@ exports[`Modal component have to match snapshot 1`] = ` "props": Object {}, } } + bypass_invalidation_selectors={null} className="dnb-dialog dnb-dialog--auto-fullscreen dnb-core-style dnb-dialog--information dnb-dialog--spacing dnb-dialog__align--left dnb-dialog--no-animation" class_name={null} close_modal={null} @@ -855,6 +859,7 @@ exports[`Modal component have to match snapshot 1`] = ` "props": Object {}, } } + bypass_invalidation_selectors={null} class={null} className="dnb-dialog dnb-dialog--auto-fullscreen dnb-core-style dnb-dialog--information dnb-dialog--spacing dnb-dialog__align--left dnb-dialog--no-animation" class_name={null} diff --git a/packages/dnb-eufemia/src/components/modal/types.ts b/packages/dnb-eufemia/src/components/modal/types.ts index 45d3ae24287..d23479bac76 100644 --- a/packages/dnb-eufemia/src/components/modal/types.ts +++ b/packages/dnb-eufemia/src/components/modal/types.ts @@ -307,6 +307,11 @@ export interface ModalContentProps { */ overlay_class?: string + /** + * Define an array with HTML class selectors (`['.element-selector']`) which should not get invalidated when the modal opens/closes. Use this in order to let some parts of your site still be accessible by screen readers. + */ + bypass_invalidation_selectors?: Array + /** * For internal usage * Will close the modal