diff --git a/apps/prs/angular/src/app/app.component.html b/apps/prs/angular/src/app/app.component.html index e25c5b401..5ab3bb243 100644 --- a/apps/prs/angular/src/app/app.component.html +++ b/apps/prs/angular/src/app/app.component.html @@ -47,6 +47,7 @@ 3072 3118 3156 + 3248 1328 diff --git a/apps/prs/angular/src/app/app.routes.ts b/apps/prs/angular/src/app/app.routes.ts index 52e3c5653..5b304b6a1 100644 --- a/apps/prs/angular/src/app/app.routes.ts +++ b/apps/prs/angular/src/app/app.routes.ts @@ -32,6 +32,8 @@ import { Bug2991Component } from "../routes/bugs/2991/bug2991.component"; import { Bug3072Component } from "../routes/bugs/3072/bug3072.component"; import { Bug3118Component } from "../routes/bugs/3118/bug3118.component"; import { Bug3156Component } from "../routes/bugs/3156/bug3156.component"; +import { Bug3248Component } from "../routes/bugs/3248/bug3248.component"; + import { Feat1328Component } from "../routes/features/feat1328/feat1328.component"; import { Feat1547Component } from "../routes/features/feat1547/feat1547.component"; import { Feat1813Component } from "../routes/features/feat1813/feat1813.component"; @@ -80,6 +82,7 @@ export const appRoutes: Route[] = [ { path: "bugs/3072", component: Bug3072Component }, { path: "bugs/3118", component: Bug3118Component }, { path: "bugs/3156", component: Bug3156Component }, + { path: "bugs/3248", component: Bug3248Component }, { path: "features/1328", component: Feat1328Component }, { path: "features/1547", component: Feat1547Component }, diff --git a/apps/prs/angular/src/routes/bugs/3248/bug3248.component.html b/apps/prs/angular/src/routes/bugs/3248/bug3248.component.html new file mode 100644 index 000000000..06ce57a28 --- /dev/null +++ b/apps/prs/angular/src/routes/bugs/3248/bug3248.component.html @@ -0,0 +1,61 @@ +
+ Bug #2333: Dropdown Reset Test + + + This test demonstrates the dropdown reset issue. When dropdown items are dynamically removed, + the dropdown should properly sync its internal state to reflect the updated list of options. + + + Test Scenario + + 1. Select a color from the dropdown below + + 2. Click one of the buttons to reduce the number of available options + + + 3. Open the dropdown again - it should only show the remaining options + + + 4. The bug occurred when the filtered options weren't synced after items were destroyed + + + + Currently showing {{ colorsCount }} color(s): {{ colorsList }} + + + + Selected value: {{ selectedColor || "None" }} + + + + @for (color of colors; track color) { + + } + + +
+ + Reduce to 1 item (blue) + + + Reduce to 2 items (green, yellow) + + Reset to all items +
+ + + Expected behavior: After clicking a reduction button, opening the dropdown + should only display the items that remain in the list. + + + Bug behavior (before fix): The dropdown would still show all original items + even after they were removed, because syncFilteredOptions() wasn't called when child items + were destroyed. + +
diff --git a/apps/prs/angular/src/routes/bugs/3248/bug3248.component.ts b/apps/prs/angular/src/routes/bugs/3248/bug3248.component.ts new file mode 100644 index 000000000..230f85d58 --- /dev/null +++ b/apps/prs/angular/src/routes/bugs/3248/bug3248.component.ts @@ -0,0 +1,45 @@ +import { Component } from "@angular/core"; +import { CommonModule } from "@angular/common"; +import { + GoabButton, + GoabDropdown, + GoabDropdownItem, + GoabDropdownOnChangeDetail, + GoabText, +} from "@abgov/angular-components"; + +@Component({ + standalone: true, + selector: "abgov-bug3248", + templateUrl: "./bug3248.component.html", + imports: [CommonModule, GoabButton, GoabDropdown, GoabDropdownItem, GoabText], +}) +export class Bug3248Component { + colors = ["red", "blue", "green", "yellow", "purple"]; + selectedColor = ""; + + reduceToOne(): void { + this.colors = ["blue"]; + } + + reduceToTwo(): void { + this.colors = ["green", "yellow"]; + } + + resetToAll(): void { + this.colors = ["red", "blue", "green", "yellow", "purple"]; + } + + onChange(detail: GoabDropdownOnChangeDetail): void { + console.log("Dropdown changed:", detail); + this.selectedColor = Array.isArray(detail.value) ? detail.value[0] : detail.value; + } + + get colorsList(): string { + return this.colors.join(", "); + } + + get colorsCount(): number { + return this.colors.length; + } +} diff --git a/apps/prs/react/src/app/app.tsx b/apps/prs/react/src/app/app.tsx index 30a95426d..614156540 100644 --- a/apps/prs/react/src/app/app.tsx +++ b/apps/prs/react/src/app/app.tsx @@ -52,6 +52,7 @@ export function App() { 2943 2948 3118 + 3248 1547 diff --git a/apps/prs/react/src/main.tsx b/apps/prs/react/src/main.tsx index 7314cc4eb..5cf2b4afd 100644 --- a/apps/prs/react/src/main.tsx +++ b/apps/prs/react/src/main.tsx @@ -32,7 +32,9 @@ import { Bug2892Route } from "./routes/bugs/bug2892"; import { Bug2922Route } from "./routes/bugs/bug2922"; import { Bug2943Route } from "./routes/bugs/bug2943"; import { Bug2948Route } from "./routes/bugs/bug2948"; +import { Bug3248Route } from "./routes/bugs/bug3248"; import Bug3118Route from "./routes/bugs/bug3118"; + import { EverythingRoute } from "./routes/everything"; import { Feat1547Route } from "./routes/features/feat1547"; import { Feat1813Route } from "./routes/features/feat1813"; @@ -84,6 +86,7 @@ root.render( } /> } /> } /> + } /> } /> } /> diff --git a/apps/prs/react/src/routes/bugs/bug3248.tsx b/apps/prs/react/src/routes/bugs/bug3248.tsx new file mode 100644 index 000000000..ae9d04ddf --- /dev/null +++ b/apps/prs/react/src/routes/bugs/bug3248.tsx @@ -0,0 +1,103 @@ +import React, { useState } from "react"; +import { + GoabButton, + GoabDropdown, + GoabDropdownItem, + GoabText, +} from "@abgov/react-components"; +import { GoabDropdownOnChangeDetail } from "@abgov/ui-components-common"; + +export function Bug3248Route() { + const [colors, setColors] = useState(["red", "blue", "green", "yellow", "purple"]); + const [selectedColor, setSelectedColor] = useState(""); + + const reduceToOne = () => { + setColors(["blue"]); + }; + + const reduceToTwo = () => { + setColors(["green", "yellow"]); + }; + + const resetToAll = () => { + setColors(["red", "blue", "green", "yellow", "purple"]); + }; + + const onChange = (detail: GoabDropdownOnChangeDetail) => { + console.log("Dropdown changed:", detail); + setSelectedColor(detail.value || ""); + }; + + return ( +
+ + Bug #2333: Dropdown Reset Test + + + + This test demonstrates the dropdown reset issue. When dropdown items are dynamically + removed, the dropdown should properly sync its internal state to reflect the updated + list of options. + + + + Test Scenario + + + + 1. Select a color from the dropdown below + + + 2. Click one of the buttons to reduce the number of available options + + + 3. Open the dropdown again - it should only show the remaining options + + + 4. The bug occurred when the filtered options weren't synced after items were destroyed + + + + Currently showing {colors.length} color(s): {colors.join(", ")} + + + + Selected value: {selectedColor || "None"} + + + + {colors.map((color) => ( + + ))} + + +
+ + Reduce to 1 item (blue) + + + Reduce to 2 items (green, yellow) + + + Reset to all items + +
+ + + Expected behavior: After clicking a reduction button, opening the + dropdown should only display the items that remain in the list. + + + Bug behavior (before fix): The dropdown would still show all original + items even after they were removed, because syncFilteredOptions() wasn't called when + child items were destroyed. + +
+ ); +} diff --git a/libs/react-components/specs/dropdown.browser.spec.tsx b/libs/react-components/specs/dropdown.browser.spec.tsx index 23483cf82..c7d5b0dd5 100644 --- a/libs/react-components/specs/dropdown.browser.spec.tsx +++ b/libs/react-components/specs/dropdown.browser.spec.tsx @@ -585,4 +585,48 @@ describe("Dropdown", () => { }); }) }) + + describe("Dropdown reset", () => { + it("should reduce the number of element displayed within the dropdown", async () => { + let values: string[] = ["red", "blue", "green"] + + const Component = () => { + return ( + + {values.map((item) => + + )} + + ); + }; + + const result = render(); + const input = result.getByRole("combobox"); + const items = result.getByRole("option"); + + // Initial state + + await vi.waitFor(async () => { + const inputEl = input.element() as HTMLInputElement + inputEl.click(); + expect(items.elements().length).toBe(values.length); + items.elements().forEach((el, index) => { + expect(el.innerHTML.trim()).toBe(values[index]); + }) + }); + + // Reduce to 1 item + + values = ["blue"]; // the previous failure happened with this item, was one of the previous items + result.rerender() + + await vi.waitFor(async () => { + const inputEl = input.element() as HTMLInputElement + inputEl.click(); + const items = result.getByRole("option"); + expect(items.elements().length).toBe(1); + expect(items.element().innerHTML.trim()).toBe("blue"); + }); + }) + }) }); diff --git a/libs/web-components/src/components/dropdown/Dropdown.svelte b/libs/web-components/src/components/dropdown/Dropdown.svelte index 047ddd1cb..3503d5fe9 100644 --- a/libs/web-components/src/components/dropdown/Dropdown.svelte +++ b/libs/web-components/src/components/dropdown/Dropdown.svelte @@ -108,8 +108,6 @@ let _bindTimeoutId: any; - let _mountStatus: "active" | "ready" = "ready"; - let _mountTimeoutId: any = undefined; let _error = toBoolean(error); let _prevError = _error; @@ -308,6 +306,7 @@ */ function onChildDestroyed(detail: DropdownItemDestroyRelayDetail) { _options = _options.filter((option) => option.value !== detail.value); + syncFilteredOptions(); } function setSelected() { diff --git a/libs/web-components/src/components/dropdown/DropdownItem.svelte b/libs/web-components/src/components/dropdown/DropdownItem.svelte index 1a82b0d30..0bec548db 100644 --- a/libs/web-components/src/components/dropdown/DropdownItem.svelte +++ b/libs/web-components/src/components/dropdown/DropdownItem.svelte @@ -62,7 +62,7 @@ }) } - onDestroy(async () => { + onDestroy(() => { relay( _parentEl, DropdownItemDestroyMsg,