123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191 |
- import getRootNode from './get-root-node';
- import isOffscreen from './is-offscreen';
- import findUp from './find-up';
- import {
- getScroll,
- getNodeFromTree,
- querySelectorAll,
- escapeSelector
- } from '../../core/utils';
- const clipRegex = /rect\s*\(([0-9]+)px,?\s*([0-9]+)px,?\s*([0-9]+)px,?\s*([0-9]+)px\s*\)/;
- const clipPathRegex = /(\w+)\((\d+)/;
- /**
- * Determines if an element is hidden with a clip or clip-path technique
- * @method isClipped
- * @memberof axe.commons.dom
- * @private
- * @param {CSSStyleDeclaration} style Computed style
- * @return {Boolean}
- */
- function isClipped(style) {
- const matchesClip = style.getPropertyValue('clip').match(clipRegex);
- const matchesClipPath = style
- .getPropertyValue('clip-path')
- .match(clipPathRegex);
- if (matchesClip && matchesClip.length === 5) {
- return (
- matchesClip[3] - matchesClip[1] <= 0 &&
- matchesClip[2] - matchesClip[4] <= 0
- );
- }
- if (matchesClipPath) {
- const type = matchesClipPath[1];
- const value = parseInt(matchesClipPath[2], 10);
- switch (type) {
- case 'inset':
- return value >= 50;
- case 'circle':
- return value === 0;
- default:
- }
- }
- return false;
- }
- /**
- * Check `AREA` element is visible
- * - validate if it is a child of `map`
- * - ensure `map` is referred by `img` using the `usemap` attribute
- * @param {Element} areaEl `AREA` element
- * @retruns {Boolean}
- */
- function isAreaVisible(el, screenReader, recursed) {
- /**
- * Note:
- * - Verified that `map` element cannot refer to `area` elements across different document trees
- * - Verified that `map` element does not get affected by altering `display` property
- */
- const mapEl = findUp(el, 'map');
- if (!mapEl) {
- return false;
- }
- const mapElName = mapEl.getAttribute('name');
- if (!mapElName) {
- return false;
- }
- /**
- * `map` element has to be in light DOM
- */
- const mapElRootNode = getRootNode(el);
- if (!mapElRootNode || mapElRootNode.nodeType !== 9) {
- return false;
- }
- const refs = querySelectorAll(
- // TODO: es-module-_tree
- axe._tree,
- `img[usemap="#${escapeSelector(mapElName)}"]`
- );
- if (!refs || !refs.length) {
- return false;
- }
- return refs.some(({ actualNode }) =>
- isVisible(actualNode, screenReader, recursed)
- );
- }
- /**
- * Determine whether an element is visible
- * @method isVisible
- * @memberof axe.commons.dom
- * @instance
- * @param {HTMLElement} el The HTMLElement
- * @param {Boolean} screenReader When provided, will evaluate visibility from the perspective of a screen reader
- * @param {Boolean} recursed
- * @return {Boolean} The element's visibilty status
- */
- function isVisible(el, screenReader, recursed) {
- if (!el) {
- throw new TypeError(
- 'Cannot determine if element is visible for non-DOM nodes'
- );
- }
- const vNode = getNodeFromTree(el);
- const cacheName = '_isVisible' + (screenReader ? 'ScreenReader' : '');
- // 9 === Node.DOCUMENT
- if (el.nodeType === 9) {
- return true;
- }
- // 11 === Node.DOCUMENT_FRAGMENT_NODE
- if (el.nodeType === 11) {
- el = el.host; // grab the host Node
- }
- if (vNode && typeof vNode[cacheName] !== 'undefined') {
- return vNode[cacheName];
- }
- const style = window.getComputedStyle(el, null);
- if (style === null) {
- return false;
- }
- const nodeName = el.nodeName.toUpperCase();
- /**
- * check visibility of `AREA`
- * Note:
- * Firefox's user-agent always sets `AREA` element to `display:none`
- * hence excluding the edge case, for visibility computation
- */
- if (nodeName === 'AREA') {
- return isAreaVisible(el, screenReader, recursed);
- }
- // always hidden
- if (
- style.getPropertyValue('display') === 'none' ||
- ['STYLE', 'SCRIPT', 'NOSCRIPT', 'TEMPLATE'].includes(nodeName)
- ) {
- return false;
- }
- // hidden from screen readers
- if (screenReader && el.getAttribute('aria-hidden') === 'true') {
- return false;
- }
- // hidden from visual users
- if (
- !screenReader &&
- (isClipped(style) ||
- style.getPropertyValue('opacity') === '0' ||
- (getScroll(el) && parseInt(style.getPropertyValue('height')) === 0))
- ) {
- return false;
- }
- // visibility is only accurate on the first element and
- // position does not matter if it was already calculated
- if (
- !recursed &&
- (style.getPropertyValue('visibility') === 'hidden' ||
- (!screenReader && isOffscreen(el)))
- ) {
- return false;
- }
- const parent = el.assignedSlot ? el.assignedSlot : el.parentNode;
- let visible = false;
- if (parent) {
- visible = isVisible(parent, screenReader, true);
- }
- if (vNode) {
- vNode[cacheName] = visible;
- }
- return visible;
- }
- export default isVisible;
|