Skip to content

Commit f6f7fd4

Browse files
committed
Dropdown improvements
1 parent 641fb25 commit f6f7fd4

22 files changed

+1298
-664
lines changed

.changeset/chilly-singers-end.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
'@crowdstrike/glide-core': patch
3+
---
4+
5+
### Dropdown
6+
7+
- The tag overflow button is now updated when a selected Dropdown Option is enabled or disabled.
8+
- Dropdown now reverts its `value` and Dropdown Options revert back to their initial `selected` state when `reset()` is called on the form.
9+
- Selected Dropdown Options whose `value` is an empty string now appear as selected.
10+
- Only the last selected option is presented to screenreaders as selected when single-select and multiple options are initially selected.
11+
- Tags now appear in the order that Dropdown Options are selected by the user.
12+
- Removing a tag via Enter no longer submits the form.
13+
- Disabled Dropdown Options are now enabled when selected programmatically.
14+
15+
- When a Dropdown Option is disabled programmatically and more than one selected Dropdown Option has the same `value`, Dropdown now only removes the value corresponding to the disabled Dropdown Option from `value`.
16+
17+
- When `value` is set programmatically, only the first Dropdown Option with a matching value is now selected instead of every Dropdown Option with a matching value.
18+
19+
- Dropdown's input field is now populated with the `label` of the last selected Dropdown Option when filterable and `multiple` is set to `false` programmatically.
20+
21+
### Tab Group
22+
23+
- The trailing overflow button is no longer incorrectly enabled in a rare case.

.changeset/young-plums-brush.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@crowdstrike/glide-core': minor
3+
---
4+
5+
- Dropdown Option's `selected` attribute is no longer reflected to match native and to resolve a bug when `reset()` is called on its form. This may be a breaking change for you if you have a mutation observer or CSS selector that targets `selected`.

custom-elements.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2221,8 +2221,7 @@
22212221
"text": "boolean"
22222222
},
22232223
"default": "false",
2224-
"attribute": "selected",
2225-
"reflects": true
2224+
"attribute": "selected"
22262225
},
22272226
{
22282227
"kind": "field",

src/checkbox-group.stories.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ const meta: Meta = {
212212
) {
213213
checkboxGroup.reportValidity();
214214

215-
// `reportValidity` scrolls the element into view, which means the "autodocs"
215+
// `reportValidity()` scrolls the element into view, which means the "autodocs"
216216
// story upon load will be scrolled to the first error story. No good.
217217
document.documentElement.scrollTop = 0;
218218

src/checkbox.stories.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ const meta: Meta = {
206206
) {
207207
checkbox.reportValidity();
208208

209-
// `reportValidity` scrolls the element into view, which means the "autodocs"
209+
// `reportValidity()` scrolls the element into view, which means the "autodocs"
210210
// story upon load will be scrolled to the first error story. No good.
211211
document.documentElement.scrollTop = 0;
212212

src/dropdown.option.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ export default class DropdownOption extends LitElement {
158158
/**
159159
* @default false
160160
*/
161-
@property({ reflect: true, type: Boolean })
161+
@property({ type: Boolean })
162162
get selected(): boolean {
163163
return this.#selected;
164164
}
@@ -170,9 +170,6 @@ export default class DropdownOption extends LitElement {
170170
this.#checkboxElementRef.value.checked = isSelected;
171171
}
172172

173-
// Prefixed with "private" because Dropdown uses it internally, to track the set of
174-
// selected options in the case of multiselect. Dropdown itself then dispatches in
175-
// response to this event a "change" event after updating its `this.value`.
176173
this.dispatchEvent(new Event('private-selected-change', { bubbles: true }));
177174
}
178175

src/dropdown.stories.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -472,7 +472,7 @@ class Component extends LitElement {
472472
) {
473473
dropdown.reportValidity();
474474

475-
// `reportValidity` scrolls the element into view, which means the "autodocs"
475+
// `reportValidity()` scrolls the element into view, which means the "autodocs"
476476
// story upon load will be scrolled to the first error story. No good.
477477
document.documentElement.scrollTop = 0;
478478

src/dropdown.test.basics.multiple.ts

Lines changed: 66 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,13 @@ it('is accessible', async () => {
3535
await expect(host).to.be.accessible();
3636
});
3737

38-
it('sets `value` to that of its selected options', async () => {
38+
it('can have selected options', async () => {
3939
const host = await fixture<Dropdown>(
40-
html`<glide-core-dropdown label="Label" open multiple>
40+
html`<glide-core-dropdown label="Label" multiple>
4141
<glide-core-dropdown-option
4242
label="One"
4343
value="one"
44+
selected
4445
></glide-core-dropdown-option>
4546
4647
<glide-core-dropdown-option
@@ -52,54 +53,28 @@ it('sets `value` to that of its selected options', async () => {
5253
<glide-core-dropdown-option
5354
label="Three"
5455
value="three"
55-
selected
5656
></glide-core-dropdown-option>
5757
</glide-core-dropdown>`,
5858
);
5959

60-
expect(host.value).to.deep.equal(['two', 'three']);
61-
});
62-
63-
it('has selected option labels when options are selected', async () => {
64-
const host = await fixture<Dropdown>(
65-
html`<glide-core-dropdown label="Label" multiple>
66-
<glide-core-dropdown-option
67-
label="One"
68-
selected
69-
></glide-core-dropdown-option>
70-
71-
<glide-core-dropdown-option
72-
label="Two"
73-
selected
74-
></glide-core-dropdown-option>
75-
</glide-core-dropdown>`,
76-
);
60+
const options = host.querySelectorAll('glide-core-dropdown-option');
7761

7862
const labels = host.shadowRoot?.querySelectorAll(
7963
'[data-test="selected-option-label"]',
8064
);
8165

66+
const tags = host.shadowRoot?.querySelectorAll<Tag>('[data-test="tag"]');
67+
68+
expect(options[0]?.selected).to.be.true;
69+
expect(options[1]?.selected).to.be.true;
70+
expect(options[2]?.selected).to.be.false;
8271
expect(labels?.length).to.equal(2);
8372
expect(labels?.[0]?.textContent?.trim()).to.equal('One,');
8473
expect(labels?.[1]?.textContent?.trim()).to.equal('Two,');
85-
});
86-
87-
it('has a tag when an option is selected', async () => {
88-
const host = await fixture<Dropdown>(
89-
html`<glide-core-dropdown label="Label" multiple>
90-
<glide-core-dropdown-option
91-
label="One"
92-
selected
93-
></glide-core-dropdown-option>
94-
95-
<glide-core-dropdown-option label="Two"></glide-core-dropdown-option>
96-
</glide-core-dropdown>`,
97-
);
98-
99-
const tag = host.shadowRoot?.querySelector<Tag>('[data-test="tag"]');
100-
101-
expect(tag?.checkVisibility()).to.be.true;
102-
expect(tag?.label).to.equal('One');
74+
expect(host.value).to.deep.equal(['one', 'two']);
75+
expect(tags?.length).to.equal(2);
76+
expect(tags?.[0]?.label).to.equal('One');
77+
expect(tags?.[1]?.label).to.equal('Two');
10378
});
10479

10580
it('shows Select All', async () => {
@@ -367,3 +342,56 @@ it('does not include in its `value` disabled options that are selected', async (
367342

368343
expect(host.value).to.deep.equal([]);
369344
});
345+
346+
it('only selects the first option with a matching value when multiple options have the same value and `value` is set', async () => {
347+
const host = await fixture<Dropdown>(
348+
html`<glide-core-dropdown label="Label" multiple .value=${['one']}>
349+
<glide-core-dropdown-option
350+
label="One"
351+
value="one"
352+
></glide-core-dropdown-option>
353+
354+
<glide-core-dropdown-option
355+
label="One"
356+
value="one"
357+
></glide-core-dropdown-option>
358+
</glide-core-dropdown>`,
359+
);
360+
361+
const options = host.querySelectorAll('glide-core-dropdown-option');
362+
363+
const labels = host.shadowRoot?.querySelectorAll(
364+
'[data-test="selected-option-label"]',
365+
);
366+
367+
const tags = host.shadowRoot?.querySelectorAll<Tag>('[data-test="tag"]');
368+
369+
expect(options[0]?.selected).to.be.true;
370+
expect(options[1]?.selected).to.be.false;
371+
expect(labels?.length).to.equal(1);
372+
expect(labels?.[0]?.textContent?.trim()).to.equal('One,');
373+
expect(host.value).to.deep.equal(['one']);
374+
expect(tags?.length).to.equal(1);
375+
expect(tags?.[0]?.label).to.equal('One');
376+
});
377+
378+
it('selects multiple options with the same value when `value` is set programmatically', async () => {
379+
const host = await fixture<Dropdown>(
380+
html`<glide-core-dropdown label="Label" multiple .value=${['one', 'one']}>
381+
<glide-core-dropdown-option
382+
label="One"
383+
value="one"
384+
></glide-core-dropdown-option>
385+
386+
<glide-core-dropdown-option
387+
label="One"
388+
value="one"
389+
></glide-core-dropdown-option>
390+
</glide-core-dropdown>`,
391+
);
392+
393+
const options = host.querySelectorAll('glide-core-dropdown-option');
394+
395+
expect(options[0]?.selected).to.be.true;
396+
expect(options[1]?.selected).to.be.true;
397+
});

src/dropdown.test.basics.single.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,22 +12,40 @@ it('is accessible ', async () => {
1212
await expect(host).to.be.accessible();
1313
});
1414

15-
it('has a selected option label when an option is initially selected', async () => {
15+
it('can have a selected option', async () => {
1616
const host = await fixture<Dropdown>(
1717
html`<glide-core-dropdown label="Label">
1818
<glide-core-dropdown-option
1919
label="One"
20+
value="one"
21+
selected
22+
></glide-core-dropdown-option>
23+
24+
<glide-core-dropdown-option
25+
label="Two"
26+
value="two"
2027
selected
2128
></glide-core-dropdown-option>
29+
30+
<glide-core-dropdown-option
31+
label="Three"
32+
value="three"
33+
></glide-core-dropdown-option>
2234
</glide-core-dropdown>`,
2335
);
2436

37+
const options = host.querySelectorAll('glide-core-dropdown-option');
38+
2539
const labels = host.shadowRoot?.querySelectorAll(
2640
'[data-test="selected-option-label"]',
2741
);
2842

43+
expect(options[0]?.selected).to.be.true;
44+
expect(options[1]?.selected).to.be.true;
45+
expect(options[2]?.selected).to.be.false;
2946
expect(labels?.length).to.equal(1);
30-
expect(labels?.[0]?.textContent?.trim()).to.equal('One,');
47+
expect(labels?.[0]?.textContent?.trim()).to.equal('Two,');
48+
expect(host.value).to.deep.equal(['two']);
3149
});
3250

3351
it('has an Edit button when its last selected option is editable', async () => {

src/dropdown.test.focus.multiple.ts

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -105,8 +105,7 @@ it('focuses the second tag when the first one is removed', async () => {
105105
tags?.[0]?.click();
106106
await host.updateComplete;
107107

108-
// Wait for the timeout in `#onTagRemove`.
109-
await aTimeout(0);
108+
await aTimeout(0); // Wait for the timeout in `#onTagRemove()`
110109

111110
expect(host.shadowRoot?.activeElement).to.equal(tags?.[1]);
112111
});
@@ -136,8 +135,7 @@ it('focuses the third tag when the second one is removed', async () => {
136135
tags?.[1]?.click();
137136
await host.updateComplete;
138137

139-
// Wait for the timeout in `#onTagRemove`.
140-
await aTimeout(0);
138+
await aTimeout(0); // Wait for the timeout in `#onTagRemove()`
141139

142140
expect(host.shadowRoot?.activeElement).to.equal(tags?.[2]);
143141
});
@@ -167,8 +165,7 @@ it('focuses the second tag when the third tag removed', async () => {
167165
tags?.[2]?.click();
168166
await host.updateComplete;
169167

170-
// Wait for the timeout in `#onTagRemove`.
171-
await aTimeout(0);
168+
await aTimeout(0); // Wait for the timeout in `#onTagRemove()`
172169

173170
expect(host.shadowRoot?.activeElement).to.equal(tags?.[1]);
174171
});
@@ -186,9 +183,7 @@ it('focuses itself when the last tag is removed', async () => {
186183
host.shadowRoot?.querySelector<Tag>('[data-test="tag"]')?.click();
187184

188185
await host.updateComplete;
189-
190-
// Wait for the timeout in `#onTagRemove`.
191-
await aTimeout(0);
186+
await aTimeout(0); // Wait for the timeout in `#onTagRemove()`
192187

193188
expect(document.activeElement).to.equal(host);
194189
});

0 commit comments

Comments
 (0)