UNPKG

@esri/calcite-components

Version:

Web Components for Esri's Calcite Design System.

138 lines (137 loc) 5.37 kB
/*! * All material copyright ESRI, All Rights Reserved, unless otherwise specified. * See https://github.com/Esri/calcite-components/blob/master/LICENSE.md for details. * v1.5.0-next.4 */ import { getUserAgentString } from "./browser"; // ⚠️ browser-sniffing is not a best practice and should be avoided ⚠️ const isFirefox = /firefox/i.test(getUserAgentString()); const interactiveElementToParent = isFirefox ? new WeakMap() : null; function interceptedClick() { const { disabled } = this; if (!disabled) { HTMLElement.prototype.click.call(this); } } function onPointerDown(event) { const interactiveElement = event.target; if (isFirefox && !interactiveElementToParent.get(interactiveElement)) { return; } const { disabled } = interactiveElement; if (disabled) { // prevent click from moving focus on host event.preventDefault(); } } const nonBubblingWhenDisabledMouseEvents = ["mousedown", "mouseup", "click"]; function onNonBubblingWhenDisabledMouseEvent(event) { if (isFirefox && !interactiveElementToParent.get(event.target)) { return; } const { disabled } = event.target; // prevent disallowed mouse events from being emitted on the disabled host (per https://github.com/whatwg/html/issues/5886) //⚠ we generally avoid stopping propagation of events, but this is needed to adhere to the intended spec changes above ⚠ if (disabled) { event.stopImmediatePropagation(); event.preventDefault(); } } const captureOnlyOptions = { capture: true }; /** * This helper updates the host element to prevent keyboard interaction on its subtree and sets the appropriate aria attribute for accessibility. * * This should be used in the `componentDidRender` lifecycle hook. * * **Notes** * * this util is not needed for simple components whose root element or elements are an interactive component (custom element or native control). For those cases, set the `disabled` props on the root components instead. * technically, users can override `tabindex` and restore keyboard navigation, but this will be considered user error * * @param component * @param hostIsTabbable */ export function updateHostInteraction(component, hostIsTabbable = false) { if (component.disabled) { component.el.setAttribute("tabindex", "-1"); component.el.setAttribute("aria-disabled", "true"); if (component.el.contains(document.activeElement)) { document.activeElement.blur(); } blockInteraction(component); return; } restoreInteraction(component); if (typeof hostIsTabbable === "function") { component.el.setAttribute("tabindex", hostIsTabbable.call(component) ? "0" : "-1"); } else if (hostIsTabbable === true) { component.el.setAttribute("tabindex", "0"); } else if (hostIsTabbable === false) { component.el.removeAttribute("tabindex"); } else { // noop for "managed" as owning component will manage its tab index } component.el.removeAttribute("aria-disabled"); } function blockInteraction(component) { component.el.click = interceptedClick; addInteractionListeners(isFirefox ? getParentElement(component) : component.el); } function addInteractionListeners(element) { if (!element) { // this path is only applicable to Firefox return; } element.addEventListener("pointerdown", onPointerDown, captureOnlyOptions); nonBubblingWhenDisabledMouseEvents.forEach((event) => element.addEventListener(event, onNonBubblingWhenDisabledMouseEvent, captureOnlyOptions)); } function getParentElement(component) { return interactiveElementToParent.get(component.el); } function restoreInteraction(component) { delete component.el.click; // fallback on HTMLElement.prototype.click removeInteractionListeners(isFirefox ? getParentElement(component) : component.el); } function removeInteractionListeners(element) { if (!element) { // this path is only applicable to Firefox return; } element.removeEventListener("pointerdown", onPointerDown, captureOnlyOptions); nonBubblingWhenDisabledMouseEvents.forEach((event) => element.removeEventListener(event, onNonBubblingWhenDisabledMouseEvent, captureOnlyOptions)); } /** * This utility helps disable components consistently in Firefox. * * It needs to be called in `connectedCallback` and is only needed for Firefox as it does not call capture event listeners before non-capture ones (see https://bugzilla.mozilla.org/show_bug.cgi?id=1731504). * * @param component */ export function connectInteractive(component) { if (!component.disabled || !isFirefox) { return; } const parent = component.el.parentElement || component.el; /* assume element is host if it has no parent when connected */ interactiveElementToParent.set(component.el, parent); blockInteraction(component); } /** * This utility restores interactivity to disabled components consistently in Firefox. * * It needs to be called in `disconnectedCallback` and is only needed for Firefox as it does not call capture event listeners before non-capture ones (see https://bugzilla.mozilla.org/show_bug.cgi?id=1731504). * * @param component */ export function disconnectInteractive(component) { if (!isFirefox) { return; } // always remove on disconnect as render or connect will restore it interactiveElementToParent.delete(component.el); restoreInteraction(component); }