From bf21f30740bf6359159e2da7809470232f418bed Mon Sep 17 00:00:00 2001 From: bendera Date: Wed, 23 Apr 2025 23:53:51 +0200 Subject: [PATCH 01/66] Refactor _dispatchChangeEvent --- .../vscode-select/vscode-select-base.ts | 21 ------------------- .../vscode-multi-select.ts | 14 +++++++++++++ .../vscode-single-select.ts | 14 +++++++++++++ 3 files changed, 28 insertions(+), 21 deletions(-) diff --git a/src/includes/vscode-select/vscode-select-base.ts b/src/includes/vscode-select/vscode-select-base.ts index e04f80810..95ee7b28b 100644 --- a/src/includes/vscode-select/vscode-select-base.ts +++ b/src/includes/vscode-select/vscode-select-base.ts @@ -350,27 +350,6 @@ export class VscodeSelectBase extends VscElement { } protected _dispatchChangeEvent(): void { - if (!this._multiple) { - /** @deprecated */ - this.dispatchEvent( - new CustomEvent('vsc-change', { - detail: { - selectedIndex: this._selectedIndex, - value: this._value, - }, - }) - ); - } else { - /** @deprecated */ - this.dispatchEvent( - new CustomEvent('vsc-change', { - detail: { - selectedIndexes: this._selectedIndexes, - value: this._values, - }, - }) - ); - } this.dispatchEvent(new Event('change')); this.dispatchEvent(new Event('input')); } diff --git a/src/vscode-multi-select/vscode-multi-select.ts b/src/vscode-multi-select/vscode-multi-select.ts index 2f651c5c8..3c638ad22 100644 --- a/src/vscode-multi-select/vscode-multi-select.ts +++ b/src/vscode-multi-select/vscode-multi-select.ts @@ -196,6 +196,20 @@ export class VscodeMultiSelect } } + protected override _dispatchChangeEvent(): void { + /** @deprecated */ + this.dispatchEvent( + new CustomEvent('vsc-change', { + detail: { + selectedIndexes: this._selectedIndexes, + value: this._values, + }, + }) + ); + + super._dispatchChangeEvent(); + } + protected override _manageRequired() { const {value} = this; if (value.length === 0 && this.required) { diff --git a/src/vscode-single-select/vscode-single-select.ts b/src/vscode-single-select/vscode-single-select.ts index fc64d2ba0..37ce5464a 100644 --- a/src/vscode-single-select/vscode-single-select.ts +++ b/src/vscode-single-select/vscode-single-select.ts @@ -236,6 +236,20 @@ export class VscodeSingleSelect this._isPlaceholderOptionActive = false; } + protected override _dispatchChangeEvent(): void { + /** @deprecated */ + this.dispatchEvent( + new CustomEvent('vsc-change', { + detail: { + selectedIndex: this._selectedIndex, + value: this._value, + }, + }) + ); + + super._dispatchChangeEvent(); + } + protected override _onSlotChange(): void { super._onSlotChange(); From 6aa5c065ecb52de88f47d061f04b55ab42a27f4a Mon Sep 17 00:00:00 2001 From: bendera Date: Wed, 23 Apr 2025 23:55:47 +0200 Subject: [PATCH 02/66] Refactor _onFaceClick() --- src/includes/vscode-select/vscode-select-base.ts | 4 ---- src/vscode-multi-select/vscode-multi-select.ts | 5 +++++ 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/includes/vscode-select/vscode-select-base.ts b/src/includes/vscode-select/vscode-select-base.ts index 95ee7b28b..30945fe6c 100644 --- a/src/includes/vscode-select/vscode-select-base.ts +++ b/src/includes/vscode-select/vscode-select-base.ts @@ -358,10 +358,6 @@ export class VscodeSelectBase extends VscElement { protected _onFaceClick(): void { this._toggleDropdown(!this.open); - - if (this._multiple) { - this._activeIndex = 0; - } } private _onClickOutside = (event: MouseEvent): void => { diff --git a/src/vscode-multi-select/vscode-multi-select.ts b/src/vscode-multi-select/vscode-multi-select.ts index 3c638ad22..8a974c62d 100644 --- a/src/vscode-multi-select/vscode-multi-select.ts +++ b/src/vscode-multi-select/vscode-multi-select.ts @@ -210,6 +210,11 @@ export class VscodeMultiSelect super._dispatchChangeEvent(); } + protected override _onFaceClick(): void { + super._onFaceClick(); + this._activeIndex = 0; + } + protected override _manageRequired() { const {value} = this; if (value.length === 0 && this.required) { From a7a781ab93a15b1c812b2cf82b543516c1dc4f70 Mon Sep 17 00:00:00 2001 From: bendera Date: Wed, 23 Apr 2025 23:56:37 +0200 Subject: [PATCH 03/66] Refactor _toggleComboboxDropdown() --- src/includes/vscode-select/vscode-select-base.ts | 6 +----- src/vscode-multi-select/vscode-multi-select.ts | 5 +++++ 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/includes/vscode-select/vscode-select-base.ts b/src/includes/vscode-select/vscode-select-base.ts index 30945fe6c..f4cb8c477 100644 --- a/src/includes/vscode-select/vscode-select-base.ts +++ b/src/includes/vscode-select/vscode-select-base.ts @@ -375,13 +375,9 @@ export class VscodeSelectBase extends VscElement { window.removeEventListener('mousemove', this._onMouseMove); }; - private _toggleComboboxDropdown() { + protected _toggleComboboxDropdown() { this._filterPattern = ''; this._toggleDropdown(!this.open); - - if (this._multiple) { - this._activeIndex = -1; - } } protected _onComboboxButtonClick(): void { diff --git a/src/vscode-multi-select/vscode-multi-select.ts b/src/vscode-multi-select/vscode-multi-select.ts index 8a974c62d..eabed2e90 100644 --- a/src/vscode-multi-select/vscode-multi-select.ts +++ b/src/vscode-multi-select/vscode-multi-select.ts @@ -215,6 +215,11 @@ export class VscodeMultiSelect this._activeIndex = 0; } + protected override _toggleComboboxDropdown(): void { + super._toggleComboboxDropdown(); + this._activeIndex = -1; + } + protected override _manageRequired() { const {value} = this; if (value.length === 0 && this.required) { From 9a2c56ee6bcd0b4882f5b279442a6cb32e053df9 Mon Sep 17 00:00:00 2001 From: bendera Date: Thu, 24 Apr 2025 22:47:23 +0200 Subject: [PATCH 04/66] Don't remove the dropdown from the DOM --- src/includes/vscode-select/styles.ts | 5 + .../vscode-select/vscode-select-base.ts | 9 +- .../vscode-multi-select.test.ts | 24 +- .../vscode-single-select.test.ts | 283 +++++------------- 4 files changed, 89 insertions(+), 232 deletions(-) diff --git a/src/includes/vscode-select/styles.ts b/src/includes/vscode-select/styles.ts index bf5d0d028..05c8d1a72 100644 --- a/src/includes/vscode-select/styles.ts +++ b/src/includes/vscode-select/styles.ts @@ -164,6 +164,7 @@ export default [ border-style: solid; border-width: 1px; box-sizing: border-box; + display: none; left: 0; padding-bottom: 2px; position: absolute; @@ -172,6 +173,10 @@ export default [ z-index: var(--dropdown-z-index, 2); } + .dropdown.open { + display: block; + } + :host([position='above']) .dropdown { border-radius: 3px 3px 0 0; bottom: 26px; diff --git a/src/includes/vscode-select/vscode-select-base.ts b/src/includes/vscode-select/vscode-select-base.ts index f4cb8c477..2101a5a02 100644 --- a/src/includes/vscode-select/vscode-select-base.ts +++ b/src/includes/vscode-select/vscode-select-base.ts @@ -834,13 +834,14 @@ export class VscodeSelectBase extends VscElement { } private _renderDropdown() { - const classes = classMap({ + const classes = { dropdown: true, multiple: this._multiple, - }); + open: this.open, + }; return html` -
+
${this.position === 'above' ? this._renderDescription() : nothing} ${this._renderOptions()} ${this._renderDropdownControls()} ${this.position === 'below' ? this._renderDescription() : nothing} @@ -852,7 +853,7 @@ export class VscodeSelectBase extends VscElement { return html` ${this.combobox ? this._renderComboboxFace() : this._renderSelectFace()} - ${this.open ? this._renderDropdown() : nothing} + ${this._renderDropdown()} `; } } diff --git a/src/vscode-multi-select/vscode-multi-select.test.ts b/src/vscode-multi-select/vscode-multi-select.test.ts index 7ff55bb17..da29b48f6 100644 --- a/src/vscode-multi-select/vscode-multi-select.test.ts +++ b/src/vscode-multi-select/vscode-multi-select.test.ts @@ -20,19 +20,9 @@ describe('vscode-multi-select', () => { `)) as VscodeMultiSelect; - expect(el).shadowDom.to.equal( - ` - -
- - 1 item selected - - - -
- `, - {ignoreAttributes: ['tabindex']} - ); + const badge = el.shadowRoot?.querySelector('.select-face-badge'); + + expect(badge).lightDom.to.eq('1 item selected'); expect(el.selectedIndexes).to.eql([1]); expect(el.value).to.eql(['Ipsum']); }); @@ -143,7 +133,9 @@ describe('vscode-multi-select', () => { el.shadowRoot?.querySelector('.select-face'); selectFace!.click(); await el.updateComplete; - const dropdownVisibleBefore = !!el.shadowRoot?.querySelector('.dropdown'); + const dropdownVisibleBefore = el.shadowRoot + ?.querySelector('.dropdown') + ?.classList.contains('open'); const button = el.shadowRoot?.querySelector('.button-accept'); @@ -153,7 +145,9 @@ describe('vscode-multi-select', () => { button?.click(); await el.updateComplete; - const dropdownVisibleAfter = !!el.shadowRoot?.querySelector('.dropdown'); + const dropdownVisibleAfter = el.shadowRoot + ?.querySelector('.dropdown') + ?.classList.contains('open'); expect(dropdownVisibleBefore).to.eq(true); expect(dropdownVisibleAfter).to.eq(false); diff --git a/src/vscode-single-select/vscode-single-select.test.ts b/src/vscode-single-select/vscode-single-select.test.ts index f36fe898b..54ba3cf60 100644 --- a/src/vscode-single-select/vscode-single-select.test.ts +++ b/src/vscode-single-select/vscode-single-select.test.ts @@ -21,19 +21,9 @@ describe('vscode-single-select', () => { `)) as VscodeSingleSelect; - expect(el).shadowDom.to.equal( - ` - -
- - Ipsum - - - -
- `, - {ignoreAttributes: ['tabindex']} - ); + const dropdown = el.shadowRoot?.querySelector('.dropdown'); + + expect(dropdown?.classList.contains('open')).to.be.false; expect(el.selectedIndex).to.eq(1); expect(el.value).to.eq('Ipsum'); }); @@ -91,19 +81,9 @@ describe('vscode-single-select', () => { el.value = 'Ipsum'; await el.updateComplete; - expect(el).shadowDom.to.equal( - ` - -
- - Ipsum - - - -
- `, - {ignoreAttributes: ['tabindex']} - ); + const dropdown = el.shadowRoot?.querySelector('.dropdown'); + + expect(dropdown?.classList.contains('open')).to.be.false; expect(el.value).to.eq('Ipsum'); expect(el.selectedIndex).to.eq(1); }); @@ -120,17 +100,9 @@ describe('vscode-single-select', () => { el.value = 'trololo'; await el.updateComplete; - expect(el).shadowDom.to.equal( - ` - -
- - - -
- `, - {ignoreAttributes: ['tabindex']} - ); + const dropdown = el.shadowRoot?.querySelector('.dropdown'); + + expect(dropdown?.classList.contains('open')).to.be.false; expect(el.value).to.eq(''); expect(el.selectedIndex).to.eq(-1); }); @@ -147,19 +119,9 @@ describe('vscode-single-select', () => { el.selectedIndex = 1; await el.updateComplete; - expect(el).shadowDom.to.equal( - ` - -
- - Ipsum - - - -
- `, - {ignoreAttributes: ['tabindex']} - ); + const dropdown = el.shadowRoot?.querySelector('.dropdown'); + + expect(dropdown?.classList.contains('open')).to.be.false; expect(el.value).to.eq('Ipsum'); expect(el.selectedIndex).to.eq(1); }); @@ -176,17 +138,9 @@ describe('vscode-single-select', () => { el.selectedIndex = 999; await el.updateComplete; - expect(el).shadowDom.to.equal( - ` - -
- - - -
- `, - {ignoreAttributes: ['tabindex']} - ); + const dropdown = el.shadowRoot?.querySelector('.dropdown'); + + expect(dropdown?.classList.contains('open')).to.be.false; expect(el.value).to.eq(''); expect(el.selectedIndex).to.eq(999); }); @@ -237,17 +191,9 @@ describe('vscode-single-select', () => { `)) as VscodeSingleSelect; - expect(el).shadowDom.to.equal( - ` - -
- Lorem - - -
- `, - {ignoreAttributes: ['tabindex']} - ); + const dropdown = el.shadowRoot?.querySelector('.dropdown'); + + expect(dropdown?.classList.contains('open')).to.be.false; expect(el.value).to.eq('Lorem'); expect(el.selectedIndex).to.eq(0); }); @@ -266,19 +212,9 @@ describe('vscode-single-select', () => { el.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowDown'})); await el.updateComplete; - expect(el).shadowDom.to.eq( - ` - -
- - Ipsum - - - -
- `, - {ignoreAttributes: ['tabindex']} - ); + const dropdown = el.shadowRoot?.querySelector('.dropdown'); + + expect(dropdown?.classList.contains('open')).to.be.false; expect(el.value).to.eq('Ipsum'); expect(el.selectedIndex).to.eq(1); @@ -305,19 +241,11 @@ describe('vscode-single-select', () => { el.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowUp'})); await el.updateComplete; - expect(el).shadowDom.to.eq( - ` - -
- - Ipsum - - - -
- `, - {ignoreAttributes: ['tabindex']} - ); + const dropdown = el.shadowRoot?.querySelector('.dropdown'); + const text = el.shadowRoot?.querySelector('.text'); + + expect(dropdown?.classList.contains('open')).to.be.false; + expect(text).lightDom.to.eq('Ipsum'); expect(el.value).to.eq('Ipsum'); expect(el.selectedIndex).to.eq(1); @@ -338,7 +266,7 @@ describe('vscode-single-select', () => {
- `; } + //#endregion } declare global { From 720e6e30c9bc665244bfd7f35eb1ce0ea1c58325 Mon Sep 17 00:00:00 2001 From: bendera Date: Thu, 1 May 2025 18:51:34 +0200 Subject: [PATCH 13/66] Fix select next option with the arrow down key --- src/includes/vscode-select/vscode-select-base.ts | 11 +++-------- src/vscode-single-select/vscode-single-select.test.ts | 7 +++++-- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/includes/vscode-select/vscode-select-base.ts b/src/includes/vscode-select/vscode-select-base.ts index 50af3e714..b80fe231b 100644 --- a/src/includes/vscode-select/vscode-select-base.ts +++ b/src/includes/vscode-select/vscode-select-base.ts @@ -449,7 +449,6 @@ export class VscodeSelectBase extends VscElement { return; } - const list = this.combobox ? this._filteredOptions : this._options; const showDropdownNext = !this.open; this._toggleDropdown(showDropdownNext); @@ -459,8 +458,7 @@ export class VscodeSelectBase extends VscElement { !showDropdownNext && this._selectedIndex !== this._activeIndex ) { - this._selectedIndex = - this._activeIndex > -1 ? list[this._activeIndex].index : -1; + this._selectedIndex = this._activeIndex > -1 ? this._activeIndex : -1; this._value = this._selectedIndex > -1 ? this._options[this._selectedIndex].value @@ -473,10 +471,7 @@ export class VscodeSelectBase extends VscElement { this._createAndSelectSuggestedOption(); } else { if (!this._multiple && !showDropdownNext) { - this._selectedIndex = - this._activeIndex > -1 - ? this._filteredOptions[this._activeIndex].index - : -1; + this._selectedIndex = this._activeIndex > -1 ? this._activeIndex : -1; } if (!this._multiple && showDropdownNext) { @@ -621,7 +616,7 @@ export class VscodeSelectBase extends VscElement { ); if (nextSelectable > -1) { - this._activeIndex = nextSelectable; + this._activeIndex = currentOptions[nextSelectable].index; this._adjustOptionListScrollPos('down', nextSelectable); } } diff --git a/src/vscode-single-select/vscode-single-select.test.ts b/src/vscode-single-select/vscode-single-select.test.ts index 8b20e5184..977b9ec95 100644 --- a/src/vscode-single-select/vscode-single-select.test.ts +++ b/src/vscode-single-select/vscode-single-select.test.ts @@ -883,8 +883,11 @@ describe('vscode-single-select', () => { //#region keyboard interactions //#region textbox - it("when ESC key is pressed, closes the listbox if it is displayed"); - it("when the ESC key is pressed, clears the textbox if the listbox is not displayed"); + it('when ESC key is pressed, closes the listbox if it is displayed'); + it( + 'when the ESC key is pressed, clears the textbox if the listbox is not displayed' + ); + it('when the ArrowDown key is pressed selects the next option in a filtered list'); //#endregion //#endregion }); From 66ac2e948064ef79b2594f0d9e28219535ff827e Mon Sep 17 00:00:00 2001 From: bendera Date: Sat, 3 May 2025 00:49:28 +0200 Subject: [PATCH 14/66] Toggle options with Enter key --- .../vscode-select/vscode-select-base.ts | 61 +++---------------- .../vscode-multi-select.ts | 35 +++++++++++ .../vscode-single-select.ts | 39 +++++++++++- 3 files changed, 78 insertions(+), 57 deletions(-) diff --git a/src/includes/vscode-select/vscode-select-base.ts b/src/includes/vscode-select/vscode-select-base.ts index b80fe231b..83dad03d0 100644 --- a/src/includes/vscode-select/vscode-select-base.ts +++ b/src/includes/vscode-select/vscode-select-base.ts @@ -448,41 +448,6 @@ export class VscodeSelectBase extends VscElement { if (clickedOnAcceptButton) { return; } - - const showDropdownNext = !this.open; - - this._toggleDropdown(showDropdownNext); - - if ( - !this._multiple && - !showDropdownNext && - this._selectedIndex !== this._activeIndex - ) { - this._selectedIndex = this._activeIndex > -1 ? this._activeIndex : -1; - this._value = - this._selectedIndex > -1 - ? this._options[this._selectedIndex].value - : ''; - this._dispatchChangeEvent(); - } - - if (this.combobox) { - if (this._isPlaceholderOptionActive) { - this._createAndSelectSuggestedOption(); - } else { - if (!this._multiple && !showDropdownNext) { - this._selectedIndex = this._activeIndex > -1 ? this._activeIndex : -1; - } - - if (!this._multiple && showDropdownNext) { - this._scrollActiveElementToTop(); - } - } - } - - if (this._multiple && showDropdownNext) { - this._activeIndex = 0; - } } private _onSpaceKeyDown() { @@ -490,24 +455,6 @@ export class VscodeSelectBase extends VscElement { this._toggleDropdown(true); return; } - - if (this.open && this._multiple && this._activeIndex > -1) { - const opts = this.combobox ? this._filteredOptions : this._options; - const selectedOption = opts[this._activeIndex]; - const nextSelectedIndexes: number[] = []; - - this._options[selectedOption.index].selected = !selectedOption.selected; - - opts.forEach(({index}) => { - const {selected} = this._options[index]; - - if (selected) { - nextSelectedIndexes.push(index); - } - }); - - this._selectedIndexes = nextSelectedIndexes; - } } protected _scrollActiveElementToTop() { @@ -587,7 +534,6 @@ export class VscodeSelectBase extends VscElement { } protected _onArrowDownKeyDown(): void { - console.log(this); let numOpts = this.combobox ? this._filteredOptions.length : this._options.length; @@ -691,6 +637,12 @@ export class VscodeSelectBase extends VscElement { this._toggleDropdown(true); } + protected _onComboboxInputSpaceKeyDown(ev: KeyboardEvent) { + if (ev.key === ' ') { + ev.stopPropagation(); + } + } + protected _onOptionClick(_ev: MouseEvent) { this._isBeingFiltered = false; return; @@ -855,6 +807,7 @@ export class VscodeSelectBase extends VscElement { @blur=${this._onComboboxInputBlur} @input=${this._onComboboxInputInput} @click=${this._onComboboxInputClick} + @keydown=${this._onComboboxInputSpaceKeyDown} > diff --git a/eslint.config.mjs b/eslint.config.mjs index 4ac79d134..46af95780 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -38,6 +38,7 @@ export default [ }, rules: { + 'no-console': 'error', 'no-unexpected-multiline': 'off', '@typescript-eslint/indent': 'off', '@typescript-eslint/explicit-function-return-type': 'off', diff --git a/src/includes/vscode-select/vscode-select-base.ts b/src/includes/vscode-select/vscode-select-base.ts index 83dad03d0..e07d5817c 100644 --- a/src/includes/vscode-select/vscode-select-base.ts +++ b/src/includes/vscode-select/vscode-select-base.ts @@ -364,6 +364,7 @@ export class VscodeSelectBase extends VscElement { protected async _createAndSelectSuggestedOption() {} + //#region event handlers protected _onFaceClick(): void { this._toggleDropdown(!this.open); } @@ -647,6 +648,7 @@ export class VscodeSelectBase extends VscElement { this._isBeingFiltered = false; return; } + //#endregion //#region render functions protected _renderOptions(): TemplateResult | TemplateResult[] { @@ -791,12 +793,13 @@ export class VscodeSelectBase extends VscElement { ${this._multiple ? this._renderMultiSelectLabel() : nothing} { //#region keyboard interactions - it('the value should be changed when the arrow down key pressed while the dropdown is closed', async () => { const el = (await fixture(html` @@ -831,54 +830,13 @@ describe('vscode-single-select', () => { `)) as VscodeSingleSelect; - // selects the first option - await clickOnElement(el); - let options = el.shadowRoot?.querySelector('.options'); - const firstOption = options?.querySelector('li')!; - await clickOnElement(firstOption); - - expect(el.value).to.eq('Lorem'); - - // re-opens the dropdown, selects the second option with keyboard - await clickOnElement(el); - await sendKeys({down: 'ArrowDown'}); - - expect(el.shadowRoot?.querySelector('.dropdown')).lightDom.to.eq( - ` -
    -
  • Lorem
  • -
  • Ipsum
  • -
  • Dolor
  • -
- `, - { - ignoreAttributes: [ - 'aria-selected', - 'data-index', - 'data-filtered-index', - 'id', - 'role', - ], - } - ); + el.value = 'Ipsum'; + await el.updateComplete; - // clicks outside, closes the dropdown - await sendMouse({type: 'click', position: [500, 500]}); - // opens the dropdown again await clickOnElement(el); + const activeOption = el.shadowRoot?.querySelector('.option.active'); - expect(el.shadowRoot?.querySelector('.dropdown')).lightDom.to.eq( - ` -
    -
  • Lorem
  • -
  • Ipsum
  • -
  • Dolor
  • -
- `, - { - ignoreAttributes: ['data-index', 'data-filtered-index'], - } - ); + expect(activeOption).lightDom.to.eq('Ipsum'); }); //#region keyboard interactions @@ -887,7 +845,25 @@ describe('vscode-single-select', () => { it( 'when the ESC key is pressed, clears the textbox if the listbox is not displayed' ); - it('when the ArrowDown key is pressed selects the next option in a filtered list'); + + it('when the ArrowDown key is pressed selects the next option in a filtered list', async () => { + const el = await fixture( + html` + Lorem + Ipsum + Dolor + ` + ); + + el.focus(); + await sendKeys({type: 'l'}); + await sendKeys({down: 'ArrowDown'}); + await sendKeys({down: 'ArrowDown'}); + + const activeOption = el.shadowRoot?.querySelector('.option.active'); + + expect(activeOption).lightDom.to.eq('Dolor'); + }); //#endregion //#endregion }); @@ -1093,7 +1069,7 @@ describe('vscode-single-select', () => { }); it('skips disabled options', async () => { - const el = await fixture(html` + const el = await fixture(html` Lorem Ipsum @@ -1102,13 +1078,13 @@ describe('vscode-single-select', () => { `); - await clickOnElement(el); - await clickOnElement(el); - await sendKeys({down: 'ArrowDown'}); + el.focus(); + await sendKeys({press: 'ArrowDown'}); + await sendKeys({press: 'ArrowDown'}); - const text = el.shadowRoot?.querySelector('.text'); + const activeOption = el.shadowRoot?.querySelector('.option.active'); - expect(text).lightDom.to.eq('Sit'); + expect(activeOption).lightDom.to.eq('Sit'); }); it('skips disabled options when dropdown is open', async () => { @@ -1235,7 +1211,9 @@ describe('vscode-single-select', () => { }); describe('closed dropdown', () => { - it('opens the dropdown if it is not already displayed when DownArrow is pressed'); + it( + 'opens the dropdown if it is not already displayed when DownArrow is pressed' + ); it('opens the dropdown when Alt + DownArrow are pressed'); it('Up Arrow is pressed'); it('Enter is pressed'); From 541155f840901e72615bb72ae80e242d14d1b91a Mon Sep 17 00:00:00 2001 From: bendera Date: Tue, 20 May 2025 23:07:57 +0200 Subject: [PATCH 16/66] wip 0520 --- .../vscode-select/OptionListController.ts | 307 ++++++++++++++++++ src/includes/vscode-select/helpers.ts | 4 +- src/includes/vscode-select/types.ts | 8 +- .../vscode-select/vscode-select-base.ts | 153 ++++++--- .../vscode-multi-select.ts | 1 + .../vscode-single-select.test.ts | 11 +- .../vscode-single-select.ts | 1 + 7 files changed, 430 insertions(+), 55 deletions(-) create mode 100644 src/includes/vscode-select/OptionListController.ts diff --git a/src/includes/vscode-select/OptionListController.ts b/src/includes/vscode-select/OptionListController.ts new file mode 100644 index 000000000..0eea90f08 --- /dev/null +++ b/src/includes/vscode-select/OptionListController.ts @@ -0,0 +1,307 @@ +import {ReactiveController, ReactiveControllerHost} from 'lit'; +import {FilterMethod, InternalOption, Option} from './types'; +import { + containsSearch, + fuzzySearch, + SearchResult, + startsWithPerTermSearch, + startsWithSearch, +} from './helpers'; + +export class OptionListController implements ReactiveController { + private _activeIndex = -1; + private _host: ReactiveControllerHost; + private _options: InternalOption[] = []; + private _filterPattern = ''; + private _filterMethod: FilterMethod = 'fuzzy'; + private _combobox = false; + private _indexByValue: Map = new Map(); + private _selectedIndex = -1; + private _selectedIndexes: number[] = []; + private _multiSelect = false; + + constructor(host: ReactiveControllerHost) { + (this._host = host).addController(this); + } + + hostConnected(): void {} + + set activeIndex(index: number) { + this._activeIndex = index; + } + + get activeIndex(): number { + return this._activeIndex; + } + + get relativeActiveIndex(): number { + const activeOption = this._options[this._activeIndex]; + return activeOption.relativeIndex; + } + + get comboboxMode() { + return this._combobox; + } + + set multiSelect(multiSelect: boolean) { + this._multiSelect = multiSelect; + this._host.requestUpdate(); + } + + get multiSelect() { + return this._multiSelect; + } + + set selectedIndex(value: number) { + this._selectedIndex = value; + this._host.requestUpdate(); + } + + get selectedIndex() { + return this._selectedIndex; + } + + get value(): string | string[] { + if (this._multiSelect) { + return this._selectedIndexes.length > 0 + ? this._selectedIndexes.map((v) => this._options[v].value) + : []; + } else { + return this._selectedIndex > -1 + ? this._options[this._selectedIndex].value + : ''; + } + } + + set value(newValue: string | string[]) { + if (this._multiSelect) { + this._selectedIndexes = (newValue as string[]) + .map((v) => this._indexByValue.get(v)) + .filter((v) => v !== undefined); + } else { + this._selectedIndex = this._indexByValue.get(newValue as string) ?? -1; + } + this._host.requestUpdate(); + } + + set filterPattern(pattern: string) { + this._filterPattern = pattern; + this._updateState(); + } + + get filterPattern() { + return this._filterPattern; + } + + set filterMethod(method: FilterMethod) { + this._filterMethod = method; + this._updateState(); + } + + get filterMethod(): FilterMethod { + return this._filterMethod; + } + + populate(options: Option[]) { + this._indexByValue.clear(); + + this._options = options.map((op, index) => { + this._indexByValue.set(op.value, index); + + return { + description: op.description ?? '', + disabled: op.disabled ?? false, + label: op.label ?? '', + selected: op.selected ?? false, + value: op.value ?? '', + index, + relativeIndex: index, + absoluteIndex: index, + ranges: [], + visible: true, + }; + }); + } + + add(option: Option) { + const nextIndex = this._options.length; + const {description, disabled, label, selected, value} = option; + let visible = true; + let ranges: [number, number][] = []; + + if (this._combobox) { + const res = this._searchByPattern(label); + visible = res.match; + ranges = res.ranges; + } + + this._indexByValue.set(value, nextIndex); + + if (selected) { + this._selectedIndex = nextIndex; + } + + this._options.push({ + index: nextIndex, + relativeIndex: nextIndex, + description, + disabled, + label, + selected, + value, + visible, + ranges, + }); + } + + clear() { + this._options = []; + } + + get options(): InternalOption[] { + return this._options; + } + + get numOfVisibleOptions() { + return this._options.filter((o) => o.visible).length; + } + + get numOptions() { + return this._options.length; + } + + toggleComboboxMode(enabled: boolean) { + this._combobox = enabled; + this._host.requestUpdate(); + } + + getOptionByValue(value: string): InternalOption | null { + const index = this._indexByValue.get(value) ?? -1; + + if (index === -1) { + return null; + } + + return this._options[index]; + } + + getNextSelectableOption(fromIndex?: number): InternalOption | null { + const from = fromIndex ?? this._activeIndex; + + if (this._options.length === 0) { + return null; + } + + if (this._options.length === 1) { + return this._options[0]; + } + + if (!this._options[from] || !this._options[from + 1]) { + return this._options[this._options.length - 1]; + } + + let nextIndex = -1; + + for (let i = from + 1; i < this._options.length; i++) { + if ( + this._options[i] && + !this._options[i].disabled && + this._options[i].visible + ) { + nextIndex = i; + break; + } + } + + return nextIndex > -1 ? this._options[nextIndex] : null; + } + + getPrevSelectableOption(fromIndex?: number): InternalOption | null { + const from = fromIndex ?? this._activeIndex; + + if (this._options.length === 0) { + return null; + } + + if (this._options.length === 1) { + return this._options[0]; + } + + if (!this._options[from] || !this._options[from - 1]) { + return this._options[0]; + } + + let prevIndex = -1; + + for (let i = from - 1; i >= 0; i--) { + if ( + this._options[i] && + !this._options[i].disabled && + this._options[i].visible + ) { + prevIndex = i; + break; + } + } + + return prevIndex > -1 ? this._options[prevIndex] : null; + } + + activateNext() { + const nextOp = this.getNextSelectableOption(); + this._activeIndex = nextOp?.index ?? -1; + this._host.requestUpdate(); + return nextOp; + } + + selectPrev() { + const prevOp = this.getPrevSelectableOption(); + this._activeIndex = prevOp?.index ?? -1; + this._host.requestUpdate(); + return prevOp; + } + + private _searchByPattern(text: string) { + let result: SearchResult; + + switch (this._filterMethod) { + case 'startsWithPerTerm': + result = startsWithPerTermSearch(text, this._filterPattern); + break; + case 'startsWith': + result = startsWithSearch(text, this._filterPattern); + break; + case 'contains': + result = containsSearch(text, this._filterPattern); + break; + default: + result = fuzzySearch(text, this._filterPattern); + } + + return result; + } + + _updateState() { + if (!this._combobox || this._filterPattern === '') { + this._options.forEach((_, i) => { + this._options[i].visible = true; + }); + + this._host.requestUpdate(); + return; + } + + let filteredListNextIndex = -1; + + this._options.forEach(({label}, i) => { + const result = this._searchByPattern(label); + this._options[i].visible = result.match; + this._options[i].ranges = result.ranges; + this._options[i].relativeIndex = result.match + ? ++filteredListNextIndex + : -1; + }); + + this._host.requestUpdate(); + } +} diff --git a/src/includes/vscode-select/helpers.ts b/src/includes/vscode-select/helpers.ts index 4161a3a83..1bef08426 100644 --- a/src/includes/vscode-select/helpers.ts +++ b/src/includes/vscode-select/helpers.ts @@ -1,5 +1,5 @@ import {html, TemplateResult} from 'lit'; -import {InternalOption, SearchMethod} from './types.js'; +import {InternalOption, FilterMethod} from './types.js'; export type SearchResult = { match: boolean; @@ -112,7 +112,7 @@ export const fuzzySearch = (subject: string, pattern: string): SearchResult => { export const filterOptionsByPattern = ( list: InternalOption[], pattern: string, - method: SearchMethod + method: FilterMethod ): InternalOption[] => { const filtered: InternalOption[] = []; diff --git a/src/includes/vscode-select/types.ts b/src/includes/vscode-select/types.ts index 93523ef3a..28ea6741b 100644 --- a/src/includes/vscode-select/types.ts +++ b/src/includes/vscode-select/types.ts @@ -1,3 +1,4 @@ +// TODO: Make all property optional export interface Option { label: string; value: string; @@ -8,10 +9,15 @@ export interface Option { export interface InternalOption extends Option { index: number; + relativeIndex: number; + /** The original index of the option in the non-filtered list. */ + absoluteIndex?: number; + /** Character ranges to highlight matches in the filtered list. */ ranges?: [number, number][]; + visible?: boolean; } -export type SearchMethod = +export type FilterMethod = | 'startsWithPerTerm' | 'startsWith' | 'contains' diff --git a/src/includes/vscode-select/vscode-select-base.ts b/src/includes/vscode-select/vscode-select-base.ts index e07d5817c..b9ab426bb 100644 --- a/src/includes/vscode-select/vscode-select-base.ts +++ b/src/includes/vscode-select/vscode-select-base.ts @@ -10,7 +10,7 @@ import {repeat} from 'lit/directives/repeat.js'; import '../../vscode-button/index.js'; import '../../vscode-option/index.js'; import {VscodeOption} from '../../vscode-option/index.js'; -import type {InternalOption, Option, SearchMethod} from './types.js'; +import type {InternalOption, Option, FilterMethod} from './types.js'; import { filterOptionsByPattern, findNextSelectableOptionIndex, @@ -20,6 +20,7 @@ import { import {VscElement} from '../VscElement.js'; import {chevronDownIcon} from './template-elements.js'; import {ifDefined} from 'lit/directives/if-defined.js'; +import {OptionListController} from './OptionListController.js'; export const VISIBLE_OPTS = 10; export const OPT_HEIGHT = 22; @@ -35,7 +36,12 @@ export class VscodeSelectBase extends VscElement { * Options can be filtered by typing into a text input field. */ @property({type: Boolean, reflect: true}) - combobox = false; + set combobox(enabled: boolean) { + this._opts.toggleComboboxMode(enabled); + } + get combobox() { + return this._opts.comboboxMode; + } /** * Accessible label for screen readers. When a `` is connected @@ -85,25 +91,32 @@ export class VscodeSelectBase extends VscElement { */ @property() set filter(val: 'contains' | 'fuzzy' | 'startsWith' | 'startsWithPerTerm') { - const validValues: SearchMethod[] = [ + const validValues: FilterMethod[] = [ 'contains', 'fuzzy', 'startsWith', 'startsWithPerTerm', ]; - if (validValues.includes(val as SearchMethod)) { - this._filter = val as SearchMethod; + let fm: FilterMethod; + + if (validValues.includes(val as FilterMethod)) { + // this._filter = val as FilterMethod; + fm = val; } else { - this._filter = 'fuzzy'; + // this._filter = 'fuzzy'; + // eslint-disable-next-line no-console console.warn( `[VSCode Webview Elements] Invalid filter: "${val}", fallback to default. Valid values are: "contains", "fuzzy", "startsWith", "startsWithPerm".`, this ); + fm = 'fuzzy'; } + + this._opts.filterMethod = fm; } get filter(): 'contains' | 'fuzzy' | 'startsWith' | 'startsWithPerTerm' { - return this._filter; + return this._opts.filterMethod; } /** @@ -124,10 +137,11 @@ export class VscodeSelectBase extends VscElement { */ @property({type: Array}) set options(opts: Option[]) { - this._options = opts.map((op, index) => ({...op, index})); + // this._options = opts.map((op, index) => ({...op, index})); + this._opts.populate(opts); } get options(): Option[] { - return this._options.map( + return this._opts.options.map( ({label, value, description, selected, disabled}) => ({ label, value, @@ -154,6 +168,8 @@ export class VscodeSelectBase extends VscElement { }) private _assignedOptions!: VscodeOption[]; + protected _opts = new OptionListController(this); + constructor() { super(); this.addEventListener('vsc-option-state-change', (ev) => { @@ -197,7 +213,7 @@ export class VscodeSelectBase extends VscElement { protected _currentDescription = ''; @state() - protected _filter: SearchMethod = 'fuzzy'; + protected _filter: FilterMethod = 'fuzzy'; @state() protected get _filteredOptions(): InternalOption[] { @@ -212,6 +228,22 @@ export class VscodeSelectBase extends VscElement { ); } + protected get _visibleOptions(): InternalOption[] { + if (!this.combobox || this._filterPattern === '') { + return this._options; + } + + if (!this._memoizedFilteredOptions) { + this._memoizedFilteredOptions = filterOptionsByPattern( + this._options, + this._filterPattern, + this._filter + ); + } + + return this._memoizedFilteredOptions; + } + @state() protected _filterPattern = ''; @@ -252,6 +284,7 @@ export class VscodeSelectBase extends VscElement { private _isHoverForbidden = false; private _disabled = false; private _originalTabIndex: number | undefined = undefined; + private _memoizedFilteredOptions: InternalOption[] | null = null; private _setAutoFocus() { if (this.hasAttribute('autofocus')) { @@ -275,10 +308,6 @@ export class VscodeSelectBase extends VscElement { } } - protected get _currentOptions(): InternalOption[] { - return this.combobox ? this._filteredOptions : this._options; - } - protected get _isSuggestedOptionVisible() { if (!(this.combobox && this.creatable)) { return false; @@ -300,34 +329,34 @@ export class VscodeSelectBase extends VscElement { const values: string[] = []; this._valueOptionIndexMap = {}; - optionElements.forEach((el, i) => { - const {innerText, description, disabled} = el; - const value = typeof el.value === 'string' ? el.value : innerText.trim(); - const selected = el.selected ?? false; - const op: InternalOption = { - label: innerText.trim(), - value, - description, - selected, - index: nextIndex, - disabled, - }; + // optionElements.forEach((el, i) => { + // const {innerText, description, disabled} = el; + // const value = typeof el.value === 'string' ? el.value : innerText.trim(); + // const selected = el.selected ?? false; + // const op: InternalOption = { + // label: innerText.trim(), + // value, + // description, + // selected, + // index: nextIndex, + // disabled, + // }; - nextIndex = options.push(op); + // nextIndex = options.push(op); - if (selected && !this._multiple) { - this._activeIndex = i; - } + // if (selected && !this._multiple) { + // this._activeIndex = i; + // } - if (selected) { - selectedIndexes.push(options.length - 1); - values.push(value); - } + // if (selected) { + // selectedIndexes.push(options.length - 1); + // values.push(value); + // } - this._valueOptionIndexMap[op.value] = op.index; - }); + // this._valueOptionIndexMap[op.value] = op.index; + // }); - this._options = options; + // this._options = options; if (selectedIndexes.length > 0) { this._selectedIndex = selectedIndexes[0]; @@ -335,11 +364,30 @@ export class VscodeSelectBase extends VscElement { this._value = values[0]; this._values = values; } + + optionElements.forEach((el) => { + const {innerText, description, disabled} = el; + const value = typeof el.value === 'string' ? el.value : innerText.trim(); + const selected = el.selected ?? false; + const op: Option = { + label: innerText.trim(), + value, + description, + selected, + disabled, + }; + + this._opts.add(op); + }); } protected _toggleDropdown(visible: boolean) { this.open = visible; + if (!visible) { + this._opts.activeIndex = -1; + } + if (visible) { window.addEventListener('click', this._onClickOutside); } else { @@ -417,12 +465,14 @@ export class VscodeSelectBase extends VscElement { if (el.matches('.placeholder')) { this._isPlaceholderOptionActive = true; - this._activeIndex = -1; + // this._activeIndex = -1; + this._opts.activeIndex = -1; } else { this._isPlaceholderOptionActive = false; - this._activeIndex = Number( - this.combobox ? el.dataset.filteredIndex : el.dataset.index - ); + // this._activeIndex = Number( + // this.combobox ? el.dataset.filteredIndex : el.dataset.index + // ); + this._opts.activeIndex = +el.dataset.index!; } } @@ -466,9 +516,7 @@ export class VscodeSelectBase extends VscElement { direction: 'down' | 'up', optionIndex: number ) { - let numOpts = this.combobox - ? this._filteredOptions.length - : this._options.length; + let numOpts = this._opts.numOfVisibleOptions; const suggestedOptionVisible = this._isSuggestedOptionVisible; if (suggestedOptionVisible) { @@ -510,7 +558,7 @@ export class VscodeSelectBase extends VscElement { } if (this._isPlaceholderOptionActive) { - const optionIndex = this._currentOptions.length - 1; + const optionIndex = this._opts.numOfVisibleOptions - 1; this._activeIndex = optionIndex; this._isPlaceholderOptionActive = false; } else { @@ -535,9 +583,7 @@ export class VscodeSelectBase extends VscElement { } protected _onArrowDownKeyDown(): void { - let numOpts = this.combobox - ? this._filteredOptions.length - : this._options.length; + let numOpts = this._opts.numOfVisibleOptions; const currentOptions = this.combobox ? this._filteredOptions : this._options; @@ -562,9 +608,12 @@ export class VscodeSelectBase extends VscElement { this._activeIndex ); + const nextOpt = this._opts.getNextSelectableOption(); + const nextSelectableIndex = nextOpt?.relativeIndex ?? -1; + if (nextSelectable > -1) { this._activeIndex = currentOptions[nextSelectable].index; - this._adjustOptionListScrollPos('down', nextSelectable); + this._adjustOptionListScrollPos('down', nextSelectableIndex); } } } else { @@ -652,7 +701,7 @@ export class VscodeSelectBase extends VscElement { //#region render functions protected _renderOptions(): TemplateResult | TemplateResult[] { - const list = this.combobox ? this._filteredOptions : this._options; + const list = this._opts.options; //aria-multiselectable=${this._multiple ? 'true' : 'false'} @@ -671,6 +720,10 @@ export class VscodeSelectBase extends VscElement { list, (op) => op.index, (op, index) => { + if (!op.visible) { + return nothing; + } + const active = op.index === this._activeIndex && !op.disabled; const selected = this._selectedIndex === op.index; diff --git a/src/vscode-multi-select/vscode-multi-select.ts b/src/vscode-multi-select/vscode-multi-select.ts index 237352873..b31165864 100644 --- a/src/vscode-multi-select/vscode-multi-select.ts +++ b/src/vscode-multi-select/vscode-multi-select.ts @@ -153,6 +153,7 @@ export class VscodeMultiSelect constructor() { super(); + this._opts.multiSelect = true; /** @internal */ this._multiple = true; this._internals = this.attachInternals(); diff --git a/src/vscode-single-select/vscode-single-select.test.ts b/src/vscode-single-select/vscode-single-select.test.ts index a9ffec3d4..427974a34 100644 --- a/src/vscode-single-select/vscode-single-select.test.ts +++ b/src/vscode-single-select/vscode-single-select.test.ts @@ -1,4 +1,4 @@ -import {sendKeys, sendMouse} from '@web/test-runner-commands'; +import {sendKeys} from '@web/test-runner-commands'; import {clickOnElement, moveMouseOnElement} from '../includes/test-helpers.js'; import type {VscodeOption} from '../vscode-option/vscode-option.js'; import {VscodeSingleSelect} from './index.js'; @@ -768,7 +768,14 @@ describe('vscode-single-select', () => { `, { - ignoreAttributes: ['data-filtered-index', 'data-index'], + ignoreAttributes: [ + 'aria-label', + 'aria-selected', + 'data-filtered-index', + 'data-index', + 'role', + 'id', + ], } ); }); diff --git a/src/vscode-single-select/vscode-single-select.ts b/src/vscode-single-select/vscode-single-select.ts index 7e3c7e949..8e7734eb8 100644 --- a/src/vscode-single-select/vscode-single-select.ts +++ b/src/vscode-single-select/vscode-single-select.ts @@ -178,6 +178,7 @@ export class VscodeSingleSelect constructor() { super(); + this._opts.multiSelect = false; /** @internal */ this._multiple = false; this._internals = this.attachInternals(); From e2fb9745ef89d613b394fa7395f032ff43c41b7d Mon Sep 17 00:00:00 2001 From: bendera Date: Sat, 24 May 2025 00:03:13 +0200 Subject: [PATCH 17/66] wip --- dev/vscode-single-select/basic-example.html | 3 +- .../vscode-select/OptionListController.ts | 26 +++++++++++++--- .../vscode-select/vscode-select-base.ts | 30 +++++++++++-------- 3 files changed, 42 insertions(+), 17 deletions(-) diff --git a/dev/vscode-single-select/basic-example.html b/dev/vscode-single-select/basic-example.html index d346bc570..e69681667 100644 --- a/dev/vscode-single-select/basic-example.html +++ b/dev/vscode-single-select/basic-example.html @@ -28,8 +28,9 @@ Lorem - Ipsum + Ipsum Dolor + Sit diff --git a/src/includes/vscode-select/OptionListController.ts b/src/includes/vscode-select/OptionListController.ts index 0eea90f08..3fd5198aa 100644 --- a/src/includes/vscode-select/OptionListController.ts +++ b/src/includes/vscode-select/OptionListController.ts @@ -175,13 +175,20 @@ export class OptionListController implements ReactiveController { this._host.requestUpdate(); } - getOptionByValue(value: string): InternalOption | null { + getOptionByValue( + value: string, + includeHiddenOptions = false + ): InternalOption | null { const index = this._indexByValue.get(value) ?? -1; if (index === -1) { return null; } + if (!includeHiddenOptions) { + return this._options[index].visible ? this._options[index] : null; + } + return this._options[index]; } @@ -196,8 +203,8 @@ export class OptionListController implements ReactiveController { return this._options[0]; } - if (!this._options[from] || !this._options[from + 1]) { - return this._options[this._options.length - 1]; + if (from !== -1 && !this._options[from + 1]) { + return this._options[from]; } let nextIndex = -1; @@ -247,6 +254,17 @@ export class OptionListController implements ReactiveController { return prevIndex > -1 ? this._options[prevIndex] : null; } + activateDefault() { + if (this._selectedIndexes.length > 0) { + this._activeIndex = this._selectedIndexes[0]; + } else { + const nextOp = this.getNextSelectableOption(); + this._activeIndex = nextOp?.index ?? -1; + } + + this._host.requestUpdate(); + } + activateNext() { const nextOp = this.getNextSelectableOption(); this._activeIndex = nextOp?.index ?? -1; @@ -254,7 +272,7 @@ export class OptionListController implements ReactiveController { return nextOp; } - selectPrev() { + activatePrev() { const prevOp = this.getPrevSelectableOption(); this._activeIndex = prevOp?.index ?? -1; this._host.requestUpdate(); diff --git a/src/includes/vscode-select/vscode-select-base.ts b/src/includes/vscode-select/vscode-select-base.ts index b9ab426bb..ea3e8abb7 100644 --- a/src/includes/vscode-select/vscode-select-base.ts +++ b/src/includes/vscode-select/vscode-select-base.ts @@ -314,8 +314,8 @@ export class VscodeSelectBase extends VscElement { } const filterPatternExistsAsOption = - typeof this._valueOptionIndexMap[this._filterPattern] !== 'undefined'; - const filtered = this._filterPattern.length > 0; + this._opts.getOptionByValue(this._opts.filterPattern) !== null; + const filtered = this._opts.filterPattern.length > 0; return !filterPatternExistsAsOption && filtered; } @@ -385,7 +385,7 @@ export class VscodeSelectBase extends VscElement { this.open = visible; if (!visible) { - this._opts.activeIndex = -1; + // this._opts.activeIndex = -1; } if (visible) { @@ -594,30 +594,36 @@ export class VscodeSelectBase extends VscElement { } if (this.open) { - if (this._isPlaceholderOptionActive && this._activeIndex === -1) { + if (this._isPlaceholderOptionActive && this._opts.activeIndex === -1) { return; } + console.log('open'); + if (suggestedOptionVisible && this._activeIndex === numOpts - 2) { this._isPlaceholderOptionActive = true; this._adjustOptionListScrollPos('down', numOpts - 1); this._activeIndex = -1; } else if (this._activeIndex < numOpts - 1) { - const nextSelectable = findNextSelectableOptionIndex( - currentOptions, - this._activeIndex - ); + const nextOpt = this._opts.activateNext(); + console.log(nextOpt); + + // const nextSelectable = findNextSelectableOptionIndex( + // currentOptions, + // this._activeIndex + // ); - const nextOpt = this._opts.getNextSelectableOption(); + // const nextOpt = this._opts.getNextSelectableOption(); const nextSelectableIndex = nextOpt?.relativeIndex ?? -1; - if (nextSelectable > -1) { - this._activeIndex = currentOptions[nextSelectable].index; + if (nextSelectableIndex > -1) { + // this._activeIndex = currentOptions[nextSelectable].index; this._adjustOptionListScrollPos('down', nextSelectableIndex); } } } else { this._toggleDropdown(true); + this._opts.activateDefault(); } } @@ -724,7 +730,7 @@ export class VscodeSelectBase extends VscElement { return nothing; } - const active = op.index === this._activeIndex && !op.disabled; + const active = op.index === this._opts.activeIndex && !op.disabled; const selected = this._selectedIndex === op.index; const optionClasses = { From fa1b21ac11e0750d06ec06ef8416b8d32bd64915 Mon Sep 17 00:00:00 2001 From: bendera Date: Sun, 25 May 2025 02:12:52 +0200 Subject: [PATCH 18/66] wip --- dev/vscode-single-select/large-list.html | 441 ++++++++++++++++++ .../vscode-select/OptionListController.ts | 50 +- .../vscode-select/vscode-select-base.ts | 178 ++----- .../vscode-multi-select.ts | 32 +- .../vscode-single-select.ts | 103 ++-- 5 files changed, 588 insertions(+), 216 deletions(-) create mode 100644 dev/vscode-single-select/large-list.html diff --git a/dev/vscode-single-select/large-list.html b/dev/vscode-single-select/large-list.html new file mode 100644 index 000000000..01719afbf --- /dev/null +++ b/dev/vscode-single-select/large-list.html @@ -0,0 +1,441 @@ + + + + + + VSCode Elements + + + + + + + +
+ + + Afghanistan + Albania + Algeria + Andorra + Angola + Antigua and Barbuda + Argentina + Armenia + Australia + Austria + Azerbaijan + Bahamas + Bahrain + Bangladesh + Barbados + Belarus + Belgium + Belize + Benin + Bhutan + Bolivia + Bosnia and Herzegovina + Botswana + Brazil + Brunei + Bulgaria + Burkina Faso + Burundi + Côte d'Ivoire + Cabo Verde + Cambodia + Cameroon + Canada + Central African Republic + Chad + Chile + China + Colombia + Comoros + Congo (Congo-Brazzaville) + Costa Rica + Croatia + Cuba + Cyprus + Czechia (Czech Republic) + Democratic Republic of the Congo + Denmark + Djibouti + Dominica + Dominican Republic + Ecuador + Egypt + El Salvador + Equatorial Guinea + Eritrea + Estonia + Eswatini (fmr. "Swaziland") + Ethiopia + Fiji + Finland + France + Gabon + Gambia + Georgia + Germany + Ghana + Greece + Grenada + Guatemala + Guinea + Guinea-Bissau + Guyana + Haiti + Holy See + Honduras + Hungary + Iceland + India + Indonesia + Iran + Iraq + Ireland + Israel + Italy + Jamaica + Japan + Jordan + Kazakhstan + Kenya + Kiribati + Kuwait + Kyrgyzstan + Laos + Latvia + Lebanon + Lesotho + Liberia + Libya + Liechtenstein + Lithuania + Luxembourg + Madagascar + Malawi + Malaysia + Maldives + Mali + Malta + Marshall Islands + Mauritania + Mauritius + Mexico + Micronesia + Moldova + Monaco + Mongolia + Montenegro + Morocco + Mozambique + Myanmar (formerly Burma) + Namibia + Nauru + Nepal + Netherlands + New Zealand + Nicaragua + Niger + Nigeria + North Korea + North Macedonia + Norway + Oman + Pakistan + Palau + Palestine State + Panama + Papua New Guinea + Paraguay + Peru + Philippines + Poland + Portugal + Qatar + Romania + Russia + Rwanda + Saint Kitts and Nevis + Saint Lucia + Saint Vincent and the Grenadines + Samoa + San Marino + Sao Tome and Principe + Saudi Arabia + Senegal + Serbia + Seychelles + Sierra Leone + Singapore + Slovakia + Slovenia + Solomon Islands + Somalia + South Africa + South Korea + South Sudan + Spain + Sri Lanka + Sudan + Suriname + Sweden + Switzerland + Syria + Tajikistan + Tanzania + Thailand + Timor-Leste + Togo + Tonga + Trinidad and Tobago + Tunisia + Turkey + Turkmenistan + Tuvalu + Uganda + Ukraine + United Arab Emirates + United Kingdom + United States of America + Uruguay + Uzbekistan + Vanuatu + Venezuela + Vietnam + Yemen + Zambia + Zimbabwe + + + + +
+ Native select + + +
+
+ + diff --git a/src/includes/vscode-select/OptionListController.ts b/src/includes/vscode-select/OptionListController.ts index 3fd5198aa..e3648a852 100644 --- a/src/includes/vscode-select/OptionListController.ts +++ b/src/includes/vscode-select/OptionListController.ts @@ -16,6 +16,7 @@ export class OptionListController implements ReactiveController { private _filterMethod: FilterMethod = 'fuzzy'; private _combobox = false; private _indexByValue: Map = new Map(); + private _indexByLabel: Map = new Map(); private _selectedIndex = -1; private _selectedIndexes: number[] = []; private _multiSelect = false; @@ -28,6 +29,7 @@ export class OptionListController implements ReactiveController { set activeIndex(index: number) { this._activeIndex = index; + this._host.requestUpdate(); } get activeIndex(): number { @@ -44,6 +46,8 @@ export class OptionListController implements ReactiveController { } set multiSelect(multiSelect: boolean) { + this._selectedIndex = -1; + this._selectedIndexes = []; this._multiSelect = multiSelect; this._host.requestUpdate(); } @@ -61,6 +65,11 @@ export class OptionListController implements ReactiveController { return this._selectedIndex; } + set selectedIndexes(value: number[]) { + this._selectedIndexes = value; + this._host.requestUpdate(); + } + get value(): string | string[] { if (this._multiSelect) { return this._selectedIndexes.length > 0 @@ -104,9 +113,11 @@ export class OptionListController implements ReactiveController { populate(options: Option[]) { this._indexByValue.clear(); + this._indexByLabel.clear(); this._options = options.map((op, index) => { this._indexByValue.set(op.value, index); + this._indexByLabel.set(op.label, index); return { description: op.description ?? '', @@ -136,6 +147,7 @@ export class OptionListController implements ReactiveController { } this._indexByValue.set(value, nextIndex); + this._indexByLabel.set(label, nextIndex); if (selected) { this._selectedIndex = nextIndex; @@ -175,6 +187,32 @@ export class OptionListController implements ReactiveController { this._host.requestUpdate(); } + toggleActiveMultiselectOption() { + const activeOption = this._options[this._activeIndex] ?? null; + const checked = this._selectedIndexes.includes(activeOption.index); + + if (checked) { + this._selectedIndexes = this._selectedIndexes.filter( + (i) => i !== activeOption.index + ); + } else { + this._selectedIndexes.push(activeOption.index); + this._selectedIndexes.sort(); + } + } + + getActiveOption(): InternalOption | null { + return this._options[this._activeIndex] ?? null; + } + + getSelectedOption(): InternalOption | null { + return this._options[this._selectedIndex] ?? null; + } + + getOptionByIndex(index: number) { + return this._options[index] ?? null; + } + getOptionByValue( value: string, includeHiddenOptions = false @@ -192,6 +230,16 @@ export class OptionListController implements ReactiveController { return this._options[index]; } + getOptionByLabel(label: string) { + const index = this._indexByLabel.get(label) ?? -1; + + if (index === -1) { + return null; + } + + return this._options[index]; + } + getNextSelectableOption(fromIndex?: number): InternalOption | null { const from = fromIndex ?? this._activeIndex; @@ -258,7 +306,7 @@ export class OptionListController implements ReactiveController { if (this._selectedIndexes.length > 0) { this._activeIndex = this._selectedIndexes[0]; } else { - const nextOp = this.getNextSelectableOption(); + const nextOp = this.getNextSelectableOption(-1); this._activeIndex = nextOp?.index ?? -1; } diff --git a/src/includes/vscode-select/vscode-select-base.ts b/src/includes/vscode-select/vscode-select-base.ts index ea3e8abb7..a8be462ae 100644 --- a/src/includes/vscode-select/vscode-select-base.ts +++ b/src/includes/vscode-select/vscode-select-base.ts @@ -11,12 +11,7 @@ import '../../vscode-button/index.js'; import '../../vscode-option/index.js'; import {VscodeOption} from '../../vscode-option/index.js'; import type {InternalOption, Option, FilterMethod} from './types.js'; -import { - filterOptionsByPattern, - findNextSelectableOptionIndex, - findPrevSelectableOptionIndex, - highlightRanges, -} from './helpers.js'; +import {filterOptionsByPattern, highlightRanges} from './helpers.js'; import {VscElement} from '../VscElement.js'; import {chevronDownIcon} from './template-elements.js'; import {ifDefined} from 'lit/directives/if-defined.js'; @@ -206,9 +201,6 @@ export class VscodeSelectBase extends VscElement { } } - @state() - protected _activeIndex = -1; - @state() protected _currentDescription = ''; @@ -217,39 +209,17 @@ export class VscodeSelectBase extends VscElement { @state() protected get _filteredOptions(): InternalOption[] { - if (!this.combobox || this._filterPattern === '') { + if (!this.combobox || this._opts.filterPattern === '') { return this._options; } return filterOptionsByPattern( this._options, - this._filterPattern, + this._opts.filterPattern, this._filter ); } - protected get _visibleOptions(): InternalOption[] { - if (!this.combobox || this._filterPattern === '') { - return this._options; - } - - if (!this._memoizedFilteredOptions) { - this._memoizedFilteredOptions = filterOptionsByPattern( - this._options, - this._filterPattern, - this._filter - ); - } - - return this._memoizedFilteredOptions; - } - - @state() - protected _filterPattern = ''; - - @state() - protected _selectedIndex = -1; - @state() protected _selectedIndexes: number[] = []; @@ -284,7 +254,6 @@ export class VscodeSelectBase extends VscElement { private _isHoverForbidden = false; private _disabled = false; private _originalTabIndex: number | undefined = undefined; - private _memoizedFilteredOptions: InternalOption[] | null = null; private _setAutoFocus() { if (this.hasAttribute('autofocus')) { @@ -322,49 +291,9 @@ export class VscodeSelectBase extends VscElement { protected _manageRequired() {} protected _setStateFromSlottedElements() { - const options: InternalOption[] = []; - let nextIndex = 0; const optionElements = this._assignedOptions ?? []; - const selectedIndexes: number[] = []; - const values: string[] = []; this._valueOptionIndexMap = {}; - // optionElements.forEach((el, i) => { - // const {innerText, description, disabled} = el; - // const value = typeof el.value === 'string' ? el.value : innerText.trim(); - // const selected = el.selected ?? false; - // const op: InternalOption = { - // label: innerText.trim(), - // value, - // description, - // selected, - // index: nextIndex, - // disabled, - // }; - - // nextIndex = options.push(op); - - // if (selected && !this._multiple) { - // this._activeIndex = i; - // } - - // if (selected) { - // selectedIndexes.push(options.length - 1); - // values.push(value); - // } - - // this._valueOptionIndexMap[op.value] = op.index; - // }); - - // this._options = options; - - if (selectedIndexes.length > 0) { - this._selectedIndex = selectedIndexes[0]; - this._selectedIndexes = selectedIndexes; - this._value = values[0]; - this._values = values; - } - optionElements.forEach((el) => { const {innerText, description, disabled} = el; const value = typeof el.value === 'string' ? el.value : innerText.trim(); @@ -384,10 +313,6 @@ export class VscodeSelectBase extends VscElement { protected _toggleDropdown(visible: boolean) { this.open = visible; - if (!visible) { - // this._opts.activeIndex = -1; - } - if (visible) { window.addEventListener('click', this._onClickOutside); } else { @@ -396,10 +321,10 @@ export class VscodeSelectBase extends VscElement { } protected _createSuggestedOption() { - const nextSelectedIndex = this._options.length; + const nextSelectedIndex = this._opts.numOptions; const op = document.createElement('vscode-option'); - op.value = this._filterPattern; - render(this._filterPattern, op); + op.value = this._opts.filterPattern; + render(this._opts.filterPattern, op); this.appendChild(op); return nextSelectedIndex; @@ -433,7 +358,7 @@ export class VscodeSelectBase extends VscElement { }; protected _toggleComboboxDropdown() { - this._filterPattern = ''; + this._opts.filterPattern = ''; this._toggleDropdown(!this.open); } @@ -465,13 +390,9 @@ export class VscodeSelectBase extends VscElement { if (el.matches('.placeholder')) { this._isPlaceholderOptionActive = true; - // this._activeIndex = -1; this._opts.activeIndex = -1; } else { this._isPlaceholderOptionActive = false; - // this._activeIndex = Number( - // this.combobox ? el.dataset.filteredIndex : el.dataset.index - // ); this._opts.activeIndex = +el.dataset.index!; } } @@ -509,7 +430,9 @@ export class VscodeSelectBase extends VscElement { } protected _scrollActiveElementToTop() { - this._optionListScrollPos = Math.floor(this._activeIndex * OPT_HEIGHT); + this._optionListScrollPos = Math.floor( + this._opts.relativeActiveIndex * OPT_HEIGHT + ); } private async _adjustOptionListScrollPos( @@ -546,47 +469,39 @@ export class VscodeSelectBase extends VscElement { if (direction === 'up') { if (!fullyVisible) { - this._optionListScrollPos = Math.floor(this._activeIndex * OPT_HEIGHT); + this._optionListScrollPos = Math.floor( + this._opts.relativeActiveIndex * OPT_HEIGHT + ); } } } protected _onArrowUpKeyDown(): void { if (this.open) { - if (this._activeIndex <= 0 && !(this.combobox && this.creatable)) { + if (this._opts.activeIndex <= 0 && !(this.combobox && this.creatable)) { return; } if (this._isPlaceholderOptionActive) { const optionIndex = this._opts.numOfVisibleOptions - 1; - this._activeIndex = optionIndex; + this._opts.activeIndex = optionIndex; this._isPlaceholderOptionActive = false; } else { - const currentOptions = this.combobox - ? this._filteredOptions - : this._options; - - const prevSelectable = findPrevSelectableOptionIndex( - currentOptions, - this._activeIndex - ); + const prevOpt = this._opts.activatePrev(); + const prevSelectableIndex = prevOpt?.relativeIndex ?? -1; - if (prevSelectable > -1) { - this._activeIndex = prevSelectable; - this._adjustOptionListScrollPos('up', prevSelectable); + if (prevSelectableIndex > -1) { + this._adjustOptionListScrollPos('up', prevSelectableIndex); } } } else { this._toggleDropdown(true); - this._activeIndex = 0; + this._opts.activeIndex = 0; } } protected _onArrowDownKeyDown(): void { let numOpts = this._opts.numOfVisibleOptions; - const currentOptions = this.combobox - ? this._filteredOptions - : this._options; const suggestedOptionVisible = this._isSuggestedOptionVisible; if (suggestedOptionVisible) { @@ -598,26 +513,15 @@ export class VscodeSelectBase extends VscElement { return; } - console.log('open'); - - if (suggestedOptionVisible && this._activeIndex === numOpts - 2) { + if (suggestedOptionVisible && this._opts.activeIndex === numOpts - 2) { this._isPlaceholderOptionActive = true; this._adjustOptionListScrollPos('down', numOpts - 1); - this._activeIndex = -1; - } else if (this._activeIndex < numOpts - 1) { + this._opts.activeIndex = -1; + } else if (this._opts.activeIndex < numOpts - 1) { const nextOpt = this._opts.activateNext(); - console.log(nextOpt); - - // const nextSelectable = findNextSelectableOptionIndex( - // currentOptions, - // this._activeIndex - // ); - - // const nextOpt = this._opts.getNextSelectableOption(); const nextSelectableIndex = nextOpt?.relativeIndex ?? -1; if (nextSelectableIndex > -1) { - // this._activeIndex = currentOptions[nextSelectable].index; this._adjustOptionListScrollPos('down', nextSelectableIndex); } } @@ -674,7 +578,7 @@ export class VscodeSelectBase extends VscElement { protected _onComboboxInputFocus(ev: FocusEvent): void { (ev.target as HTMLInputElement).select(); this._isBeingFiltered = false; - this._filterPattern = ''; + this._opts.filterPattern = ''; } protected _onComboboxInputBlur() { @@ -683,13 +587,13 @@ export class VscodeSelectBase extends VscElement { protected _onComboboxInputInput(ev: InputEvent): void { this._isBeingFiltered = true; - this._filterPattern = (ev.target as HTMLInputElement).value; - this._activeIndex = -1; + this._opts.filterPattern = (ev.target as HTMLInputElement).value; + this._opts.activeIndex = -1; this._toggleDropdown(true); } protected _onComboboxInputClick(): void { - this._isBeingFiltered = this._filterPattern !== ''; + this._isBeingFiltered = this._opts.filterPattern !== ''; this._toggleDropdown(true); } @@ -731,13 +635,13 @@ export class VscodeSelectBase extends VscElement { } const active = op.index === this._opts.activeIndex && !op.disabled; - const selected = this._selectedIndex === op.index; + const selected = op.index === this._opts.selectedIndex; const optionClasses = { active, disabled: op.disabled, option: true, - selected: op.selected, + selected, }; const checkboxClasses = { @@ -777,11 +681,13 @@ export class VscodeSelectBase extends VscElement { return nothing; } - if (this._valueOptionIndexMap[this._filterPattern]) { + const foundOption = this._opts.getOptionByLabel(this._opts.filterPattern); + + if (foundOption) { return nothing; } - if (this.creatable && this._filterPattern.length > 0) { + if (this.creatable && this._opts.filterPattern.length > 0) { return html`
  • - Add "${this._filterPattern}" + Add "${this._opts.filterPattern}"
  • `; } else { return isListEmpty @@ -802,11 +708,13 @@ export class VscodeSelectBase extends VscElement { } private _renderDescription() { - if (!this._options[this._activeIndex]) { + const op = this._opts.getActiveOption(); + + if (!op) { return nothing; } - const {description} = this._options[this._activeIndex]; + const {description} = op; return description ? html`
    ${description}
    ` @@ -836,16 +744,14 @@ export class VscodeSelectBase extends VscElement { let inputVal = ''; if (this._isBeingFiltered) { - inputVal = this._filterPattern; + inputVal = this._opts.filterPattern; } else { - inputVal = - this._selectedIndex > -1 - ? (this._options[this._selectedIndex]?.label ?? '') - : ''; + const op = this._opts.getSelectedOption(); + inputVal = op?.label ?? ''; } const activeDescendant = - this._activeIndex > -1 ? `op-${this._activeIndex}` : ''; + this._opts.activeIndex > -1 ? `op-${this._opts.activeIndex}` : ''; return html`
    diff --git a/src/vscode-multi-select/vscode-multi-select.ts b/src/vscode-multi-select/vscode-multi-select.ts index b31165864..a2b9192f0 100644 --- a/src/vscode-multi-select/vscode-multi-select.ts +++ b/src/vscode-multi-select/vscode-multi-select.ts @@ -213,12 +213,12 @@ export class VscodeMultiSelect protected override _onFaceClick(): void { super._onFaceClick(); - this._activeIndex = 0; + this._opts.activeIndex = 0; } protected override _toggleComboboxDropdown(): void { super._toggleComboboxDropdown(); - this._activeIndex = -1; + this._opts.activeIndex = -1; } protected override _manageRequired() { @@ -236,28 +236,6 @@ export class VscodeMultiSelect } } - private _toggleActiveOptionSelection() { - if (this._activeIndex < 0) { - return; - } - - const opts = this.combobox ? this._filteredOptions : this._options; - const selectedOption = this._options[this._activeIndex]; - const nextSelectedIndexes: number[] = []; - - this._options[selectedOption.index].selected = !selectedOption.selected; - - opts.forEach(({index}) => { - const {selected} = this._options[index]; - - if (selected) { - nextSelectedIndexes.push(index); - } - }); - - this._selectedIndexes = nextSelectedIndexes; - } - private _setFormValue() { const fd = new FormData(); @@ -356,10 +334,10 @@ export class VscodeMultiSelect super._onEnterKeyDown(ev); if (!this.open) { - this._filterPattern = ''; + this._opts.filterPattern = ''; this._toggleDropdown(true); } else { - this._toggleActiveOptionSelection(); + this._opts.toggleActiveMultiselectOption(); } // TODO: dispatch change + set value @@ -410,7 +388,7 @@ export class VscodeMultiSelect protected override _renderSelectFace(): TemplateResult { const activeDescendant = - this._activeIndex > -1 ? `op-${this._activeIndex}` : ''; + this._opts.activeIndex > -1 ? `op-${this._opts.activeIndex}` : ''; return html`
    ; @@ -87,11 +83,13 @@ export class VscodeSingleSelect @property({type: Number, attribute: 'selected-index'}) set selectedIndex(val: number) { - this._selectedIndex = val; + this._opts.selectedIndex = val; + + const op = this._opts.getOptionByIndex(val); - if (this._options[val]) { - this._activeIndex = val; - this._value = this._options[val].value; + if (op) { + this._opts.activeIndex = val; + this._value = op.value; this._internals.setFormValue(this._value); this._manageRequired(); } else { @@ -101,23 +99,22 @@ export class VscodeSingleSelect } } get selectedIndex(): number { - return this._selectedIndex; + return this._opts.selectedIndex; } @property({type: String}) set value(val: string) { - if (this._options[this._selectedIndex]) { - this._options[this._selectedIndex].selected = false; - } + this._opts.value = val; - this._selectedIndex = this._options.findIndex((op) => op.value === val); + // if (this._options[this._selectedIndex]) { + // this._options[this._selectedIndex].selected = false; + // } - if (this._selectedIndex > -1) { - this._options[this._selectedIndex].selected = true; - this._value = val; + // this._selectedIndex = this._options.findIndex((op) => op.value === val); + + if (this._opts.selectedIndex > -1) { this._requestedValueToSetLater = ''; } else { - this._value = ''; this._requestedValueToSetLater = val; } @@ -125,11 +122,11 @@ export class VscodeSingleSelect this._manageRequired(); } get value(): string { - if (this._options[this._selectedIndex]) { - return this._options[this._selectedIndex]?.value ?? ''; - } + // if (this._options[this._selectedIndex]) { + // return this._options[this._selectedIndex]?.value ?? ''; + // } - return ''; + return this._opts.value as string; } @property({type: Boolean, reflect: true}) @@ -170,9 +167,8 @@ export class VscodeSingleSelect ) as HTMLInputElement; if (input) { - input.value = this._options[this._selectedIndex] - ? this._options[this._selectedIndex].label - : ''; + const selectedOption = this._opts.getSelectedOption(); + input.value = selectedOption?.label ?? ''; } } @@ -243,7 +239,7 @@ export class VscodeSingleSelect this.dispatchEvent( new CustomEvent('vsc-change', { detail: { - selectedIndex: this._selectedIndex, + selectedIndex: this._opts.selectedIndex, value: this._value, }, }) @@ -256,7 +252,7 @@ export class VscodeSingleSelect super._setStateFromSlottedElements(); if (!this.combobox && this._selectedIndexes.length === 0) { - this._selectedIndex = this._options.length > 0 ? 0 : -1; + this._opts.selectedIndex = this._opts.options.length > 0 ? 0 : -1; } } @@ -264,14 +260,16 @@ export class VscodeSingleSelect super._toggleDropdown(visible); if (visible) { - this._activeIndex = this._selectedIndex; + this._opts.activeIndex = this._opts.selectedIndex; } if (visible && !this.combobox) { - this._activeIndex = this._selectedIndex; + this._opts.activeIndex = this._opts.selectedIndex; - if (this._activeIndex > VISIBLE_OPTS - 1) { - this._optionListScrollPos = Math.floor(this._activeIndex * OPT_HEIGHT); + if (this._opts.activeIndex > VISIBLE_OPTS - 1) { + this._optionListScrollPos = Math.floor( + this._opts.activeIndex * OPT_HEIGHT + ); } } } @@ -281,18 +279,18 @@ export class VscodeSingleSelect if (this._requestedValueToSetLater) { // the value is set before the available options are appended - const foundIndex = this._options.findIndex( - (op) => op.value === this._requestedValueToSetLater + const foundOption = this._opts.getOptionByValue( + this._requestedValueToSetLater ); - if (foundIndex > 0) { - this._selectedIndex = foundIndex; + if (foundOption) { + this._opts.selectedIndex = foundOption.index; this._requestedValueToSetLater = ''; } } - if (this._selectedIndex > -1 && this._options.length > 0) { - this._internals.setFormValue(this._options[this._selectedIndex].value); + if (this._opts.selectedIndex > -1 && this._opts.numOptions > 0) { + this._internals.setFormValue(this._opts.value as string); this._manageRequired(); } else { this._internals.setFormValue(null); @@ -303,7 +301,7 @@ export class VscodeSingleSelect protected override _onArrowUpKeyDown(): void { super._onArrowUpKeyDown(); - if (this.open || this._selectedIndex <= 0) { + /* if (this.open || this._selectedIndex <= 0) { return; } @@ -314,8 +312,8 @@ export class VscodeSingleSelect this._selectedIndex = prevIndex; this._activeIndex = prevIndex; this._value = prevIndex > -1 ? this._options[prevIndex].value : ''; - - this._internals.setFormValue(this._value); + */ + this._internals.setFormValue(this._opts.value as string); this._manageRequired(); this._dispatchChangeEvent(); } @@ -323,7 +321,7 @@ export class VscodeSingleSelect protected override _onArrowDownKeyDown(): void { super._onArrowDownKeyDown(); - if (this.open || this._selectedIndex >= this._options.length - 1) { + /* if (this.open || this._selectedIndex >= this._options.length - 1) { return; } @@ -333,7 +331,7 @@ export class VscodeSingleSelect this._filterPattern = ''; this._selectedIndex = nextIndex; this._activeIndex = nextIndex; - this._value = nextIndex > -1 ? this._options[nextIndex].value : ''; + this._value = nextIndex > -1 ? this._options[nextIndex].value : ''; */ this._internals.setFormValue(this._value); this._manageRequired(); this._dispatchChangeEvent(); @@ -348,8 +346,8 @@ export class VscodeSingleSelect if (this._isPlaceholderOptionActive) { this._createAndSelectSuggestedOption(); } else { - valueChanged = this._activeIndex !== this._selectedIndex; - this._selectedIndex = this._activeIndex; + valueChanged = this._opts.activeIndex !== this._opts.selectedIndex; + this._opts.selectedIndex = this._opts.activeIndex; this._toggleDropdown(false); } } else { @@ -358,8 +356,8 @@ export class VscodeSingleSelect } } else { if (this.open) { - valueChanged = this._activeIndex !== this._selectedIndex; - this._selectedIndex = this._activeIndex; + valueChanged = this._opts.activeIndex !== this._opts.selectedIndex; + this._opts.selectedIndex = this._opts.activeIndex; this._toggleDropdown(false); } else { this._toggleDropdown(true); @@ -368,14 +366,14 @@ export class VscodeSingleSelect } if (valueChanged) { - this._value = + /* this._value = this._selectedIndex > -1 ? this._options[this._selectedIndex].value - : ''; + : ''; */ this._dispatchChangeEvent(); this.updateInputValue(); - this._internals.setFormValue(this._value); + this._internals.setFormValue(this._opts.value as string); this._manageRequired(); } } @@ -404,8 +402,8 @@ export class VscodeSingleSelect this._createAndSelectSuggestedOption(); } } else { - this._selectedIndex = Number((optEl as HTMLElement).dataset.index); - this._value = this._options[this._selectedIndex].value; + this._opts.selectedIndex = Number((optEl as HTMLElement).dataset.index); + // this._value = this._options[this._selectedIndex].value; this._toggleDropdown(false); this._internals.setFormValue(this._value); @@ -429,9 +427,10 @@ export class VscodeSingleSelect //#region render functions protected override _renderSelectFace(): TemplateResult { - const label = this._options[this._selectedIndex]?.label ?? ''; + const selectedOption = this._opts.getSelectedOption(); + const label = selectedOption?.label ?? ''; const activeDescendant = - this._activeIndex > -1 ? `op-${this._activeIndex}` : ''; + this._opts.activeIndex > -1 ? `op-${this._opts.activeIndex}` : ''; return html`
    Date: Tue, 27 May 2025 00:30:42 +0200 Subject: [PATCH 19/66] Fix duplicated options Resolved the issue where the list of options was duplicated in the SingleSelect/MultiSelect component when an vscode-option was modified. --- src/includes/vscode-select/vscode-select-base.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/includes/vscode-select/vscode-select-base.ts b/src/includes/vscode-select/vscode-select-base.ts index a8be462ae..ef1872ffc 100644 --- a/src/includes/vscode-select/vscode-select-base.ts +++ b/src/includes/vscode-select/vscode-select-base.ts @@ -293,6 +293,7 @@ export class VscodeSelectBase extends VscElement { protected _setStateFromSlottedElements() { const optionElements = this._assignedOptions ?? []; this._valueOptionIndexMap = {}; + this._opts.clear(); optionElements.forEach((el) => { const {innerText, description, disabled} = el; From 4e5f94af44495926438e6909aaadad337c3c3000 Mon Sep 17 00:00:00 2001 From: bendera Date: Tue, 27 May 2025 00:31:04 +0200 Subject: [PATCH 20/66] Fix runtime error --- src/includes/vscode-select/OptionListController.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/includes/vscode-select/OptionListController.ts b/src/includes/vscode-select/OptionListController.ts index e3648a852..f5fcfdf62 100644 --- a/src/includes/vscode-select/OptionListController.ts +++ b/src/includes/vscode-select/OptionListController.ts @@ -37,8 +37,7 @@ export class OptionListController implements ReactiveController { } get relativeActiveIndex(): number { - const activeOption = this._options[this._activeIndex]; - return activeOption.relativeIndex; + return this._options[this._activeIndex]?.relativeIndex ?? -1; } get comboboxMode() { From 26ab351a1c3368651735cc4d747275eca2b481e0 Mon Sep 17 00:00:00 2001 From: bendera Date: Fri, 30 May 2025 22:21:25 +0200 Subject: [PATCH 21/66] wip --- .../vscode-select/OptionListController.ts | 40 ++++++++++++------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/src/includes/vscode-select/OptionListController.ts b/src/includes/vscode-select/OptionListController.ts index f5fcfdf62..eb8579b7c 100644 --- a/src/includes/vscode-select/OptionListController.ts +++ b/src/includes/vscode-select/OptionListController.ts @@ -18,7 +18,7 @@ export class OptionListController implements ReactiveController { private _indexByValue: Map = new Map(); private _indexByLabel: Map = new Map(); private _selectedIndex = -1; - private _selectedIndexes: number[] = []; + private _selectedIndexes: Set = new Set(); private _multiSelect = false; constructor(host: ReactiveControllerHost) { @@ -46,7 +46,7 @@ export class OptionListController implements ReactiveController { set multiSelect(multiSelect: boolean) { this._selectedIndex = -1; - this._selectedIndexes = []; + this._selectedIndexes.clear(); this._multiSelect = multiSelect; this._host.requestUpdate(); } @@ -65,14 +65,14 @@ export class OptionListController implements ReactiveController { } set selectedIndexes(value: number[]) { - this._selectedIndexes = value; + this._selectedIndexes = new Set(value); this._host.requestUpdate(); } get value(): string | string[] { if (this._multiSelect) { - return this._selectedIndexes.length > 0 - ? this._selectedIndexes.map((v) => this._options[v].value) + return this._selectedIndexes.size > 0 + ? Array.from(this._selectedIndexes).map((v) => this._options[v].value) : []; } else { return this._selectedIndex > -1 @@ -83,9 +83,11 @@ export class OptionListController implements ReactiveController { set value(newValue: string | string[]) { if (this._multiSelect) { - this._selectedIndexes = (newValue as string[]) + const valueList = (newValue as string[]) .map((v) => this._indexByValue.get(v)) .filter((v) => v !== undefined); + + this._selectedIndexes = new Set(valueList); } else { this._selectedIndex = this._indexByValue.get(newValue as string) ?? -1; } @@ -150,6 +152,7 @@ export class OptionListController implements ReactiveController { if (selected) { this._selectedIndex = nextIndex; + this._selectedIndexes.add(nextIndex); } this._options.push({ @@ -167,6 +170,8 @@ export class OptionListController implements ReactiveController { clear() { this._options = []; + this._indexByValue.clear(); + this._indexByLabel.clear(); } get options(): InternalOption[] { @@ -181,6 +186,14 @@ export class OptionListController implements ReactiveController { return this._options.length; } + getIsIndexSelected(index: number) { + if (this._multiSelect) { + return this._selectedIndexes.has(index); + } else { + return this._selectedIndex === index; + } + } + toggleComboboxMode(enabled: boolean) { this._combobox = enabled; this._host.requestUpdate(); @@ -188,15 +201,12 @@ export class OptionListController implements ReactiveController { toggleActiveMultiselectOption() { const activeOption = this._options[this._activeIndex] ?? null; - const checked = this._selectedIndexes.includes(activeOption.index); + const checked = this._selectedIndexes.has(activeOption.index); if (checked) { - this._selectedIndexes = this._selectedIndexes.filter( - (i) => i !== activeOption.index - ); + this._selectedIndexes.delete(activeOption.index); } else { - this._selectedIndexes.push(activeOption.index); - this._selectedIndexes.sort(); + this._selectedIndexes.add(activeOption.index); } } @@ -302,8 +312,10 @@ export class OptionListController implements ReactiveController { } activateDefault() { - if (this._selectedIndexes.length > 0) { - this._activeIndex = this._selectedIndexes[0]; + if (this._multiSelect && this._selectedIndexes.size > 0) { + const indexes = this._selectedIndexes.values(); + const first = indexes.next(); + this._activeIndex = first.value ? first.value : 0; } else { const nextOp = this.getNextSelectableOption(-1); this._activeIndex = nextOp?.index ?? -1; From 67c60b8d78d663e72cf68e13d884fe9efea92586 Mon Sep 17 00:00:00 2001 From: bendera Date: Fri, 30 May 2025 22:39:51 +0200 Subject: [PATCH 22/66] Refactor dropdown visibility logic --- .../vscode-select/vscode-select-base.ts | 55 ++++++++++++------- .../vscode-multi-select.ts | 6 +- .../vscode-single-select.ts | 36 +++--------- 3 files changed, 45 insertions(+), 52 deletions(-) diff --git a/src/includes/vscode-select/vscode-select-base.ts b/src/includes/vscode-select/vscode-select-base.ts index ef1872ffc..cee8e6028 100644 --- a/src/includes/vscode-select/vscode-select-base.ts +++ b/src/includes/vscode-select/vscode-select-base.ts @@ -201,6 +201,31 @@ export class VscodeSelectBase extends VscElement { } } + protected override update(changedProperties: PropertyValues) { + super.update(changedProperties); + + if (changedProperties.has('open')) { + if (this.open) { + this._opts.activateDefault(); + + if (!this._multiple && !this.combobox) { + const activeOption = this._opts.getActiveOption(); + const filteredActiveIndex = activeOption?.relativeIndex ?? -1; + + if (filteredActiveIndex > VISIBLE_OPTS - 1) { + this._optionListScrollPos = Math.floor( + filteredActiveIndex * OPT_HEIGHT + ); + } + } + + window.addEventListener('click', this._onClickOutside); + } else { + window.removeEventListener('click', this._onClickOutside); + } + } + } + @state() protected _currentDescription = ''; @@ -311,16 +336,6 @@ export class VscodeSelectBase extends VscElement { }); } - protected _toggleDropdown(visible: boolean) { - this.open = visible; - - if (visible) { - window.addEventListener('click', this._onClickOutside); - } else { - window.removeEventListener('click', this._onClickOutside); - } - } - protected _createSuggestedOption() { const nextSelectedIndex = this._opts.numOptions; const op = document.createElement('vscode-option'); @@ -340,7 +355,7 @@ export class VscodeSelectBase extends VscElement { //#region event handlers protected _onFaceClick(): void { - this._toggleDropdown(!this.open); + this.open = !this.open; } private _onClickOutside = (event: MouseEvent): void => { @@ -348,8 +363,7 @@ export class VscodeSelectBase extends VscElement { const found = path.findIndex((et) => et === this); if (found === -1) { - this._toggleDropdown(false); - window.removeEventListener('click', this._onClickOutside); + this.open = false; } }; @@ -358,9 +372,10 @@ export class VscodeSelectBase extends VscElement { window.removeEventListener('mousemove', this._onMouseMove); }; + // TODO protected _toggleComboboxDropdown() { this._opts.filterPattern = ''; - this._toggleDropdown(!this.open); + this.open = !this.open; } protected _onComboboxButtonClick(): void { @@ -425,7 +440,7 @@ export class VscodeSelectBase extends VscElement { private _onSpaceKeyDown() { if (!this.open) { - this._toggleDropdown(true); + this.open = true; return; } } @@ -496,7 +511,7 @@ export class VscodeSelectBase extends VscElement { } } } else { - this._toggleDropdown(true); + this.open = true; this._opts.activeIndex = 0; } } @@ -527,13 +542,13 @@ export class VscodeSelectBase extends VscElement { } } } else { - this._toggleDropdown(true); + this.open = true; this._opts.activateDefault(); } } private _onEscapeKeyDown() { - this._toggleDropdown(false); + this.open = false; } private _onComponentKeyDown = (event: KeyboardEvent) => { @@ -590,12 +605,12 @@ export class VscodeSelectBase extends VscElement { this._isBeingFiltered = true; this._opts.filterPattern = (ev.target as HTMLInputElement).value; this._opts.activeIndex = -1; - this._toggleDropdown(true); + this.open = true; } protected _onComboboxInputClick(): void { this._isBeingFiltered = this._opts.filterPattern !== ''; - this._toggleDropdown(true); + this.open = true; } protected _onComboboxInputSpaceKeyDown(ev: KeyboardEvent) { diff --git a/src/vscode-multi-select/vscode-multi-select.ts b/src/vscode-multi-select/vscode-multi-select.ts index a2b9192f0..1f99711f8 100644 --- a/src/vscode-multi-select/vscode-multi-select.ts +++ b/src/vscode-multi-select/vscode-multi-select.ts @@ -261,7 +261,7 @@ export class VscodeMultiSelect {detail: {value: this._options[nextIndex]?.value ?? ''}} ); this.dispatchEvent(opCreateEvent); - this._toggleDropdown(false); + this.open = false; this._isPlaceholderOptionActive = false; } @@ -335,7 +335,7 @@ export class VscodeMultiSelect if (!this.open) { this._opts.filterPattern = ''; - this._toggleDropdown(true); + this.open = true; } else { this._opts.toggleActiveMultiselectOption(); } @@ -344,7 +344,7 @@ export class VscodeMultiSelect } private _onMultiAcceptClick(): void { - this._toggleDropdown(false); + this.open = false; } private _onMultiDeselectAllClick(): void { diff --git a/src/vscode-single-select/vscode-single-select.ts b/src/vscode-single-select/vscode-single-select.ts index 04cff570e..50cd428c8 100644 --- a/src/vscode-single-select/vscode-single-select.ts +++ b/src/vscode-single-select/vscode-single-select.ts @@ -2,11 +2,7 @@ import {html, LitElement, TemplateResult} from 'lit'; import {property, query} from 'lit/decorators.js'; import {customElement} from '../includes/VscElement.js'; import {chevronDownIcon} from '../includes/vscode-select/template-elements.js'; -import { - OPT_HEIGHT, - VISIBLE_OPTS, - VscodeSelectBase, -} from '../includes/vscode-select/vscode-select-base.js'; +import {VscodeSelectBase} from '../includes/vscode-select/vscode-select-base.js'; import styles from './vscode-single-select.styles.js'; import {AssociatedFormControl} from '../includes/AssociatedFormControl.js'; import {ifDefined} from 'lit/directives/if-defined.js'; @@ -230,7 +226,7 @@ export class VscodeSingleSelect {detail: {value: this._options[nextIndex]?.value ?? ''}} ); this.dispatchEvent(opCreateEvent); - this._toggleDropdown(false); + this.open = false; this._isPlaceholderOptionActive = false; } @@ -256,24 +252,6 @@ export class VscodeSingleSelect } } - protected override _toggleDropdown(visible: boolean): void { - super._toggleDropdown(visible); - - if (visible) { - this._opts.activeIndex = this._opts.selectedIndex; - } - - if (visible && !this.combobox) { - this._opts.activeIndex = this._opts.selectedIndex; - - if (this._opts.activeIndex > VISIBLE_OPTS - 1) { - this._optionListScrollPos = Math.floor( - this._opts.activeIndex * OPT_HEIGHT - ); - } - } - } - protected override _onSlotChange(): void { super._onSlotChange(); @@ -348,19 +326,19 @@ export class VscodeSingleSelect } else { valueChanged = this._opts.activeIndex !== this._opts.selectedIndex; this._opts.selectedIndex = this._opts.activeIndex; - this._toggleDropdown(false); + this.open = false; } } else { - this._toggleDropdown(true); + this.open = true; this._scrollActiveElementToTop(); } } else { if (this.open) { valueChanged = this._opts.activeIndex !== this._opts.selectedIndex; this._opts.selectedIndex = this._opts.activeIndex; - this._toggleDropdown(false); + this.open = false; } else { - this._toggleDropdown(true); + this.open = true; this._scrollActiveElementToTop(); } } @@ -405,7 +383,7 @@ export class VscodeSingleSelect this._opts.selectedIndex = Number((optEl as HTMLElement).dataset.index); // this._value = this._options[this._selectedIndex].value; - this._toggleDropdown(false); + this.open = false; this._internals.setFormValue(this._value); this._manageRequired(); this._dispatchChangeEvent(); From 161307777d2614d0f434de27aae08293e67d2543 Mon Sep 17 00:00:00 2001 From: bendera Date: Sat, 31 May 2025 00:09:43 +0200 Subject: [PATCH 23/66] Fix active element highlight in default open state --- src/includes/vscode-select/OptionListController.ts | 7 +++++++ src/includes/vscode-select/vscode-select-base.ts | 2 +- src/vscode-single-select/vscode-single-select.ts | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/includes/vscode-select/OptionListController.ts b/src/includes/vscode-select/OptionListController.ts index eb8579b7c..1a59eed8b 100644 --- a/src/includes/vscode-select/OptionListController.ts +++ b/src/includes/vscode-select/OptionListController.ts @@ -69,6 +69,10 @@ export class OptionListController implements ReactiveController { this._host.requestUpdate(); } + get selectedIndexes() { + return Array.from(this._selectedIndexes); + } + get value(): string | string[] { if (this._multiSelect) { return this._selectedIndexes.size > 0 @@ -153,6 +157,7 @@ export class OptionListController implements ReactiveController { if (selected) { this._selectedIndex = nextIndex; this._selectedIndexes.add(nextIndex); + this._activeIndex = nextIndex; } this._options.push({ @@ -316,6 +321,8 @@ export class OptionListController implements ReactiveController { const indexes = this._selectedIndexes.values(); const first = indexes.next(); this._activeIndex = first.value ? first.value : 0; + } else if (!this._multiSelect && this._selectedIndex > -1) { + this._activeIndex = this._selectedIndex; } else { const nextOp = this.getNextSelectableOption(-1); this._activeIndex = nextOp?.index ?? -1; diff --git a/src/includes/vscode-select/vscode-select-base.ts b/src/includes/vscode-select/vscode-select-base.ts index cee8e6028..9b3de5a09 100644 --- a/src/includes/vscode-select/vscode-select-base.ts +++ b/src/includes/vscode-select/vscode-select-base.ts @@ -651,7 +651,7 @@ export class VscodeSelectBase extends VscElement { } const active = op.index === this._opts.activeIndex && !op.disabled; - const selected = op.index === this._opts.selectedIndex; + const selected = this._opts.getIsIndexSelected(op.index); const optionClasses = { active, diff --git a/src/vscode-single-select/vscode-single-select.ts b/src/vscode-single-select/vscode-single-select.ts index 50cd428c8..827f5f969 100644 --- a/src/vscode-single-select/vscode-single-select.ts +++ b/src/vscode-single-select/vscode-single-select.ts @@ -247,7 +247,7 @@ export class VscodeSingleSelect protected override _setStateFromSlottedElements(): void { super._setStateFromSlottedElements(); - if (!this.combobox && this._selectedIndexes.length === 0) { + if (!this.combobox && this._opts.selectedIndexes.length === 0) { this._opts.selectedIndex = this._opts.options.length > 0 ? 0 : -1; } } From 89652c5c596b0330abfe5ed6f1f2ff73ec5cea5d Mon Sep 17 00:00:00 2001 From: bendera Date: Sat, 31 May 2025 00:34:04 +0200 Subject: [PATCH 24/66] Prevent error when invalid index is selected --- src/includes/vscode-select/OptionListController.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/includes/vscode-select/OptionListController.ts b/src/includes/vscode-select/OptionListController.ts index 1a59eed8b..0880859be 100644 --- a/src/includes/vscode-select/OptionListController.ts +++ b/src/includes/vscode-select/OptionListController.ts @@ -55,8 +55,10 @@ export class OptionListController implements ReactiveController { return this._multiSelect; } - set selectedIndex(value: number) { - this._selectedIndex = value; + set selectedIndex(index: number) { + const op = this.getOptionByIndex(index) + + this._selectedIndex = op ? index : -1; this._host.requestUpdate(); } From 5cd5a38954e027f430da0d8698f213637d0d3851 Mon Sep 17 00:00:00 2001 From: bendera Date: Sat, 31 May 2025 00:52:33 +0200 Subject: [PATCH 25/66] wip --- .../disabled-options.html | 400 +----------------- .../vscode-select/OptionListController.ts | 7 +- .../vscode-select/vscode-select-base.ts | 5 +- .../vscode-single-select.test.ts | 58 +-- .../vscode-single-select.ts | 16 - 5 files changed, 33 insertions(+), 453 deletions(-) diff --git a/dev/vscode-single-select/disabled-options.html b/dev/vscode-single-select/disabled-options.html index 1e6969969..d01da9426 100644 --- a/dev/vscode-single-select/disabled-options.html +++ b/dev/vscode-single-select/disabled-options.html @@ -26,202 +26,11 @@
    - - Afghanistan - Albania - Algeria - Andorra - Angola - Antigua and Barbuda - Argentina - Armenia - Australia - Austria - Azerbaijan - Bahamas - Bahrain - Bangladesh - Barbados - Belarus - Belgium - Belize - Benin - Bhutan - Bolivia - Bosnia and Herzegovina - Botswana - Brazil - Brunei - Bulgaria - Burkina Faso - Burundi - Côte d'Ivoire - Cabo Verde - Cambodia - Cameroon - Canada - Central African Republic - Chad - Chile - China - Colombia - Comoros - Congo (Congo-Brazzaville) - Costa Rica - Croatia - Cuba - Cyprus - Czechia (Czech Republic) - Democratic Republic of the Congo - Denmark - Djibouti - Dominica - Dominican Republic - Ecuador - Egypt - El Salvador - Equatorial Guinea - Eritrea - Estonia - Eswatini (fmr. "Swaziland") - Ethiopia - Fiji - Finland - France - Gabon - Gambia - Georgia - Germany - Ghana - Greece - Grenada - Guatemala - Guinea - Guinea-Bissau - Guyana - Haiti - Holy See - Honduras - Hungary - Iceland - India - Indonesia - Iran - Iraq - Ireland - Israel - Italy - Jamaica - Japan - Jordan - Kazakhstan - Kenya - Kiribati - Kuwait - Kyrgyzstan - Laos - Latvia - Lebanon - Lesotho - Liberia - Libya - Liechtenstein - Lithuania - Luxembourg - Madagascar - Malawi - Malaysia - Maldives - Mali - Malta - Marshall Islands - Mauritania - Mauritius - Mexico - Micronesia - Moldova - Monaco - Mongolia - Montenegro - Morocco - Mozambique - Myanmar (formerly Burma) - Namibia - Nauru - Nepal - Netherlands - New Zealand - Nicaragua - Niger - Nigeria - North Korea - North Macedonia - Norway - Oman - Pakistan - Palau - Palestine State - Panama - Papua New Guinea - Paraguay - Peru - Philippines - Poland - Portugal - Qatar - Romania - Russia - Rwanda - Saint Kitts and Nevis - Saint Lucia - Saint Vincent and the Grenadines - Samoa - San Marino - Sao Tome and Principe - Saudi Arabia - Senegal - Serbia - Seychelles - Sierra Leone - Singapore - Slovakia - Slovenia - Solomon Islands - Somalia - South Africa - South Korea - South Sudan - Spain - Sri Lanka - Sudan - Suriname - Sweden - Switzerland - Syria - Tajikistan - Tanzania - Thailand - Timor-Leste - Togo - Tonga - Trinidad and Tobago - Tunisia - Turkey - Turkmenistan - Tuvalu - Uganda - Ukraine - United Arab Emirates - United Kingdom - United States of America - Uruguay - Uzbekistan - Vanuatu - Venezuela - Vietnam - Yemen - Zambia - Zimbabwe + + Lorem + Ipsum + Dolor + Sit + + + + + +
    + + + Banana + Cherry + Apple + Strawberry + Lemon + Orange + + +
    + + diff --git a/dev/vscode-multi-select/a11y-select.html b/dev/vscode-multi-select/a11y-select.html new file mode 100644 index 000000000..0ddc3a1d4 --- /dev/null +++ b/dev/vscode-multi-select/a11y-select.html @@ -0,0 +1,40 @@ + + + + + + VSCode Elements + + + + + + + +
    + + + Banana + Cherry + Apple + Strawberry + Lemon + Orange + + +
    + + diff --git a/dev/vscode-single-select/combobox-mode/a11y.html b/dev/vscode-single-select/combobox-mode/a11y.html new file mode 100644 index 000000000..e1b73f59a --- /dev/null +++ b/dev/vscode-single-select/combobox-mode/a11y.html @@ -0,0 +1,40 @@ + + + + + + VSCode Elements + + + + + + + +
    + + + Banana + Cherry + Apple + Strawberry + Lemon + Orange + + +
    + + diff --git a/dev/vscode-single-select/select-mode/a11y.html b/dev/vscode-single-select/select-mode/a11y.html new file mode 100644 index 000000000..ad8854a19 --- /dev/null +++ b/dev/vscode-single-select/select-mode/a11y.html @@ -0,0 +1,40 @@ + + + + + + VSCode Elements + + + + + + + +
    + + + Banana + Cherry + Apple + Strawberry + Lemon + Orange + + +
    + + From 23fbbaec789806268ae0a7db00dc8274e4935d3d Mon Sep 17 00:00:00 2001 From: bendera Date: Sun, 15 Jun 2025 21:41:30 +0200 Subject: [PATCH 52/66] Refactor the arrow up key press handler --- .../vscode-select/OptionListController.ts | 21 +------------------ .../vscode-select/vscode-select-base.ts | 7 ++++--- 2 files changed, 5 insertions(+), 23 deletions(-) diff --git a/src/includes/vscode-select/OptionListController.ts b/src/includes/vscode-select/OptionListController.ts index 25702853a..b365c1acf 100644 --- a/src/includes/vscode-select/OptionListController.ts +++ b/src/includes/vscode-select/OptionListController.ts @@ -351,21 +351,9 @@ export class OptionListController implements ReactiveController { return nextIndex > -1 ? this._options[nextIndex] : null; } - getPrevSelectableOption(fromIndex?: number): InternalOption | null { + prev(fromIndex?: number): InternalOption | null { const from = fromIndex ?? this._activeIndex; - if (this._options.length === 0) { - return null; - } - - if (this._options.length === 1) { - return this._options[0]; - } - - if (!this._options[from] || !this._options[from - 1]) { - return this._options[0]; - } - let prevIndex = -1; for (let i = from - 1; i >= 0; i--) { @@ -400,13 +388,6 @@ export class OptionListController implements ReactiveController { this._host.requestUpdate(); } - activatePrev() { - const prevOp = this.getPrevSelectableOption(); - this._activeIndex = prevOp?.index ?? -1; - this._host.requestUpdate(); - return prevOp; - } - //#endregion //#region private functions diff --git a/src/includes/vscode-select/vscode-select-base.ts b/src/includes/vscode-select/vscode-select-base.ts index 70f8613a7..7a1cbb9c4 100644 --- a/src/includes/vscode-select/vscode-select-base.ts +++ b/src/includes/vscode-select/vscode-select-base.ts @@ -479,8 +479,9 @@ export class VscodeSelectBase extends VscElement { this._opts.activeIndex = optionIndex; this._isPlaceholderOptionActive = false; } else { - const prevOpt = this._opts.activatePrev(); - const prevSelectableIndex = prevOpt?.filteredIndex ?? -1; + const prevOp = this._opts.prev(); + this._opts.activeIndex = prevOp?.index ?? -1; + const prevSelectableIndex = prevOp?.filteredIndex ?? -1; if (prevSelectableIndex > -1) { this._adjustOptionListScrollPos('up', prevSelectableIndex); @@ -488,7 +489,7 @@ export class VscodeSelectBase extends VscElement { } } else { this.open = true; - this._opts.activeIndex = 0; + this._opts.activateDefault(); } } From 163eee660a3bef1248afc3b5eddb5076978200e6 Mon Sep 17 00:00:00 2001 From: bendera Date: Sun, 15 Jun 2025 23:19:56 +0200 Subject: [PATCH 53/66] The arrow keys should not change the value --- .../vscode-single-select.ts | 39 ------------------- 1 file changed, 39 deletions(-) diff --git a/src/vscode-single-select/vscode-single-select.ts b/src/vscode-single-select/vscode-single-select.ts index 2d468d3cd..c34f20686 100644 --- a/src/vscode-single-select/vscode-single-select.ts +++ b/src/vscode-single-select/vscode-single-select.ts @@ -267,45 +267,6 @@ export class VscodeSingleSelect } } - protected override _onArrowUpKeyDown(): void { - super._onArrowUpKeyDown(); - - /* if (this.open || this._selectedIndex <= 0) { - return; - } - - const options = this.combobox ? this._filteredOptions : this._options; - const prevIndex = findPrevSelectableOptionIndex(options, this._activeIndex); - - this._filterPattern = ''; - this._selectedIndex = prevIndex; - this._activeIndex = prevIndex; - this._value = prevIndex > -1 ? this._options[prevIndex].value : ''; - */ - this._internals.setFormValue(this._opts.value as string); - this._manageRequired(); - this._dispatchChangeEvent(); - } - - protected override _onArrowDownKeyDown(): void { - super._onArrowDownKeyDown(); - - /* if (this.open || this._selectedIndex >= this._options.length - 1) { - return; - } - - const options = this.combobox ? this._filteredOptions : this._options; - const nextIndex = findNextSelectableOptionIndex(options, this._activeIndex); - - this._filterPattern = ''; - this._selectedIndex = nextIndex; - this._activeIndex = nextIndex; - this._value = nextIndex > -1 ? this._options[nextIndex].value : ''; */ - this._internals.setFormValue(this._opts.value as string); - this._manageRequired(); - this._dispatchChangeEvent(); - } - protected override _onEnterKeyDown(ev: KeyboardEvent): void { super._onEnterKeyDown(ev); let valueChanged = false; From 4df2ec35e787b7211f3c71d9fc19a5cca70e376f Mon Sep 17 00:00:00 2001 From: bendera Date: Sun, 15 Jun 2025 23:21:06 +0200 Subject: [PATCH 54/66] Fine-tuning ARIA attributes --- src/includes/vscode-select/vscode-select-base.ts | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/includes/vscode-select/vscode-select-base.ts b/src/includes/vscode-select/vscode-select-base.ts index 7a1cbb9c4..a8f841e2e 100644 --- a/src/includes/vscode-select/vscode-select-base.ts +++ b/src/includes/vscode-select/vscode-select-base.ts @@ -611,6 +611,9 @@ export class VscodeSelectBase extends VscElement { return html`
      ${this._multiple ? this._renderMultiSelectLabel() : nothing} Date: Mon, 16 Jun 2025 22:34:28 +0200 Subject: [PATCH 55/66] Revert playwright install in verify action --- .github/workflows/verify.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index 696cb483e..e7cc7eb61 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -24,9 +24,6 @@ jobs: - name: Install dependencies run: npm ci - - name: Install Playwright browsers - run: npx playwright install chromium --only-shell - - name: Transpile files run: npm run build:ts From 98fd480623cfc02d8f842517cf4b661d236cb83b Mon Sep 17 00:00:00 2001 From: bendera Date: Mon, 16 Jun 2025 23:42:48 +0200 Subject: [PATCH 56/66] Restructure the render functions --- .../vscode-select/vscode-select-base.ts | 66 +------------------ .../vscode-multi-select.ts | 55 ++++++++++++++-- .../vscode-single-select.ts | 49 ++++++++++++++ 3 files changed, 102 insertions(+), 68 deletions(-) diff --git a/src/includes/vscode-select/vscode-select-base.ts b/src/includes/vscode-select/vscode-select-base.ts index a8f841e2e..076ceeddd 100644 --- a/src/includes/vscode-select/vscode-select-base.ts +++ b/src/includes/vscode-select/vscode-select-base.ts @@ -13,7 +13,6 @@ import '../../vscode-option/index.js'; import {VscodeOption} from '../../vscode-option/index.js'; import {VscElement} from '../VscElement.js'; import {filterOptionsByPattern, highlightRanges} from './helpers.js'; -import {chevronDownIcon} from './template-elements.js'; import type {InternalOption, Option, FilterMethod} from './types.js'; import {OptionListController} from './OptionListController.js'; @@ -246,7 +245,7 @@ export class VscodeSelectBase extends VscElement { protected _isPlaceholderOptionActive = false; @state() - private _isBeingFiltered = false; + protected _isBeingFiltered = false; @state() protected _optionListScrollPos = 0; @@ -722,69 +721,8 @@ export class VscodeSelectBase extends VscElement { return html`${nothing}`; } - private _renderMultiSelectLabel() { - switch (this._opts.selectedIndexes.length) { - case 0: - return html`No items selected`; - case 1: - return html`1 item selected`; - default: - return html`${this._opts.selectedIndexes.length} items selected`; - } - } - protected _renderComboboxFace(): TemplateResult { - let inputVal = ''; - - if (this._isBeingFiltered) { - inputVal = this._opts.filterPattern; - } else { - const op = this._opts.getSelectedOption(); - inputVal = op?.label ?? ''; - } - - const activeDescendant = - this._opts.activeIndex > -1 ? `op-${this._opts.activeIndex}` : ''; - const expanded = this.open ? 'true' : 'false'; - - return html` -
      - ${this._multiple ? this._renderMultiSelectLabel() : nothing} - - -
      - `; + return html`${nothing}`; } protected _renderDropdownControls(): TemplateResult { diff --git a/src/vscode-multi-select/vscode-multi-select.ts b/src/vscode-multi-select/vscode-multi-select.ts index 2ab197bfe..5f3dbe4af 100644 --- a/src/vscode-multi-select/vscode-multi-select.ts +++ b/src/vscode-multi-select/vscode-multi-select.ts @@ -346,6 +346,56 @@ export class VscodeMultiSelect } } + protected override _renderComboboxFace(): TemplateResult { + let inputVal = ''; + + if (this._isBeingFiltered) { + inputVal = this._opts.filterPattern; + } else { + const op = this._opts.getSelectedOption(); + inputVal = op?.label ?? ''; + } + + const activeDescendant = + this._opts.activeIndex > -1 ? `op-${this._opts.activeIndex}` : ''; + const expanded = this.open ? 'true' : 'false'; + + return html` +
      + ${this._multiple ? this._renderLabel() : nothing} + + +
      + `; + } + protected override _renderSelectFace(): TemplateResult { const activeDescendant = this._opts.activeIndex > -1 ? `op-${this._opts.activeIndex}` : ''; @@ -405,10 +455,7 @@ export class VscodeMultiSelect override render(): TemplateResult { return html` -
      +
      ${this.combobox ? this._renderComboboxFace() : this._renderSelectFace()} ${this._renderDropdown()} diff --git a/src/vscode-single-select/vscode-single-select.ts b/src/vscode-single-select/vscode-single-select.ts index c34f20686..561c18825 100644 --- a/src/vscode-single-select/vscode-single-select.ts +++ b/src/vscode-single-select/vscode-single-select.ts @@ -374,6 +374,55 @@ export class VscodeSingleSelect `; } + protected override _renderComboboxFace(): TemplateResult { + let inputVal = ''; + + if (this._isBeingFiltered) { + inputVal = this._opts.filterPattern; + } else { + const op = this._opts.getSelectedOption(); + inputVal = op?.label ?? ''; + } + + const activeDescendant = + this._opts.activeIndex > -1 ? `op-${this._opts.activeIndex}` : ''; + const expanded = this.open ? 'true' : 'false'; + + return html` +
      + + +
      + `; + } + override render(): TemplateResult { return html`
      From 45aa8cdb3097edda8c77f389fe8ea764efe00b27 Mon Sep 17 00:00:00 2001 From: bendera Date: Mon, 16 Jun 2025 23:51:47 +0200 Subject: [PATCH 57/66] More compact badge in the multi select --- src/includes/vscode-select/styles.ts | 1 - src/vscode-multi-select/vscode-multi-select.ts | 18 +++--------------- 2 files changed, 3 insertions(+), 16 deletions(-) diff --git a/src/includes/vscode-select/styles.ts b/src/includes/vscode-select/styles.ts index 05c8d1a72..ade506da8 100644 --- a/src/includes/vscode-select/styles.ts +++ b/src/includes/vscode-select/styles.ts @@ -68,7 +68,6 @@ export default [ line-height: 16px; margin: 2px; padding: 2px 3px; - text-transform: uppercase; white-space: nowrap; } diff --git a/src/vscode-multi-select/vscode-multi-select.ts b/src/vscode-multi-select/vscode-multi-select.ts index 5f3dbe4af..b51c72ad7 100644 --- a/src/vscode-multi-select/vscode-multi-select.ts +++ b/src/vscode-multi-select/vscode-multi-select.ts @@ -326,22 +326,10 @@ export class VscodeMultiSelect private _renderLabel() { switch (this._opts.selectedIndexes.length) { case 0: - return html`0 selected`; - case 1: - return html`1 item selected`; + return html`0 selected`; default: - return html`${this._opts.selectedIndexes.length} items selected${this._opts.selectedIndexes.length} selected`; } } From 13fdafd0e4691375a229be46d92f96d12520e0fa Mon Sep 17 00:00:00 2001 From: bendera Date: Mon, 16 Jun 2025 23:56:50 +0200 Subject: [PATCH 58/66] Refactor --- src/includes/vscode-select/vscode-select-base.ts | 7 ++----- src/vscode-multi-select/vscode-multi-select.ts | 4 +--- src/vscode-single-select/vscode-single-select.ts | 2 -- 3 files changed, 3 insertions(+), 10 deletions(-) diff --git a/src/includes/vscode-select/vscode-select-base.ts b/src/includes/vscode-select/vscode-select-base.ts index 076ceeddd..f609437e1 100644 --- a/src/includes/vscode-select/vscode-select-base.ts +++ b/src/includes/vscode-select/vscode-select-base.ts @@ -250,9 +250,6 @@ export class VscodeSelectBase extends VscElement { @state() protected _optionListScrollPos = 0; - /** @internal */ - protected _multiple = false; - private _isHoverForbidden = false; private _disabled = false; private _originalTabIndex: number | undefined = undefined; @@ -659,7 +656,7 @@ export class VscodeSelectBase extends VscElement { role="option" tabindex="-1" > - ${this._multiple + ${this._opts.multiSelect ? html`${labelText}` : labelText} @@ -732,7 +729,7 @@ export class VscodeSelectBase extends VscElement { protected _renderDropdown() { const classes = { dropdown: true, - multiple: this._multiple, + multiple: this._opts.multiSelect, open: this.open, }; diff --git a/src/vscode-multi-select/vscode-multi-select.ts b/src/vscode-multi-select/vscode-multi-select.ts index b51c72ad7..b819ca7f0 100644 --- a/src/vscode-multi-select/vscode-multi-select.ts +++ b/src/vscode-multi-select/vscode-multi-select.ts @@ -127,8 +127,6 @@ export class VscodeMultiSelect constructor() { super(); this._opts.multiSelect = true; - /** @internal */ - this._multiple = true; this._internals = this.attachInternals(); } @@ -350,7 +348,7 @@ export class VscodeMultiSelect return html`
      - ${this._multiple ? this._renderLabel() : nothing} + ${this._opts.multiSelect ? this._renderLabel() : nothing} Date: Wed, 18 Jun 2025 17:52:33 +0200 Subject: [PATCH 59/66] Implement `selectAll()` and `selectNone()` --- .../vscode-select/OptionListController.ts | 26 +++++++++++++++++++ .../vscode-multi-select.ts | 8 ++++++ 2 files changed, 34 insertions(+) diff --git a/src/includes/vscode-select/OptionListController.ts b/src/includes/vscode-select/OptionListController.ts index b365c1acf..40ea6f61a 100644 --- a/src/includes/vscode-select/OptionListController.ts +++ b/src/includes/vscode-select/OptionListController.ts @@ -388,6 +388,32 @@ export class OptionListController implements ReactiveController { this._host.requestUpdate(); } + selectAll() { + if (!this._multiSelect) { + return; + } + + this._options.forEach((_, i) => { + this._options[i].selected = true; + this._selectedIndexes.add(i); + }); + + this._host.requestUpdate(); + } + + selectNone() { + if (!this._multiSelect) { + return; + } + + this._options.forEach((_, i) => { + this._options[i].selected = false; + }); + this._selectedIndexes.clear(); + + this._host.requestUpdate(); + } + //#endregion //#region private functions diff --git a/src/vscode-multi-select/vscode-multi-select.ts b/src/vscode-multi-select/vscode-multi-select.ts index b819ca7f0..a46c7a4fc 100644 --- a/src/vscode-multi-select/vscode-multi-select.ts +++ b/src/vscode-multi-select/vscode-multi-select.ts @@ -122,6 +122,14 @@ export class VscodeMultiSelect return this._internals.reportValidity(); } + selectAll() { + this._opts.selectAll(); + } + + selectNone() { + this._opts.selectNone(); + } + private _internals: ElementInternals; constructor() { From 82678647e1ff40b95c29a92b98c07c2ea0f0e09e Mon Sep 17 00:00:00 2001 From: bendera Date: Wed, 18 Jun 2025 18:00:09 +0200 Subject: [PATCH 60/66] Fix test --- src/vscode-multi-select/vscode-multi-select.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vscode-multi-select/vscode-multi-select.test.ts b/src/vscode-multi-select/vscode-multi-select.test.ts index 64e09d05b..2296a46f1 100644 --- a/src/vscode-multi-select/vscode-multi-select.test.ts +++ b/src/vscode-multi-select/vscode-multi-select.test.ts @@ -22,7 +22,7 @@ describe('vscode-multi-select', () => { const badge = el.shadowRoot?.querySelector('.select-face-badge'); - expect(badge).lightDom.to.eq('1 item selected'); + expect(badge).lightDom.to.eq('1 selected'); expect(el.selectedIndexes).to.eql([1]); expect(el.value).to.eql(['Ipsum']); }); From 8ff18bc83347a6181613eaad09390b5adbd37314 Mon Sep 17 00:00:00 2001 From: bendera Date: Sat, 21 Jun 2025 11:38:25 +0200 Subject: [PATCH 61/66] Fix runtime error --- src/includes/vscode-select/OptionListController.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/includes/vscode-select/OptionListController.ts b/src/includes/vscode-select/OptionListController.ts index 40ea6f61a..1fcb27991 100644 --- a/src/includes/vscode-select/OptionListController.ts +++ b/src/includes/vscode-select/OptionListController.ts @@ -266,6 +266,11 @@ export class OptionListController implements ReactiveController { toggleActiveMultiselectOption() { const activeOption = this._options[this._activeIndex] ?? null; + + if (!activeOption) { + return; + } + const checked = this._selectedIndexes.has(activeOption.index); if (checked) { From c21d2ac686643ab4a5c9df1b5d4737e82ceb9ccd Mon Sep 17 00:00:00 2001 From: bendera Date: Sat, 21 Jun 2025 11:41:25 +0200 Subject: [PATCH 62/66] Fix disappearing highlight --- src/includes/vscode-select/vscode-select-base.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/includes/vscode-select/vscode-select-base.ts b/src/includes/vscode-select/vscode-select-base.ts index f609437e1..127f6004a 100644 --- a/src/includes/vscode-select/vscode-select-base.ts +++ b/src/includes/vscode-select/vscode-select-base.ts @@ -476,11 +476,14 @@ export class VscodeSelectBase extends VscElement { this._isPlaceholderOptionActive = false; } else { const prevOp = this._opts.prev(); - this._opts.activeIndex = prevOp?.index ?? -1; - const prevSelectableIndex = prevOp?.filteredIndex ?? -1; - if (prevSelectableIndex > -1) { - this._adjustOptionListScrollPos('up', prevSelectableIndex); + if (prevOp !== null) { + this._opts.activeIndex = prevOp?.index ?? -1; + const prevSelectableIndex = prevOp?.filteredIndex ?? -1; + + if (prevSelectableIndex > -1) { + this._adjustOptionListScrollPos('up', prevSelectableIndex); + } } } } else { @@ -613,6 +616,7 @@ export class VscodeSelectBase extends VscElement { class="options" id="select-listbox" role="listbox" + tabindex="-1" @click=${this._onOptionClick} @mouseover=${this._onOptionMouseOver} @scroll=${this._onOptionListScroll} From edcf2da79e4115d25da106f7e84a353a4707f3de Mon Sep 17 00:00:00 2001 From: bendera Date: Sat, 21 Jun 2025 12:17:10 +0200 Subject: [PATCH 63/66] Fix create option with Enter key press --- src/vscode-multi-select/vscode-multi-select.test.ts | 1 + src/vscode-multi-select/vscode-multi-select.ts | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/vscode-multi-select/vscode-multi-select.test.ts b/src/vscode-multi-select/vscode-multi-select.test.ts index 2296a46f1..7291bb30f 100644 --- a/src/vscode-multi-select/vscode-multi-select.test.ts +++ b/src/vscode-multi-select/vscode-multi-select.test.ts @@ -385,4 +385,5 @@ describe('vscode-multi-select', () => { it('selects multiple options with keyboard'); it('selectedIndexes sync with values'); + it('creates and select suggested option (enter key press)'); }); diff --git a/src/vscode-multi-select/vscode-multi-select.ts b/src/vscode-multi-select/vscode-multi-select.ts index a46c7a4fc..64bfb846e 100644 --- a/src/vscode-multi-select/vscode-multi-select.ts +++ b/src/vscode-multi-select/vscode-multi-select.ts @@ -295,7 +295,11 @@ export class VscodeMultiSelect this._opts.filterPattern = ''; this.open = true; } else { - this._opts.toggleActiveMultiselectOption(); + if (this._isPlaceholderOptionActive) { + this._createAndSelectSuggestedOption(); + } else { + this._opts.toggleActiveMultiselectOption(); + } } // TODO: dispatch change + set value From bd27dad78cde3d2463281f8ee654b42dbb47598a Mon Sep 17 00:00:00 2001 From: bendera Date: Sat, 21 Jun 2025 12:26:20 +0200 Subject: [PATCH 64/66] Fix change event dispatching --- dev/vscode-multi-select/combobox-suggestion-mode.html | 10 ++-------- src/vscode-multi-select/vscode-multi-select.test.ts | 1 + src/vscode-multi-select/vscode-multi-select.ts | 5 +++-- 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/dev/vscode-multi-select/combobox-suggestion-mode.html b/dev/vscode-multi-select/combobox-suggestion-mode.html index db92e9bdf..c8116b02d 100644 --- a/dev/vscode-multi-select/combobox-suggestion-mode.html +++ b/dev/vscode-multi-select/combobox-suggestion-mode.html @@ -224,14 +224,8 @@ Zimbabwe
    diff --git a/src/vscode-multi-select/vscode-multi-select.test.ts b/src/vscode-multi-select/vscode-multi-select.test.ts index 7291bb30f..e2104d958 100644 --- a/src/vscode-multi-select/vscode-multi-select.test.ts +++ b/src/vscode-multi-select/vscode-multi-select.test.ts @@ -386,4 +386,5 @@ describe('vscode-multi-select', () => { it('selects multiple options with keyboard'); it('selectedIndexes sync with values'); it('creates and select suggested option (enter key press)'); + it('dispatch change event, set form value, manage required state (enter key press)'); }); diff --git a/src/vscode-multi-select/vscode-multi-select.ts b/src/vscode-multi-select/vscode-multi-select.ts index 64bfb846e..428dddb4e 100644 --- a/src/vscode-multi-select/vscode-multi-select.ts +++ b/src/vscode-multi-select/vscode-multi-select.ts @@ -299,10 +299,11 @@ export class VscodeMultiSelect this._createAndSelectSuggestedOption(); } else { this._opts.toggleActiveMultiselectOption(); + this._setFormValue(); + this._manageRequired(); + this._dispatchChangeEvent(); } } - - // TODO: dispatch change + set value } private _onMultiAcceptClick(): void { From 253ec5caeefe4e4ae001c2969ab848e25b4afc24 Mon Sep 17 00:00:00 2001 From: bendera Date: Sat, 21 Jun 2025 12:50:51 +0200 Subject: [PATCH 65/66] Add nicer checkboxes --- src/includes/vscode-select/styles.ts | 36 ++++++++----------- .../vscode-select/template-elements.ts | 16 ++++++++- .../vscode-select/vscode-select-base.ts | 29 ++++++++++----- 3 files changed, 49 insertions(+), 32 deletions(-) diff --git a/src/includes/vscode-select/styles.ts b/src/includes/vscode-select/styles.ts index ade506da8..742c08a20 100644 --- a/src/includes/vscode-select/styles.ts +++ b/src/includes/vscode-select/styles.ts @@ -313,37 +313,29 @@ export default [ } .checkbox-icon { - background-color: var(--vscode-settings-checkboxBackground, #313131); - border: 1px solid currentColor; + align-items: center; + background-color: var(--vscode-checkbox-background, #313131); + border: 1px solid var(--vscode-checkbox-border); border-radius: 2px; box-sizing: border-box; - height: 14px; + color: var(--vscode-checkbox-foreground); + display: inline-flex; + height: 15px; + justify-content: center; margin-right: 5px; overflow: hidden; position: relative; - width: 14px; - } - - .checkbox-icon.checked:before, - .checkbox-icon.checked:after { - content: ''; - display: block; - height: 5px; - position: absolute; - transform: rotate(-45deg); - width: 10px; + width: 15px; } - .checkbox-icon.checked:before { - background-color: var(--vscode-foreground, #cccccc); - left: 1px; - top: 2.5px; + .checkbox-icon svg { + display: none; + height: 13px; + width: 13px; } - .checkbox-icon.checked:after { - background-color: var(--vscode-settings-checkboxBackground, #313131); - left: 1px; - top: -0.5px; + .checkbox-icon.checked svg { + display: block; } .dropdown-controls { diff --git a/src/includes/vscode-select/template-elements.ts b/src/includes/vscode-select/template-elements.ts index a58e1c9e2..bbe3525af 100644 --- a/src/includes/vscode-select/template-elements.ts +++ b/src/includes/vscode-select/template-elements.ts @@ -1,4 +1,4 @@ -import {html} from 'lit'; +import {html, svg} from 'lit'; export const chevronDownIcon = html` @@ -17,3 +17,17 @@ export const chevronDownIcon = html` `; + +export const checkIcon = svg` + +`; diff --git a/src/includes/vscode-select/vscode-select-base.ts b/src/includes/vscode-select/vscode-select-base.ts index 127f6004a..bfb89eff0 100644 --- a/src/includes/vscode-select/vscode-select-base.ts +++ b/src/includes/vscode-select/vscode-select-base.ts @@ -8,6 +8,7 @@ import { import {classMap} from 'lit/directives/class-map.js'; import {ifDefined} from 'lit/directives/if-defined.js'; import {repeat} from 'lit/directives/repeat.js'; +import {when} from 'lit/directives/when.js'; import '../../vscode-button/index.js'; import '../../vscode-option/index.js'; import {VscodeOption} from '../../vscode-option/index.js'; @@ -15,6 +16,7 @@ import {VscElement} from '../VscElement.js'; import {filterOptionsByPattern, highlightRanges} from './helpers.js'; import type {InternalOption, Option, FilterMethod} from './types.js'; import {OptionListController} from './OptionListController.js'; +import {checkIcon} from './template-elements.js'; export const VISIBLE_OPTS = 10; export const OPT_HEIGHT = 22; @@ -604,6 +606,19 @@ export class VscodeSelectBase extends VscElement { //#endregion //#region render functions + private _renderCheckbox( + checked: boolean, + label: string | TemplateResult | TemplateResult[] + ) { + const checkboxClasses = { + 'checkbox-icon': true, + checked, + }; + + return html`${checkIcon}${label}`; + } + protected _renderOptions(): TemplateResult | TemplateResult[] { const list = this._opts.options; @@ -640,11 +655,6 @@ export class VscodeSelectBase extends VscElement { selected, }; - const checkboxClasses = { - 'checkbox-icon': true, - checked: selected, - }; - const labelText = (op.ranges?.length ?? 0 > 0) ? highlightRanges(op.label, op.ranges ?? []) @@ -660,10 +670,11 @@ export class VscodeSelectBase extends VscElement { role="option" tabindex="-1" > - ${this._opts.multiSelect - ? html`${labelText}` - : labelText} + ${when( + this._opts.multiSelect, + () => this._renderCheckbox(selected, labelText), + () => labelText + )} `; } From 6a8ff18ac16530083b1377f41c9dd6996497f10c Mon Sep 17 00:00:00 2001 From: bendera Date: Sat, 21 Jun 2025 13:11:26 +0200 Subject: [PATCH 66/66] Minor fixes --- .../vscode-multi-select.test.ts | 80 ++++++------------- .../vscode-multi-select.ts | 4 +- 2 files changed, 27 insertions(+), 57 deletions(-) diff --git a/src/vscode-multi-select/vscode-multi-select.test.ts b/src/vscode-multi-select/vscode-multi-select.test.ts index e2104d958..9d69724b9 100644 --- a/src/vscode-multi-select/vscode-multi-select.test.ts +++ b/src/vscode-multi-select/vscode-multi-select.test.ts @@ -22,7 +22,7 @@ describe('vscode-multi-select', () => { const badge = el.shadowRoot?.querySelector('.select-face-badge'); - expect(badge).lightDom.to.eq('1 selected'); + expect(badge).lightDom.to.eq('1 Selected'); expect(el.selectedIndexes).to.eql([1]); expect(el.value).to.eql(['Ipsum']); }); @@ -120,39 +120,6 @@ describe('vscode-multi-select', () => { }); }); - it('dropdown should be hidden after the OK button is clicked', async () => { - const el = (await fixture(html` - - One - Two - Three - - `)) as VscodeMultiSelect; - - const selectFace = - el.shadowRoot?.querySelector('.select-face'); - selectFace!.click(); - await el.updateComplete; - const dropdownVisibleBefore = el.shadowRoot - ?.querySelector('.dropdown') - ?.classList.contains('open'); - - const button = - el.shadowRoot?.querySelector('.button-accept'); - button!.click(); - await el.updateComplete; - - button?.click(); - await el.updateComplete; - - const dropdownVisibleAfter = el.shadowRoot - ?.querySelector('.dropdown') - ?.classList.contains('open'); - - expect(dropdownVisibleBefore).to.eq(true); - expect(dropdownVisibleAfter).to.eq(false); - }); - it('should apply combobox mode', async () => { const el = await fixture( html` @@ -167,7 +134,7 @@ describe('vscode-multi-select', () => { expect(comboboxFace).to.be.ok; }); - it('should "select all" and "deselect all" work properly', async () => { + it('selects all options', async () => { const el = await fixture( html` One @@ -176,32 +143,33 @@ describe('vscode-multi-select', () => { ` ); - el.shadowRoot?.querySelector('.select-face')!.click(); + el.selectAll(); await el.updateComplete; - const btSelectAll = - el.shadowRoot?.querySelector('#select-all'); - const btSelectNone = - el.shadowRoot?.querySelector('#select-none'); - btSelectAll!.click(); - await el.updateComplete; + expect(el.shadowRoot?.querySelector('.select-face-badge')).lightDom.to.eq( + '3 Selected' + ); + }); - let caption = - el.shadowRoot?.querySelector( - '.select-face-badge' - )!.innerText; + it('de-selects all options', async () => { + const el = await fixture( + html` + One + Two + Three + ` + ); - expect(caption).to.eq('3 ITEMS SELECTED'); + expect(el.shadowRoot?.querySelector('.select-face-badge')).lightDom.to.eq( + '3 Selected' + ); - btSelectNone!.click(); + el.selectNone(); await el.updateComplete; - caption = - el.shadowRoot?.querySelector( - '.select-face-badge' - )!.innerText; - - expect(caption).to.eq('NO ITEMS SELECTED'); + expect(el.shadowRoot?.querySelector('.select-face-badge')).lightDom.to.eq( + '0 Selected' + ); }); it('should be unfocusable when it is disabled', () => { @@ -386,5 +354,7 @@ describe('vscode-multi-select', () => { it('selects multiple options with keyboard'); it('selectedIndexes sync with values'); it('creates and select suggested option (enter key press)'); - it('dispatch change event, set form value, manage required state (enter key press)'); + it( + 'dispatch change event, set form value, manage required state (enter key press)' + ); }); diff --git a/src/vscode-multi-select/vscode-multi-select.ts b/src/vscode-multi-select/vscode-multi-select.ts index 428dddb4e..52813571e 100644 --- a/src/vscode-multi-select/vscode-multi-select.ts +++ b/src/vscode-multi-select/vscode-multi-select.ts @@ -337,10 +337,10 @@ export class VscodeMultiSelect private _renderLabel() { switch (this._opts.selectedIndexes.length) { case 0: - return html`0 selected`; + return html`0 Selected`; default: return html`${this._opts.selectedIndexes.length} selected${this._opts.selectedIndexes.length} Selected`; } }