diff --git a/.changeset/cool-peaches-burn.md b/.changeset/cool-peaches-burn.md new file mode 100644 index 000000000..bed822eab --- /dev/null +++ b/.changeset/cool-peaches-burn.md @@ -0,0 +1,12 @@ +--- +'@lion/combobox': minor +'@lion/form-core': patch +'@lion/form-integrations': patch +--- + +Allow Subclassers of LionCombobox to set '\_syncToTextboxCondition' in closing phase of overlay + +## Fixes + +- form-core: allow an extra microtask in registration phase to make forms inside dialogs compatible. +- combobox: open on focused when showAllOnEmpty diff --git a/docs/components/inputs/combobox/features.md b/docs/components/inputs/combobox/features.md index 801d06bef..5a8cdd9a5 100644 --- a/docs/components/inputs/combobox/features.md +++ b/docs/components/inputs/combobox/features.md @@ -215,7 +215,10 @@ This will: ```js preview-story export const multipleChoice = () => html` - + ${lazyRender( listboxData.map( (entry, i) => diff --git a/docs/docs/systems/overlays/assets/umbrella-form.js b/docs/docs/systems/overlays/assets/umbrella-form.js new file mode 100644 index 000000000..578575026 --- /dev/null +++ b/docs/docs/systems/overlays/assets/umbrella-form.js @@ -0,0 +1,126 @@ +import { LitElement, html } from '@lion/core'; +import { Required, MinLength } from '@lion/form-core'; +import '@lion/form/define'; +import '@lion/fieldset/define'; +import '@lion/input/define'; +import '@lion/input-date/define'; +import '@lion/input-datepicker/define'; +import '@lion/input-amount/define'; +import '@lion/input-iban/define'; +import '@lion/input-email/define'; +import '@lion/checkbox-group/define'; +import '@lion/radio-group/define'; +import '@lion/select/define'; +import '@lion/select-rich/define'; +import '@lion/input-range/define'; +import '@lion/textarea/define'; +import '@lion/button/define'; + +export class UmbrellaForm extends LitElement { + get _lionFormNode() { + return /** @type {import('@lion/form').LionForm} */ (this.shadowRoot?.querySelector( + 'lion-form', + )); + } + + render() { + return html` + +
+ + + + + + + + + + + + + + + + + + + + + + + Red + Hotpink + Teal + + + + + + + + + + + +
+ Submit + { + const lionForm = this._lionFormNode; + lionForm.resetGroup(); + }} + >Reset +
+
+
+ `; + } +} +customElements.define('umbrella-form', UmbrellaForm); diff --git a/packages/combobox/src/LionCombobox.js b/packages/combobox/src/LionCombobox.js index 0d4b394e7..3de26b7af 100644 --- a/packages/combobox/src/LionCombobox.js +++ b/packages/combobox/src/LionCombobox.js @@ -390,6 +390,9 @@ export class LionCombobox extends OverlayMixin(LionListbox) { */ // eslint-disable-next-line class-methods-use-this _showOverlayCondition({ lastKey }) { + if (this.showAllOnEmpty && this.focused) { + return true; + } // when no keyboard action involved (on focused change), return current opened state if (!lastKey) { return this.opened; @@ -440,7 +443,12 @@ export class LionCombobox extends OverlayMixin(LionListbox) { __onOverlayClose() { if (!this.multipleChoice) { - if (this.checkedIndex !== -1) { + if ( + this.checkedIndex !== -1 && + this._syncToTextboxCondition(this.modelValue, this.__oldModelValue, { + phase: 'overlay-close', + }) + ) { this._inputNode.value = this.formElements[ /** @type {number} */ (this.checkedIndex) ].choiceValue; @@ -739,12 +747,15 @@ export class LionCombobox extends OverlayMixin(LionListbox) { } /** + * @overridable * @param {string|string[]} modelValue * @param {string|string[]} oldModelValue */ // eslint-disable-next-line no-unused-vars - _syncToTextboxCondition(modelValue, oldModelValue) { - return this.autocomplete === 'inline' || this.autocomplete === 'both'; + _syncToTextboxCondition(modelValue, oldModelValue, { phase } = {}) { + return ( + this.autocomplete === 'inline' || this.autocomplete === 'both' || phase === 'overlay-close' + ); } /** diff --git a/packages/combobox/test/lion-combobox.test.js b/packages/combobox/test/lion-combobox.test.js index f95242c75..4c3131c41 100644 --- a/packages/combobox/test/lion-combobox.test.js +++ b/packages/combobox/test/lion-combobox.test.js @@ -177,6 +177,22 @@ describe('lion-combobox', () => { el.autocomplete = 'both'; await performChecks(); }); + + it('shows overlay on focusin', async () => { + const el = /** @type {LionCombobox} */ (await fixture(html` + + Artichoke + Chard + Chicory + Victoria Plum + + `)); + + expect(el.opened).to.be.false; + el._comboboxNode.dispatchEvent(new Event('focusin', { bubbles: true, composed: true })); + await el.updateComplete; + expect(el.opened).to.be.true; + }); }); }); @@ -936,6 +952,46 @@ describe('lion-combobox', () => { await performChecks('both', [0, 1], ''); }); + it('is possible to adjust textbox synchronize condition on overlay close', async () => { + const el = /** @type {LionCombobox} */ (await fixture(html` + + Artichoke + Chard + Chicory + Victoria Plum + + `)); + expect(el._inputNode.value).to.equal(''); + + /** + * @param {'none' | 'list' | 'inline' | 'both'} autocomplete + * @param {number|number[]} index + * @param {string} valueOnClose + */ + async function performChecks(autocomplete, index, valueOnClose) { + await el.updateComplete; + el.opened = true; + el.setCheckedIndex(-1); + await el.updateComplete; + el.autocomplete = autocomplete; + el.setCheckedIndex(index); + el.opened = false; + await el.updateComplete; + expect(el._inputNode.value).to.equal(valueOnClose); + } + + await performChecks('none', 0, ''); + await performChecks('list', 0, ''); + await performChecks('inline', 0, ''); + await performChecks('both', 0, ''); + + el.multipleChoice = true; + await performChecks('none', [0, 1], ''); + await performChecks('list', [0, 1], ''); + await performChecks('inline', [0, 1], ''); + await performChecks('both', [0, 1], ''); + }); + it('does inline autocompletion when adding chars', async () => { const el = /** @type {LionCombobox} */ (await fixture(html` diff --git a/packages/form-core/src/choice-group/ChoiceGroupMixin.js b/packages/form-core/src/choice-group/ChoiceGroupMixin.js index 3595f5b68..7e62e44eb 100644 --- a/packages/form-core/src/choice-group/ChoiceGroupMixin.js +++ b/packages/form-core/src/choice-group/ChoiceGroupMixin.js @@ -181,7 +181,11 @@ const ChoiceGroupMixinImplementation = superclass => super.disconnectedCallback(); if (this.registrationComplete.done === false) { - this.__rejectRegistrationComplete(); + Promise.resolve().then(() => { + Promise.resolve().then(() => { + this.__rejectRegistrationComplete(); + }); + }); } } diff --git a/packages/form-core/src/form-group/FormGroupMixin.js b/packages/form-core/src/form-group/FormGroupMixin.js index eed4dea46..2190d6ca8 100644 --- a/packages/form-core/src/form-group/FormGroupMixin.js +++ b/packages/form-core/src/form-group/FormGroupMixin.js @@ -184,7 +184,9 @@ const FormGroupMixinImplementation = superclass => this.__hasActiveOutsideClickHandling = false; } if (this.registrationComplete.done === false) { - this.__rejectRegistrationComplete(); + Promise.resolve().then(() => { + this.__rejectRegistrationComplete(); + }); } } diff --git a/packages/form-integrations/test/dialog-integrations.js b/packages/form-integrations/test/dialog-integrations.js new file mode 100644 index 000000000..e661f544b --- /dev/null +++ b/packages/form-integrations/test/dialog-integrations.js @@ -0,0 +1,20 @@ +import { expect, fixture, html } from '@open-wc/testing'; +import './helpers/umbrella-form.js'; +import '@lion/dialog/lion-dialog.js'; + +/** + * @typedef {import('./helpers/umbrella-form.js').UmbrellaForm} UmbrellaForm + * @typedef {import('@lion/dialog/').LionDialog} LionDialog + */ + +// Test umbrella form inside dialog +describe('Form inside dialog Integrations', () => { + it('"Successfully spawns all form components inside a dialog', async () => { + expect( + await fixture(html` + + + `), + ).to.not.throw(); + }); +});