@spectrum-web-components/shared
Version:
The `@spectrum-web-components/shared` package provides essential base classes, mixins, and utilities that support developing Spectrum Web Components. This package contains foundational tools for focus management, slot observation, accessibility enhancemen
245 lines (244 loc) • 7.11 kB
JavaScript
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __decorateClass = (decorators, target, key, kind) => {
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
for (var i = decorators.length - 1, decorator; i >= 0; i--)
if (decorator = decorators[i])
result = (kind ? decorator(target, key, result) : decorator(result)) || result;
if (kind && result) __defProp(target, key, result);
return result;
};
import { SpectrumElement } from "@spectrum-web-components/base";
import { property } from "@spectrum-web-components/base/src/decorators.js";
import { FocusVisiblePolyfillMixin } from "./focus-visible.dev.js";
function nextFrame() {
return new Promise((res) => requestAnimationFrame(() => res()));
}
export class Focusable extends FocusVisiblePolyfillMixin(SpectrumElement) {
constructor() {
super(...arguments);
this.disabled = false;
this.autofocus = false;
this._tabIndex = 0;
this.manipulatingTabindex = false;
this.autofocusReady = Promise.resolve();
}
get tabIndex() {
if (this.focusElement === this) {
const tabindex = this.hasAttribute("tabindex") ? Number(this.getAttribute("tabindex")) : NaN;
return !isNaN(tabindex) ? tabindex : -1;
}
const tabIndexAttribute = parseFloat(
this.hasAttribute("tabindex") ? this.getAttribute("tabindex") || "0" : "0"
);
if (this.disabled || tabIndexAttribute < 0) {
return -1;
}
if (!this.focusElement) {
return tabIndexAttribute;
}
return this._tabIndex;
}
set tabIndex(tabIndex) {
var _a;
if (this.manipulatingTabindex) {
this.manipulatingTabindex = false;
return;
}
if (this.focusElement === this) {
if (this.disabled) {
this._tabIndex = tabIndex;
} else if (tabIndex !== this._tabIndex) {
this._tabIndex = tabIndex;
const tabindex = "" + tabIndex;
this.manipulatingTabindex = true;
this.setAttribute("tabindex", tabindex);
}
return;
}
if (tabIndex === -1) {
this.addEventListener(
"pointerdown",
this.onPointerdownManagementOfTabIndex
);
} else {
this.manipulatingTabindex = true;
this.removeEventListener(
"pointerdown",
this.onPointerdownManagementOfTabIndex
);
}
if (tabIndex === -1 || this.disabled) {
this.manipulatingTabindex = true;
this.setAttribute("tabindex", "-1");
this.removeAttribute("focusable");
if (this.selfManageFocusElement) {
return;
}
if (tabIndex !== -1) {
this._tabIndex = tabIndex;
this.manageFocusElementTabindex(tabIndex);
} else {
(_a = this.focusElement) == null ? void 0 : _a.removeAttribute("tabindex");
}
return;
}
this.setAttribute("focusable", "");
if (this.hasAttribute("tabindex")) {
this.removeAttribute("tabindex");
} else {
this.manipulatingTabindex = false;
}
this._tabIndex = tabIndex;
this.manageFocusElementTabindex(tabIndex);
}
onPointerdownManagementOfTabIndex() {
if (this.tabIndex === -1) {
setTimeout(() => {
this.tabIndex = 0;
this.focus({ preventScroll: true });
this.tabIndex = -1;
});
}
}
async manageFocusElementTabindex(tabIndex) {
if (!this.focusElement) {
await this.updateComplete;
}
if (tabIndex === null) {
this.focusElement.removeAttribute("tabindex");
} else {
if (this.focusElement !== this) {
this.focusElement.tabIndex = tabIndex;
}
}
}
/**
* @private
*/
get focusElement() {
throw new Error("Must implement focusElement getter!");
}
/**
* @public
* @returns {boolean} whether the component should manage its focusElement tab-index or not
* Needed for action-menu to be supported in action-group in an accessible way
*/
get selfManageFocusElement() {
return false;
}
focus(options) {
if (this.disabled || !this.focusElement) {
return;
}
if (this.focusElement !== this) {
this.focusElement.focus(options);
} else {
HTMLElement.prototype.focus.apply(this, [options]);
}
}
blur() {
const focusElement = this.focusElement || this;
if (focusElement !== this) {
focusElement.blur();
} else {
HTMLElement.prototype.blur.apply(this);
}
}
click() {
if (this.disabled) {
return;
}
const focusElement = this.focusElement || this;
if (focusElement !== this) {
focusElement.click();
} else {
HTMLElement.prototype.click.apply(this);
}
}
manageAutoFocus() {
if (this.autofocus) {
this.dispatchEvent(
new KeyboardEvent("keydown", {
code: "Tab"
})
);
this.focusElement.focus();
}
}
firstUpdated(changes) {
super.firstUpdated(changes);
if (!this.hasAttribute("tabindex") || this.getAttribute("tabindex") !== "-1") {
this.setAttribute("focusable", "");
}
}
update(changedProperties) {
if (changedProperties.has("disabled")) {
this.handleDisabledChanged(
this.disabled,
changedProperties.get("disabled")
);
}
super.update(changedProperties);
}
updated(changedProperties) {
super.updated(changedProperties);
if (changedProperties.has("disabled") && this.disabled) {
this.blur();
}
}
async handleDisabledChanged(disabled, oldDisabled) {
const canSetDisabled = () => this.focusElement !== this && typeof this.focusElement.disabled !== "undefined";
if (disabled) {
this.manipulatingTabindex = true;
this.setAttribute("tabindex", "-1");
await this.updateComplete;
if (canSetDisabled()) {
this.focusElement.disabled = true;
} else {
this.setAttribute("aria-disabled", "true");
}
} else if (oldDisabled) {
this.manipulatingTabindex = true;
if (this.focusElement === this) {
this.setAttribute("tabindex", "" + this._tabIndex);
} else {
this.removeAttribute("tabindex");
}
await this.updateComplete;
if (canSetDisabled()) {
this.focusElement.disabled = false;
} else {
this.removeAttribute("aria-disabled");
}
}
}
async getUpdateComplete() {
const complete = await super.getUpdateComplete();
await this.autofocusReady;
return complete;
}
connectedCallback() {
super.connectedCallback();
if (this.autofocus) {
this.autofocusReady = (async () => {
await nextFrame();
await nextFrame();
})();
this.updateComplete.then(() => {
this.manageAutoFocus();
});
}
}
}
__decorateClass([
property({ type: Boolean, reflect: true })
], Focusable.prototype, "disabled", 2);
__decorateClass([
property({ type: Boolean })
], Focusable.prototype, "autofocus", 2);
__decorateClass([
property({ type: Number })
], Focusable.prototype, "tabIndex", 1);
//# sourceMappingURL=focusable.dev.js.map