UNPKG

@ionic/core

Version:
129 lines (126 loc) • 5.56 kB
/*! * (C) Ionic http://ionicframework.com - MIT License */ import { b as inheritAttributes, w as win, q as ariaAttributes } from './helpers-BJqKF1pr.js'; /** * Copies a set of attributes off the host element so they can be applied to an element * inside the shadow root, then keeps that copy in sync as the host attributes change. * Using `inheritAttributes` alone copies once at load, so anything written to the host * afterwards never reaches the element the assistive technology reads. * * Create the controller in `componentWillLoad`. It takes the initial copy and starts * watching right away, so `init()` is only needed to resume after a move. * * Two things to know before adopting it. Only the initial copy removes the attributes * from the host, so a value written after load sits on the host as well as on the * element it is applied to, which matters if the host has a role of its own. And an * attribute from the initial markup can never be removed, only overwritten, since the * copy already took it off the host and a `removeAttribute` there fires no mutation. * An empty string works for `aria-label` and friends, but a token-valued attribute * has to be set to its default instead, so `aria-pressed="false"` rather than empty. * * @internal * @param el The host element to copy from and watch. * @param attributes The attributes to copy and watch. * @param onChange Called after `attributes` changes, so the component can re-render. * @param hostOwnedAttributes Attributes copied at load but not watched. Use it for * attributes the component renders on its own `<Host>`, since a later change to one of * those is the component's own write, and for attributes that would put a second node in * the accessibility tree if the host kept a copy alongside the one we apply. */ const createAttributeController = (el, attributes, onChange, hostOwnedAttributes = []) => { let inherited = inheritAttributes(el, attributes); let observer; const watchedAttributes = attributes.filter((attr) => !hostOwnedAttributes.includes(attr)); /** * The names still on the host, which is everything written after load. The initial * copy removes what it captures, so a missing attribute only counts as a removal * when its name is in here. */ const hostWritten = new Set(); const setPresence = (name, value) => { if (value === null) { hostWritten.delete(name); } else { hostWritten.add(name); } }; /** * Nothing is watching while the host is out of the tree, since `disconnectedCallback` * calls `destroy()`, and `forceUpdate` is a no-op on a disconnected host anyway. So * the host has to be re-read on the way back in. */ const readMissedChanges = () => { const changed = {}; for (const name of watchedAttributes) { const value = el.getAttribute(name); const wasOnHost = hostWritten.has(name); setPresence(name, value); /** * An attribute that was never written to the host is missing because the initial * copy took it, not because the developer cleared it. */ if ((value === null && !wasOnHost) || value === inherited[name]) { continue; } changed[name] = value; } if (Object.keys(changed).length > 0) { inherited = { ...inherited, ...changed }; onChange(); } }; const init = () => { // There is no MutationObserver in SSR or the hydrate build. if (observer !== undefined || watchedAttributes.length === 0 || win === undefined || !('MutationObserver' in win)) { return; } readMissedChanges(); observer = new MutationObserver((mutations) => { const changed = {}; for (const mutation of mutations) { const name = mutation.attributeName; // A removed attribute reads back as null, which clears it from the element the // values are spread onto. const value = el.getAttribute(name); setPresence(name, value); changed[name] = value; } inherited = { ...inherited, ...changed }; onChange(); }); observer.observe(el, { attributeFilter: watchedAttributes }); }; const destroy = () => { observer?.disconnect(); observer = undefined; }; /** * Has to run after the initial copy, because that copy removes the attributes from the * host and an observer armed any earlier would read the removal as a developer clearing * them. */ init(); return { get attributes() { return inherited; }, init, destroy, }; }; /** * The `createAttributeController` equivalent of `inheritAriaAttributes`, which copies and * watches every ARIA attribute plus `role`. * * @internal * @param el The host element to copy from and watch. * @param onChange Called after the attributes change, so the component can re-render. * @param hostOwnedAttributes Attributes copied at load but not watched. See * `createAttributeController`. */ const createAriaAttributeController = (el, onChange, hostOwnedAttributes = []) => { return createAttributeController(el, ariaAttributes, onChange, hostOwnedAttributes); }; export { createAttributeController as a, createAriaAttributeController as c };