lion/packages/ui/components/form-core/FocusMixin.js
2022-10-31 16:55:07 +01:00

192 lines
5.6 KiB
JavaScript

import { dedupeMixin } from '@open-wc/dedupe-mixin';
const windowWithOptionalPolyfill =
/** @type {Window & typeof globalThis & {applyFocusVisiblePolyfill?: function}} */ (window);
const polyfilledNodes = new WeakMap();
/**
* @param {Node} node
*/
function applyFocusVisiblePolyfillWhenNeeded(node) {
if (windowWithOptionalPolyfill.applyFocusVisiblePolyfill && !polyfilledNodes.has(node)) {
windowWithOptionalPolyfill.applyFocusVisiblePolyfill(node);
polyfilledNodes.set(node, undefined);
}
}
/**
* @typedef {import('../types/FocusMixinTypes').FocusMixin} FocusMixin
* @type {FocusMixin}
* @param {import('@open-wc/dedupe-mixin').Constructor<import('@lion/core').LitElement>} superclass
*/
const FocusMixinImplementation = superclass =>
// @ts-ignore https://github.com/microsoft/TypeScript/issues/36821#issuecomment-588375051
class FocusMixin extends superclass {
/** @type {any} */
static get properties() {
return {
focused: { type: Boolean, reflect: true },
focusedVisible: { type: Boolean, reflect: true, attribute: 'focused-visible' },
};
}
constructor() {
super();
/**
* Whether the focusable element within (`._focusableNode`) is focused.
* Reflects to attribute '[focused]' as a styling hook
* @type {boolean}
*/
this.focused = false;
/**
* Whether the focusable element within (`._focusableNode`) matches ':focus-visible'
* Reflects to attribute '[focused-visible]' as a styling hook
* See: https://developer.mozilla.org/en-US/docs/Web/CSS/:focus-visible
* @type {boolean}
*/
this.focusedVisible = false;
}
connectedCallback() {
super.connectedCallback();
this.__registerEventsForFocusMixin();
}
disconnectedCallback() {
super.disconnectedCallback();
this.__teardownEventsForFocusMixin();
}
/**
* Calls `focus()` on focusable element within
*/
focus() {
this._focusableNode?.focus();
}
/**
* Calls `blur()` on focusable element within
*/
blur() {
this._focusableNode?.blur();
}
/**
* The focusable element:
* could be an input, textarea, select, button or any other element with tabindex > -1
* @protected
* @type {HTMLElement}
*/
// @ts-ignore it's up to Subclassers to return the right element. This is needed for docs/types
// eslint-disable-next-line class-methods-use-this, getter-return, no-empty-function
get _focusableNode() {
// TODO: [v1]: remove return of _inputNode (it's now here for backwards compatibility)
// @ts-expect-error see above
return /** @type {HTMLElement} */ (this._inputNode || document.createElement('input'));
}
/**
* @private
*/
__onFocus() {
this.focused = true;
if (typeof windowWithOptionalPolyfill.applyFocusVisiblePolyfill === 'function') {
this.focusedVisible = this._focusableNode.hasAttribute('data-focus-visible-added');
} else
try {
// Safari throws when matches is called
this.focusedVisible = this._focusableNode.matches(':focus-visible');
} catch (_) {
this.focusedVisible = false;
}
}
/**
* @private
*/
__onBlur() {
this.focused = false;
this.focusedVisible = false;
}
/**
* @private
*/
__registerEventsForFocusMixin() {
applyFocusVisiblePolyfillWhenNeeded(this.getRootNode());
/**
* focus
* @param {Event} ev
*/
this.__redispatchFocus = ev => {
ev.stopPropagation();
this.dispatchEvent(new Event('focus'));
};
this._focusableNode.addEventListener('focus', this.__redispatchFocus);
/**
* blur
* @param {Event} ev
*/
this.__redispatchBlur = ev => {
ev.stopPropagation();
this.dispatchEvent(new Event('blur'));
};
this._focusableNode.addEventListener('blur', this.__redispatchBlur);
/**
* focusin
* @param {Event} ev
*/
this.__redispatchFocusin = ev => {
ev.stopPropagation();
this.__onFocus();
this.dispatchEvent(new Event('focusin', { bubbles: true, composed: true }));
};
this._focusableNode.addEventListener('focusin', this.__redispatchFocusin);
/**
* focusout
* @param {Event} ev
*/
this.__redispatchFocusout = ev => {
ev.stopPropagation();
this.__onBlur();
this.dispatchEvent(new Event('focusout', { bubbles: true, composed: true }));
};
this._focusableNode.addEventListener('focusout', this.__redispatchFocusout);
}
/**
* @private
*/
__teardownEventsForFocusMixin() {
this._focusableNode.removeEventListener(
'focus',
/** @type {EventListenerOrEventListenerObject} */ (this.__redispatchFocus),
);
this._focusableNode.removeEventListener(
'blur',
/** @type {EventListenerOrEventListenerObject} */ (this.__redispatchBlur),
);
this._focusableNode.removeEventListener(
'focusin',
/** @type {EventListenerOrEventListenerObject} */ (this.__redispatchFocusin),
);
this._focusableNode.removeEventListener(
'focusout',
/** @type {EventListenerOrEventListenerObject} */ (this.__redispatchFocusout),
);
}
};
/**
* For browsers that not support the [spec](https://developer.mozilla.org/en-US/docs/Web/CSS/:focus-visible),
* be sure to load the polyfill into your application https://github.com/WICG/focus-visible
* (or go for progressive enhancement).
*/
export const FocusMixin = dedupeMixin(FocusMixinImplementation);