diff --git a/packages/dnb-ui-lib/src/components/modal/ModalContent.js b/packages/dnb-ui-lib/src/components/modal/ModalContent.js index 21d168fefd9..ac84f987cba 100644 --- a/packages/dnb-ui-lib/src/components/modal/ModalContent.js +++ b/packages/dnb-ui-lib/src/components/modal/ModalContent.js @@ -27,8 +27,9 @@ import Context from '../../shared/Context' export default class ModalContent extends React.PureComponent { static propTypes = { modal_content: PropTypes.node.isRequired, - hide: PropTypes.bool, mode: PropTypes.string, + hide: PropTypes.bool, + root_id: PropTypes.string, labelled_by: PropTypes.string, content_id: PropTypes.string, title: PropTypes.node, @@ -73,6 +74,7 @@ export default class ModalContent extends React.PureComponent { static defaultProps = { mode: null, hide: null, + root_id: null, labelled_by: null, content_id: null, title: null, @@ -102,9 +104,11 @@ export default class ModalContent extends React.PureComponent { super(props) this._contentRef = React.createRef() this._id = makeUniqueId() - this._ii = new InteractionInvalidation().setBypassSelector( - '.dnb-modal__content' - ) + this._ii = new InteractionInvalidation() + this._ii.setBypassSelector([ + '.dnb-modal__content', + `#dnb-modal-${props.root_id || 'root'}` + ]) } componentDidMount() { @@ -126,7 +130,7 @@ export default class ModalContent extends React.PureComponent { try { this._contentRef.current.focus() // in case the button is disabled const focusElement = this._contentRef.current.querySelector( - '.dnb-h--xx-large:first-of-type, .dnb-h--large:first-of-type, .dnb-modal__close-button' + 'h1:first-of-type, h2:first-of-type, .dnb-modal__close-button' ) if (focusElement) { focusElement.focus() diff --git a/packages/dnb-ui-lib/src/components/modal/__tests__/Modal.test.js b/packages/dnb-ui-lib/src/components/modal/__tests__/Modal.test.js index ebbaf813b3d..22b1e181d46 100644 --- a/packages/dnb-ui-lib/src/components/modal/__tests__/Modal.test.js +++ b/packages/dnb-ui-lib/src/components/modal/__tests__/Modal.test.js @@ -29,6 +29,11 @@ props.close_title = 'close_title' props.direct_dom_return = true props.no_animation = true +beforeAll(() => { + const button = document.createElement('BUTTON') + document.body.appendChild(button) +}) + describe('Modal component', () => { const Comp = mount() Comp.setState({ @@ -37,6 +42,46 @@ describe('Modal component', () => { it('have to match snapshot', () => { expect(toJson(Comp)).toMatchSnapshot() }) + it('should have aria-hidden and tabindex on other elements', () => { + const Comp = mount( + + + + ) + + // Check the global button + Comp.find('Modal').find('button.dnb-modal__trigger').simulate('click') + expect(document.querySelector('button') instanceof HTMLElement).toBe( + true + ) + expect( + document.querySelector('button').hasAttribute('aria-hidden') + ).toBe(true) + expect(document.querySelector('button').getAttribute('tabindex')).toBe( + '-1' + ) + Comp.update() + expect( + Comp.find('.dnb-modal__content') + .instance() + .hasAttribute('aria-hidden') + ).toBe(false) + expect( + Comp.find('.dnb-modal__content') + .find('button') + .instance() + .hasAttribute('aria-hidden') + ).toBe(false) + + // And close it again + Comp.find('button.dnb-modal__close-button').simulate('click') + expect( + document.querySelector('button').hasAttribute('aria-hidden') + ).toBe(false) + expect(document.querySelector('button').hasAttribute('tabindex')).toBe( + false + ) + }) it('has to have the correct title', () => { expect(Comp.find('h1').text()).toBe(props.title) }) diff --git a/packages/dnb-ui-lib/src/shared/component-helper.js b/packages/dnb-ui-lib/src/shared/component-helper.js index 86f5fcd95c3..c8f1a55d2a7 100644 --- a/packages/dnb-ui-lib/src/shared/component-helper.js +++ b/packages/dnb-ui-lib/src/shared/component-helper.js @@ -678,23 +678,29 @@ export const convertJsxToString = (elements, separator = undefined) => { export class InteractionInvalidation { constructor() { - this.bypassSelector = '.not-specified' + this.bypassElement = null + this.bypassSelectors = [] return this } - setBypassSelector(bypassSelector = null) { - if (bypassSelector instanceof HTMLElement) { - this.bypassElement = bypassSelector - } else { - this.bypassElement = null - this.bypassSelector = bypassSelector || '.not-specified' + setBypassElement(bypassElement) { + if (bypassElement instanceof HTMLElement) { + this.bypassElement = bypassElement } return this } - activate(TargetElement = null) { + setBypassSelector(bypassSelector) { + if (!Array.isArray(bypassSelector)) { + bypassSelector = [bypassSelector] + } + this.bypassSelectors = bypassSelector + return this + } + + activate(targetElement = null) { if (!this.nodesToInvalidate) { - this._runInvalidaiton(TargetElement) + this._runInvalidaiton(targetElement) } } @@ -703,7 +709,7 @@ export class InteractionInvalidation { this.nodesToInvalidate = null } - _runInvalidaiton(TargetElement) { + _runInvalidaiton(targetElement) { if ( typeof document === 'undefined' // || isTouchDevice() // as for now, we do the same on touch devices @@ -711,7 +717,7 @@ export class InteractionInvalidation { return // stop here } - this._setNodesToInvalidate(TargetElement) + this._setNodesToInvalidate(targetElement) if (Array.isArray(this.nodesToInvalidate)) { this.nodesToInvalidate.forEach((node) => { @@ -731,13 +737,14 @@ export class InteractionInvalidation { ) { node._orig_ariahidden = node.getAttribute('aria-hidden') } - if ( - node && - typeof node._orig_style === 'undefined' && - node.hasAttribute('style') - ) { - node._orig_style = node.getAttribute('style') - } + // Skip the outline for now - or does it give a value? + // if ( + // node && + // typeof node._orig_outline === 'undefined' && + // node.style.outline + // ) { + // node._orig_outline = node.style.outline + // } node.setAttribute('tabindex', '-1') node.setAttribute('aria-hidden', 'true') @@ -745,7 +752,7 @@ export class InteractionInvalidation { // tabindex=-1 does not prevent the mouse from focusing the node (which // would show a focus outline around the element). prevent this by disabling // outline styles while the modal is open - node.style.outline = 'none' + // node.style.outline = 'none' } catch (e) { // } @@ -775,39 +782,47 @@ export class InteractionInvalidation { } else { node.removeAttribute('aria-hidden') } - if (node && typeof node._orig_style !== 'undefined') { - node.setAttribute('style', node._orig_style) - node._orig_style = null - delete node._orig_style - } else { - node.removeAttribute('style') - } + + // Skip the outline for now - or does it give a value? + // if (node && typeof node._orig_outline !== 'undefined') { + // node.style.outline = node._orig_outline + // delete node._orig_outline + // } else if(node.style) { + // node.style.outline = null + // } } catch (e) { // } }) } - _setNodesToInvalidate(TargetElement = null) { + _setNodesToInvalidate(targetElement = null) { if (typeof document === 'undefined') { return // stop here } - if (typeof TargetElement === 'string') { - TargetElement = document.querySelector(TargetElement) + if (typeof targetElement === 'string') { + targetElement = document.querySelector(targetElement) } - const skipTheseNodes = Array.from( - (this.bypassElement || document).querySelectorAll( - this.bypassSelector ? `${this.bypassSelector} *` : '*' - ) - ) + const skipTheseNodes = + this.bypassSelectors && this.bypassSelectors.length > 0 + ? Array.from( + (this.bypassElement || document).querySelectorAll( + this.bypassSelectors + ? this.bypassSelectors.map((s) => `${s} *`).join(', ') + : '*' + ) + ) + : [] // by only finding elements that do not have tabindex="-1" we ensure we don't // corrupt the previous state of the element if a modal was already open this.nodesToInvalidate = Array.from( - (TargetElement || document).querySelectorAll( - `body *:not(${this.bypassSelector}):not(script)` + (targetElement || document).querySelectorAll( + `body *${this.bypassSelectors + .map((s) => `:not(${s})`) + .join('')}:not(script):not(style):not(path)` ) ).filter((node) => !skipTheseNodes.includes(node)) } diff --git a/packages/dnb-ui-lib/stories/components/Modal.js b/packages/dnb-ui-lib/stories/components/Modal.js index e462c18007a..e700471fa53 100644 --- a/packages/dnb-ui-lib/stories/components/Modal.js +++ b/packages/dnb-ui-lib/stories/components/Modal.js @@ -185,8 +185,12 @@ export const DrawerSandbox = () => ( // class="inner_class" > - Modal.Inner - {/* */} + Focus me with Tab key +
+

+ +

+