is-modal-open.js 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104
  1. import isVisible from './is-visible';
  2. import getViewportSize from './get-viewport-size';
  3. import cache from '../../core/base/cache';
  4. import { querySelectorAllFilter } from '../../core/utils';
  5. /**
  6. * Determines if there is a modal currently open.
  7. * @method isModalOpen
  8. * @memberof axe.commons.dom
  9. * @instance
  10. * @return {Boolean|undefined} True if we know (or our best guess) that a modal is open, undefined if we can't tell (doesn't mean there isn't one open)
  11. */
  12. function isModalOpen(options) {
  13. options = options || {};
  14. const modalPercent = options.modalPercent || 0.75;
  15. // there is no "definitive" way to code a modal so detecting when one is open
  16. // is a bit of a guess. a modal won't always be accessible, so we can't rely
  17. // on the `role` attribute, and relying on a class name as a convention is
  18. // unreliable. we also cannot rely on the body/html not scrolling.
  19. //
  20. // because of this, we will look for two different types of modals:
  21. // "definitely a modal" and "could be a modal."
  22. //
  23. // "definitely a modal" is any visible element that is coded to be a modal
  24. // by using one of the following criteria:
  25. //
  26. // - has the attribute `role=dialog`
  27. // - has the attribute `aria-modal=true`
  28. // - is the dialog element
  29. //
  30. // "could be a modal" is a visible element that takes up more than 75% of
  31. // the screen (though typically full width/height) and is the top-most element
  32. // in the viewport. since we aren't sure if it is or is not a modal this is
  33. // just our best guess of being one based on convention.
  34. if (cache.get('isModalOpen')) {
  35. return cache.get('isModalOpen');
  36. }
  37. const definiteModals = querySelectorAllFilter(
  38. // TODO: es-module-_tree
  39. axe._tree[0],
  40. 'dialog, [role=dialog], [aria-modal=true]',
  41. vNode => isVisible(vNode.actualNode)
  42. );
  43. if (definiteModals.length) {
  44. cache.set('isModalOpen', true);
  45. return true;
  46. }
  47. // to find a "could be a modal" we will take the element stack from each of
  48. // four corners and one from the middle of the viewport (total of 5). if each
  49. // stack contains an element whose width/height is >= 75% of the screen, we
  50. // found a "could be a modal"
  51. const viewport = getViewportSize(window);
  52. const percentWidth = viewport.width * modalPercent;
  53. const percentHeight = viewport.height * modalPercent;
  54. const x = (viewport.width - percentWidth) / 2;
  55. const y = (viewport.height - percentHeight) / 2;
  56. const points = [
  57. // top-left corner
  58. { x, y },
  59. // top-right corner
  60. { x: viewport.width - x, y },
  61. // center
  62. { x: viewport.width / 2, y: viewport.height / 2 },
  63. // bottom-left corner
  64. { x, y: viewport.height - y },
  65. // bottom-right corner
  66. { x: viewport.width - x, y: viewport.height - y }
  67. ];
  68. const stacks = points.map(point => {
  69. return Array.from(document.elementsFromPoint(point.x, point.y));
  70. });
  71. for (let i = 0; i < stacks.length; i++) {
  72. // a modal isn't guaranteed to be the top most element so we'll have to
  73. // find the first element in the stack that meets the modal criteria
  74. // and make sure it's in the other stacks
  75. const modalElement = stacks[i].find(elm => {
  76. const style = window.getComputedStyle(elm);
  77. return (
  78. parseInt(style.width, 10) >= percentWidth &&
  79. parseInt(style.height, 10) >= percentHeight &&
  80. style.getPropertyValue('pointer-events') !== 'none' &&
  81. (style.position === 'absolute' || style.position === 'fixed')
  82. );
  83. });
  84. if (modalElement && stacks.every(stack => stack.includes(modalElement))) {
  85. cache.set('isModalOpen', true);
  86. return true;
  87. }
  88. }
  89. cache.set('isModalOpen', undefined);
  90. return undefined;
  91. }
  92. export default isModalOpen;