Skip to content

Commit f8387bd

Browse files
committed
fix(#2333): sync the values after receiving a destroy msg from child element
1 parent 39df355 commit f8387bd

File tree

10 files changed

+260
-3
lines changed

10 files changed

+260
-3
lines changed

apps/prs/angular/src/app/app.component.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
<goab-side-menu-group heading="Bugs">
1818
<a href="/bugs/2152">2152</a>
1919
<a href="/bugs/2331">2331</a>
20+
<a href="/bugs/2333">2333</a>
2021
<a href="/bugs/2393">2393</a>
2122
<a href="/bugs/2404">2404</a>
2223
<a href="/bugs/2408">2408</a>

apps/prs/angular/src/app/app.routes.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Route } from "@angular/router";
33
import { EverythingComponent } from "./everything.component";
44
import { Bug2152Component } from "../routes/bugs/2152/bug2152.component";
55
import { Bug2331Component } from "../routes/bugs/2331/bug2331.component";
6+
import { Bug2333Component } from "../routes/bugs/2333/bug2333.component";
67
import { Bug2393Component } from "../routes/bugs/2393/bug2393.component";
78
import { Bug2404Component } from "../routes/bugs/2404/bug2404.component";
89
import { Bug2408Component } from "../routes/bugs/2408/bug2408.component";
@@ -48,6 +49,7 @@ import { Feat3102Component } from "../routes/features/feat3102/feat3102.componen
4849
export const appRoutes: Route[] = [
4950
{ path: "bugs/2152", component: Bug2152Component },
5051
{ path: "bugs/2331", component: Bug2331Component },
52+
{ path: "bugs/2333", component: Bug2333Component },
5153
{ path: "bugs/2393", component: Bug2393Component },
5254
{ path: "bugs/2404", component: Bug2404Component },
5355
{ path: "bugs/2408", component: Bug2408Component },
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<div style="width: 1024px; margin: 0 auto; padding: 2rem">
2+
<goab-text size="heading-l" mb="xl"> Bug #2333: Dropdown Reset Test </goab-text>
3+
4+
<goab-text size="body-m" mb="2xl">
5+
This test demonstrates the dropdown reset issue. When dropdown items are dynamically removed,
6+
the dropdown should properly sync its internal state to reflect the updated list of options.
7+
</goab-text>
8+
9+
<goab-text size="heading-m" mb="l"> Test Scenario </goab-text>
10+
11+
<goab-text size="body-s" mb="m"> 1. Select a color from the dropdown below </goab-text>
12+
<goab-text size="body-s" mb="m">
13+
2. Click one of the buttons to reduce the number of available options
14+
</goab-text>
15+
<goab-text size="body-s" mb="m">
16+
3. Open the dropdown again - it should only show the remaining options
17+
</goab-text>
18+
<goab-text size="body-s" mb="2xl">
19+
4. The bug occurred when the filtered options weren't synced after items were destroyed
20+
</goab-text>
21+
22+
<goab-text size="body-m" mb="m">
23+
Currently showing {{ colorsCount }} color(s): {{ colorsList }}
24+
</goab-text>
25+
26+
<goab-text size="body-m" mb="m">
27+
Selected value: {{ selectedColor || "None" }}
28+
</goab-text>
29+
30+
<goab-dropdown
31+
name="favcolor"
32+
placeholder="Select a color"
33+
[value]="selectedColor"
34+
(onChange)="onChange($event)"
35+
mb="xl"
36+
>
37+
@for (color of colors; track color) {
38+
<goab-dropdown-item [label]="color" [value]="color"></goab-dropdown-item>
39+
}
40+
</goab-dropdown>
41+
42+
<div style="display: flex; gap: 1rem; margin-bottom: 1rem">
43+
<goab-button type="secondary" (click)="reduceToOne()">
44+
Reduce to 1 item (blue)
45+
</goab-button>
46+
<goab-button type="secondary" (click)="reduceToTwo()">
47+
Reduce to 2 items (green, yellow)
48+
</goab-button>
49+
<goab-button type="primary" (click)="resetToAll()"> Reset to all items </goab-button>
50+
</div>
51+
52+
<goab-text size="body-s" mb="m" mt="2xl">
53+
<strong>Expected behavior:</strong> After clicking a reduction button, opening the dropdown
54+
should only display the items that remain in the list.
55+
</goab-text>
56+
<goab-text size="body-s">
57+
<strong>Bug behavior (before fix):</strong> The dropdown would still show all original items
58+
even after they were removed, because syncFilteredOptions() wasn't called when child items
59+
were destroyed.
60+
</goab-text>
61+
</div>
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { Component } from "@angular/core";
2+
import { CommonModule } from "@angular/common";
3+
import {
4+
GoabButton,
5+
GoabDropdown,
6+
GoabDropdownItem,
7+
GoabDropdownOnChangeDetail,
8+
GoabText,
9+
} from "@abgov/angular-components";
10+
11+
@Component({
12+
standalone: true,
13+
selector: "abgov-bug2333",
14+
templateUrl: "./bug2333.component.html",
15+
imports: [CommonModule, GoabButton, GoabDropdown, GoabDropdownItem, GoabText],
16+
})
17+
export class Bug2333Component {
18+
colors = ["red", "blue", "green", "yellow", "purple"];
19+
selectedColor = "";
20+
21+
reduceToOne(): void {
22+
this.colors = ["blue"];
23+
}
24+
25+
reduceToTwo(): void {
26+
this.colors = ["green", "yellow"];
27+
}
28+
29+
resetToAll(): void {
30+
this.colors = ["red", "blue", "green", "yellow", "purple"];
31+
}
32+
33+
onChange(detail: GoabDropdownOnChangeDetail): void {
34+
console.log("Dropdown changed:", detail);
35+
this.selectedColor = Array.isArray(detail.value) ? detail.value[0] : detail.value;
36+
}
37+
38+
get colorsList(): string {
39+
return this.colors.join(", ");
40+
}
41+
42+
get colorsCount(): number {
43+
return this.colors.length;
44+
}
45+
}

apps/prs/react/src/app/app.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export function App() {
2525
<GoabSideMenuGroup heading="Bugs">
2626
<Link to="/bugs/2152">2152</Link>
2727
<Link to="/bugs/2331">2331</Link>
28+
<Link to="/bugs/2333">2333</Link>
2829
<Link to="/bugs/2393">2393</Link>
2930
<Link to="/bugs/2404">2404</Link>
3031
<Link to="/bugs/2408">2408</Link>

apps/prs/react/src/main.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import App from "./app/app";
77

88
import { Bug2152Route } from "./routes/bugs/bug2152";
99
import { Bug2331Route } from "./routes/bugs/bug2331";
10+
import { Bug2333Route } from "./routes/bugs/bug2333";
1011
import { Bug2393Route } from "./routes/bugs/bug2393";
1112
import { Bug2404Route } from "./routes/bugs/bug2404";
1213
import { Bug2408Route } from "./routes/bugs/bug2408";
@@ -54,6 +55,7 @@ root.render(
5455
<Route path="/" element={<App />}>
5556
<Route path="bugs/2152" element={<Bug2152Route />} />
5657
<Route path="bugs/2331" element={<Bug2331Route />} />
58+
<Route path="bugs/2333" element={<Bug2333Route />} />
5759
<Route path="bugs/2393" element={<Bug2393Route />} />
5860
<Route path="bugs/2404" element={<Bug2404Route />} />
5961
<Route path="bugs/2408" element={<Bug2408Route />} />
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import React, { useState } from "react";
2+
import {
3+
GoabButton,
4+
GoabDropdown,
5+
GoabDropdownItem,
6+
GoabText,
7+
} from "@abgov/react-components";
8+
9+
export function Bug2333Route() {
10+
const [colors, setColors] = useState<string[]>(["red", "blue", "green", "yellow", "purple"]);
11+
const [selectedColor, setSelectedColor] = useState<string>("");
12+
13+
const reduceToOne = () => {
14+
setColors(["blue"]);
15+
};
16+
17+
const reduceToTwo = () => {
18+
setColors(["green", "yellow"]);
19+
};
20+
21+
const resetToAll = () => {
22+
setColors(["red", "blue", "green", "yellow", "purple"]);
23+
};
24+
25+
const onChange = (name: string, value: string | string[]) => {
26+
console.log("Dropdown changed:", name, value);
27+
setSelectedColor(Array.isArray(value) ? value[0] : value);
28+
};
29+
30+
return (
31+
<div style={{ width: "1024px", margin: "0 auto", padding: "2rem" }}>
32+
<GoabText size="heading-l" mb="xl">
33+
Bug #2333: Dropdown Reset Test
34+
</GoabText>
35+
36+
<GoabText size="body-m" mb="2xl">
37+
This test demonstrates the dropdown reset issue. When dropdown items are dynamically
38+
removed, the dropdown should properly sync its internal state to reflect the updated
39+
list of options.
40+
</GoabText>
41+
42+
<GoabText size="heading-m" mb="l">
43+
Test Scenario
44+
</GoabText>
45+
46+
<GoabText size="body-s" mb="m">
47+
1. Select a color from the dropdown below
48+
</GoabText>
49+
<GoabText size="body-s" mb="m">
50+
2. Click one of the buttons to reduce the number of available options
51+
</GoabText>
52+
<GoabText size="body-s" mb="m">
53+
3. Open the dropdown again - it should only show the remaining options
54+
</GoabText>
55+
<GoabText size="body-s" mb="2xl">
56+
4. The bug occurred when the filtered options weren't synced after items were destroyed
57+
</GoabText>
58+
59+
<GoabText size="body-m" mb="m">
60+
Currently showing {colors.length} color(s): {colors.join(", ")}
61+
</GoabText>
62+
63+
<GoabText size="body-m" mb="m">
64+
Selected value: {selectedColor || "None"}
65+
</GoabText>
66+
67+
<GoabDropdown
68+
name="favcolor"
69+
placeholder="Select a color"
70+
value={selectedColor}
71+
onChange={onChange}
72+
mb="xl"
73+
>
74+
{colors.map((color) => (
75+
<GoabDropdownItem key={color} label={color} value={color} />
76+
))}
77+
</GoabDropdown>
78+
79+
<div style={{ display: "flex", gap: "1rem", marginBottom: "1rem" }}>
80+
<GoabButton type="secondary" onClick={reduceToOne}>
81+
Reduce to 1 item (blue)
82+
</GoabButton>
83+
<GoabButton type="secondary" onClick={reduceToTwo}>
84+
Reduce to 2 items (green, yellow)
85+
</GoabButton>
86+
<GoabButton type="primary" onClick={resetToAll}>
87+
Reset to all items
88+
</GoabButton>
89+
</div>
90+
91+
<GoabText size="body-s" mb="m" mt="2xl">
92+
<strong>Expected behavior:</strong> After clicking a reduction button, opening the
93+
dropdown should only display the items that remain in the list.
94+
</GoabText>
95+
<GoabText size="body-s">
96+
<strong>Bug behavior (before fix):</strong> The dropdown would still show all original
97+
items even after they were removed, because syncFilteredOptions() wasn't called when
98+
child items were destroyed.
99+
</GoabText>
100+
</div>
101+
);
102+
}

libs/react-components/specs/dropdown.browser.spec.tsx

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -585,4 +585,48 @@ describe("Dropdown", () => {
585585
});
586586
})
587587
})
588+
589+
describe("Dropdown reset", () => {
590+
it("should reduce the number of element displayed within the dropdown", async () => {
591+
let values: string[] = ["red", "blue", "green"]
592+
593+
const Component = () => {
594+
return (
595+
<GoabDropdown name="favcolor" onChange={noop}>
596+
{values.map((item) =>
597+
<GoabDropdownItem label={item} value={item} key={item} />
598+
)}
599+
</GoabDropdown>
600+
);
601+
};
602+
603+
const result = render(<Component />);
604+
const input = result.getByRole("combobox");
605+
const items = result.getByRole("option");
606+
607+
// Initial state
608+
609+
await vi.waitFor(async () => {
610+
const inputEl = input.element() as HTMLInputElement
611+
inputEl.click();
612+
expect(items.elements().length).toBe(values.length);
613+
items.elements().forEach((el, index) => {
614+
expect(el.innerHTML.trim()).toBe(values[index]);
615+
})
616+
});
617+
618+
// Reduce to 1 item
619+
620+
values = ["blue"]; // the previous failure happened with this item, was one of the previous items
621+
result.rerender(<Component />)
622+
623+
await vi.waitFor(async () => {
624+
const inputEl = input.element() as HTMLInputElement
625+
inputEl.click();
626+
const items = result.getByRole("option");
627+
expect(items.elements().length).toBe(1);
628+
expect(items.element().innerHTML.trim()).toBe("blue");
629+
});
630+
})
631+
})
588632
});

libs/web-components/src/components/dropdown/Dropdown.svelte

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,8 +108,6 @@
108108
109109
let _bindTimeoutId: any;
110110
111-
let _mountStatus: "active" | "ready" = "ready";
112-
let _mountTimeoutId: any = undefined;
113111
let _error = toBoolean(error);
114112
let _prevError = _error;
115113
@@ -308,6 +306,7 @@
308306
*/
309307
function onChildDestroyed(detail: DropdownItemDestroyRelayDetail) {
310308
_options = _options.filter((option) => option.value !== detail.value);
309+
syncFilteredOptions();
311310
}
312311
313312
function setSelected() {

libs/web-components/src/components/dropdown/DropdownItem.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@
6262
})
6363
}
6464
65-
onDestroy(async () => {
65+
onDestroy(() => {
6666
relay<DropdownItemDestroyRelayDetail>(
6767
_parentEl,
6868
DropdownItemDestroyMsg,

0 commit comments

Comments
 (0)