From 93174555315350c86bcf20b5751ee44a7d1a285b Mon Sep 17 00:00:00 2001 From: Anton Korzunov Date: Sun, 21 Aug 2022 11:17:52 +1000 Subject: [PATCH] fix: do not hide aria-live elements, fixes #10 --- __tests__/__snapshots__/index.tsx.snap | 2 + __tests__/index.tsx | 127 +++++++++++------- src/index.ts | 178 +++++++++++++------------ 3 files changed, 177 insertions(+), 130 deletions(-) diff --git a/__tests__/__snapshots__/index.tsx.snap b/__tests__/__snapshots__/index.tsx.snap index 2def7c8..59ba73c 100644 --- a/__tests__/__snapshots__/index.tsx.snap +++ b/__tests__/__snapshots__/index.tsx.snap @@ -1,5 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`Specs handles aria-live 1`] = `"
kept
hidden
not-hidden life
hidden life
not-hidden
"`; + exports[`Specs hides cross 1`] = `"
hide me 1
not me 2
not me 3
hide me 4
svg
I am already hidden! 5
dont touch me 6
"`; exports[`Specs hides cross 2`] = `"
hide me 1
not me 2
not me 3
hide me 4
svg
I am already hidden! 5
dont touch me 6
"`; diff --git a/__tests__/index.tsx b/__tests__/index.tsx index 7bd6bc1..a282090 100644 --- a/__tests__/index.tsx +++ b/__tests__/index.tsx @@ -1,6 +1,9 @@ +import { render } from '@testing-library/react'; import * as React from 'react'; -import {mount} from 'enzyme'; -import {hideOthers} from "../src"; + +import { RefObject } from 'react'; + +import { hideOthers } from '../src'; describe('Specs', () => { const factory = () => { @@ -8,7 +11,7 @@ describe('Specs', () => { const target1 = React.createRef(); const target2 = React.createRef(); const targetOutside1 = React.createRef(); - const wrapper = mount( + const wrapper = render(
hide me 1
@@ -25,103 +28,105 @@ describe('Specs', () => {
dont touch me 6
); - const base = wrapper.html(); + + const html = () => wrapper.baseElement.firstElementChild!.innerHTML; return { - base, wrapper, - parent, target1, target2, targetOutside1, - } + base: html(), + wrapper: { + html, + }, + parent, + target1, + target2, + targetOutside1, + }; }; - const getNearestAttribute = (node, name, parent) => { + const getNearestAttribute = (node: Element, name: string, parent: RefObject): any => { const attr = node.getAttribute(name); + if (attr) { return attr; } - if (node === parent || !node.parentNode) { + + if (node === parent.current || !node.parentNode) { return null; } - return getNearestAttribute(node.parentNode, name, parent) - } + + return getNearestAttribute(node.parentNode as any, name, parent); + }; it('hides single', () => { - const { - base, parent, target1, target2, targetOutside1, wrapper - } = factory(); + const { base, parent, target1, target2, targetOutside1, wrapper } = factory(); const unhide = hideOthers(target1.current, parent.current); - expect(wrapper.update().html()).toMatchSnapshot(); + expect(wrapper.html()).toMatchSnapshot(); expect(getNearestAttribute(target1.current, 'aria-hidden', parent)).toBe(null); - expect(getNearestAttribute(target2.current, 'aria-hidden', parent)).toBe("true"); - expect(getNearestAttribute(targetOutside1.current, 'aria-hidden', parent)).toBe("true"); + expect(getNearestAttribute(target2.current, 'aria-hidden', parent)).toBe('true'); + expect(getNearestAttribute(targetOutside1.current, 'aria-hidden', parent)).toBe('true'); unhide(); expect(wrapper.html()).toEqual(base); }); it('hides multiple', () => { - const { - base, parent, target1, target2, targetOutside1, wrapper - } = factory(); + const { base, parent, target1, target2, targetOutside1, wrapper } = factory(); const unhide = hideOthers([target1.current, target2.current], parent.current); expect(wrapper.html()).toMatchSnapshot(); expect(getNearestAttribute(target1.current, 'aria-hidden', parent)).toBe(null); expect(getNearestAttribute(target2.current, 'aria-hidden', parent)).toBe(null); - expect(getNearestAttribute(targetOutside1.current, 'aria-hidden', parent)).toBe("true"); + expect(getNearestAttribute(targetOutside1.current, 'aria-hidden', parent)).toBe('true'); unhide(); expect(wrapper.html()).toEqual(base); }); it('hides cross', () => { - const { - base, parent, target1, target2, targetOutside1, wrapper - } = factory(); + const { base, parent, target1, target2, targetOutside1, wrapper } = factory(); const unhide1 = hideOthers(target1.current, parent.current); expect(wrapper.html()).toMatchSnapshot(); expect(getNearestAttribute(target1.current, 'aria-hidden', parent)).toBe(null); - expect(getNearestAttribute(target2.current, 'aria-hidden', parent)).toBe("true"); - expect(getNearestAttribute(targetOutside1.current, 'aria-hidden', parent)).toBe("true"); - expect(getNearestAttribute(targetOutside1.current, 'data-aria-hidden', parent)).toBe("true"); + expect(getNearestAttribute(target2.current, 'aria-hidden', parent)).toBe('true'); + expect(getNearestAttribute(targetOutside1.current, 'aria-hidden', parent)).toBe('true'); + expect(getNearestAttribute(targetOutside1.current, 'data-aria-hidden', parent)).toBe('true'); const unhide2 = hideOthers(target2.current, parent.current); - expect(getNearestAttribute(target1.current, 'aria-hidden', parent)).toBe("true"); - expect(getNearestAttribute(target2.current, 'aria-hidden', parent)).toBe("true"); - expect(getNearestAttribute(targetOutside1.current, 'aria-hidden', parent)).toBe("true"); - expect(getNearestAttribute(targetOutside1.current, 'data-aria-hidden', parent)).toBe("true"); + expect(getNearestAttribute(target1.current, 'aria-hidden', parent)).toBe('true'); + expect(getNearestAttribute(target2.current, 'aria-hidden', parent)).toBe('true'); + expect(getNearestAttribute(targetOutside1.current, 'aria-hidden', parent)).toBe('true'); + expect(getNearestAttribute(targetOutside1.current, 'data-aria-hidden', parent)).toBe('true'); expect(wrapper.html()).toMatchSnapshot(); unhide1(); - expect(getNearestAttribute(target1.current, 'aria-hidden', parent)).toBe("true"); + expect(getNearestAttribute(target1.current, 'aria-hidden', parent)).toBe('true'); expect(getNearestAttribute(target2.current, 'aria-hidden', parent)).toBe(null); - expect(getNearestAttribute(targetOutside1.current, 'aria-hidden', parent)).toBe("true"); - expect(getNearestAttribute(targetOutside1.current, 'data-aria-hidden', parent)).toBe("true"); + expect(getNearestAttribute(targetOutside1.current, 'aria-hidden', parent)).toBe('true'); + expect(getNearestAttribute(targetOutside1.current, 'data-aria-hidden', parent)).toBe('true'); expect(wrapper.html()).toMatchSnapshot(); unhide2(); - expect(wrapper.html()).toEqual(base) + expect(wrapper.html()).toEqual(base); }); it('hides cross markers', () => { - const { - base, parent, target1, target2, targetOutside1, wrapper - } = factory(); + const { base, parent, target1, target2, targetOutside1, wrapper } = factory(); const unhide1 = hideOthers(target1.current, parent.current, 'marker1'); - expect(getNearestAttribute(targetOutside1.current, 'marker1', parent)).toBe("true"); + expect(getNearestAttribute(targetOutside1.current, 'marker1', parent)).toBe('true'); const unhide2 = hideOthers(target2.current, parent.current, 'marker2'); - expect(getNearestAttribute(targetOutside1.current, 'marker1', parent)).toBe("true"); - expect(getNearestAttribute(targetOutside1.current, 'marker2', parent)).toBe("true"); + expect(getNearestAttribute(targetOutside1.current, 'marker1', parent)).toBe('true'); + expect(getNearestAttribute(targetOutside1.current, 'marker2', parent)).toBe('true'); unhide1(); @@ -130,22 +135,48 @@ describe('Specs', () => { expect(wrapper.html()).toMatchSnapshot(); unhide2(); - expect(wrapper.html()).toEqual(base) + expect(wrapper.html()).toEqual(base); + }); + + it('handles aria-live', () => { + const wrapper = render( +
+
+
hidden
+
not-hidden life
+
hidden life
+
+
not-hidden
+
+ ); + const root = wrapper.baseElement.firstElementChild!; + + const unhide = hideOthers(root.firstElementChild!.lastElementChild!, root as any); + + expect(getNearestAttribute(root.querySelector('[aria-live]')!, 'aria-hidden', { current: root })).toBe(null); + expect(getNearestAttribute(root.querySelector('#to-be-hidden')!, 'aria-hidden', { current: root })).toBe('true'); + + expect(wrapper.baseElement.innerHTML).toMatchInlineSnapshot( + `"
hidden
not-hidden life
hidden life
not-hidden
"` + ); + + unhide(); }); + it('works on IE11', () => { // Simulate IE11 DOM Node implementation. HTMLElement.prototype.contains = Node.prototype.contains; + // @ts-ignore delete Node.prototype.contains; - const { - base, parent, target1, target2, targetOutside1, wrapper - } = factory(); + + const { base, parent, target1, target2, targetOutside1, wrapper } = factory(); const unhide = hideOthers(target1.current, parent.current); - expect(wrapper.update().html()).toMatchSnapshot(); + expect(wrapper.html()).toMatchSnapshot(); expect(getNearestAttribute(target1.current, 'aria-hidden', parent)).toBe(null); - expect(getNearestAttribute(target2.current, 'aria-hidden', parent)).toBe("true"); - expect(getNearestAttribute(targetOutside1.current, 'aria-hidden', parent)).toBe("true"); + expect(getNearestAttribute(target2.current, 'aria-hidden', parent)).toBe('true'); + expect(getNearestAttribute(targetOutside1.current, 'aria-hidden', parent)).toBe('true'); unhide(); expect(wrapper.html()).toEqual(base); diff --git a/src/index.ts b/src/index.ts index 5e62f84..d77fe74 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,12 +1,13 @@ export type Undo = () => void; -const getDefaultParent = (originalTarget: Element | Element[]) => { - if (typeof document === 'undefined') { - return null; - } +const getDefaultParent = (originalTarget: Element | Element[]): Element | null => { + if (typeof document === 'undefined') { + return null; + } + + const sampleTarget = Array.isArray(originalTarget) ? originalTarget[0] : originalTarget; - const sampleTarget = Array.isArray(originalTarget) ? originalTarget[0] : originalTarget; - return sampleTarget.ownerDocument.body; + return sampleTarget.ownerDocument.body; }; let counterMap = new WeakMap(); @@ -21,89 +22,102 @@ let lockCount = 0; * @return {Undo} undo command */ export const hideOthers = (originalTarget: Element | Element[], parentNode = getDefaultParent(originalTarget), markerName = "data-aria-hidden"): Undo => { - const targets = Array.isArray(originalTarget) ? originalTarget : [originalTarget]; - - if (!markerMap[markerName]) { - markerMap[markerName] = new WeakMap(); - } - const markerCounter = markerMap[markerName]; - const hiddenNodes: Element[] = []; - - const elementsToKeep = new Set(); - const keep = ((el:Node | undefined) =>{ - if(!el || elementsToKeep.has(el)){ - return; - } - elementsToKeep.add(el); - keep(el.parentNode); - }); - targets.forEach(keep) - - const deep = (parent: Element | null) => { - if (!parent || targets.indexOf(parent) >= 0) { - return; - } + const targets = Array.isArray(originalTarget) ? originalTarget : [originalTarget]; - Array.prototype.forEach.call(parent.children, (node: Element) => { - if (elementsToKeep.has(node)) { - deep(node); - } else { - const attr = node.getAttribute('aria-hidden'); - const alreadyHidden = attr !== null && attr !== 'false'; - const counterValue = (counterMap.get(node) || 0) + 1; - const markerValue = (markerCounter.get(node) || 0) + 1; - - counterMap.set(node, counterValue); - markerCounter.set(node, markerValue); - hiddenNodes.push(node); - - if (counterValue === 1 && alreadyHidden) { - uncontrolledNodes.set(node, true); - } + if (!markerMap[markerName]) { + markerMap[markerName] = new WeakMap(); + } - if (markerValue === 1) { - node.setAttribute(markerName, 'true'); - } + const markerCounter = markerMap[markerName]; + const hiddenNodes: Element[] = []; - if (!alreadyHidden) { - node.setAttribute('aria-hidden', 'true') + const elementsToKeep = new Set(); + const elementsToStop = new Set(targets); + const keep = ((el: Node | undefined) => { + if (!el || elementsToKeep.has(el)) { + return; } - } - }) - }; - - deep(parentNode); - elementsToKeep.clear(); - lockCount++; - - return () => { - hiddenNodes.forEach(node => { - const counterValue = counterMap.get(node) - 1; - const markerValue = markerCounter.get(node) - 1; - - counterMap.set(node, counterValue); - markerCounter.set(node, markerValue); + elementsToKeep.add(el); + keep(el.parentNode!); + }); + targets.forEach(keep); + + if (parentNode) { + // we should not hide ariaLive elements - https://github.com/theKashey/aria-hidden/issues/10 + parentNode!.querySelectorAll('[aria-live]').forEach(el => { + elementsToStop.add(el); + keep(el); + }); + } - if (!counterValue) { - if (!uncontrolledNodes.has(node)) { - node.removeAttribute('aria-hidden') + const deep = (parent: Element | null) => { + if (!parent || elementsToStop.has(parent)) { + return; } - uncontrolledNodes.delete(node) - } - if (!markerValue) { - node.removeAttribute(markerName); - } - }); - - lockCount--; - if (!lockCount) { - // clear - counterMap = new WeakMap(); - counterMap = new WeakMap(); - uncontrolledNodes = new WeakMap(); - markerMap = {}; + Array.prototype.forEach.call(parent.children, (node: Element) => { + if (elementsToKeep.has(node)) { + deep(node); + } else { + const attr = node.getAttribute('aria-hidden'); + const alreadyHidden = attr !== null && attr !== 'false'; + const counterValue = (counterMap.get(node) || 0) + 1; + const markerValue = (markerCounter.get(node) || 0) + 1; + + counterMap.set(node, counterValue); + markerCounter.set(node, markerValue); + hiddenNodes.push(node); + + if (counterValue === 1 && alreadyHidden) { + uncontrolledNodes.set(node, true); + } + + if (markerValue === 1) { + node.setAttribute(markerName, 'true'); + } + + if (!alreadyHidden) { + node.setAttribute('aria-hidden', 'true') + } + } + }) + }; + + deep(parentNode); + elementsToKeep.clear(); + + lockCount++; + + return () => { + hiddenNodes.forEach(node => { + const counterValue = counterMap.get(node)! - 1; + const markerValue = markerCounter.get(node)! - 1; + + counterMap.set(node, counterValue); + markerCounter.set(node, markerValue); + + if (!counterValue) { + if (!uncontrolledNodes.has(node)) { + node.removeAttribute('aria-hidden') + } + + uncontrolledNodes.delete(node) + } + + if (!markerValue) { + node.removeAttribute(markerName); + } + }); + + lockCount--; + + if (!lockCount) { + // clear + counterMap = new WeakMap(); + counterMap = new WeakMap(); + uncontrolledNodes = new WeakMap(); + markerMap = {}; + } } - } };