index.js 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
  1. const getDefaultParent = (originalTarget) => {
  2. if (typeof document === 'undefined') {
  3. return null;
  4. }
  5. const sampleTarget = Array.isArray(originalTarget) ? originalTarget[0] : originalTarget;
  6. return sampleTarget.ownerDocument.body;
  7. };
  8. let counterMap = new WeakMap();
  9. let uncontrolledNodes = new WeakMap();
  10. let markerMap = {};
  11. let lockCount = 0;
  12. const unwrapHost = (node) => node && (node.host || unwrapHost(node.parentNode));
  13. const correctTargets = (parent, targets) => targets
  14. .map((target) => {
  15. if (parent.contains(target)) {
  16. return target;
  17. }
  18. const correctedTarget = unwrapHost(target);
  19. if (correctedTarget && parent.contains(correctedTarget)) {
  20. return correctedTarget;
  21. }
  22. console.error('aria-hidden', target, 'in not contained inside', parent, '. Doing nothing');
  23. return null;
  24. })
  25. .filter((x) => Boolean(x));
  26. /**
  27. * Marks everything except given node(or nodes) as aria-hidden
  28. * @param {Element | Element[]} originalTarget - elements to keep on the page
  29. * @param [parentNode] - top element, defaults to document.body
  30. * @param {String} [markerName] - a special attribute to mark every node
  31. * @param {String} [controlAttribute] - html Attribute to control
  32. * @return {Undo} undo command
  33. */
  34. const applyAttributeToOthers = (originalTarget, parentNode, markerName, controlAttribute) => {
  35. const targets = correctTargets(parentNode, Array.isArray(originalTarget) ? originalTarget : [originalTarget]);
  36. if (!markerMap[markerName]) {
  37. markerMap[markerName] = new WeakMap();
  38. }
  39. const markerCounter = markerMap[markerName];
  40. const hiddenNodes = [];
  41. const elementsToKeep = new Set();
  42. const elementsToStop = new Set(targets);
  43. const keep = (el) => {
  44. if (!el || elementsToKeep.has(el)) {
  45. return;
  46. }
  47. elementsToKeep.add(el);
  48. keep(el.parentNode);
  49. };
  50. targets.forEach(keep);
  51. const deep = (parent) => {
  52. if (!parent || elementsToStop.has(parent)) {
  53. return;
  54. }
  55. Array.prototype.forEach.call(parent.children, (node) => {
  56. if (elementsToKeep.has(node)) {
  57. deep(node);
  58. }
  59. else {
  60. const attr = node.getAttribute(controlAttribute);
  61. const alreadyHidden = attr !== null && attr !== 'false';
  62. const counterValue = (counterMap.get(node) || 0) + 1;
  63. const markerValue = (markerCounter.get(node) || 0) + 1;
  64. counterMap.set(node, counterValue);
  65. markerCounter.set(node, markerValue);
  66. hiddenNodes.push(node);
  67. if (counterValue === 1 && alreadyHidden) {
  68. uncontrolledNodes.set(node, true);
  69. }
  70. if (markerValue === 1) {
  71. node.setAttribute(markerName, 'true');
  72. }
  73. if (!alreadyHidden) {
  74. node.setAttribute(controlAttribute, 'true');
  75. }
  76. }
  77. });
  78. };
  79. deep(parentNode);
  80. elementsToKeep.clear();
  81. lockCount++;
  82. return () => {
  83. hiddenNodes.forEach((node) => {
  84. const counterValue = counterMap.get(node) - 1;
  85. const markerValue = markerCounter.get(node) - 1;
  86. counterMap.set(node, counterValue);
  87. markerCounter.set(node, markerValue);
  88. if (!counterValue) {
  89. if (!uncontrolledNodes.has(node)) {
  90. node.removeAttribute(controlAttribute);
  91. }
  92. uncontrolledNodes.delete(node);
  93. }
  94. if (!markerValue) {
  95. node.removeAttribute(markerName);
  96. }
  97. });
  98. lockCount--;
  99. if (!lockCount) {
  100. // clear
  101. counterMap = new WeakMap();
  102. counterMap = new WeakMap();
  103. uncontrolledNodes = new WeakMap();
  104. markerMap = {};
  105. }
  106. };
  107. };
  108. /**
  109. * Marks everything except given node(or nodes) as aria-hidden
  110. * @param {Element | Element[]} originalTarget - elements to keep on the page
  111. * @param [parentNode] - top element, defaults to document.body
  112. * @param {String} [markerName] - a special attribute to mark every node
  113. * @return {Undo} undo command
  114. */
  115. export const hideOthers = (originalTarget, parentNode, markerName = 'data-aria-hidden') => {
  116. const targets = Array.from(Array.isArray(originalTarget) ? originalTarget : [originalTarget]);
  117. const activeParentNode = parentNode || getDefaultParent(originalTarget);
  118. if (!activeParentNode) {
  119. return () => null;
  120. }
  121. // we should not hide ariaLive elements - https://github.com/theKashey/aria-hidden/issues/10
  122. targets.push(...Array.from(activeParentNode.querySelectorAll('[aria-live]')));
  123. return applyAttributeToOthers(targets, activeParentNode, markerName, 'aria-hidden');
  124. };
  125. /**
  126. * Marks everything except given node(or nodes) as inert
  127. * @param {Element | Element[]} originalTarget - elements to keep on the page
  128. * @param [parentNode] - top element, defaults to document.body
  129. * @param {String} [markerName] - a special attribute to mark every node
  130. * @return {Undo} undo command
  131. */
  132. export const inertOthers = (originalTarget, parentNode, markerName = 'data-inert-ed') => {
  133. const activeParentNode = parentNode || getDefaultParent(originalTarget);
  134. if (!activeParentNode) {
  135. return () => null;
  136. }
  137. return applyAttributeToOthers(originalTarget, activeParentNode, markerName, 'inert');
  138. };
  139. /**
  140. * @returns if current browser supports inert
  141. */
  142. export const supportsInert = () => typeof HTMLElement !== 'undefined' && HTMLElement.prototype.hasOwnProperty('inert');
  143. /**
  144. * Automatic function to "suppress" DOM elements - _hide_ or _inert_ in the best possible way
  145. * @param {Element | Element[]} originalTarget - elements to keep on the page
  146. * @param [parentNode] - top element, defaults to document.body
  147. * @param {String} [markerName] - a special attribute to mark every node
  148. * @return {Undo} undo command
  149. */
  150. export const suppressOthers = (originalTarget, parentNode, markerName = 'data-suppressed') => (supportsInert() ? inertOthers : hideOthers)(originalTarget, parentNode, markerName);