From 2683a730807fa4fcf3ce44a9f4e3294c8e060147 Mon Sep 17 00:00:00 2001 From: Sciurus7 Date: Fri, 14 Apr 2023 13:39:05 +0200 Subject: [PATCH] fix(form-core) sync autofocus to focusable node --- .changeset/silly-shirts-promise.md | 5 +++ .../ui/components/form-core/src/FocusMixin.js | 31 +++++++++++++++++ .../form-core/test/FocusMixin.test.js | 33 +++++++++++++++++++ .../form-core/types/FocusMixinTypes.ts | 6 ++++ 4 files changed, 75 insertions(+) create mode 100644 .changeset/silly-shirts-promise.md diff --git a/.changeset/silly-shirts-promise.md b/.changeset/silly-shirts-promise.md new file mode 100644 index 000000000..9cb5fb8da --- /dev/null +++ b/.changeset/silly-shirts-promise.md @@ -0,0 +1,5 @@ +--- +'@lion/ui': patch +--- + +[FocusMixin] now syncs autofocus between host and the focusable node. diff --git a/packages/ui/components/form-core/src/FocusMixin.js b/packages/ui/components/form-core/src/FocusMixin.js index 7490e54ec..54aaf735d 100644 --- a/packages/ui/components/form-core/src/FocusMixin.js +++ b/packages/ui/components/form-core/src/FocusMixin.js @@ -27,6 +27,7 @@ const FocusMixinImplementation = superclass => return { focused: { type: Boolean, reflect: true }, focusedVisible: { type: Boolean, reflect: true, attribute: 'focused-visible' }, + autofocus: { type: Boolean, reflect: true }, // Required in Lit to observe autofocus }; } @@ -52,6 +53,7 @@ const FocusMixinImplementation = superclass => connectedCallback() { super.connectedCallback(); this.__registerEventsForFocusMixin(); + this.__syncAutofocusToFocusableElement(); } disconnectedCallback() { @@ -59,6 +61,35 @@ const FocusMixinImplementation = superclass => this.__teardownEventsForFocusMixin(); } + /** + * Gets called when an attribute is changed. + * @param {String} name + * @param {String | null} _old + * @param {String | null} value + * @protected + */ + attributeChangedCallback(name, _old, value) { + super.attributeChangedCallback(name, _old, value); + if (name === 'autofocus') { + this.__syncAutofocusToFocusableElement(); + } + } + + /** + * @private + */ + __syncAutofocusToFocusableElement() { + if (!this._focusableNode) { + return; + } + + if (this.hasAttribute('autofocus')) { + this._focusableNode.setAttribute('autofocus', ''); + } else { + this._focusableNode.removeAttribute('autofocus'); + } + } + /** * Calls `focus()` on focusable element within */ diff --git a/packages/ui/components/form-core/test/FocusMixin.test.js b/packages/ui/components/form-core/test/FocusMixin.test.js index c5c5edb01..3638e55f8 100644 --- a/packages/ui/components/form-core/test/FocusMixin.test.js +++ b/packages/ui/components/form-core/test/FocusMixin.test.js @@ -323,6 +323,39 @@ describe('FocusMixin', () => { spy4.restore(); restoreMock4(); }); + + it('has mirrors syncs autofocus on the focusable element when autofocus changes', async () => { + const el = /** @type {Focusable} */ ( + await fixture(html` + <${tag}> + `) + ); + + // @ts-ignore [allow-protected] in test + const { _focusableNode } = el; + + el.setAttribute('autofocus', ''); + await el.updateComplete; + expect(_focusableNode.hasAttribute('autofocus')).to.be.true; + + el.removeAttribute('autofocus'); + await el.updateComplete; + expect(_focusableNode.hasAttribute('autofocus')).not.to.be.true; + }); + + it('has mirrors syncs autofocus on the focusable element when autofocus was set on render', async () => { + const el = /** @type {Focusable} */ ( + await fixture(html` + <${tag} autofocus> + `) + ); + + // @ts-ignore [allow-protected] in test + const { _focusableNode } = el; + + expect(el.hasAttribute('autofocus')).to.be.true; + expect(_focusableNode.hasAttribute('autofocus')).to.be.true; + }); }); }); }); diff --git a/packages/ui/components/form-core/types/FocusMixinTypes.ts b/packages/ui/components/form-core/types/FocusMixinTypes.ts index 3f3445c9e..11d79aa36 100644 --- a/packages/ui/components/form-core/types/FocusMixinTypes.ts +++ b/packages/ui/components/form-core/types/FocusMixinTypes.ts @@ -25,6 +25,12 @@ export declare class FocusHost { */ blur(): void; + /** + * Synchronizes property values when attributes change. + * @category attributes + */ + attributeChangedCallback(name: string, _old: string | null, value: string | null): void; + /** * The focusable element: * could be an input, textarea, select, button or any other element with tabindex > -1