Merge pull request #1963 from Sciurus7/autofocus-ui
fix(form-core) sync autofocus to focusable node (fixes #1775)
This commit is contained in:
commit
7089ea54e3
5 changed files with 140 additions and 0 deletions
5
.changeset/silly-shirts-promise.md
Normal file
5
.changeset/silly-shirts-promise.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'@lion/ui': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
[FocusMixin] now syncs autofocus between host and the focusable node.
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
/* eslint-disable lit-a11y/no-autofocus */
|
||||||
import { expect, fixture as _fixture, html, unsafeStatic } from '@open-wc/testing';
|
import { expect, fixture as _fixture, html, unsafeStatic } from '@open-wc/testing';
|
||||||
import { runOverlayMixinSuite } from '../../overlays/test-suites/OverlayMixin.suite.js';
|
import { runOverlayMixinSuite } from '../../overlays/test-suites/OverlayMixin.suite.js';
|
||||||
import '@lion/ui/define/lion-dialog.js';
|
import '@lion/ui/define/lion-dialog.js';
|
||||||
|
|
@ -72,4 +73,59 @@ describe('lion-dialog', () => {
|
||||||
expect(nestedDialogEl.opened).to.be.true;
|
expect(nestedDialogEl.opened).to.be.true;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('focus', () => {
|
||||||
|
it('sets focus on contentSlot by default', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-dialog>
|
||||||
|
<button slot="invoker">invoker button</button>
|
||||||
|
<div slot="content">
|
||||||
|
<label for="myInput">Label</label>
|
||||||
|
<input id="myInput" />
|
||||||
|
</div>
|
||||||
|
</lion-dialog>
|
||||||
|
`);
|
||||||
|
// @ts-expect-error [allow-protected-in-tests]
|
||||||
|
const invokerNode = el._overlayInvokerNode;
|
||||||
|
invokerNode.focus();
|
||||||
|
invokerNode.click();
|
||||||
|
const contentNode = el.querySelector('[slot="content"]');
|
||||||
|
expect(document.activeElement).to.equal(contentNode);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets focus on autofocused element', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-dialog>
|
||||||
|
<button slot="invoker">invoker button</button>
|
||||||
|
<div slot="content">
|
||||||
|
<label for="myInput">Label</label>
|
||||||
|
<input id="myInput" autofocus />
|
||||||
|
</div>
|
||||||
|
</lion-dialog>
|
||||||
|
`);
|
||||||
|
// @ts-expect-error [allow-protected-in-tests]
|
||||||
|
const invokerNode = el._overlayInvokerNode;
|
||||||
|
invokerNode.focus();
|
||||||
|
invokerNode.click();
|
||||||
|
const input = el.querySelector('input');
|
||||||
|
expect(document.activeElement).to.equal(input);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('with trapsKeyboardFocus set to false the focus stays on the invoker', async () => {
|
||||||
|
const el = /** @type {LionDialog} */ await fixture(html`
|
||||||
|
<lion-dialog .config=${{ trapsKeyboardFocus: false }}>
|
||||||
|
<button slot="invoker">invoker button</button>
|
||||||
|
<div slot="content">
|
||||||
|
<label for="myInput">Label</label>
|
||||||
|
<input id="myInput" autofocus />
|
||||||
|
</div>
|
||||||
|
</lion-dialog>
|
||||||
|
`);
|
||||||
|
// @ts-expect-error [allow-protected-in-tests]
|
||||||
|
const invokerNode = el._overlayInvokerNode;
|
||||||
|
invokerNode.focus();
|
||||||
|
invokerNode.click();
|
||||||
|
expect(document.activeElement).to.equal(invokerNode);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ const FocusMixinImplementation = superclass =>
|
||||||
return {
|
return {
|
||||||
focused: { type: Boolean, reflect: true },
|
focused: { type: Boolean, reflect: true },
|
||||||
focusedVisible: { type: Boolean, reflect: true, attribute: 'focused-visible' },
|
focusedVisible: { type: Boolean, reflect: true, attribute: 'focused-visible' },
|
||||||
|
autofocus: { type: Boolean, reflect: true }, // Required in Lit to observe autofocus
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -47,11 +48,13 @@ const FocusMixinImplementation = superclass =>
|
||||||
* @type {boolean}
|
* @type {boolean}
|
||||||
*/
|
*/
|
||||||
this.focusedVisible = false;
|
this.focusedVisible = false;
|
||||||
|
this.autofocus = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
this.__registerEventsForFocusMixin();
|
this.__registerEventsForFocusMixin();
|
||||||
|
this.__syncAutofocusToFocusableElement();
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnectedCallback() {
|
disconnectedCallback() {
|
||||||
|
|
@ -59,6 +62,31 @@ const FocusMixinImplementation = superclass =>
|
||||||
this.__teardownEventsForFocusMixin();
|
this.__teardownEventsForFocusMixin();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('lit').PropertyValues } changedProperties
|
||||||
|
*/
|
||||||
|
updated(changedProperties) {
|
||||||
|
super.updated(changedProperties);
|
||||||
|
if (changedProperties.has('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
|
* Calls `focus()` on focusable element within
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -323,6 +323,39 @@ describe('FocusMixin', () => {
|
||||||
spy4.restore();
|
spy4.restore();
|
||||||
restoreMock4();
|
restoreMock4();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('has mirrors syncs autofocus on the focusable element when autofocus changes', async () => {
|
||||||
|
const el = /** @type {Focusable} */ (
|
||||||
|
await fixture(html`
|
||||||
|
<${tag}><input slot="input"></${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><input slot="input"></${tag}>
|
||||||
|
`)
|
||||||
|
);
|
||||||
|
|
||||||
|
// @ts-ignore [allow-protected] in test
|
||||||
|
const { _focusableNode } = el;
|
||||||
|
|
||||||
|
expect(el.hasAttribute('autofocus')).to.be.true;
|
||||||
|
expect(_focusableNode.hasAttribute('autofocus')).to.be.true;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
/* eslint-disable lit-a11y/no-autofocus */
|
||||||
import { expect, fixture } from '@open-wc/testing';
|
import { expect, fixture } from '@open-wc/testing';
|
||||||
import { html } from 'lit';
|
import { html } from 'lit';
|
||||||
import { getAllTagNames } from './helpers/helpers.js';
|
import { getAllTagNames } from './helpers/helpers.js';
|
||||||
|
|
@ -70,4 +71,21 @@ describe('Form inside dialog Integrations', () => {
|
||||||
'lion-textarea',
|
'lion-textarea',
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('sets focus on first focusable element with autofocus', async () => {
|
||||||
|
const el = /** @type {LionDialog} */ await fixture(html`
|
||||||
|
<lion-dialog>
|
||||||
|
<button slot="invoker">invoker button</button>
|
||||||
|
<div slot="content">
|
||||||
|
<lion-input label="label" name="input" autofocus></lion-input>
|
||||||
|
<lion-textarea label="label" name="textarea" autofocus></lion-textarea>
|
||||||
|
</div>
|
||||||
|
</lion-dialog>
|
||||||
|
`);
|
||||||
|
// @ts-expect-error [allow-protected-in-tests]
|
||||||
|
el._overlayInvokerNode.click();
|
||||||
|
const lionInput = el.querySelector('[name="input"]');
|
||||||
|
// @ts-expect-error [allow-protected-in-tests]
|
||||||
|
expect(document.activeElement).to.equal(lionInput._focusableNode);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue