Skip to content

Commit

Permalink
fix: fix #modal #drawer screen reader focus handling
Browse files Browse the repository at this point in the history
  • Loading branch information
tujoworker committed Nov 11, 2020
1 parent 84ffaac commit 0b9c4e7
Show file tree
Hide file tree
Showing 4 changed files with 111 additions and 43 deletions.
14 changes: 9 additions & 5 deletions packages/dnb-ui-lib/src/components/modal/ModalContent.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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() {
Expand All @@ -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()
Expand Down
45 changes: 45 additions & 0 deletions packages/dnb-ui-lib/src/components/modal/__tests__/Modal.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(<Component {...props} />)
Comp.setState({
Expand All @@ -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(
<Component {...props}>
<button>button</button>
</Component>
)

// 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)
})
Expand Down
87 changes: 51 additions & 36 deletions packages/dnb-ui-lib/src/shared/component-helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

Expand All @@ -703,15 +709,15 @@ 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
) {
return // stop here
}

this._setNodesToInvalidate(TargetElement)
this._setNodesToInvalidate(targetElement)

if (Array.isArray(this.nodesToInvalidate)) {
this.nodesToInvalidate.forEach((node) => {
Expand All @@ -731,21 +737,22 @@ 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')

// 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) {
//
}
Expand Down Expand Up @@ -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))
}
Expand Down
8 changes: 6 additions & 2 deletions packages/dnb-ui-lib/stories/components/Modal.js
Original file line number Diff line number Diff line change
Expand Up @@ -185,8 +185,12 @@ export const DrawerSandbox = () => (
// class="inner_class"
>
<Modal.Inner style_type="pistachio">
Modal.Inner
{/* <FillContent /> */}
<Input>Focus me with Tab key</Input>
<Section top spacing>
<P>
<Switch label="Checked:" checked />
</P>
</Section>
</Modal.Inner>
</Modal>
</Box>
Expand Down

0 comments on commit 0b9c4e7

Please sign in to comment.