Skip to content

Commit

Permalink
feat(form-core): add [focused-visible] when matching :focus-visible
Browse files Browse the repository at this point in the history
  • Loading branch information
tlouisse committed Apr 30, 2021
1 parent 6a614dd commit 2c313eb
Show file tree
Hide file tree
Showing 8 changed files with 334 additions and 31 deletions.
5 changes: 5 additions & 0 deletions .changeset/short-llamas-share.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@lion/form-core': patch
---

support [focused-visible] when focusable node within matches :focus-visible
96 changes: 76 additions & 20 deletions packages/form-core/src/FocusMixin.js
Original file line number Diff line number Diff line change
@@ -1,25 +1,48 @@
import { dedupeMixin } from '@lion/core';
import { FormControlMixin } from './FormControlMixin.js';

const polyfilledNodes = new WeakMap();
const win = /** @type {Window & typeof globalThis & {applyFocusVisiblePolyfill: null | function}} */ (window);

/**
* @param {Node} node
*/
function applyFocusVisiblePolyfillWhenNeeded(node) {
if (win.applyFocusVisiblePolyfill && !polyfilledNodes.has(node)) {
win.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 =>
class FocusMixin extends FormControlMixin(superclass) {
class FocusMixin extends superclass {
/** @type {any} */
static get properties() {
return {
focused: {
type: Boolean,
reflect: true,
},
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() {
Expand All @@ -32,38 +55,66 @@ const FocusMixinImplementation = superclass =>
this.__teardownEventsForFocusMixin();
}

/**
* Calls `focus()` on focusable element within
*/
focus() {
const native = this._inputNode;
if (native) {
native.focus();
if (this._focusableNode) {
this._focusableNode.focus();
}
}

/**
* Calls `blur()` on focusable element within
*/
blur() {
const native = this._inputNode;
if (native) {
native.blur();
if (this._focusableNode) {
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() {
return /** @type {HTMLElement} */ (document.createElement('input'));
}

/**
* @private
*/
__onFocus() {
this.focused = true;
try {
this.focusedVisible =
this._focusableNode.matches(':focus-visible') ||
// as a fallback, support polyfill:
(win.applyFocusVisiblePolyfill != null &&
this._focusableNode.hasAttribute('data-focus-visible-added'));
} catch (_) {
this.focusedVisible = false;
}
}

/**
* @private
*/
__onBlur() {
this.focused = false;
this.focusedVisible = false;
}

/**
* @private
*/
__registerEventsForFocusMixin() {
applyFocusVisiblePolyfillWhenNeeded(this.getRootNode());

/**
* focus
* @param {Event} ev
Expand All @@ -72,7 +123,7 @@ const FocusMixinImplementation = superclass =>
ev.stopPropagation();
this.dispatchEvent(new Event('focus'));
};
this._inputNode.addEventListener('focus', this.__redispatchFocus);
this._focusableNode.addEventListener('focus', this.__redispatchFocus);

/**
* blur
Expand All @@ -82,7 +133,7 @@ const FocusMixinImplementation = superclass =>
ev.stopPropagation();
this.dispatchEvent(new Event('blur'));
};
this._inputNode.addEventListener('blur', this.__redispatchBlur);
this._focusableNode.addEventListener('blur', this.__redispatchBlur);

/**
* focusin
Expand All @@ -93,7 +144,7 @@ const FocusMixinImplementation = superclass =>
this.__onFocus();
this.dispatchEvent(new Event('focusin', { bubbles: true, composed: true }));
};
this._inputNode.addEventListener('focusin', this.__redispatchFocusin);
this._focusableNode.addEventListener('focusin', this.__redispatchFocusin);

/**
* focusout
Expand All @@ -104,30 +155,35 @@ const FocusMixinImplementation = superclass =>
this.__onBlur();
this.dispatchEvent(new Event('focusout', { bubbles: true, composed: true }));
};
this._inputNode.addEventListener('focusout', this.__redispatchFocusout);
this._focusableNode.addEventListener('focusout', this.__redispatchFocusout);
}

/**
* @private
*/
__teardownEventsForFocusMixin() {
this._inputNode.removeEventListener(
this._focusableNode.removeEventListener(
'focus',
/** @type {EventListenerOrEventListenerObject} */ (this.__redispatchFocus),
);
this._inputNode.removeEventListener(
this._focusableNode.removeEventListener(
'blur',
/** @type {EventListenerOrEventListenerObject} */ (this.__redispatchBlur),
);
this._inputNode.removeEventListener(
this._focusableNode.removeEventListener(
'focusin',
/** @type {EventListenerOrEventListenerObject} */ (this.__redispatchFocusin),
);
this._inputNode.removeEventListener(
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);
7 changes: 7 additions & 0 deletions packages/form-core/src/LionField.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,4 +106,11 @@ export class LionField extends FormControlMixin(
get _feedbackConditionMeta() {
return { ...super._feedbackConditionMeta, focused: this.focused };
}

/**
* @configure FocusMixin
*/
get _focusableNode() {
return this._inputNode;
}
}
7 changes: 7 additions & 0 deletions packages/form-core/src/NativeTextFieldMixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,13 @@ const NativeTextFieldMixinImplementation = superclass =>
} catch (_) {}
}
}

/**
* @configure FocusMixin
*/
get _focusableNode() {
return this._inputNode;
}
};

export const NativeTextFieldMixin = dedupeMixin(NativeTextFieldMixinImplementation);
Loading

0 comments on commit 2c313eb

Please sign in to comment.