/**
* @license
* Copyright 2023 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { isServer } from 'lit';
import { ARIA_PROPERTIES, ariaPropertyToAttribute, isAriaAttribute, } from './aria.js';
// Private symbols
const privateIgnoreAttributeChangesFor = Symbol('privateIgnoreAttributeChangesFor');
/**
* Mixes in aria delegation for elements that delegate focus and aria to inner
* shadow root elements.
*
* This mixin fixes invalid aria announcements with shadow roots, caused by
* duplicate aria attributes on both the host and the inner shadow root element.
*
* Note: this mixin **does not yet support** ID reference attributes, such as
* `aria-labelledby` or `aria-controls`.
*
* @example
* ```ts
* class MyButton extends mixinDelegatesAria(LitElement) {
* static shadowRootOptions = {mode: 'open', delegatesFocus: true};
*
* render() {
* return html`
* <button aria-label=${this.ariaLabel || nothing}>
* <slot></slot>
* </button>
* `;
* }
* }
* ```
* ```html
* <my-button aria-label="Plus one">+1</my-button>
* ```
*
* Use `ARIAMixinStrict` for lit analyzer strict types, such as the "role"
* attribute.
*
* @example
* ```ts
* return html`
* <button role=${(this as ARIAMixinStrict).role || nothing}>
* <slot></slot>
* </button>
* `;
* ```
*
* In the future, updates to the Accessibility Object Model (AOM) will provide
* built-in aria delegation features that will replace this mixin.
*
* @param base The class to mix functionality into.
* @return The provided class with aria delegation mixed in.
*/
export function mixinDelegatesAria(base) {
var _a;
if (isServer) {
// Don't shift attributes when running with lit-ssr. The SSR renderer
// implements a subset of DOM APIs, including the methods this mixin
// overrides, causing errors. We don't need to shift on the server anyway
// since elements will shift attributes immediately once they hydrate.
return base;
}
class WithDelegatesAriaElement extends base {
constructor() {
super(...arguments);
this[_a] = new Set();
}
attributeChangedCallback(name, oldValue, newValue) {
if (!isAriaAttribute(name)) {
super.attributeChangedCallback(name, oldValue, newValue);
return;
}
if (this[privateIgnoreAttributeChangesFor].has(name)) {
return;
}
// Don't trigger another `attributeChangedCallback` once we remove the
// aria attribute from the host. We check the explicit name of the
// attribute to ignore since `attributeChangedCallback` can be called
// multiple times out of an expected order when hydrating an element with
// multiple attributes.
this[privateIgnoreAttributeChangesFor].add(name);
this.removeAttribute(name);
this[privateIgnoreAttributeChangesFor].delete(name);
const dataProperty = ariaAttributeToDataProperty(name);
if (newValue === null) {
delete this.dataset[dataProperty];
}
else {
this.dataset[dataProperty] = newValue;
}
this.requestUpdate(ariaAttributeToDataProperty(name), oldValue);
}
getAttribute(name) {
if (isAriaAttribute(name)) {
return super.getAttribute(ariaAttributeToDataAttribute(name));
}
return super.getAttribute(name);
}
removeAttribute(name) {
super.removeAttribute(name);
if (isAriaAttribute(name)) {
super.removeAttribute(ariaAttributeToDataAttribute(name));
// Since `aria-*` attributes are already removed`, we need to request
// an update because `attributeChangedCallback` will not be called.
this.requestUpdate();
}
}
}
_a = privateIgnoreAttributeChangesFor;
setupDelegatesAriaProperties(WithDelegatesAriaElement);
return WithDelegatesAriaElement;
}
/**
* Overrides the constructor's native `ARIAMixin` properties to ensure that
* aria properties reflect the values that were shifted to a data attribute.
*
* @param ctor The `ReactiveElement` constructor to patch.
*/
function setupDelegatesAriaProperties(ctor) {
for (const ariaProperty of ARIA_PROPERTIES) {
// The casing between ariaProperty and the dataProperty may be different.
// ex: aria-haspopup -> ariaHasPopup
const ariaAttribute = ariaPropertyToAttribute(ariaProperty);
// ex: aria-haspopup -> data-aria-haspopup
const dataAttribute = ariaAttributeToDataAttribute(ariaAttribute);
// ex: aria-haspopup -> dataset.ariaHaspopup
const dataProperty = ariaAttributeToDataProperty(ariaAttribute);
// Call `ReactiveElement.createProperty()` so that the `aria-*` and `data-*`
// attributes are added to the `static observedAttributes` array. This
// triggers `attributeChangedCallback` for the delegates aria mixin to
// handle.
ctor.createProperty(ariaProperty, {
attribute: ariaAttribute,
noAccessor: true,
});
ctor.createProperty(Symbol(dataAttribute), {
attribute: dataAttribute,
noAccessor: true,
});
// Re-define the `ARIAMixin` properties to handle data attribute shifting.
// It is safe to use `Object.defineProperty` here because the properties
// are native and not renamed.
// tslint:disable-next-line:ban-unsafe-reflection
Object.defineProperty(ctor.prototype, ariaProperty, {
configurable: true,
enumerable: true,
get() {
return this.dataset[dataProperty] ?? null;
},
set(value) {
const prevValue = this.dataset[dataProperty] ?? null;
if (value === prevValue) {
return;
}
if (value === null) {
delete this.dataset[dataProperty];
}
else {
this.dataset[dataProperty] = value;
}
this.requestUpdate(ariaProperty, prevValue);
},
});
}
}
function ariaAttributeToDataAttribute(ariaAttribute) {
// aria-haspopup -> data-aria-haspopup
return `data-${ariaAttribute}`;
}
function ariaAttributeToDataProperty(ariaAttribute) {
// aria-haspopup -> dataset.ariaHaspopup
return ariaAttribute.replace(/-\w/, (dashLetter) => dashLetter[1].toUpperCase());
}
//# sourceMappingURL=delegate.js.map