From c12b68f80f9b5d1fb886d1fd50aaeff21a27a53a Mon Sep 17 00:00:00 2001 From: Jay-Kunaico <jay.brass@kunaico.com> Date: Tue, 27 Aug 2024 14:12:35 -0400 Subject: [PATCH 01/27] Co-authored-by: Jack Shelton <thejackshelton@users.noreply.github.com> --- .../checkbox/examples/controlled-values.tsx | 42 ++ .../routes/checkbox/examples/controlled.tsx | 24 + .../src/routes/checkbox/examples/hero.tsx | 17 + .../src/routes/checkbox/examples/pizza.tsx | 42 ++ .../examples/test-controlled-list-false.tsx | 49 ++ .../examples/test-controlled-list-falses.tsx | 41 ++ .../examples/test-controlled-list-mixed.tsx | 46 ++ .../examples/test-controlled-list-true.tsx | 48 ++ .../examples/test-controlled-list-trues.tsx | 41 ++ .../examples/test-controlled-list.tsx | 38 ++ .../routes/checkbox/examples/test-default.tsx | 17 + .../routes/checkbox/examples/test-hero.tsx | 19 + .../routes/checkbox/examples/test-list.tsx | 41 ++ .../checkbox/examples/test-props-ids-list.tsx | 40 ++ apps/docs/src/routes/checkbox/index.mdx | 39 ++ .../src/checkbox/checkbox-indicator.tsx | 15 + .../src/checkbox/checkbox.driver.ts | 41 ++ libs/components/src/checkbox/checkbox.test.ts | 440 ++++++++++++++++++ libs/components/src/checkbox/checkbox.tsx | 230 +++++++++ libs/components/src/checkbox/context-id.ts | 13 + libs/components/src/checkbox/index.ts | 3 + .../checklist/checklist-context-wrapper.tsx | 64 +++ .../src/checklist/checklist-indicator.tsx | 29 ++ libs/components/src/checklist/checklist.tsx | 105 +++++ libs/components/src/checklist/context-id.ts | 10 + libs/components/src/checklist/index.ts | 4 + libs/components/src/index.ts | 2 +- 27 files changed, 1499 insertions(+), 1 deletion(-) create mode 100644 apps/docs/src/routes/checkbox/examples/controlled-values.tsx create mode 100644 apps/docs/src/routes/checkbox/examples/controlled.tsx create mode 100644 apps/docs/src/routes/checkbox/examples/hero.tsx create mode 100644 apps/docs/src/routes/checkbox/examples/pizza.tsx create mode 100644 apps/docs/src/routes/checkbox/examples/test-controlled-list-false.tsx create mode 100644 apps/docs/src/routes/checkbox/examples/test-controlled-list-falses.tsx create mode 100644 apps/docs/src/routes/checkbox/examples/test-controlled-list-mixed.tsx create mode 100644 apps/docs/src/routes/checkbox/examples/test-controlled-list-true.tsx create mode 100644 apps/docs/src/routes/checkbox/examples/test-controlled-list-trues.tsx create mode 100644 apps/docs/src/routes/checkbox/examples/test-controlled-list.tsx create mode 100644 apps/docs/src/routes/checkbox/examples/test-default.tsx create mode 100644 apps/docs/src/routes/checkbox/examples/test-hero.tsx create mode 100644 apps/docs/src/routes/checkbox/examples/test-list.tsx create mode 100644 apps/docs/src/routes/checkbox/examples/test-props-ids-list.tsx create mode 100644 apps/docs/src/routes/checkbox/index.mdx create mode 100644 libs/components/src/checkbox/checkbox-indicator.tsx create mode 100644 libs/components/src/checkbox/checkbox.driver.ts create mode 100644 libs/components/src/checkbox/checkbox.test.ts create mode 100644 libs/components/src/checkbox/checkbox.tsx create mode 100644 libs/components/src/checkbox/context-id.ts create mode 100644 libs/components/src/checkbox/index.ts create mode 100644 libs/components/src/checklist/checklist-context-wrapper.tsx create mode 100644 libs/components/src/checklist/checklist-indicator.tsx create mode 100644 libs/components/src/checklist/checklist.tsx create mode 100644 libs/components/src/checklist/context-id.ts create mode 100644 libs/components/src/checklist/index.ts diff --git a/apps/docs/src/routes/checkbox/examples/controlled-values.tsx b/apps/docs/src/routes/checkbox/examples/controlled-values.tsx new file mode 100644 index 00000000..54295a02 --- /dev/null +++ b/apps/docs/src/routes/checkbox/examples/controlled-values.tsx @@ -0,0 +1,42 @@ +import { component$, useSignal, useStyles$ } from '@builder.io/qwik'; +import { Checkbox } from '@kunai-consulting/qwik-components'; +export default component$(() => { + const initialVal1 = false; + const controlledSig1 = useSignal(initialVal1); + const initialVal2 = true; + const controlledSig2 = useSignal(initialVal2); + return ( + <> + <div class="flex gap-8"> + <div class="flex flex-col gap-3"> + <Checkbox.Root + bind:checked={controlledSig1} + id="test" + class="flex items-center gap-3 border-2 border-black p-2 " + > + <Checkbox.Indicator class="flex h-[25px] w-[25px] items-center justify-center bg-slate-600"> + ✅ + </Checkbox.Indicator> + Toggle Value + </Checkbox.Root> + <p>The initial value was: {`${initialVal1}`}</p> + <p>The current value is: {`${controlledSig1.value}`}</p> + </div> + <div class="flex flex-col gap-3"> + <Checkbox.Root + bind:checked={controlledSig2} + id="test" + class="flex items-center gap-3 border-2 border-black p-2 " + > + <Checkbox.Indicator class="flex h-[25px] w-[25px] items-center justify-center bg-slate-600"> + ✅ + </Checkbox.Indicator> + Toggle Value + </Checkbox.Root> + <p>The initial value was: {`${initialVal2}`}</p> + <p>The current value is: {`${controlledSig2.value}`}</p> + </div> + </div> + </> + ); +}); diff --git a/apps/docs/src/routes/checkbox/examples/controlled.tsx b/apps/docs/src/routes/checkbox/examples/controlled.tsx new file mode 100644 index 00000000..2cf64fc3 --- /dev/null +++ b/apps/docs/src/routes/checkbox/examples/controlled.tsx @@ -0,0 +1,24 @@ +import { component$, useSignal } from '@builder.io/qwik'; +import { Checkbox } from '@kunai-consulting/qwik-components'; +export default component$(() => { + const initialVal1 = false; + const controlledSig1 = useSignal(initialVal1); + return ( + <> + <div class="flex gap-8"> + <div class="flex flex-col gap-3"> + <Checkbox.Root + bind:checked={controlledSig1} + id="test" + class="flex items-center gap-3 border-2 border-black p-2 " + > + <Checkbox.Indicator class="flex h-[25px] w-[25px] items-center justify-center bg-slate-600"> + ✅ + </Checkbox.Indicator> + Toggle Value + </Checkbox.Root> + </div> + </div> + </> + ); +}); diff --git a/apps/docs/src/routes/checkbox/examples/hero.tsx b/apps/docs/src/routes/checkbox/examples/hero.tsx new file mode 100644 index 00000000..ec329b03 --- /dev/null +++ b/apps/docs/src/routes/checkbox/examples/hero.tsx @@ -0,0 +1,17 @@ +import { component$, useSignal, useStyles$ } from '@builder.io/qwik'; +import { Checkbox } from '@kunai-consulting/qwik-components'; +export default component$(() => { + return ( + <> + <Checkbox.Root + id="test" + class="flex items-center gap-3 border-2 border-black p-2 " + > + <Checkbox.Indicator class="flex h-[25px] w-[25px] items-center justify-center bg-slate-600"> + ✅ + </Checkbox.Indicator> + I have read the README + </Checkbox.Root> + </> + ); +}); diff --git a/apps/docs/src/routes/checkbox/examples/pizza.tsx b/apps/docs/src/routes/checkbox/examples/pizza.tsx new file mode 100644 index 00000000..6034e745 --- /dev/null +++ b/apps/docs/src/routes/checkbox/examples/pizza.tsx @@ -0,0 +1,42 @@ +import { component$, useSignal, useStyles$ } from '@builder.io/qwik'; +import { Checkbox, Checklist } from '@kunai-consulting/qwik-components'; +const toppingNames = ['hot peppers', 'ham', 'pineaple', 'mushroom']; +const toppingImages = ['🌶️', '🍗', '🍍', '🍄']; +export default component$(() => { + return ( + <> + <h3 id="pizza-toppings">Pizza toppings</h3> + <Checklist.Root + ariaLabeledBy="pizza-toppings" + class="flex flex-col gap-4" + > + <Checkbox.Root + class="flex items-center gap-3 border-2 border-black p-2" + checklist={true} + > + <Checklist.Indicator class="flex h-[25px] w-[25px] items-center justify-center bg-slate-600"> + <div q:slot="true" id="true-img"> + 🍕 + </div> + + <div q:slot="mixed" id="mixed-img"> + ➖ + </div> + </Checklist.Indicator> + Pick all toppings + </Checkbox.Root> + + {toppingNames.map((name, i) => { + return ( + <Checkbox.Root class="ml-8 flex items-center gap-3 border-2 border-black p-2"> + <Checkbox.Indicator class="flex h-[25px] w-[25px] items-center justify-center bg-slate-600"> + {toppingImages[i]} + </Checkbox.Indicator> + {name} + </Checkbox.Root> + ); + })} + </Checklist.Root> + </> + ); +}); diff --git a/apps/docs/src/routes/checkbox/examples/test-controlled-list-false.tsx b/apps/docs/src/routes/checkbox/examples/test-controlled-list-false.tsx new file mode 100644 index 00000000..86a11701 --- /dev/null +++ b/apps/docs/src/routes/checkbox/examples/test-controlled-list-false.tsx @@ -0,0 +1,49 @@ +import { component$, useSignal, useStyles$ } from '@builder.io/qwik'; +import { Checkbox, Checklist } from '@kunai-consulting/qwik-components'; +// TODO: add logic to handle user passed sigs with trues +// this test basically ensures that the sig passed to the checklist controlls trumps all its children +export default component$(() => { + const checklistSig = useSignal(false); + return ( + <> + <h3 id="test123">Pick a cat</h3> + <Checklist.Root class="flex flex-col gap-3" ariaLabeledBy="test123"> + <Checkbox.Root + class="flex items-center gap-3 bg-slate-900 text-white" + checklist={true} + bind:checked={checklistSig} + id="checklist" + > + <Checklist.Indicator class="w-fit"> + <div q:slot="true" id="true-img"> + ✅ + </div> + + <div q:slot="mixed" id="mixed-img"> + ➖ + </div> + </Checklist.Indicator> + <p>Controlls all</p> + </Checkbox.Root> + <Checkbox.Root + id="child-1" + class="flex items-center gap-3 bg-slate-900 pr-2 text-white" + > + <Checkbox.Indicator class="w-fit bg-slate-600">✅</Checkbox.Indicator> + <p>No other stuff is needed here</p> + </Checkbox.Root> + + <Checkbox.Root id="child-2" class="bg-slate-900 text-white"> + <div class="flex items-center gap-3"> + <Checkbox.Indicator class="w-fit bg-slate-600"> + ✅ + </Checkbox.Indicator> + <p>Im a true.tsx</p> + </div> + </Checkbox.Root> + </Checklist.Root> + <p>You signal is: </p> + <p id="signal-to-text">{`${checklistSig.value}`}</p> + </> + ); +}); diff --git a/apps/docs/src/routes/checkbox/examples/test-controlled-list-falses.tsx b/apps/docs/src/routes/checkbox/examples/test-controlled-list-falses.tsx new file mode 100644 index 00000000..d9c407d3 --- /dev/null +++ b/apps/docs/src/routes/checkbox/examples/test-controlled-list-falses.tsx @@ -0,0 +1,41 @@ +import { component$, useSignal, useStyles$ } from '@builder.io/qwik'; +import { Checkbox, Checklist } from '@kunai-consulting/qwik-components'; +export default component$(() => { + const firstUserSig = useSignal(false); + const secondUserSig = useSignal(false); + return ( + <> + <h3 id="test123">Pick a cat</h3> + <Checklist.Root class="flex flex-col gap-3" ariaLabeledBy="test123"> + <Checkbox.Root + class="flex items-center gap-3 bg-slate-900 p-2 text-white" + checklist={true} + > + <Checkbox.Indicator class=" flex w-[80px] justify-center bg-white p-3"> + ✅ + </Checkbox.Indicator> + <p>Controlls all</p> + </Checkbox.Root> + <Checkbox.Root + bind:checked={firstUserSig} + class="flex items-center gap-3 bg-slate-900 pr-2 text-white" + > + <Checkbox.Indicator class="w-fit bg-slate-600">✅</Checkbox.Indicator> + <p>No other stuff is needed here</p> + </Checkbox.Root> + + <Checkbox.Root + bind:checked={secondUserSig} + class="bg-slate-900 text-white" + > + <div class="flex items-center gap-3"> + <Checkbox.Indicator class="w-fit bg-slate-600"> + ✅ + </Checkbox.Indicator> + <p>No other stuff is needed here</p> + </div> + </Checkbox.Root> + </Checklist.Root> + </> + ); +}); diff --git a/apps/docs/src/routes/checkbox/examples/test-controlled-list-mixed.tsx b/apps/docs/src/routes/checkbox/examples/test-controlled-list-mixed.tsx new file mode 100644 index 00000000..e122796a --- /dev/null +++ b/apps/docs/src/routes/checkbox/examples/test-controlled-list-mixed.tsx @@ -0,0 +1,46 @@ +import { component$, useSignal, useStyles$ } from '@builder.io/qwik'; +import { Checkbox, Checklist } from '@kunai-consulting/qwik-components'; +export default component$(() => { + const firstUserSig = useSignal(false); + const secondUserSig = useSignal(true); + return ( + <> + <h3 id="test123">Pick a cat</h3> + <Checklist.Root class="flex flex-col gap-3" ariaLabeledBy="test123"> + <Checkbox.Root + class="flex items-center gap-3 bg-slate-900 p-2 text-white" + checklist={true} + > + <Checklist.Indicator class="w-fit"> + <div q:slot="true" id="true-img"> + ✅ + </div> + <div q:slot="mixed" id="mixed-img"> + ➖ + </div> + </Checklist.Indicator> + <p>Controlls all</p> + </Checkbox.Root> + <Checkbox.Root + bind:checked={firstUserSig} + class="flex items-center gap-3 bg-slate-900 pr-2 text-white" + > + <Checkbox.Indicator class="w-fit bg-slate-600">✅</Checkbox.Indicator> + <p>No other stuff is needed here</p> + </Checkbox.Root> + + <Checkbox.Root + bind:checked={secondUserSig} + class="bg-slate-900 text-white" + > + <div class="flex items-center gap-3"> + <Checkbox.Indicator class="w-fit bg-slate-600"> + ✅ + </Checkbox.Indicator> + <p>No other stuff is needed here</p> + </div> + </Checkbox.Root> + </Checklist.Root> + </> + ); +}); diff --git a/apps/docs/src/routes/checkbox/examples/test-controlled-list-true.tsx b/apps/docs/src/routes/checkbox/examples/test-controlled-list-true.tsx new file mode 100644 index 00000000..1b794a85 --- /dev/null +++ b/apps/docs/src/routes/checkbox/examples/test-controlled-list-true.tsx @@ -0,0 +1,48 @@ +import { component$, useSignal, useStyles$ } from '@builder.io/qwik'; +import { Checkbox, Checklist } from '@kunai-consulting/qwik-components'; +// this test basically ensures that the sig passed to the checklist controlls trumps all its children +export default component$(() => { + const checklistSig = useSignal(true); + return ( + <> + <h3 id="test123">Pick a cat</h3> + <Checklist.Root class="flex flex-col gap-3" ariaLabeledBy="test123"> + <Checkbox.Root + class="flex items-center gap-3 bg-slate-900 text-white" + checklist={true} + bind:checked={checklistSig} + id="checklist" + > + <Checklist.Indicator class="w-fit"> + <div q:slot="true" id="true-img"> + ✅ + </div> + + <div q:slot="mixed" id="mixed-img"> + ➖ + </div> + </Checklist.Indicator> + <p>Controlls all</p> + </Checkbox.Root> + <Checkbox.Root + id="child-1" + class="flex items-center gap-3 bg-slate-900 pr-2 text-white" + > + <Checkbox.Indicator class="w-fit bg-slate-600">✅</Checkbox.Indicator> + <p>No other stuff is needed here</p> + </Checkbox.Root> + + <Checkbox.Root id="child-2" class="bg-slate-900 text-white"> + <div class="flex items-center gap-3"> + <Checkbox.Indicator class="w-fit bg-slate-600"> + ✅ + </Checkbox.Indicator> + <p>Im a true.tsx</p> + </div> + </Checkbox.Root> + </Checklist.Root> + <p>You signal is: </p> + <p id="signal-to-text">{`${checklistSig.value}`}</p> + </> + ); +}); diff --git a/apps/docs/src/routes/checkbox/examples/test-controlled-list-trues.tsx b/apps/docs/src/routes/checkbox/examples/test-controlled-list-trues.tsx new file mode 100644 index 00000000..18951ff4 --- /dev/null +++ b/apps/docs/src/routes/checkbox/examples/test-controlled-list-trues.tsx @@ -0,0 +1,41 @@ +import { component$, useSignal, useStyles$ } from '@builder.io/qwik'; +import { Checkbox, Checklist } from '@kunai-consulting/qwik-components'; +export default component$(() => { + const firstUserSig = useSignal(true); + const secondUserSig = useSignal(true); + return ( + <> + <h3 id="test123">Pick a cat</h3> + <Checklist.Root class="flex flex-col gap-3" ariaLabeledBy="test123"> + <Checkbox.Root + class="flex items-center gap-3 bg-slate-900 p-2 text-white" + checklist={true} + > + <Checkbox.Indicator class=" flex w-[80px] justify-center bg-white p-3"> + ✅ + </Checkbox.Indicator> + <p>Controlls all</p> + </Checkbox.Root> + <Checkbox.Root + bind:checked={firstUserSig} + class="flex items-center gap-3 bg-slate-900 pr-2 text-white" + > + <Checkbox.Indicator class="w-fit bg-slate-600">✅</Checkbox.Indicator> + <p>No other stuff is needed here</p> + </Checkbox.Root> + + <Checkbox.Root + bind:checked={secondUserSig} + class="bg-slate-900 text-white" + > + <div class="flex items-center gap-3"> + <Checkbox.Indicator class="w-fit bg-slate-600"> + ✅ + </Checkbox.Indicator> + <p>No other stuff is needed here</p> + </div> + </Checkbox.Root> + </Checklist.Root> + </> + ); +}); diff --git a/apps/docs/src/routes/checkbox/examples/test-controlled-list.tsx b/apps/docs/src/routes/checkbox/examples/test-controlled-list.tsx new file mode 100644 index 00000000..da68fa41 --- /dev/null +++ b/apps/docs/src/routes/checkbox/examples/test-controlled-list.tsx @@ -0,0 +1,38 @@ +import { component$, useSignal, useStyles$ } from '@builder.io/qwik'; +import { Checkbox, Checklist } from '@kunai-consulting/qwik-components'; +export default component$(() => { + const firstUserSig = useSignal(true); + const secondUserSig = useSignal(true); + return ( + <> + <h3 id="test123">Pick a cat</h3> + <Checklist.Root class="flex flex-col gap-3" ariaLabeledBy="test123"> + <Checkbox.Root + class="flex items-center gap-3 bg-slate-900 p-2 text-white" + checklist={true} + > + <Checkbox.Indicator class=" flex w-[80px] justify-center bg-white p-3"> + ✅ + </Checkbox.Indicator> + <p>Controlls all</p> + </Checkbox.Root> + <Checkbox.Root + bind:checked={firstUserSig} + class="flex items-center gap-3 bg-slate-900 pr-2 text-white" + > + <Checkbox.Indicator class="w-fit bg-slate-600">✅</Checkbox.Indicator> + <p>No other stuff is needed here</p> + </Checkbox.Root> + + <Checkbox.Root class="bg-slate-900 text-white"> + <div class="flex items-center gap-3"> + <Checkbox.Indicator class="w-fit bg-slate-600"> + ✅ + </Checkbox.Indicator> + <p>No other stuff is needed here</p> + </div> + </Checkbox.Root> + </Checklist.Root> + </> + ); +}); diff --git a/apps/docs/src/routes/checkbox/examples/test-default.tsx b/apps/docs/src/routes/checkbox/examples/test-default.tsx new file mode 100644 index 00000000..16df66dd --- /dev/null +++ b/apps/docs/src/routes/checkbox/examples/test-default.tsx @@ -0,0 +1,17 @@ +import { component$, useSignal, useStyles$ } from '@builder.io/qwik'; +import { Checkbox, Checklist } from '@kunai-consulting/qwik-components'; +export default component$(() => { + return ( + <> + <p>I'm the default checkbox!!!</p> + <Checkbox.Root class=" text-white"> + <div class="flex items-center gap-3"> + <Checkbox.Indicator class="w-fit bg-slate-600"> + <p id="indicator">✅</p> + </Checkbox.Indicator> + <p>No other stuff is needed here</p> + </div> + </Checkbox.Root> + </> + ); +}); diff --git a/apps/docs/src/routes/checkbox/examples/test-hero.tsx b/apps/docs/src/routes/checkbox/examples/test-hero.tsx new file mode 100644 index 00000000..970a188a --- /dev/null +++ b/apps/docs/src/routes/checkbox/examples/test-hero.tsx @@ -0,0 +1,19 @@ +import { component$, useSignal, useStyles$ } from '@builder.io/qwik'; +import { Checkbox } from '@kunai-consulting/qwik-components'; + +export default component$(() => { + const userSig = useSignal(true); + return ( + <> + <Checkbox.Root class="bg-slate-900 text-white" bind:checked={userSig}> + <div class="flex items-center gap-3"> + <Checkbox.Indicator class="w-fit bg-slate-600"> + <p id="indicator">✅</p> + </Checkbox.Indicator> + <p>No other stuff is needed here</p> + </div> + </Checkbox.Root> + <div>{`${userSig.value}`}</div> + </> + ); +}); diff --git a/apps/docs/src/routes/checkbox/examples/test-list.tsx b/apps/docs/src/routes/checkbox/examples/test-list.tsx new file mode 100644 index 00000000..081478e9 --- /dev/null +++ b/apps/docs/src/routes/checkbox/examples/test-list.tsx @@ -0,0 +1,41 @@ +import { component$ } from '@builder.io/qwik'; +import { Checkbox, Checklist } from '@kunai-consulting/qwik-components'; +export default component$(() => { + return ( + <> + <h3 id="test123">Pick a cat</h3> + <Checklist.Root class="flex flex-col gap-3" ariaLabeledBy="test123"> + <Checkbox.Root + class="flex items-center gap-3 bg-slate-900 p-2 text-white" + checklist={true} + > + <Checklist.Indicator class="w-fit bg-black"> + <div q:slot="true" id="true-img"> + ✅ + </div> + + <div q:slot="mixed" id="mixed-img"> + ➖ + </div> + </Checklist.Indicator> + + <p>Get All</p> + </Checkbox.Root> + + <Checkbox.Root class="flex items-center gap-3 bg-slate-900 pr-2 text-white"> + <Checkbox.Indicator class="w-fit bg-slate-600">✅</Checkbox.Indicator> + <p>Cat One</p> + </Checkbox.Root> + + <Checkbox.Root class="bg-slate-900 text-white"> + <div class="flex items-center gap-3"> + <Checkbox.Indicator class="w-fit bg-slate-600"> + ✅ + </Checkbox.Indicator> + <p>Cat Two</p> + </div> + </Checkbox.Root> + </Checklist.Root> + </> + ); +}); diff --git a/apps/docs/src/routes/checkbox/examples/test-props-ids-list.tsx b/apps/docs/src/routes/checkbox/examples/test-props-ids-list.tsx new file mode 100644 index 00000000..c68ac176 --- /dev/null +++ b/apps/docs/src/routes/checkbox/examples/test-props-ids-list.tsx @@ -0,0 +1,40 @@ +import { component$, useSignal, useStyles$ } from '@builder.io/qwik'; +import { Checkbox, Checklist } from '@kunai-consulting/qwik-components'; +export default component$(() => { + const firstUserSig = useSignal(true); + const secondUserSig = useSignal(true); + return ( + <> + <h3 id="test123">Pick a cat</h3> + <Checklist.Root class="flex flex-col gap-3" ariaLabeledBy="test123"> + <Checkbox.Root + class="flex items-center gap-3 bg-slate-900 p-2 text-white" + checklist={true} + id="checklist" + > + <Checkbox.Indicator class=" flex w-[80px] justify-center bg-white p-3"> + ✅ + </Checkbox.Indicator> + <p>Controlls all</p> + </Checkbox.Root> + <Checkbox.Root + id="child-1" + bind:checked={firstUserSig} + class="flex items-center gap-3 bg-slate-900 pr-2 text-white" + > + <Checkbox.Indicator class="w-fit bg-slate-600">✅</Checkbox.Indicator> + <p>First Child</p> + </Checkbox.Root> + + <Checkbox.Root id="child-2" class="bg-slate-900 text-white"> + <div class="flex items-center gap-3"> + <Checkbox.Indicator class="w-fit bg-slate-600"> + ✅ + </Checkbox.Indicator> + <p>Second child</p> + </div> + </Checkbox.Root> + </Checklist.Root> + </> + ); +}); diff --git a/apps/docs/src/routes/checkbox/index.mdx b/apps/docs/src/routes/checkbox/index.mdx new file mode 100644 index 00000000..5be10ee0 --- /dev/null +++ b/apps/docs/src/routes/checkbox/index.mdx @@ -0,0 +1,39 @@ +--- +title: Qwik UI | Checkbox +--- + +# Checkbox (Two-State) + +Allows users to visually select an option by checking it. + +<Showcase name="hero" /> + +## Anatomy + +## Examples + +### Controlled Checkbox + +To add reactive state, use the `bind:checked` prop on the `<Checkbox.Root />` component. + +<Showcase name="controlled" /> + +> Note that the initial value of the signal will affect the initial value of the checkbox. + +In the example below, the left checkbox starts as **false**, while the right checkbox starts as **true**. + +<Showcase name="controlled-values" /> + +## Customization & Caveats + +You can apply CSS classes to any part of the component. Any valid HTML can be used as an icon, but children of the Checkbox Indicator are removed from the accessibility tree to prevent them from being added to the Checkbox Label. + +### Automatic Labeling + +The Checkbox element is a div with the role of checkbox, so any text inside the Checkbox Root is interpreted as the checkbox label. + +Text tags lose their semantic meaning. For example, an h1 tag is treated the same as a p tag. See [this MDN section](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/checkbox_role#all_descendants_are_presentational) for more details. + +## API + +### Checkbox.Root diff --git a/libs/components/src/checkbox/checkbox-indicator.tsx b/libs/components/src/checkbox/checkbox-indicator.tsx new file mode 100644 index 00000000..87ac5ba8 --- /dev/null +++ b/libs/components/src/checkbox/checkbox-indicator.tsx @@ -0,0 +1,15 @@ +import { component$, useContext, type PropsOf, Slot } from '@builder.io/qwik'; +import { CheckboxContext } from './context-id'; + +export type CheckboxIndicatorProps = PropsOf<'div'>; + +export const CheckboxIndicator = component$<CheckboxIndicatorProps>((props) => { + const checkSig = useContext(CheckboxContext); + return ( + <div {...props}> + <div hidden={checkSig.value} aria-hidden="true"> + <Slot /> + </div> + </div> + ); +}); diff --git a/libs/components/src/checkbox/checkbox.driver.ts b/libs/components/src/checkbox/checkbox.driver.ts new file mode 100644 index 00000000..bfb4712d --- /dev/null +++ b/libs/components/src/checkbox/checkbox.driver.ts @@ -0,0 +1,41 @@ +import type { Locator, Page } from '@playwright/test'; +export type DriverLocator = Locator | Page; + +export function createTestDriver<T extends DriverLocator>(rootLocator: T) { + const getRoot = () => { + return rootLocator; + }; + + const getIcon = () => { + return getRoot().locator('#indicator'); + }; + const getCheckList = () => { + return getRoot().getByRole('group'); + }; + const getChecklistUL = () => { + // note: filter method is always relative to the original locator not document root despite using root + const ul = getCheckList().filter({ has: rootLocator.locator('css=ul') }); + return ul; + }; + const getChecklistLIs = () => { + const li = getChecklistUL().filter({ has: rootLocator.locator('css=li') }); + return li; + }; + const getCheckbox = () => { + return getRoot().getByRole('checkbox'); + }; + const getTriCheckbox = () => { + return getRoot().locator('css=[aria-controls]'); + }; + return { + ...rootLocator, + locator: rootLocator, + getRoot, + getIcon, + getCheckList, + getCheckbox, + getChecklistUL, + getChecklistLIs, + getTriCheckbox, + }; +} diff --git a/libs/components/src/checkbox/checkbox.test.ts b/libs/components/src/checkbox/checkbox.test.ts new file mode 100644 index 00000000..75b80890 --- /dev/null +++ b/libs/components/src/checkbox/checkbox.test.ts @@ -0,0 +1,440 @@ +import { expect, test, type Page } from '@playwright/test'; +import { createTestDriver } from './checkbox.driver'; +import { getTriBool } from '../checklist/checklist-context-wrapper'; +async function setup(page: Page, exampleName: string) { + await page.goto(`/headless/checkbox/${exampleName}`); + + const driver = createTestDriver(page); + + const { + getCheckbox, + getTriCheckbox, + getRoot, + getIcon, + getCheckList, + getChecklistUL, + getChecklistLIs, + } = driver; + + return { + driver, + getCheckbox, + getIcon, + getCheckList, + getChecklistUL, + getChecklistLIs, + getTriCheckbox, + getRoot, + }; +} + +test.describe('checklist', () => { + test(`GIVEN a mixed checklist + WHEN the checklist renders + IT should render the mixed img + AND not the true img`, async ({ page }) => { + const exampleName = 'test-controlled-list-mixed'; + await setup(page, exampleName); + await expect(page.locator('#mixed-img')).toBeVisible(); + await expect(page.locator('#true-img')).toBeHidden(); + }); + + test(`GIVEN an all-checked checklist + WHEN the checklist renders + IT should render the true img + AND not the mixed img`, async ({ page }) => { + const exampleName = 'test-controlled-list-true'; + await setup(page, exampleName); + await expect(page.locator('#true-img')).toBeVisible(); + await expect(page.locator('#mixed-img')).toBeHidden(); + }); + + test(`GIVEN an all-unchecked checklist + WHEN the checklist renders + IT should render the true img and the mixed img`, async ({ page }) => { + const exampleName = 'test-controlled-list-false'; + await setup(page, exampleName); + await expect(page.locator('#true-img')).toBeHidden(); + await expect(page.locator('#mixed-img')).toBeHidden(); + }); + + test(`GIVEN a checklist with checkboxes + WHEN the elements render + THEN the checklist should be a <ul> with <li>s of checkboxes, all wrapped around a div with a role and aria-labeledby attributes`, async ({ + page, + }) => { + const { getCheckList, getChecklistUL, getChecklistLIs } = await setup( + page, + 'test-list', + ); + await expect(getCheckList()).toBeVisible(); + await expect(getCheckList()).toHaveAttribute('aria-labelledby', 'test123'); + await expect(getChecklistUL()).toBeVisible(); + await expect(getChecklistLIs()).toBeVisible(); + }); + + test(`GIVEN a tri boolean function + WHEN it recieves an array of booleans + IT should return the correct tri bool`, async () => { + const indeterminateArr = [true, true, false]; + const trueArr = [true, true, true]; + const falseArr = [false, false, false]; + const emptyArr: boolean[] = []; + expect(getTriBool(indeterminateArr)).toBe('indeterminate'); + expect(getTriBool(trueArr)).toBe(true); + expect(getTriBool(falseArr)).toBe(false); + expect(getTriBool(emptyArr)).toBe('indeterminate'); + }); + + test(`GIVEN checklist with all unchecked checkboxes + WHEN it renders + the chekbox with aria-controls should have aria-checked false`, async ({ + page, + }) => { + const exampleName = 'test-list'; + const { getTriCheckbox } = await setup(page, exampleName); + await expect(getTriCheckbox()).toBeVisible(); + await expect(getTriCheckbox()).toHaveAttribute('aria-checked', 'false'); + }); + test(`GIVEN checklist with all unchecked checkboxes + WHEN the first checkbox is checked + the chekbox with aria-controls should have aria-checked mixed`, async ({ + page, + }) => { + const exampleName = 'test-list'; + const { getTriCheckbox, getCheckbox } = await setup(page, exampleName); + await expect(getTriCheckbox()).toBeVisible(); + await getCheckbox().nth(1).press(' '); + await expect(getTriCheckbox()).toHaveAttribute('aria-checked', 'mixed'); + }); + + test(`GIVEN checklist with all unchecked checkboxes + WHEN all checkboxes are checked with space + the tri state checkbox should have aria-checked true`, async ({ page }) => { + const exampleName = 'test-list'; + const { getTriCheckbox, getCheckbox } = await setup(page, exampleName); + await expect(getTriCheckbox()).toBeVisible(); + await getCheckbox().nth(1).press(' '); + await getCheckbox().nth(2).press(' '); + await expect(getTriCheckbox()).toHaveAttribute('aria-checked', 'true'); + }); + + test(`GIVEN checklist with all unchecked checkboxes + WHEN the checklist's checkbox is checked with space + THEN all chekboxes should have aria-checked true`, async ({ page }) => { + const exampleName = 'test-list'; + const { getTriCheckbox, getCheckbox } = await setup(page, exampleName); + await expect(getTriCheckbox()).toBeVisible(); + await getTriCheckbox().press(' '); + await expect(getTriCheckbox()).toHaveAttribute('aria-checked', 'true'); + await expect(getCheckbox().nth(1)).toHaveAttribute('aria-checked', 'true'); + await expect(getCheckbox().nth(2)).toHaveAttribute('aria-checked', 'true'); + }); + + // TODO: reme two part of test by adding new test file + test(`GIVEN checklist with all unchecked checkboxes + WHEN the checklist's checkbox is checked twice using space + THEN all chekboxes should go from aria-checked true to aria-checkded false`, async ({ + page, + }) => { + const exampleName = 'test-list'; + const { getTriCheckbox, getCheckbox } = await setup(page, exampleName); + await expect(getTriCheckbox()).toBeVisible(); + await getTriCheckbox().press(' '); + await expect(getTriCheckbox()).toHaveAttribute('aria-checked', 'true'); + await expect(getCheckbox().nth(1)).toHaveAttribute('aria-checked', 'true'); + await expect(getCheckbox().nth(2)).toHaveAttribute('aria-checked', 'true'); + await getTriCheckbox().press(' '); + await expect(getTriCheckbox()).toHaveAttribute('aria-checked', 'false'); + await expect(getCheckbox().nth(1)).toHaveAttribute('aria-checked', 'false'); + await expect(getCheckbox().nth(2)).toHaveAttribute('aria-checked', 'false'); + }); + test(`GIVEN checklist with checkboxes + WHEN the values of aria-controls are search + IT should always return a valid, non-duplicate, checkboxes`, async ({ page }) => { + const { getTriCheckbox } = await setup(page, 'test-list'); + await expect(getTriCheckbox()).toHaveAttribute('aria-controls'); + const magic = await getTriCheckbox().getAttribute('aria-controls'); + expect(magic).not.toBe(null); + if (magic === null) { + throw new Error( + 'no mixed checkbox found. Was the driver or test template changed?', + ); + } + const idArr = magic.split(' '); + expect(isUniqArr(idArr)).toBe(true); + for (let index = 0; index < idArr.length; index++) { + const elementId = idArr[index]; + const PosCheckbox = page.locator(`#${elementId}`); + await expect(PosCheckbox).toBeVisible(); + const role = await PosCheckbox.getAttribute('role'); + expect(role).toBe('checkbox'); + } + }); + + test(`GIVEN a controlled checklist with one default checkbox and a controlled checkbox of true + WHEN it renders + IT should have aria-checked mixed`, async ({ page }) => { + const exampleName = 'test-controlled-list'; + const { getTriCheckbox } = await setup(page, exampleName); + await expect(getTriCheckbox()).toHaveAttribute('aria-checked', 'mixed'); + }); + + test(`GIVEN a controlled checklist with two true checkboxes + WHEN it renders + IT should have aria-checked true`, async ({ page }) => { + const exampleName = 'test-controlled-list-trues'; + const { getTriCheckbox } = await setup(page, exampleName); + await expect(getTriCheckbox()).toHaveAttribute('aria-checked', 'true'); + }); + test(`GIVEN a controlled checklist with two false checkboxes + WHEN it renders + IT should have aria-checked true`, async ({ page }) => { + const exampleName = 'test-controlled-list-falses'; + const { getTriCheckbox } = await setup(page, exampleName); + await expect(getTriCheckbox()).toHaveAttribute('aria-checked', 'false'); + }); + + test(`GIVEN a controlled checklist with mixed checkboxes + WHEN it renders + IT should have aria-checked mixed`, async ({ page }) => { + const exampleName = 'test-controlled-list-mixed'; + const { getTriCheckbox } = await setup(page, exampleName); + await expect(getTriCheckbox()).toHaveAttribute('aria-checked', 'mixed'); + }); + + test(`GIVEN a controlled checklist with a checklist signal of true and default checkboxes as children + WHEN the checklist renders + IT shoud have aria-checked true`, async ({ page }) => { + const exampleName = 'test-controlled-list-true'; + const { getTriCheckbox } = await setup(page, exampleName); + await expect(getTriCheckbox()).toHaveAttribute('aria-checked', 'true'); + }); + test(`GIVEN a controlled checklist with a checklist signal of true and default checkboxes as children + WHEN the checklist renders + ALL its child checkboxes should have aria-checked true`, async ({ page }) => { + const exampleName = 'test-controlled-list-true'; + const { getCheckbox } = await setup(page, exampleName); + const allCheckboxes = await getCheckbox().all(); + for (let index = 0; index < allCheckboxes.length; index++) { + const checkbox = allCheckboxes[index]; + await expect(checkbox).toHaveAttribute('aria-checked', 'true'); + } + }); + + // TODO: change api to not use indeterminate and used mixed instead + test(`GIVEN a controlled checklist with a checklist signal of true and default checkboxes as children + WHEN a child checkbox is unchecked + THEN the checklist signal should have aria-checked mixed`, async ({ page }) => { + const exampleName = 'test-controlled-list-true'; + const { getTriCheckbox } = await setup(page, exampleName); + const firstCheckbox = page.locator('#child-1'); + await firstCheckbox.press(' '); + await expect(getTriCheckbox()).toHaveAttribute('aria-checked', 'mixed'); + }); + + test(`GIVEN a controlled checklist with a checklist signal of true and default checkboxes as children + WHEN all child checkbox are unchecked + THEN the checklist signal should have aria-checked false`, async ({ page }) => { + const exampleName = 'test-controlled-list-true'; + const { getTriCheckbox } = await setup(page, exampleName); + await page.locator('#child-1').press(' '); + await page.locator('#child-2').press(' '); + await expect(getTriCheckbox()).toHaveAttribute('aria-checked', 'false'); + }); + test(`GIVEN a controlled checklist with every checkbox having a defined ID + WHEN it renders + ALL IDs should be present/rendered`, async ({ page }) => { + await setup(page, 'test-props-ids-list'); + const hardCodedIds = ['checklist', 'child-1', 'child-2']; + for (let index = 0; index < hardCodedIds.length; index++) { + const id = hardCodedIds[index]; + await expect(page.locator(`#${id}`)).toBeVisible(); + } + }); + + test(`GIVEN a controlled checklist with every checkbox having a defined ID + WHEN it renders + THEN all IDs should be present in the aria-controls`, async ({ page }) => { + const { getTriCheckbox } = await setup(page, 'test-props-ids-list'); + const hardChildren = ['child-1', 'child-2']; + const magic = await getTriCheckbox().getAttribute('aria-controls'); + const twin = magic?.split(' '); + expect(hardChildren).toStrictEqual(twin); + }); + + test(`GIVEN checklist with all unchecked checkboxes + WHEN the first child checkbox is clicked + the chekbox with aria-controls should have aria-checked mixed`, async ({ + page, + }) => { + const exampleName = 'test-list'; + const { getTriCheckbox, getCheckbox } = await setup(page, exampleName); + await expect(getTriCheckbox()).toBeVisible(); + await getCheckbox().nth(1).click(); + await expect(getTriCheckbox()).toHaveAttribute('aria-checked', 'mixed'); + }); + + test(`GIVEN checklist with all unchecked checkboxes + WHEN all checkboxes are checked using click + THEN the checkbox with aria-controls should have aria-checked true`, async ({ + page, + }) => { + const exampleName = 'test-list'; + const { getTriCheckbox, getCheckbox } = await setup(page, exampleName); + await expect(getTriCheckbox()).toBeVisible(); + await getCheckbox().nth(1).click(); + await getCheckbox().nth(2).click(); + await expect(getTriCheckbox()).toHaveAttribute('aria-checked', 'true'); + }); + + test(`GIVEN checklist with all unchecked checkboxes + WHEN the checklist's checkbox is checked by clicking + THEN all checkboxes should have aria-checked true`, async ({ page }) => { + const exampleName = 'test-list'; + const { getTriCheckbox, getCheckbox } = await setup(page, exampleName); + await expect(getTriCheckbox()).toBeVisible(); + await getTriCheckbox().click(); + await expect(getTriCheckbox()).toHaveAttribute('aria-checked', 'true'); + await expect(getCheckbox().nth(1)).toHaveAttribute('aria-checked', 'true'); + await expect(getCheckbox().nth(2)).toHaveAttribute('aria-checked', 'true'); + }); + + // TODO: reme two part of test by adding new test file + test(`GIVEN checklist with all unchecked checkboxes + WHEN the checklist's checkbox is checked twice using click + THEN all chekboxes should go from aria-checked true to aria-checkded false`, async ({ + page, + }) => { + const exampleName = 'test-list'; + const { getTriCheckbox, getCheckbox } = await setup(page, exampleName); + await expect(getTriCheckbox()).toBeVisible(); + await getTriCheckbox().click(); + await expect(getTriCheckbox()).toHaveAttribute('aria-checked', 'true'); + await expect(getCheckbox().nth(1)).toHaveAttribute('aria-checked', 'true'); + await expect(getCheckbox().nth(2)).toHaveAttribute('aria-checked', 'true'); + await getTriCheckbox().click(); + await expect(getTriCheckbox()).toHaveAttribute('aria-checked', 'false'); + await expect(getCheckbox().nth(1)).toHaveAttribute('aria-checked', 'false'); + await expect(getCheckbox().nth(2)).toHaveAttribute('aria-checked', 'false'); + }); + + // test(`GIVEN checklist with checkboxes + // WHEN the values of aria-controls are search + // IT should always return a valid, non-duplicate, checkboxes`, async ({ page }) => { + // const { getTriCheckbox } = await setup(page, 'test-list'); + // await expect(getTriCheckbox()).toHaveAttribute('aria-controls'); + // const magic = await getTriCheckbox().getAttribute('aria-controls'); + // expect(magic).not.toBe(null); + // const idArr = magic!.split(' '); + // expect(isUniqArr(idArr)).toBe(true); + // for (let index = 0; index < idArr.length; index++) { + // const elementId = idArr[index]; + // const PosCheckbox = page.locator(`#${elementId}`); + // await expect(PosCheckbox).toBeVisible(); + // const role = await PosCheckbox.getAttribute('role'); + // expect(role).toBe('checkbox'); + // } + // }); + + test(`GIVEN a controlled checklist with a checklist signal of true and default checkboxes as children + WHEN a child checkbox is unchecked + THEN the checklist signal should have aria-checked mixed`, async ({ page }) => { + const exampleName = 'test-controlled-list-true'; + const { getTriCheckbox } = await setup(page, exampleName); + const firstCheckbox = page.locator('#child-1'); + await firstCheckbox.click(); + await expect(getTriCheckbox()).toHaveAttribute('aria-checked', 'mixed'); + }); + + test(`GIVEN a controlled checklist with a checklist signal of true and default checkboxes as children + WHEN all child checkbox are unchecked + THEN the checklist signal should have aria-checked false`, async ({ page }) => { + const exampleName = 'test-controlled-list-true'; + const { getTriCheckbox } = await setup(page, exampleName); + await page.locator('#child-1').click(); + await page.locator('#child-2').click(); + await expect(getTriCheckbox()).toHaveAttribute('aria-checked', 'false'); + }); +}); +test.describe('checkbox', () => { + test(`GIVEN a checkbox with a user sig value of true + WHEN the checkbox renders + IT should have aria-checked as true`, async ({ page }) => { + const exampleName = 'test-hero'; + const { getCheckbox } = await setup(page, exampleName); + await expect(getCheckbox()).toBeVisible(); + await expect(getCheckbox()).toHaveAttribute('aria-checked', 'true'); + }), + test(`GIVEN a checkbox with a user sig value of true +WHEN the checkbox is focused and the spacebar is pressed +IT should have aria-checked as false`, async ({ page }) => { + const exampleName = 'test-hero'; + const { getCheckbox } = await setup(page, exampleName); + await expect(getCheckbox()).toBeVisible(); + await getCheckbox().focus(); + await getCheckbox().press(' '); + await expect(getCheckbox()).toHaveAttribute('aria-checked', 'false'); + }); + + test(`GIVEN a checkbox with a user sig value of true + WHEN the checkbox is focused and the spacebar is pressed + IT should have its icon hidden`, async ({ page }) => { + const exampleName = 'test-hero'; + const { getCheckbox, getIcon } = await setup(page, exampleName); + await expect(getIcon()).toBeVisible(); + await getCheckbox().focus(); + await getCheckbox().press(' '); + await expect(getCheckbox()).toHaveAttribute('aria-checked', 'false'); + await expect(getIcon()).toBeHidden(); + }); + test(`GIVEN a default checkbox with a default sig value of false + WHEN the checkbox is focused and the spacebar is pressed + IT should have its icon visible`, async ({ page }) => { + const exampleName = 'test-default'; + const { getCheckbox, getIcon } = await setup(page, exampleName); + await expect(getIcon()).toBeHidden(); + await getCheckbox().focus(); + await getCheckbox().press(' '); + await expect(getCheckbox()).toHaveAttribute('aria-checked', 'false'); + await expect(getIcon()).toBeVisible(); + }); + + test(`GIVEN a checkbox with a user sig value of true + WHEN the checkbox is clicked + IT should have aria-checked as false`, async ({ page }) => { + const exampleName = 'test-hero'; + const { getCheckbox } = await setup(page, exampleName); + await expect(getCheckbox()).toBeVisible(); + await getCheckbox().click(); + await expect(getCheckbox()).toHaveAttribute('aria-checked', 'false'); + }); + + test(`GIVEN a checkbox with a user sig value of true + WHEN the checkbox is clicked + IT should have its icon hidden`, async ({ page }) => { + const exampleName = 'test-hero'; + const { getCheckbox, getIcon } = await setup(page, exampleName); + await expect(getIcon()).toBeVisible(); + await getCheckbox().click(); + await expect(getCheckbox()).toHaveAttribute('aria-checked', 'false'); + await expect(getIcon()).toBeHidden(); + }); + test(`GIVEN a default checkbox with a default sig value of false + WHEN the checkbox is clicked + IT should have its icon visible`, async ({ page }) => { + const exampleName = 'test-default'; + const { getCheckbox, getIcon } = await setup(page, exampleName); + await expect(getIcon()).toBeHidden(); + await getCheckbox().click(); + await expect(getCheckbox()).toHaveAttribute('aria-checked', 'false'); + await expect(getIcon()).toBeVisible(); + }); +}); +//TODO: create util file +//TODO: add click +//TODO: refactor to use ids instead of nths since its test-only now +function isUniqArr(arr: string[]) { + const singleInstances = new Set(arr); + return arr.length === singleInstances.size; +} diff --git a/libs/components/src/checkbox/checkbox.tsx b/libs/components/src/checkbox/checkbox.tsx new file mode 100644 index 00000000..42c416fe --- /dev/null +++ b/libs/components/src/checkbox/checkbox.tsx @@ -0,0 +1,230 @@ +import { + type PropsOf, + type Signal, + Slot, + component$, + sync$, + useContextProvider, + useContext, + $, + useSignal, + useTask$, +} from '@builder.io/qwik'; +import { CheckListContext, CheckboxContext } from './context-id'; +import { type TriBool, getTriBool } from '../../utils/tri-bool'; +export type MixedStateCheckboxProps = { + 'bind:checked'?: Signal<boolean>; + checklist?: boolean; + _useCheckListContext?: boolean; + _overWriteCheckbox?: boolean; +} & PropsOf<'div'>; +export type TwoStateCheckboxProps = { + 'bind:checked'?: Signal<boolean>; + _useCheckListContext?: boolean; + _overWriteCheckbox?: boolean; +} & PropsOf<'div'>; + +type TwoStateCheckboxBehaviorProps = { + 'bind:checked': Signal<boolean>; +} & PropsOf<'div'>; +export type ChecklistTwoStateCheckboxProps = { + 'bind:checked'?: Signal<boolean>; + _useCheckListContext?: boolean; + _overWriteCheckbox?: boolean; +} & PropsOf<'div'>; +export const CheckboxRoot = component$<MixedStateCheckboxProps>((props) => { + // this is done to avoid consumers dealing with two types checkboxes, could go in different files + if (props.checklist) { + return ( + <MixedStateCheckbox {...props}> + <Slot /> + </MixedStateCheckbox> + ); + } + if (props._useCheckListContext) { + return ( + <ChecklistTwoStateCheckbox {...props}> + <Slot /> + </ChecklistTwoStateCheckbox> + ); + } + return ( + <TwoStateCheckbox {...props}> + <Slot /> + </TwoStateCheckbox> + ); +}); + +function getAriaChecked(triBool: TriBool): 'mixed' | 'true' | 'false' { + if (triBool === 'indeterminate') { + return 'mixed'; + } + return `${triBool === true}`; +} + +export const TwoStateCheckbox = component$<TwoStateCheckboxProps>((props) => { + // all the sig stuff should be refactored into a fancy hook + const defaultSig = useSignal(false); + const appliedSig = props['bind:checked'] ?? defaultSig; + const checklistID = useSignal<string | undefined>(props.id); + useContextProvider(CheckboxContext, appliedSig); + return ( + <TwoStateCheckboxBehavior + bind:checked={appliedSig} + tabIndex={0} + role="checkbox" + aria-checked={getAriaChecked(appliedSig.value)} + {...props} + id={checklistID.value} + > + <Slot /> + </TwoStateCheckboxBehavior> + ); +}); + +export const ChecklistTwoStateCheckbox = + component$<ChecklistTwoStateCheckboxProps>((props) => { + // this code is duplicate bcs you cant use conditionals on hooks (checklistContext could be undefined) + // this has room for improvement: remove most of the code duplivation + // making this a wrapper over the simpler component or using hooks + const checklistContext = useContext(CheckListContext); + const defaultSig = useSignal(false); + const appliedSig = props['bind:checked'] ?? defaultSig; + const checklistID = useSignal<string | undefined>(props.id); + // makes sure that the checklist's value is the same as its child + const syncToChecklist = useSignal<undefined | boolean>( + props._overWriteCheckbox + ); + useContextProvider(CheckboxContext, appliedSig); + useTask$(({ track }) => { + if (syncToChecklist.value !== undefined) { + appliedSig.value = syncToChecklist.value; + syncToChecklist.value = undefined; + } + track(() => { + appliedSig.value; + }); + + // now i can say that there's one good application for object identity + if (!checklistContext.checkboxes.value.some((e) => e === appliedSig)) { + const currIndex = checklistContext.checkboxes.value.length; + + // TODO: refactor id to not run on wrapper but after conditional + if (checklistID.value === undefined) { + checklistID.value = checklistContext.idArr[currIndex]; + } else { + checklistContext.idArr[currIndex] = checklistID.value; + } + checklistContext.checkboxes.value = [ + ...checklistContext.checkboxes.value, + appliedSig, + ]; + } + + const boolArr = checklistContext?.checkboxes.value.map((e) => e.value); + const newVal = getTriBool(boolArr); + checklistContext.checklistSig.value = newVal; + }); + return ( + <TwoStateCheckboxBehavior + {...props} + tabIndex={0} + role="checkbox" + aria-checked={getAriaChecked(appliedSig.value)} + id={checklistID.value} + bind:checked={appliedSig} + > + <Slot /> + </TwoStateCheckboxBehavior> + ); + }); + +export const MixedStateCheckbox = component$<MixedStateCheckboxProps>( + (props) => { + console.log('mixed'); + + // all the sig stuff should be refactored into a fancy hook + const checklistContext = useContext(CheckListContext); + const childCheckboxes = checklistContext.checkboxes; + const appliedSig = props['bind:checked'] ?? checklistContext.checklistSig; + const ariaControlsStrg = + checklistContext.idArr.length === 0 + ? '' + : checklistContext.idArr.reduce((p, c) => p + ' ' + c); + useContextProvider(CheckboxContext, appliedSig); + + // im not enterily sure why, but the if statement only runs once + if (props['bind:checked'] !== undefined) { + checklistContext.checklistSig = props['bind:checked']; + } + + const changeChecklistSig = $(() => { + if (appliedSig.value !== true) { + appliedSig.value = true; + childCheckboxes.value.forEach((sig) => (sig.value = true)); + return; + } + appliedSig.value = false; + childCheckboxes.value.forEach((sig) => (sig.value = false)); + }); + const handleKeyDownSync$ = sync$((e: KeyboardEvent) => { + if (e.key === ' ') { + e.preventDefault(); + } + }); + const handleKeyDown$ = $((e: KeyboardEvent) => { + if (e.key === ' ') { + changeChecklistSig(); + } + }); + const handleClick$ = $(() => { + changeChecklistSig(); + }); + + return ( + <div + tabIndex={0} + role="checkbox" + aria-checked={getAriaChecked(appliedSig.value)} + onKeyDown$={[handleKeyDownSync$, handleKeyDown$]} + aria-controls={ariaControlsStrg} + onClick$={[handleClick$]} + {...props} + > + <Slot /> + </div> + ); + } +); + +const TwoStateCheckboxBehavior = component$<TwoStateCheckboxBehaviorProps>( + (props) => { + const handleKeyDownSync$ = sync$((e: KeyboardEvent) => { + if (e.key === ' ') { + e.preventDefault(); + } + }); + // this logic is duplicared thrice, make into hook pls + const handleClick = $(() => { + props['bind:checked'].value = !props['bind:checked'].value; + }); + const handleKeyDown$ = $((e: KeyboardEvent) => { + if (e.key === ' ') { + props['bind:checked'].value = !props['bind:checked'].value; + } + }); + // TODO: refactor to usetask code into fancy hook thingy + return ( + <div + tabIndex={0} + role="checkbox" + aria-checked={getAriaChecked(props['bind:checked'].value)} + {...props} + onKeyDown$={[handleKeyDownSync$, handleKeyDown$]} + onClick$={handleClick} + > + <Slot /> + </div> + ); + } +); diff --git a/libs/components/src/checkbox/context-id.ts b/libs/components/src/checkbox/context-id.ts new file mode 100644 index 00000000..a00956b9 --- /dev/null +++ b/libs/components/src/checkbox/context-id.ts @@ -0,0 +1,13 @@ +import { type Signal, createContextId } from '@builder.io/qwik'; +import type { TriBool } from '../checklist'; + +export type ArrSigs = Signal<boolean>[]; +type CheckContextObj = { + checklistSig: Signal<TriBool>; + checkboxes: Signal<ArrSigs>; + idArr: string[]; +}; +export const CheckboxContext = + createContextId<Signal<boolean>>('CheckBox.context'); +export const CheckListContext = + createContextId<CheckContextObj>('CheckList.context'); diff --git a/libs/components/src/checkbox/index.ts b/libs/components/src/checkbox/index.ts new file mode 100644 index 00000000..12d25986 --- /dev/null +++ b/libs/components/src/checkbox/index.ts @@ -0,0 +1,3 @@ +export { CheckboxRoot as Root } from './checkbox'; +export { CheckboxIndicator as Indicator } from './checkbox-indicator'; +export * from './context-id'; diff --git a/libs/components/src/checklist/checklist-context-wrapper.tsx b/libs/components/src/checklist/checklist-context-wrapper.tsx new file mode 100644 index 00000000..4e6c2561 --- /dev/null +++ b/libs/components/src/checklist/checklist-context-wrapper.tsx @@ -0,0 +1,64 @@ +import { + PropsOf, + Slot, + useId, + component$, + useContextProvider, + useSignal, + useTask$, +} from '@builder.io/qwik'; +import { CheckListContext } from './context-id'; + +export type CheckListContextWrapperProps = { + ariaLabeledBy: string; + arrSize: number; + idArr: Array<false | string>; + initialTriBool: TriBool; +} & PropsOf<'div'>; + +export type TriBool = boolean | 'indeterminate'; +export const ChecklistContextWrapper = component$<CheckListContextWrapperProps>( + (props) => { + const helpme = useSignal([]); + const idArr: string[] = []; + // this sig vals should be a prop + const mehelp = useSignal<TriBool>(props.initialTriBool); + const id = useId(); + for (let index = 0; index < props.arrSize; index++) { + if (props.idArr[index] != false) { + idArr.push(props.idArr[index] as string); + continue; + } + + const uniqId = id + index.toString(); + idArr.push(uniqId); + } + const obj = { checkboxes: helpme, checklistSig: mehelp, idArr }; + useContextProvider(CheckListContext, obj); + useTask$(({ track }) => { + track(() => { + return obj.checkboxes; + }); + }); + return ( + <div role="group" aria-labelledby={props.ariaLabeledBy}> + <Slot /> + </div> + ); + }, +); + +export function getTriBool(boolArr: boolean[]): TriBool { + if (boolArr.length === 0) { + return 'indeterminate'; + } + if (boolArr.every((e) => e === true)) { + return true; + } + + if (boolArr.every((e) => e === false)) { + return false; + } + + return 'indeterminate'; +} diff --git a/libs/components/src/checklist/checklist-indicator.tsx b/libs/components/src/checklist/checklist-indicator.tsx new file mode 100644 index 00000000..2ae376f3 --- /dev/null +++ b/libs/components/src/checklist/checklist-indicator.tsx @@ -0,0 +1,29 @@ +import { component$, useContext, PropsOf, Slot, useTask$ } from '@builder.io/qwik'; +import { CheckListContext } from './context-id'; + +export type ChecklistIndicatorProps = PropsOf<'div'>; + +export const ChecklistIndicator = component$<ChecklistIndicatorProps>((props) => { + const { checklistSig } = useContext(CheckListContext); + + useTask$(({ track }) => { + track(() => checklistSig.value); + }); + + // weird comparions, but it gets the right behavior + return ( + <div {...props}> + <p>Here lol: {JSON.stringify(checklistSig.value)}</p> + {checklistSig.value === true && ( + <div> + <Slot name="true" /> + </div> + )} + {checklistSig.value === 'indeterminate' && ( + <div> + <Slot name="mixed" /> + </div> + )} + </div> + ); +}); diff --git a/libs/components/src/checklist/checklist.tsx b/libs/components/src/checklist/checklist.tsx new file mode 100644 index 00000000..a5d8d51c --- /dev/null +++ b/libs/components/src/checklist/checklist.tsx @@ -0,0 +1,105 @@ +import { type JSXNode, Component, PropsOf } from '@builder.io/qwik'; +import { CheckboxRoot, type MixedStateCheckboxProps } from '../checkbox/checkbox'; +import { ChecklistContextWrapper, getTriBool } from './checklist-context-wrapper'; + +type CheckListProps = PropsOf<'ul'> & { ariaLabeledBy: string }; +// type CheckBoxes= +/* + This is an inline component. An example use case of an inline component to get the proper indexes with CSR. See issue #4757 + for more information. +*/ +export const Checklist: Component<CheckListProps> = (props: CheckListProps) => { + const checkArr: JSXNode[] = []; + const hellSigs = []; + let checklistCheckbox = undefined; + const boolArr: boolean[] = []; + const idArr: Array<false | string> = []; + const checklistChilds: JSXNode[] = []; + const { children: myChildren } = props; + + const childrenToProcess = ( + Array.isArray(myChildren) ? [...myChildren] : [myChildren] + ) as Array<JSXNode>; + + while (childrenToProcess.length) { + const child = childrenToProcess.shift(); + + if (!child) { + continue; + } + + if (Array.isArray(child)) { + childrenToProcess.unshift(...child); + continue; + } + + switch (child.type) { + case CheckboxRoot: { + const typedProps = child.props as MixedStateCheckboxProps; + // FYI: Obj.assign mutates + Object.assign(typedProps, { _useCheckListContext: true }); + + checkArr.push(child); + // TODO: fix this if hell by making fn + if (!typedProps.checklist) { + checklistChilds.push(child); + + if (typedProps.id != undefined) { + idArr.push(typedProps.id as string); + } else { + idArr.push(false); + } + if (typedProps['bind:checked'] && typedProps['bind:checked'].value) { + boolArr.push(typedProps['bind:checked'].value); + hellSigs.push(typedProps['bind:checked']); + } else { + boolArr.push(false); + } + } else { + checklistCheckbox = child; + } + + break; + } + + default: { + if (child) { + const anyChildren = Array.isArray(child.children) + ? [...child.children] + : [child.children]; + childrenToProcess.unshift(...(anyChildren as JSXNode[])); + } + + break; + } + } + } + if (checklistCheckbox === undefined) { + throw Error( + "QWIKUI: checklist doesn't have a checkbox. Did you give the atribute to *checklist* to any of the checkboxes inside the checklist?", + ); + } + if (checklistCheckbox.props['bind:checked']) { + checklistChilds.forEach((checkbox) => { + Object.assign(checkbox.props, { _overWriteCheckbox: true }); + }); + } + + return ( + <> + <ChecklistContextWrapper + ariaLabeledBy={props.ariaLabeledBy} + arrSize={boolArr.length} + initialTriBool={getTriBool(boolArr)} + idArr={idArr} + > + <ul class={props.class}> + {checkArr.map((checkbox, i) => ( + <li key={i}>{checkbox}</li> + ))} + </ul> + </ChecklistContextWrapper> + </> + ); +}; +// TODO: deprecate ariaLabelledBy diff --git a/libs/components/src/checklist/context-id.ts b/libs/components/src/checklist/context-id.ts new file mode 100644 index 00000000..13bdbd3b --- /dev/null +++ b/libs/components/src/checklist/context-id.ts @@ -0,0 +1,10 @@ +import { Signal, createContextId } from '@builder.io/qwik'; +import { TriBool } from './checklist-context-wrapper'; + +export type ArrSigs = Signal<boolean>[]; +type CheckContextObj = { + checklistSig: Signal<TriBool>; + checkboxes: Signal<ArrSigs>; + idArr: string[]; +}; +export const CheckListContext = createContextId<CheckContextObj>('CheckList.context'); diff --git a/libs/components/src/checklist/index.ts b/libs/components/src/checklist/index.ts new file mode 100644 index 00000000..9db387ec --- /dev/null +++ b/libs/components/src/checklist/index.ts @@ -0,0 +1,4 @@ +export { Checklist as Root } from './checklist'; +export { ChecklistIndicator as Indicator } from './checklist-indicator'; +export * from './checklist-context-wrapper'; +export * from './context-id'; diff --git a/libs/components/src/index.ts b/libs/components/src/index.ts index ff5f2f0e..60c65443 100644 --- a/libs/components/src/index.ts +++ b/libs/components/src/index.ts @@ -1,2 +1,2 @@ export * as Otp from './otp'; -// export * as Checkbox from './checkbox'; +export * as Checkbox from './checkbox'; From 329b7044b20ec667900af055acde7e33ebbb270b Mon Sep 17 00:00:00 2001 From: Jay-Kunaico <jay.brass@kunaico.com> Date: Tue, 27 Aug 2024 14:29:23 -0400 Subject: [PATCH 02/27] Updated tests --- .../src/checkbox/checkbox-indicator.tsx | 2 +- libs/components/src/checkbox/checkbox.test.ts | 38 +++++++++++++------ package.json | 2 +- 3 files changed, 29 insertions(+), 13 deletions(-) diff --git a/libs/components/src/checkbox/checkbox-indicator.tsx b/libs/components/src/checkbox/checkbox-indicator.tsx index 87ac5ba8..6b2f6aea 100644 --- a/libs/components/src/checkbox/checkbox-indicator.tsx +++ b/libs/components/src/checkbox/checkbox-indicator.tsx @@ -7,7 +7,7 @@ export const CheckboxIndicator = component$<CheckboxIndicatorProps>((props) => { const checkSig = useContext(CheckboxContext); return ( <div {...props}> - <div hidden={checkSig.value} aria-hidden="true"> + <div hidden={!checkSig.value} aria-hidden="true"> <Slot /> </div> </div> diff --git a/libs/components/src/checkbox/checkbox.test.ts b/libs/components/src/checkbox/checkbox.test.ts index 75b80890..0c3b0f4a 100644 --- a/libs/components/src/checkbox/checkbox.test.ts +++ b/libs/components/src/checkbox/checkbox.test.ts @@ -2,7 +2,7 @@ import { expect, test, type Page } from '@playwright/test'; import { createTestDriver } from './checkbox.driver'; import { getTriBool } from '../checklist/checklist-context-wrapper'; async function setup(page: Page, exampleName: string) { - await page.goto(`/headless/checkbox/${exampleName}`); + await page.goto(`http://localhost:6174/checkbox/${exampleName}`); const driver = createTestDriver(page); @@ -65,7 +65,7 @@ test.describe('checklist', () => { }) => { const { getCheckList, getChecklistUL, getChecklistLIs } = await setup( page, - 'test-list', + 'test-list' ); await expect(getCheckList()).toBeVisible(); await expect(getCheckList()).toHaveAttribute('aria-labelledby', 'test123'); @@ -110,7 +110,9 @@ test.describe('checklist', () => { test(`GIVEN checklist with all unchecked checkboxes WHEN all checkboxes are checked with space - the tri state checkbox should have aria-checked true`, async ({ page }) => { + the tri state checkbox should have aria-checked true`, async ({ + page, + }) => { const exampleName = 'test-list'; const { getTriCheckbox, getCheckbox } = await setup(page, exampleName); await expect(getTriCheckbox()).toBeVisible(); @@ -121,7 +123,9 @@ test.describe('checklist', () => { test(`GIVEN checklist with all unchecked checkboxes WHEN the checklist's checkbox is checked with space - THEN all chekboxes should have aria-checked true`, async ({ page }) => { + THEN all chekboxes should have aria-checked true`, async ({ + page, + }) => { const exampleName = 'test-list'; const { getTriCheckbox, getCheckbox } = await setup(page, exampleName); await expect(getTriCheckbox()).toBeVisible(); @@ -151,14 +155,16 @@ test.describe('checklist', () => { }); test(`GIVEN checklist with checkboxes WHEN the values of aria-controls are search - IT should always return a valid, non-duplicate, checkboxes`, async ({ page }) => { + IT should always return a valid, non-duplicate, checkboxes`, async ({ + page, + }) => { const { getTriCheckbox } = await setup(page, 'test-list'); await expect(getTriCheckbox()).toHaveAttribute('aria-controls'); const magic = await getTriCheckbox().getAttribute('aria-controls'); expect(magic).not.toBe(null); if (magic === null) { throw new Error( - 'no mixed checkbox found. Was the driver or test template changed?', + 'no mixed checkbox found. Was the driver or test template changed?' ); } const idArr = magic.split(' '); @@ -225,7 +231,9 @@ test.describe('checklist', () => { // TODO: change api to not use indeterminate and used mixed instead test(`GIVEN a controlled checklist with a checklist signal of true and default checkboxes as children WHEN a child checkbox is unchecked - THEN the checklist signal should have aria-checked mixed`, async ({ page }) => { + THEN the checklist signal should have aria-checked mixed`, async ({ + page, + }) => { const exampleName = 'test-controlled-list-true'; const { getTriCheckbox } = await setup(page, exampleName); const firstCheckbox = page.locator('#child-1'); @@ -235,7 +243,9 @@ test.describe('checklist', () => { test(`GIVEN a controlled checklist with a checklist signal of true and default checkboxes as children WHEN all child checkbox are unchecked - THEN the checklist signal should have aria-checked false`, async ({ page }) => { + THEN the checklist signal should have aria-checked false`, async ({ + page, + }) => { const exampleName = 'test-controlled-list-true'; const { getTriCheckbox } = await setup(page, exampleName); await page.locator('#child-1').press(' '); @@ -290,7 +300,9 @@ test.describe('checklist', () => { test(`GIVEN checklist with all unchecked checkboxes WHEN the checklist's checkbox is checked by clicking - THEN all checkboxes should have aria-checked true`, async ({ page }) => { + THEN all checkboxes should have aria-checked true`, async ({ + page, + }) => { const exampleName = 'test-list'; const { getTriCheckbox, getCheckbox } = await setup(page, exampleName); await expect(getTriCheckbox()).toBeVisible(); @@ -339,7 +351,9 @@ test.describe('checklist', () => { test(`GIVEN a controlled checklist with a checklist signal of true and default checkboxes as children WHEN a child checkbox is unchecked - THEN the checklist signal should have aria-checked mixed`, async ({ page }) => { + THEN the checklist signal should have aria-checked mixed`, async ({ + page, + }) => { const exampleName = 'test-controlled-list-true'; const { getTriCheckbox } = await setup(page, exampleName); const firstCheckbox = page.locator('#child-1'); @@ -349,7 +363,9 @@ test.describe('checklist', () => { test(`GIVEN a controlled checklist with a checklist signal of true and default checkboxes as children WHEN all child checkbox are unchecked - THEN the checklist signal should have aria-checked false`, async ({ page }) => { + THEN the checklist signal should have aria-checked false`, async ({ + page, + }) => { const exampleName = 'test-controlled-list-true'; const { getTriCheckbox } = await setup(page, exampleName); await page.locator('#child-1').click(); diff --git a/package.json b/package.json index eace20bb..9bdcf565 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "0.0.0", "scripts": { "dev": "pnpm --filter ./apps/docs dev", - "dev.ct": "pnpm --filter ./apps/component-tests dev -- --port 5174", + "dev.ct": "pnpm --filter ./apps/component-tests dev -- --port 6174", "build": "pnpm --filter ./libs/hooks build && pnpm --filter ./libs/components build", "preview": "pnpm --filter ./apps/docs preview", "check": "biome ci .", From 051578a2a8cd0a289334553526e3a943c0d8304a Mon Sep 17 00:00:00 2001 From: Jay-Kunaico <jay.brass@kunaico.com> Date: Tue, 27 Aug 2024 14:31:51 -0400 Subject: [PATCH 03/27] all tests passing --- libs/components/src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/libs/components/src/index.ts b/libs/components/src/index.ts index 60c65443..607892ca 100644 --- a/libs/components/src/index.ts +++ b/libs/components/src/index.ts @@ -1,2 +1,3 @@ export * as Otp from './otp'; export * as Checkbox from './checkbox'; +export * as Checklist from './checklist'; From 653174b3cd59ae23b8c1c775472659cc93ff0968 Mon Sep 17 00:00:00 2001 From: Jay-Kunaico <jay.brass@kunaico.com> Date: Tue, 27 Aug 2024 14:43:17 -0400 Subject: [PATCH 04/27] Added hero tests --- .../src/checkbox/checkbox.driver.ts | 1 + libs/components/src/checkbox/checkbox.test.ts | 165 ++++++++++-------- 2 files changed, 93 insertions(+), 73 deletions(-) diff --git a/libs/components/src/checkbox/checkbox.driver.ts b/libs/components/src/checkbox/checkbox.driver.ts index bfb4712d..9e9f3162 100644 --- a/libs/components/src/checkbox/checkbox.driver.ts +++ b/libs/components/src/checkbox/checkbox.driver.ts @@ -24,6 +24,7 @@ export function createTestDriver<T extends DriverLocator>(rootLocator: T) { const getCheckbox = () => { return getRoot().getByRole('checkbox'); }; + const getTriCheckbox = () => { return getRoot().locator('css=[aria-controls]'); }; diff --git a/libs/components/src/checkbox/checkbox.test.ts b/libs/components/src/checkbox/checkbox.test.ts index 0c3b0f4a..b5777274 100644 --- a/libs/components/src/checkbox/checkbox.test.ts +++ b/libs/components/src/checkbox/checkbox.test.ts @@ -28,6 +28,98 @@ async function setup(page: Page, exampleName: string) { }; } +test.describe('checkbox', () => { + test(`GIVEN a checkbox + WHEN the checkbox renders + It should have aria-checked as false`, async ({ page }) => { + const exampleName = 'hero'; + const { getCheckbox } = await setup(page, exampleName); + await expect(getCheckbox()).toHaveAttribute('aria-checked', 'false'); + }); + + test(`GIVEN a checkbox + WHEN the checkbox is clicked + It should have aria-checked as true`, async ({ page }) => { + const exampleName = 'hero'; + const { getCheckbox } = await setup(page, exampleName); + await getCheckbox().click(); + await expect(getCheckbox()).toHaveAttribute('aria-checked', 'true'); + }); + + test(`GIVEN a checkbox that is checked + WHEN the checkbox renders + IT should have aria-checked as true`, async ({ page }) => { + const exampleName = 'test-hero'; + const { getCheckbox } = await setup(page, exampleName); + await expect(getCheckbox()).toBeVisible(); + await expect(getCheckbox()).toHaveAttribute('aria-checked', 'true'); + }); + + test(`GIVEN a checkbox that is checked + WHEN the spacebar is pressed + IT should have aria-checked as false`, async ({ page }) => { + const exampleName = 'test-hero'; + const { getCheckbox } = await setup(page, exampleName); + await expect(getCheckbox()).toBeVisible(); + await getCheckbox().press(' '); + await expect(getCheckbox()).toHaveAttribute('aria-checked', 'false'); + }); + + test(`GIVEN a checkbox with a user sig value of true + WHEN the checkbox is focused and the spacebar is pressed + IT should have its icon hidden`, async ({ page }) => { + const exampleName = 'test-hero'; + const { getCheckbox, getIcon } = await setup(page, exampleName); + await expect(getIcon()).toBeVisible(); + await getCheckbox().focus(); + await getCheckbox().press(' '); + await expect(getCheckbox()).toHaveAttribute('aria-checked', 'false'); + await expect(getIcon()).toBeHidden(); + }); + test(`GIVEN a default checkbox with a default sig value of false + WHEN the checkbox is focused and the spacebar is pressed + IT should have its icon visible`, async ({ page }) => { + const exampleName = 'test-default'; + const { getCheckbox, getIcon } = await setup(page, exampleName); + await expect(getIcon()).toBeHidden(); + await getCheckbox().focus(); + await getCheckbox().press(' '); + await expect(getCheckbox()).toHaveAttribute('aria-checked', 'false'); + await expect(getIcon()).toBeVisible(); + }); + + test(`GIVEN a checkbox with a user sig value of true + WHEN the checkbox is clicked + IT should have aria-checked as false`, async ({ page }) => { + const exampleName = 'test-hero'; + const { getCheckbox } = await setup(page, exampleName); + await expect(getCheckbox()).toBeVisible(); + await getCheckbox().click(); + await expect(getCheckbox()).toHaveAttribute('aria-checked', 'false'); + }); + + test(`GIVEN a checkbox with a user sig value of true + WHEN the checkbox is clicked + IT should have its icon hidden`, async ({ page }) => { + const exampleName = 'test-hero'; + const { getCheckbox, getIcon } = await setup(page, exampleName); + await expect(getIcon()).toBeVisible(); + await getCheckbox().click(); + await expect(getCheckbox()).toHaveAttribute('aria-checked', 'false'); + await expect(getIcon()).toBeHidden(); + }); + test(`GIVEN a default checkbox with a default sig value of false + WHEN the checkbox is clicked + IT should have its icon visible`, async ({ page }) => { + const exampleName = 'test-default'; + const { getCheckbox, getIcon } = await setup(page, exampleName); + await expect(getIcon()).toBeHidden(); + await getCheckbox().click(); + await expect(getCheckbox()).toHaveAttribute('aria-checked', 'false'); + await expect(getIcon()).toBeVisible(); + }); +}); + test.describe('checklist', () => { test(`GIVEN a mixed checklist WHEN the checklist renders @@ -373,80 +465,7 @@ test.describe('checklist', () => { await expect(getTriCheckbox()).toHaveAttribute('aria-checked', 'false'); }); }); -test.describe('checkbox', () => { - test(`GIVEN a checkbox with a user sig value of true - WHEN the checkbox renders - IT should have aria-checked as true`, async ({ page }) => { - const exampleName = 'test-hero'; - const { getCheckbox } = await setup(page, exampleName); - await expect(getCheckbox()).toBeVisible(); - await expect(getCheckbox()).toHaveAttribute('aria-checked', 'true'); - }), - test(`GIVEN a checkbox with a user sig value of true -WHEN the checkbox is focused and the spacebar is pressed -IT should have aria-checked as false`, async ({ page }) => { - const exampleName = 'test-hero'; - const { getCheckbox } = await setup(page, exampleName); - await expect(getCheckbox()).toBeVisible(); - await getCheckbox().focus(); - await getCheckbox().press(' '); - await expect(getCheckbox()).toHaveAttribute('aria-checked', 'false'); - }); - - test(`GIVEN a checkbox with a user sig value of true - WHEN the checkbox is focused and the spacebar is pressed - IT should have its icon hidden`, async ({ page }) => { - const exampleName = 'test-hero'; - const { getCheckbox, getIcon } = await setup(page, exampleName); - await expect(getIcon()).toBeVisible(); - await getCheckbox().focus(); - await getCheckbox().press(' '); - await expect(getCheckbox()).toHaveAttribute('aria-checked', 'false'); - await expect(getIcon()).toBeHidden(); - }); - test(`GIVEN a default checkbox with a default sig value of false - WHEN the checkbox is focused and the spacebar is pressed - IT should have its icon visible`, async ({ page }) => { - const exampleName = 'test-default'; - const { getCheckbox, getIcon } = await setup(page, exampleName); - await expect(getIcon()).toBeHidden(); - await getCheckbox().focus(); - await getCheckbox().press(' '); - await expect(getCheckbox()).toHaveAttribute('aria-checked', 'false'); - await expect(getIcon()).toBeVisible(); - }); - - test(`GIVEN a checkbox with a user sig value of true - WHEN the checkbox is clicked - IT should have aria-checked as false`, async ({ page }) => { - const exampleName = 'test-hero'; - const { getCheckbox } = await setup(page, exampleName); - await expect(getCheckbox()).toBeVisible(); - await getCheckbox().click(); - await expect(getCheckbox()).toHaveAttribute('aria-checked', 'false'); - }); - test(`GIVEN a checkbox with a user sig value of true - WHEN the checkbox is clicked - IT should have its icon hidden`, async ({ page }) => { - const exampleName = 'test-hero'; - const { getCheckbox, getIcon } = await setup(page, exampleName); - await expect(getIcon()).toBeVisible(); - await getCheckbox().click(); - await expect(getCheckbox()).toHaveAttribute('aria-checked', 'false'); - await expect(getIcon()).toBeHidden(); - }); - test(`GIVEN a default checkbox with a default sig value of false - WHEN the checkbox is clicked - IT should have its icon visible`, async ({ page }) => { - const exampleName = 'test-default'; - const { getCheckbox, getIcon } = await setup(page, exampleName); - await expect(getIcon()).toBeHidden(); - await getCheckbox().click(); - await expect(getCheckbox()).toHaveAttribute('aria-checked', 'false'); - await expect(getIcon()).toBeVisible(); - }); -}); //TODO: create util file //TODO: add click //TODO: refactor to use ids instead of nths since its test-only now From f985c827d594a8fbdb25ee9c43c426602d828333 Mon Sep 17 00:00:00 2001 From: Jay-Kunaico <jay.brass@kunaico.com> Date: Tue, 27 Aug 2024 14:53:36 -0400 Subject: [PATCH 05/27] changed for loop to for of --- apps/component-tests/package.json | 6 +++-- ...test-hero.tsx => test-initial-checked.tsx} | 0 libs/components/src/checkbox/checkbox.test.ts | 10 ++++---- libs/components/src/checkbox/checkbox.tsx | 12 ++++++++-- playwright.config.ts | 24 +++++++++---------- 5 files changed, 31 insertions(+), 21 deletions(-) rename apps/docs/src/routes/checkbox/examples/{test-hero.tsx => test-initial-checked.tsx} (100%) diff --git a/apps/component-tests/package.json b/apps/component-tests/package.json index 485ec01d..e7c68d6e 100644 --- a/apps/component-tests/package.json +++ b/apps/component-tests/package.json @@ -6,7 +6,9 @@ }, "engines-annotation": "Mostly required by sharp which needs a Node-API v9 compatible runtime", "private": true, - "trustedDependencies": ["sharp"], + "trustedDependencies": [ + "sharp" + ], "trustedDependencies-annotation": "Needed for bun to allow running install scripts", "type": "module", "scripts": { @@ -15,7 +17,7 @@ "build.preview": "vite build --ssr src/entry.preview.tsx", "build.types": "tsc --incremental --noEmit", "deploy": "echo 'Run \"npm run qwik add\" to install a server adapter'", - "dev": "vite --mode ssr", + "dev": "vite --mode ssr --host", "dev.debug": "node --inspect-brk ./node_modules/vite/bin/vite.js --mode ssr --force", "fmt": "prettier --write .", "fmt.check": "prettier --check .", diff --git a/apps/docs/src/routes/checkbox/examples/test-hero.tsx b/apps/docs/src/routes/checkbox/examples/test-initial-checked.tsx similarity index 100% rename from apps/docs/src/routes/checkbox/examples/test-hero.tsx rename to apps/docs/src/routes/checkbox/examples/test-initial-checked.tsx diff --git a/libs/components/src/checkbox/checkbox.test.ts b/libs/components/src/checkbox/checkbox.test.ts index b5777274..67d58e8d 100644 --- a/libs/components/src/checkbox/checkbox.test.ts +++ b/libs/components/src/checkbox/checkbox.test.ts @@ -49,7 +49,7 @@ test.describe('checkbox', () => { test(`GIVEN a checkbox that is checked WHEN the checkbox renders IT should have aria-checked as true`, async ({ page }) => { - const exampleName = 'test-hero'; + const exampleName = 'test-initial-checked'; const { getCheckbox } = await setup(page, exampleName); await expect(getCheckbox()).toBeVisible(); await expect(getCheckbox()).toHaveAttribute('aria-checked', 'true'); @@ -58,7 +58,7 @@ test.describe('checkbox', () => { test(`GIVEN a checkbox that is checked WHEN the spacebar is pressed IT should have aria-checked as false`, async ({ page }) => { - const exampleName = 'test-hero'; + const exampleName = 'test-initial-checked'; const { getCheckbox } = await setup(page, exampleName); await expect(getCheckbox()).toBeVisible(); await getCheckbox().press(' '); @@ -68,7 +68,7 @@ test.describe('checkbox', () => { test(`GIVEN a checkbox with a user sig value of true WHEN the checkbox is focused and the spacebar is pressed IT should have its icon hidden`, async ({ page }) => { - const exampleName = 'test-hero'; + const exampleName = 'test-initial-checked'; const { getCheckbox, getIcon } = await setup(page, exampleName); await expect(getIcon()).toBeVisible(); await getCheckbox().focus(); @@ -91,7 +91,7 @@ test.describe('checkbox', () => { test(`GIVEN a checkbox with a user sig value of true WHEN the checkbox is clicked IT should have aria-checked as false`, async ({ page }) => { - const exampleName = 'test-hero'; + const exampleName = 'test-initial-checked'; const { getCheckbox } = await setup(page, exampleName); await expect(getCheckbox()).toBeVisible(); await getCheckbox().click(); @@ -101,7 +101,7 @@ test.describe('checkbox', () => { test(`GIVEN a checkbox with a user sig value of true WHEN the checkbox is clicked IT should have its icon hidden`, async ({ page }) => { - const exampleName = 'test-hero'; + const exampleName = 'test-initial-checked'; const { getCheckbox, getIcon } = await setup(page, exampleName); await expect(getIcon()).toBeVisible(); await getCheckbox().click(); diff --git a/libs/components/src/checkbox/checkbox.tsx b/libs/components/src/checkbox/checkbox.tsx index 42c416fe..b4ba3204 100644 --- a/libs/components/src/checkbox/checkbox.tsx +++ b/libs/components/src/checkbox/checkbox.tsx @@ -161,11 +161,19 @@ export const MixedStateCheckbox = component$<MixedStateCheckboxProps>( const changeChecklistSig = $(() => { if (appliedSig.value !== true) { appliedSig.value = true; - childCheckboxes.value.forEach((sig) => (sig.value = true)); + + for (const checkbox of childCheckboxes.value) { + checkbox.value = true; + } + return; } + appliedSig.value = false; - childCheckboxes.value.forEach((sig) => (sig.value = false)); + + for (const checkbox of childCheckboxes.value) { + checkbox.value = false; + } }); const handleKeyDownSync$ = sync$((e: KeyboardEvent) => { if (e.key === ' ') { diff --git a/playwright.config.ts b/playwright.config.ts index 7a0e4794..ecfd4f50 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -1,11 +1,11 @@ -import { defineConfig, devices } from "@playwright/test"; +import { defineConfig, devices } from '@playwright/test'; /** * See https://playwright.dev/docs/test-configuration. */ export default defineConfig({ - testDir: "./libs/components", - testMatch: "**/*.test.ts", + testDir: './libs/components', + testMatch: '**/*.test.ts', timeout: 30000, /* Run tests in files in parallel */ fullyParallel: true, @@ -16,24 +16,24 @@ export default defineConfig({ /* Opt out of parallel tests on CI. */ workers: process.env.CI ? 1 : undefined, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ - reporter: "html", + reporter: 'html', /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ - trace: "on-first-retry" + trace: 'on-first-retry', }, projects: [ { - name: "chromium", - use: { ...devices["Desktop Chrome"] } - } + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, ], /* Run your local dev server before starting the tests */ webServer: { - command: "npm run -C apps/component-tests dev -- --port 6174", - url: "http://localhost:6174", + command: 'npm run -C apps/component-tests dev -- --port 6174', + url: 'http://localhost:6174', timeout: 120000, - reuseExistingServer: !process.env.CI - } + reuseExistingServer: !process.env.CI, + }, }); From 8c27d1968f9d804d205397730c4fdcd27fe31464 Mon Sep 17 00:00:00 2001 From: Jay-Kunaico <jay.brass@kunaico.com> Date: Tue, 27 Aug 2024 15:48:21 -0400 Subject: [PATCH 06/27] Fix Biome suggestions --- apps/docs/src/routes/checkbox/index.mdx | 2 + libs/components/src/checkbox/checkbox.tsx | 4 +- .../checklist/checklist-context-wrapper.tsx | 6 +- .../src/checklist/checklist-indicator.tsx | 54 +++++++------ libs/components/src/checklist/checklist.tsx | 75 +++++++++++++++---- libs/components/src/checklist/context-id.ts | 7 +- libs/components/utils/unique-id.tsx | 0 7 files changed, 102 insertions(+), 46 deletions(-) create mode 100644 libs/components/utils/unique-id.tsx diff --git a/apps/docs/src/routes/checkbox/index.mdx b/apps/docs/src/routes/checkbox/index.mdx index 5be10ee0..c45f91a7 100644 --- a/apps/docs/src/routes/checkbox/index.mdx +++ b/apps/docs/src/routes/checkbox/index.mdx @@ -37,3 +37,5 @@ Text tags lose their semantic meaning. For example, an h1 tag is treated the sam ## API ### Checkbox.Root + +<Showcase name="pizza" /> diff --git a/libs/components/src/checkbox/checkbox.tsx b/libs/components/src/checkbox/checkbox.tsx index b4ba3204..d9c3e7d8 100644 --- a/libs/components/src/checkbox/checkbox.tsx +++ b/libs/components/src/checkbox/checkbox.tsx @@ -150,7 +150,9 @@ export const MixedStateCheckbox = component$<MixedStateCheckboxProps>( const ariaControlsStrg = checklistContext.idArr.length === 0 ? '' - : checklistContext.idArr.reduce((p, c) => p + ' ' + c); + : checklistContext.idArr.reduce( + (previous, current) => `${previous} ${current}` + ); useContextProvider(CheckboxContext, appliedSig); // im not enterily sure why, but the if statement only runs once diff --git a/libs/components/src/checklist/checklist-context-wrapper.tsx b/libs/components/src/checklist/checklist-context-wrapper.tsx index 4e6c2561..38eaa649 100644 --- a/libs/components/src/checklist/checklist-context-wrapper.tsx +++ b/libs/components/src/checklist/checklist-context-wrapper.tsx @@ -1,5 +1,5 @@ import { - PropsOf, + type PropsOf, Slot, useId, component$, @@ -25,7 +25,7 @@ export const ChecklistContextWrapper = component$<CheckListContextWrapperProps>( const mehelp = useSignal<TriBool>(props.initialTriBool); const id = useId(); for (let index = 0; index < props.arrSize; index++) { - if (props.idArr[index] != false) { + if (props.idArr[index] !== false) { idArr.push(props.idArr[index] as string); continue; } @@ -45,7 +45,7 @@ export const ChecklistContextWrapper = component$<CheckListContextWrapperProps>( <Slot /> </div> ); - }, + } ); export function getTriBool(boolArr: boolean[]): TriBool { diff --git a/libs/components/src/checklist/checklist-indicator.tsx b/libs/components/src/checklist/checklist-indicator.tsx index 2ae376f3..95504f8e 100644 --- a/libs/components/src/checklist/checklist-indicator.tsx +++ b/libs/components/src/checklist/checklist-indicator.tsx @@ -1,29 +1,37 @@ -import { component$, useContext, PropsOf, Slot, useTask$ } from '@builder.io/qwik'; +import { + component$, + useContext, + type PropsOf, + Slot, + useTask$, +} from '@builder.io/qwik'; import { CheckListContext } from './context-id'; export type ChecklistIndicatorProps = PropsOf<'div'>; -export const ChecklistIndicator = component$<ChecklistIndicatorProps>((props) => { - const { checklistSig } = useContext(CheckListContext); +export const ChecklistIndicator = component$<ChecklistIndicatorProps>( + (props) => { + const { checklistSig } = useContext(CheckListContext); - useTask$(({ track }) => { - track(() => checklistSig.value); - }); + useTask$(({ track }) => { + track(() => checklistSig.value); + }); - // weird comparions, but it gets the right behavior - return ( - <div {...props}> - <p>Here lol: {JSON.stringify(checklistSig.value)}</p> - {checklistSig.value === true && ( - <div> - <Slot name="true" /> - </div> - )} - {checklistSig.value === 'indeterminate' && ( - <div> - <Slot name="mixed" /> - </div> - )} - </div> - ); -}); + // weird comparions, but it gets the right behavior + return ( + <div {...props}> + <p>Here lol: {JSON.stringify(checklistSig.value)}</p> + {checklistSig.value === true && ( + <div> + <Slot name="true" /> + </div> + )} + {checklistSig.value === 'indeterminate' && ( + <div> + <Slot name="mixed" /> + </div> + )} + </div> + ); + } +); diff --git a/libs/components/src/checklist/checklist.tsx b/libs/components/src/checklist/checklist.tsx index a5d8d51c..c07bb496 100644 --- a/libs/components/src/checklist/checklist.tsx +++ b/libs/components/src/checklist/checklist.tsx @@ -1,6 +1,18 @@ -import { type JSXNode, Component, PropsOf } from '@builder.io/qwik'; -import { CheckboxRoot, type MixedStateCheckboxProps } from '../checkbox/checkbox'; -import { ChecklistContextWrapper, getTriBool } from './checklist-context-wrapper'; +import type { JSXNode, Component, PropsOf } from '@builder.io/qwik'; +import { + CheckboxRoot, + type MixedStateCheckboxProps, +} from '../checkbox/checkbox'; +import { + ChecklistContextWrapper, + getTriBool, +} from './checklist-context-wrapper'; + +import { findComponent, processChildren } from '../../utils/inline-component'; + +function generateUniqueId(): string { + return Date.now().toString(36) + Math.random().toString(36).substr(2, 5); +} type CheckListProps = PropsOf<'ul'> & { ariaLabeledBy: string }; // type CheckBoxes= @@ -9,17 +21,17 @@ type CheckListProps = PropsOf<'ul'> & { ariaLabeledBy: string }; for more information. */ export const Checklist: Component<CheckListProps> = (props: CheckListProps) => { - const checkArr: JSXNode[] = []; + const checkboxJSXArray: JSXNode[] = []; const hellSigs = []; let checklistCheckbox = undefined; const boolArr: boolean[] = []; const idArr: Array<false | string> = []; const checklistChilds: JSXNode[] = []; - const { children: myChildren } = props; + const { children } = props; const childrenToProcess = ( - Array.isArray(myChildren) ? [...myChildren] : [myChildren] - ) as Array<JSXNode>; + Array.isArray(children) ? [...children] : [children] + ) as JSXNode[]; while (childrenToProcess.length) { const child = childrenToProcess.shift(); @@ -36,20 +48,21 @@ export const Checklist: Component<CheckListProps> = (props: CheckListProps) => { switch (child.type) { case CheckboxRoot: { const typedProps = child.props as MixedStateCheckboxProps; + // FYI: Obj.assign mutates Object.assign(typedProps, { _useCheckListContext: true }); - checkArr.push(child); + checkboxJSXArray.push(child); // TODO: fix this if hell by making fn if (!typedProps.checklist) { checklistChilds.push(child); - if (typedProps.id != undefined) { + if (typedProps.id !== undefined) { idArr.push(typedProps.id as string); } else { idArr.push(false); } - if (typedProps['bind:checked'] && typedProps['bind:checked'].value) { + if (typedProps['bind:checked']?.value) { boolArr.push(typedProps['bind:checked'].value); hellSigs.push(typedProps['bind:checked']); } else { @@ -76,13 +89,14 @@ export const Checklist: Component<CheckListProps> = (props: CheckListProps) => { } if (checklistCheckbox === undefined) { throw Error( - "QWIKUI: checklist doesn't have a checkbox. Did you give the atribute to *checklist* to any of the checkboxes inside the checklist?", + "QWIKUI: checklist doesn't have a checkbox. Did you give the atribute to *checklist* to any of the checkboxes inside the checklist?" ); } + if (checklistCheckbox.props['bind:checked']) { - checklistChilds.forEach((checkbox) => { + for (const checkbox of checklistChilds) { Object.assign(checkbox.props, { _overWriteCheckbox: true }); - }); + } } return ( @@ -94,12 +108,41 @@ export const Checklist: Component<CheckListProps> = (props: CheckListProps) => { idArr={idArr} > <ul class={props.class}> - {checkArr.map((checkbox, i) => ( - <li key={i}>{checkbox}</li> - ))} + {checkboxJSXArray.map((checkbox, i) => { + const uniqueId = generateUniqueId(); + + return <li key={uniqueId}>{checkbox}</li>; + })} </ul> </ChecklistContextWrapper> </> ); }; // TODO: deprecate ariaLabelledBy + +/** + * + * User defined checkbox component: ChecklistItem + * + * returns: + * + * <Checklist.Item> + * <Checkbox.Root> + * <Checkbox.Indicator /> + * </Checkbox.Root> + * </Checklist.Item> + * + * + */ + +/** + * + * + * + * <Checklist.Root> + * <ChecklistItem /> + * <ChecklistItem /> + * ... + * </Checklist.Root> + * + */ diff --git a/libs/components/src/checklist/context-id.ts b/libs/components/src/checklist/context-id.ts index 13bdbd3b..7481c0f8 100644 --- a/libs/components/src/checklist/context-id.ts +++ b/libs/components/src/checklist/context-id.ts @@ -1,5 +1,5 @@ -import { Signal, createContextId } from '@builder.io/qwik'; -import { TriBool } from './checklist-context-wrapper'; +import { type Signal, createContextId } from '@builder.io/qwik'; +import type { TriBool } from './checklist-context-wrapper'; export type ArrSigs = Signal<boolean>[]; type CheckContextObj = { @@ -7,4 +7,5 @@ type CheckContextObj = { checkboxes: Signal<ArrSigs>; idArr: string[]; }; -export const CheckListContext = createContextId<CheckContextObj>('CheckList.context'); +export const CheckListContext = + createContextId<CheckContextObj>('CheckList.context'); diff --git a/libs/components/utils/unique-id.tsx b/libs/components/utils/unique-id.tsx new file mode 100644 index 00000000..e69de29b From 61bbf93c4584fa00df70efb07601ef4d83303b44 Mon Sep 17 00:00:00 2001 From: Jay-Kunaico <jay.brass@kunaico.com> Date: Tue, 27 Aug 2024 16:18:07 -0400 Subject: [PATCH 07/27] updated checklist and indicator --- .../src/checkbox/checkbox-indicator.tsx | 6 ++---- libs/components/src/checklist/checklist.tsx | 19 ++++++++++++------- libs/components/utils/unique-id.tsx | 0 3 files changed, 14 insertions(+), 11 deletions(-) delete mode 100644 libs/components/utils/unique-id.tsx diff --git a/libs/components/src/checkbox/checkbox-indicator.tsx b/libs/components/src/checkbox/checkbox-indicator.tsx index 6b2f6aea..809566d9 100644 --- a/libs/components/src/checkbox/checkbox-indicator.tsx +++ b/libs/components/src/checkbox/checkbox-indicator.tsx @@ -6,10 +6,8 @@ export type CheckboxIndicatorProps = PropsOf<'div'>; export const CheckboxIndicator = component$<CheckboxIndicatorProps>((props) => { const checkSig = useContext(CheckboxContext); return ( - <div {...props}> - <div hidden={!checkSig.value} aria-hidden="true"> - <Slot /> - </div> + <div hidden={!checkSig.value} aria-hidden="true" {...props}> + <Slot /> </div> ); }); diff --git a/libs/components/src/checklist/checklist.tsx b/libs/components/src/checklist/checklist.tsx index c07bb496..ac04d5ff 100644 --- a/libs/components/src/checklist/checklist.tsx +++ b/libs/components/src/checklist/checklist.tsx @@ -21,7 +21,7 @@ type CheckListProps = PropsOf<'ul'> & { ariaLabeledBy: string }; for more information. */ export const Checklist: Component<CheckListProps> = (props: CheckListProps) => { - const checkboxJSXArray: JSXNode[] = []; + const checkboxRootsJSXArray: JSXNode[] = []; const hellSigs = []; let checklistCheckbox = undefined; const boolArr: boolean[] = []; @@ -52,7 +52,7 @@ export const Checklist: Component<CheckListProps> = (props: CheckListProps) => { // FYI: Obj.assign mutates Object.assign(typedProps, { _useCheckListContext: true }); - checkboxJSXArray.push(child); + checkboxRootsJSXArray.push(child); // TODO: fix this if hell by making fn if (!typedProps.checklist) { checklistChilds.push(child); @@ -70,6 +70,7 @@ export const Checklist: Component<CheckListProps> = (props: CheckListProps) => { } } else { checklistCheckbox = child; + console.log('checklistCheckbox', checklistCheckbox); } break; @@ -87,9 +88,10 @@ export const Checklist: Component<CheckListProps> = (props: CheckListProps) => { } } } + if (checklistCheckbox === undefined) { throw Error( - "QWIKUI: checklist doesn't have a checkbox. Did you give the atribute to *checklist* to any of the checkboxes inside the checklist?" + "QWIK UI: checklist doesn't have a checkbox. Did you give the atribute of *checklist* to any of the checkboxes inside the checklist?" ); } @@ -108,10 +110,10 @@ export const Checklist: Component<CheckListProps> = (props: CheckListProps) => { idArr={idArr} > <ul class={props.class}> - {checkboxJSXArray.map((checkbox, i) => { + {checkboxRootsJSXArray.map((checkboxRootJSX, i) => { const uniqueId = generateUniqueId(); - return <li key={uniqueId}>{checkbox}</li>; + return <li key={uniqueId}>{checkboxRootJSX}</li>; })} </ul> </ChecklistContextWrapper> @@ -127,9 +129,9 @@ export const Checklist: Component<CheckListProps> = (props: CheckListProps) => { * returns: * * <Checklist.Item> - * <Checkbox.Root> + * <Checkbox.Root> * <Checkbox.Indicator /> - * </Checkbox.Root> + * </Checkbox.Root> * </Checklist.Item> * * @@ -140,6 +142,9 @@ export const Checklist: Component<CheckListProps> = (props: CheckListProps) => { * * * <Checklist.Root> + * <Checklist.SelectAll> + * <CheckListItem /> + * </Checklist.SelectAll> * <ChecklistItem /> * <ChecklistItem /> * ... diff --git a/libs/components/utils/unique-id.tsx b/libs/components/utils/unique-id.tsx deleted file mode 100644 index e69de29b..00000000 From 88852cd9f39cba09321e2226cc41f54ae668dd9b Mon Sep 17 00:00:00 2001 From: Jay-Kunaico <jay.brass@kunaico.com> Date: Tue, 27 Aug 2024 16:37:51 -0400 Subject: [PATCH 08/27] create outline for checkbox components --- .../components/src/checkbox/checkbox-root.tsx | 9 ++ libs/components/src/checkbox/research.mdx | 130 ++++++++++++++++++ .../checkbox/checkbox-indicator.tsx | 0 .../checkbox/checkbox.driver.ts | 0 .../{ => legacy-2}/checkbox/checkbox.test.ts | 0 .../src/{ => legacy-2}/checkbox/checkbox.tsx | 0 .../src/{ => legacy-2}/checkbox/context-id.ts | 0 .../src/{ => legacy-2}/checkbox/index.ts | 0 .../checklist/checklist-context-wrapper.tsx | 0 .../checklist/checklist-indicator.tsx | 0 .../{ => legacy-2}/checklist/checklist.tsx | 30 ---- .../{ => legacy-2}/checklist/context-id.ts | 0 .../src/{ => legacy-2}/checklist/index.ts | 0 13 files changed, 139 insertions(+), 30 deletions(-) create mode 100644 libs/components/src/checkbox/checkbox-root.tsx create mode 100644 libs/components/src/checkbox/research.mdx rename libs/components/src/{ => legacy-2}/checkbox/checkbox-indicator.tsx (100%) rename libs/components/src/{ => legacy-2}/checkbox/checkbox.driver.ts (100%) rename libs/components/src/{ => legacy-2}/checkbox/checkbox.test.ts (100%) rename libs/components/src/{ => legacy-2}/checkbox/checkbox.tsx (100%) rename libs/components/src/{ => legacy-2}/checkbox/context-id.ts (100%) rename libs/components/src/{ => legacy-2}/checkbox/index.ts (100%) rename libs/components/src/{ => legacy-2}/checklist/checklist-context-wrapper.tsx (100%) rename libs/components/src/{ => legacy-2}/checklist/checklist-indicator.tsx (100%) rename libs/components/src/{ => legacy-2}/checklist/checklist.tsx (89%) rename libs/components/src/{ => legacy-2}/checklist/context-id.ts (100%) rename libs/components/src/{ => legacy-2}/checklist/index.ts (100%) diff --git a/libs/components/src/checkbox/checkbox-root.tsx b/libs/components/src/checkbox/checkbox-root.tsx new file mode 100644 index 00000000..99871f42 --- /dev/null +++ b/libs/components/src/checkbox/checkbox-root.tsx @@ -0,0 +1,9 @@ +import { component$ } from '@builder.io/qwik'; + +export const CheckboxRoot = component$(() => { + return ( + <div> + <Slot /> + </div> + ); +}); diff --git a/libs/components/src/checkbox/research.mdx b/libs/components/src/checkbox/research.mdx new file mode 100644 index 00000000..1efb251d --- /dev/null +++ b/libs/components/src/checkbox/research.mdx @@ -0,0 +1,130 @@ +# Checkbox Research + +## Initial ideas + +Checkbox list -> inline component (increments index of checklist item similar to qwik ui carousel inline) + +IF YOU EVER NEED TO, YOU CAN ALSO DO SOMETHING LIKE THIS FOR CHECKLIST: + +TAKE A LOOK AT QWIK UI CAROUSEL CODE FOR INSPIRATION + +```tsx +const ChecklistItemIndicator = component$(() => { + return <Checkbox.Indicator />; +}); + +// in the case where you need to pass specific stuff +const ChecklistItem = component$(() => { + return ( + <Checkbox.Root> + <Slot /> + </Checkbox.Root> + ); +}); +``` + +Polymorphism: + +In this case, we can make it to where the Checkbox root should be polymorphic, meaning we can render it as something else, by specifying an `as` prop.A + +```tsx +import { component$, Slot } from '@builder.io/qwik'; +import type { QwikIntrinsicElements } from '@builder.io/qwik'; + +type AllowedElements = 'div' | 'li'; + +export const CheckboxRoot = component$( + <C extends AllowedElements = 'div'>( + props: QwikIntrinsicElements[C] & { as?: C } + ) => { + const { as, ...rest } = props; + const Comp = as ?? 'div'; + + return ( + <> + {/* @ts-expect-error annoying polymorphism */} + <Comp {...rest}> + <Slot /> + </Comp> + </> + ); + } +); + +// ChecklistRoot -> ul + +const ChecklistItem = component$(() => { + // our own checklist specific logic here + + return ( + <Checkbox.Root as="li"> + <Slot /> + </Checkbox.Root> + ); +}); + +// item and item indicator pattern is similar to select and combobox + +// IF THEY WANT CHECKLIST + +/** + * + * <Checklist.Root> + * <Checklist.SelectAll> + * <Checklist.Item> + * <Checklist.ItemIndicator /> + * </Checklist.Item> + * </Checklist.SelectAll> + * + * <Checklist.Item> + * <Checklist.ItemIndicator /> + * </Checklist.Item> + * + * <Checklist.Item> + * <Checklist.ItemIndicator /> + * </Checklist.Item> + * + * </Checklist.Root> + * + */ + +// IF THEY WANT CHECKBOX + +/** + * + * <Checkbox.Root> + * <Checkbox.Indicator /> + * </Checkbox.Root> + * + */ +``` + +## API Exploration + +### Components + +#### Checklist.Item + +- Wrapper component for a single checklist item + +#### Checkbox.Root + +- Root component for the checkbox + +#### Checkbox.Indicator + +- Indicator component for the checkbox state + +#### Checklist.Root + +- Container component for the entire checklist + +#### Checklist.SelectAll + +- Component for the "Select All" functionality + +#### ChecklistItem + +- User-defined component that combines Checklist.Item and Checkbox components + +### Usage Example diff --git a/libs/components/src/checkbox/checkbox-indicator.tsx b/libs/components/src/legacy-2/checkbox/checkbox-indicator.tsx similarity index 100% rename from libs/components/src/checkbox/checkbox-indicator.tsx rename to libs/components/src/legacy-2/checkbox/checkbox-indicator.tsx diff --git a/libs/components/src/checkbox/checkbox.driver.ts b/libs/components/src/legacy-2/checkbox/checkbox.driver.ts similarity index 100% rename from libs/components/src/checkbox/checkbox.driver.ts rename to libs/components/src/legacy-2/checkbox/checkbox.driver.ts diff --git a/libs/components/src/checkbox/checkbox.test.ts b/libs/components/src/legacy-2/checkbox/checkbox.test.ts similarity index 100% rename from libs/components/src/checkbox/checkbox.test.ts rename to libs/components/src/legacy-2/checkbox/checkbox.test.ts diff --git a/libs/components/src/checkbox/checkbox.tsx b/libs/components/src/legacy-2/checkbox/checkbox.tsx similarity index 100% rename from libs/components/src/checkbox/checkbox.tsx rename to libs/components/src/legacy-2/checkbox/checkbox.tsx diff --git a/libs/components/src/checkbox/context-id.ts b/libs/components/src/legacy-2/checkbox/context-id.ts similarity index 100% rename from libs/components/src/checkbox/context-id.ts rename to libs/components/src/legacy-2/checkbox/context-id.ts diff --git a/libs/components/src/checkbox/index.ts b/libs/components/src/legacy-2/checkbox/index.ts similarity index 100% rename from libs/components/src/checkbox/index.ts rename to libs/components/src/legacy-2/checkbox/index.ts diff --git a/libs/components/src/checklist/checklist-context-wrapper.tsx b/libs/components/src/legacy-2/checklist/checklist-context-wrapper.tsx similarity index 100% rename from libs/components/src/checklist/checklist-context-wrapper.tsx rename to libs/components/src/legacy-2/checklist/checklist-context-wrapper.tsx diff --git a/libs/components/src/checklist/checklist-indicator.tsx b/libs/components/src/legacy-2/checklist/checklist-indicator.tsx similarity index 100% rename from libs/components/src/checklist/checklist-indicator.tsx rename to libs/components/src/legacy-2/checklist/checklist-indicator.tsx diff --git a/libs/components/src/checklist/checklist.tsx b/libs/components/src/legacy-2/checklist/checklist.tsx similarity index 89% rename from libs/components/src/checklist/checklist.tsx rename to libs/components/src/legacy-2/checklist/checklist.tsx index ac04d5ff..c7e31b0c 100644 --- a/libs/components/src/checklist/checklist.tsx +++ b/libs/components/src/legacy-2/checklist/checklist.tsx @@ -121,33 +121,3 @@ export const Checklist: Component<CheckListProps> = (props: CheckListProps) => { ); }; // TODO: deprecate ariaLabelledBy - -/** - * - * User defined checkbox component: ChecklistItem - * - * returns: - * - * <Checklist.Item> - * <Checkbox.Root> - * <Checkbox.Indicator /> - * </Checkbox.Root> - * </Checklist.Item> - * - * - */ - -/** - * - * - * - * <Checklist.Root> - * <Checklist.SelectAll> - * <CheckListItem /> - * </Checklist.SelectAll> - * <ChecklistItem /> - * <ChecklistItem /> - * ... - * </Checklist.Root> - * - */ diff --git a/libs/components/src/checklist/context-id.ts b/libs/components/src/legacy-2/checklist/context-id.ts similarity index 100% rename from libs/components/src/checklist/context-id.ts rename to libs/components/src/legacy-2/checklist/context-id.ts diff --git a/libs/components/src/checklist/index.ts b/libs/components/src/legacy-2/checklist/index.ts similarity index 100% rename from libs/components/src/checklist/index.ts rename to libs/components/src/legacy-2/checklist/index.ts From a18c0e084ed2bea5bcc0700bce9820f411359d5a Mon Sep 17 00:00:00 2001 From: Jay-Kunaico <jay.brass@kunaico.com> Date: Wed, 28 Aug 2024 12:10:37 -0400 Subject: [PATCH 09/27] New Checkbox and files --- .../src/routes/checkbox/examples/hero.tsx | 37 +++++++++++++------ apps/docs/src/routes/checkbox/index.mdx | 6 +-- .../src/checkbox/checkbox-context.tsx | 4 ++ .../src/checkbox/checkbox-indicator.tsx | 15 ++++++++ .../components/src/checkbox/checkbox-root.tsx | 31 ++++++++++++++-- libs/components/src/checkbox/index.ts | 2 + libs/components/src/index.ts | 2 +- libs/components/utils/bound-signal.tsx | 2 - 8 files changed, 79 insertions(+), 20 deletions(-) create mode 100644 libs/components/src/checkbox/checkbox-context.tsx create mode 100644 libs/components/src/checkbox/checkbox-indicator.tsx create mode 100644 libs/components/src/checkbox/index.ts diff --git a/apps/docs/src/routes/checkbox/examples/hero.tsx b/apps/docs/src/routes/checkbox/examples/hero.tsx index ec329b03..8d0fe93c 100644 --- a/apps/docs/src/routes/checkbox/examples/hero.tsx +++ b/apps/docs/src/routes/checkbox/examples/hero.tsx @@ -1,17 +1,32 @@ import { component$, useSignal, useStyles$ } from '@builder.io/qwik'; import { Checkbox } from '@kunai-consulting/qwik-components'; export default component$(() => { + const bindChecked = useSignal(false); return ( - <> - <Checkbox.Root - id="test" - class="flex items-center gap-3 border-2 border-black p-2 " - > - <Checkbox.Indicator class="flex h-[25px] w-[25px] items-center justify-center bg-slate-600"> - ✅ - </Checkbox.Indicator> - I have read the README - </Checkbox.Root> - </> + <Checkbox.Root + bindChecked={bindChecked} + id="test" + class="flex items-center gap-3 border-2 border-black p-2" + > + <Checkbox.Indicator class="flex h-[25px] w-[25px] items-center justify-center bg-slate-600"> + ✅ + </Checkbox.Indicator> + I have read the README + </Checkbox.Root> ); }); + +// <div class="flex gap-8"> +// <div class="flex flex-col gap-3"> +// <Checkbox.TwoState +// bind:checked={isChecked} +// id="test" +// class="flex items-center gap-3 border-2 border-black p-2 " +// > +// <Checkbox.Indicator class="flex h-[25px] w-[25px] items-center justify-center bg-slate-600"> +// ✅ +// </Checkbox.Indicator> +// Toggle Value +// </Checkbox.TwoState> +// </div> +// </div> diff --git a/apps/docs/src/routes/checkbox/index.mdx b/apps/docs/src/routes/checkbox/index.mdx index c45f91a7..9f1d8951 100644 --- a/apps/docs/src/routes/checkbox/index.mdx +++ b/apps/docs/src/routes/checkbox/index.mdx @@ -16,13 +16,13 @@ Allows users to visually select an option by checking it. To add reactive state, use the `bind:checked` prop on the `<Checkbox.Root />` component. -<Showcase name="controlled" /> +{/* <Showcase name="controlled" /> */} > Note that the initial value of the signal will affect the initial value of the checkbox. In the example below, the left checkbox starts as **false**, while the right checkbox starts as **true**. -<Showcase name="controlled-values" /> +{/* <Showcase name="controlled-values" /> */} ## Customization & Caveats @@ -38,4 +38,4 @@ Text tags lose their semantic meaning. For example, an h1 tag is treated the sam ### Checkbox.Root -<Showcase name="pizza" /> +{/* <Showcase name="pizza" /> */} diff --git a/libs/components/src/checkbox/checkbox-context.tsx b/libs/components/src/checkbox/checkbox-context.tsx new file mode 100644 index 00000000..b5e8c534 --- /dev/null +++ b/libs/components/src/checkbox/checkbox-context.tsx @@ -0,0 +1,4 @@ +import { createContextId, type Signal } from '@builder.io/qwik'; + +export const CheckboxContext = + createContextId<Signal<boolean>>('CheckBox.context'); diff --git a/libs/components/src/checkbox/checkbox-indicator.tsx b/libs/components/src/checkbox/checkbox-indicator.tsx new file mode 100644 index 00000000..902e4bab --- /dev/null +++ b/libs/components/src/checkbox/checkbox-indicator.tsx @@ -0,0 +1,15 @@ +import { component$, useContext, type PropsOf, Slot } from '@builder.io/qwik'; +import { CheckboxContext } from './checkbox-context'; + +export type CheckboxIndicatorProps = PropsOf<'div'>; + +export const CheckboxIndicator = component$<CheckboxIndicatorProps>((props) => { + const checkSig = useContext(CheckboxContext); + return ( + <div {...props}> + <div hidden={!checkSig.value} aria-hidden="true"> + <Slot /> + </div> + </div> + ); +}); diff --git a/libs/components/src/checkbox/checkbox-root.tsx b/libs/components/src/checkbox/checkbox-root.tsx index 99871f42..4c0e3645 100644 --- a/libs/components/src/checkbox/checkbox-root.tsx +++ b/libs/components/src/checkbox/checkbox-root.tsx @@ -1,8 +1,33 @@ -import { component$ } from '@builder.io/qwik'; +import { + component$, + type PropsOf, + Slot, + type Signal, + $, + useContextProvider, +} from '@builder.io/qwik'; +import { useBoundSignal } from '../../utils/bound-signal'; +import { CheckboxContext } from './checkbox-context'; -export const CheckboxRoot = component$(() => { +export type CheckboxProps = { + bindChecked: Signal<boolean>; + initialValue?: boolean; +} & PropsOf<'div'>; + +export const CheckboxRoot = component$<CheckboxProps>((props) => { + const checkedSignal = useBoundSignal(props.bindChecked, props.initialValue); + useContextProvider(CheckboxContext, checkedSignal); + const handleClick = $(() => { + checkedSignal.value = !checkedSignal.value; + }); return ( - <div> + <div + {...props} + tabIndex={0} + role="checkbox" + aria-checked={checkedSignal.value} + onClick$={handleClick} + > <Slot /> </div> ); diff --git a/libs/components/src/checkbox/index.ts b/libs/components/src/checkbox/index.ts new file mode 100644 index 00000000..b16beff2 --- /dev/null +++ b/libs/components/src/checkbox/index.ts @@ -0,0 +1,2 @@ +export { CheckboxRoot as Root } from './checkbox-root'; +export { CheckboxIndicator as Indicator } from './checkbox-indicator'; diff --git a/libs/components/src/index.ts b/libs/components/src/index.ts index 607892ca..8417197c 100644 --- a/libs/components/src/index.ts +++ b/libs/components/src/index.ts @@ -1,3 +1,3 @@ export * as Otp from './otp'; export * as Checkbox from './checkbox'; -export * as Checklist from './checklist'; +// export * as Checklist from './checklist'; diff --git a/libs/components/utils/bound-signal.tsx b/libs/components/utils/bound-signal.tsx index 6e8a5df6..761fa100 100644 --- a/libs/components/utils/bound-signal.tsx +++ b/libs/components/utils/bound-signal.tsx @@ -19,9 +19,7 @@ export function useBoundSignal<T>( const internalSignal = useSignal<T>( givenSignal?.value ?? (initialValue as T) ); - console.log('useBoundSignal internalSignal', internalSignal); const boundSignal = givenSignal ?? internalSignal; - console.log('useBoundSignal boundSignal', boundSignal); useTask$(({ track }) => { const value = track(() => boundSignal.value); From 599677bd9b7e5b4bda0ca3444c561c9dbdf4d98d Mon Sep 17 00:00:00 2001 From: Jay-Kunaico <jay.brass@kunaico.com> Date: Wed, 28 Aug 2024 12:28:27 -0400 Subject: [PATCH 10/27] All tests pass for Checkbox --- .../src/routes/checkbox/examples/test-default.tsx | 5 +++-- .../checkbox/examples/test-initial-checked.tsx | 2 +- libs/components/src/checkbox/checkbox-root.tsx | 15 +++++++++++++++ 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/apps/docs/src/routes/checkbox/examples/test-default.tsx b/apps/docs/src/routes/checkbox/examples/test-default.tsx index 16df66dd..160e1503 100644 --- a/apps/docs/src/routes/checkbox/examples/test-default.tsx +++ b/apps/docs/src/routes/checkbox/examples/test-default.tsx @@ -1,10 +1,11 @@ import { component$, useSignal, useStyles$ } from '@builder.io/qwik'; -import { Checkbox, Checklist } from '@kunai-consulting/qwik-components'; +import { Checkbox } from '@kunai-consulting/qwik-components'; export default component$(() => { + const bindChecked = useSignal(false); return ( <> <p>I'm the default checkbox!!!</p> - <Checkbox.Root class=" text-white"> + <Checkbox.Root class=" text-white" bindChecked={bindChecked}> <div class="flex items-center gap-3"> <Checkbox.Indicator class="w-fit bg-slate-600"> <p id="indicator">✅</p> diff --git a/apps/docs/src/routes/checkbox/examples/test-initial-checked.tsx b/apps/docs/src/routes/checkbox/examples/test-initial-checked.tsx index 970a188a..0a1dc145 100644 --- a/apps/docs/src/routes/checkbox/examples/test-initial-checked.tsx +++ b/apps/docs/src/routes/checkbox/examples/test-initial-checked.tsx @@ -5,7 +5,7 @@ export default component$(() => { const userSig = useSignal(true); return ( <> - <Checkbox.Root class="bg-slate-900 text-white" bind:checked={userSig}> + <Checkbox.Root class="bg-slate-900 text-white" bindChecked={userSig}> <div class="flex items-center gap-3"> <Checkbox.Indicator class="w-fit bg-slate-600"> <p id="indicator">✅</p> diff --git a/libs/components/src/checkbox/checkbox-root.tsx b/libs/components/src/checkbox/checkbox-root.tsx index 4c0e3645..1d8d8b82 100644 --- a/libs/components/src/checkbox/checkbox-root.tsx +++ b/libs/components/src/checkbox/checkbox-root.tsx @@ -5,6 +5,7 @@ import { type Signal, $, useContextProvider, + sync$, } from '@builder.io/qwik'; import { useBoundSignal } from '../../utils/bound-signal'; import { CheckboxContext } from './checkbox-context'; @@ -17,9 +18,21 @@ export type CheckboxProps = { export const CheckboxRoot = component$<CheckboxProps>((props) => { const checkedSignal = useBoundSignal(props.bindChecked, props.initialValue); useContextProvider(CheckboxContext, checkedSignal); + + const handleKeyDownSync$ = sync$((e: KeyboardEvent) => { + if (e.key === ' ') { + e.preventDefault(); + } + }); const handleClick = $(() => { checkedSignal.value = !checkedSignal.value; }); + const handleKeyDown$ = $((e: KeyboardEvent) => { + if (e.key === ' ') { + props.bindChecked.value = !props.bindChecked.value; + } + }); + return ( <div {...props} @@ -27,6 +40,8 @@ export const CheckboxRoot = component$<CheckboxProps>((props) => { role="checkbox" aria-checked={checkedSignal.value} onClick$={handleClick} + onKeyDown$={[handleKeyDownSync$, handleKeyDown$]} + onKeyPress$={handleClick} > <Slot /> </div> From 98ba16915d11f6a8ecfba478ae378df77101712f Mon Sep 17 00:00:00 2001 From: Jay-Kunaico <jay.brass@kunaico.com> Date: Wed, 28 Aug 2024 16:48:00 -0400 Subject: [PATCH 11/27] Add Checklist components --- .../routes/checkbox/examples/checklist.tsx | 21 +++++++++++++ .../src/routes/checkbox/examples/hero.tsx | 15 --------- .../routes/checkbox/examples/test-default.tsx | 6 ++-- .../examples/test-initial-checked.tsx | 6 ++-- apps/docs/src/routes/checkbox/index.mdx | 2 ++ .../src/checklist/checklist-context.tsx | 11 +++++++ .../src/checklist/checklist-indicator.tsx | 14 +++++++++ .../src/checklist/checklist-item.tsx | 9 ++++++ .../src/checklist/checklist-root.tsx | 15 +++++++++ .../src/checklist/checklist-selectall.tsx | 17 ++++++++++ libs/components/src/checklist/index.ts | 4 +++ .../src/checklist/use-checklist.tsx | 31 +++++++++++++++++++ libs/components/src/index.ts | 2 +- 13 files changed, 130 insertions(+), 23 deletions(-) create mode 100644 apps/docs/src/routes/checkbox/examples/checklist.tsx create mode 100644 libs/components/src/checklist/checklist-context.tsx create mode 100644 libs/components/src/checklist/checklist-indicator.tsx create mode 100644 libs/components/src/checklist/checklist-item.tsx create mode 100644 libs/components/src/checklist/checklist-root.tsx create mode 100644 libs/components/src/checklist/checklist-selectall.tsx create mode 100644 libs/components/src/checklist/index.ts create mode 100644 libs/components/src/checklist/use-checklist.tsx diff --git a/apps/docs/src/routes/checkbox/examples/checklist.tsx b/apps/docs/src/routes/checkbox/examples/checklist.tsx new file mode 100644 index 00000000..c93227dc --- /dev/null +++ b/apps/docs/src/routes/checkbox/examples/checklist.tsx @@ -0,0 +1,21 @@ +import { component$ } from '@builder.io/qwik'; + +import { Checklist } from '@kunai-consulting/qwik-components'; + +export default component$(() => { + return ( + <Checklist.Root initialStates={[false, false, false]}> + <Checklist.SelectAll> + <Checklist.Item> + <Checklist.ItemIndicator index={0} /> + </Checklist.Item> + </Checklist.SelectAll> + <Checklist.Item> + <Checklist.ItemIndicator index={1} /> first item + </Checklist.Item> + <Checklist.Item> + <Checklist.ItemIndicator index={2} /> second item + </Checklist.Item> + </Checklist.Root> + ); +}); diff --git a/apps/docs/src/routes/checkbox/examples/hero.tsx b/apps/docs/src/routes/checkbox/examples/hero.tsx index 8d0fe93c..d5fe3bba 100644 --- a/apps/docs/src/routes/checkbox/examples/hero.tsx +++ b/apps/docs/src/routes/checkbox/examples/hero.tsx @@ -15,18 +15,3 @@ export default component$(() => { </Checkbox.Root> ); }); - -// <div class="flex gap-8"> -// <div class="flex flex-col gap-3"> -// <Checkbox.TwoState -// bind:checked={isChecked} -// id="test" -// class="flex items-center gap-3 border-2 border-black p-2 " -// > -// <Checkbox.Indicator class="flex h-[25px] w-[25px] items-center justify-center bg-slate-600"> -// ✅ -// </Checkbox.Indicator> -// Toggle Value -// </Checkbox.TwoState> -// </div> -// </div> diff --git a/apps/docs/src/routes/checkbox/examples/test-default.tsx b/apps/docs/src/routes/checkbox/examples/test-default.tsx index 160e1503..78598668 100644 --- a/apps/docs/src/routes/checkbox/examples/test-default.tsx +++ b/apps/docs/src/routes/checkbox/examples/test-default.tsx @@ -1,16 +1,16 @@ import { component$, useSignal, useStyles$ } from '@builder.io/qwik'; import { Checkbox } from '@kunai-consulting/qwik-components'; + export default component$(() => { const bindChecked = useSignal(false); return ( <> - <p>I'm the default checkbox!!!</p> - <Checkbox.Root class=" text-white" bindChecked={bindChecked}> + <p>default checkbox</p> + <Checkbox.Root class="bg-slate-900 text-white" bindChecked={bindChecked}> <div class="flex items-center gap-3"> <Checkbox.Indicator class="w-fit bg-slate-600"> <p id="indicator">✅</p> </Checkbox.Indicator> - <p>No other stuff is needed here</p> </div> </Checkbox.Root> </> diff --git a/apps/docs/src/routes/checkbox/examples/test-initial-checked.tsx b/apps/docs/src/routes/checkbox/examples/test-initial-checked.tsx index 0a1dc145..30d7bfd5 100644 --- a/apps/docs/src/routes/checkbox/examples/test-initial-checked.tsx +++ b/apps/docs/src/routes/checkbox/examples/test-initial-checked.tsx @@ -2,18 +2,16 @@ import { component$, useSignal, useStyles$ } from '@builder.io/qwik'; import { Checkbox } from '@kunai-consulting/qwik-components'; export default component$(() => { - const userSig = useSignal(true); + const bindChecked = useSignal(true); return ( <> - <Checkbox.Root class="bg-slate-900 text-white" bindChecked={userSig}> + <Checkbox.Root class="bg-slate-900 text-white" bindChecked={bindChecked}> <div class="flex items-center gap-3"> <Checkbox.Indicator class="w-fit bg-slate-600"> <p id="indicator">✅</p> </Checkbox.Indicator> - <p>No other stuff is needed here</p> </div> </Checkbox.Root> - <div>{`${userSig.value}`}</div> </> ); }); diff --git a/apps/docs/src/routes/checkbox/index.mdx b/apps/docs/src/routes/checkbox/index.mdx index 9f1d8951..eae51d53 100644 --- a/apps/docs/src/routes/checkbox/index.mdx +++ b/apps/docs/src/routes/checkbox/index.mdx @@ -39,3 +39,5 @@ Text tags lose their semantic meaning. For example, an h1 tag is treated the sam ### Checkbox.Root {/* <Showcase name="pizza" /> */} + +<Showcase name="checklist" /> diff --git a/libs/components/src/checklist/checklist-context.tsx b/libs/components/src/checklist/checklist-context.tsx new file mode 100644 index 00000000..6b8fcceb --- /dev/null +++ b/libs/components/src/checklist/checklist-context.tsx @@ -0,0 +1,11 @@ +import { createContextId, type Signal } from '@builder.io/qwik'; + +export interface ChecklistState { + items: Signal<boolean[]>; + toggleItem: (index: number) => void; + allSelected: Signal<boolean>; + toggleAllSelected: () => void; +} + +export const ChecklistContext = + createContextId<ChecklistState>('ChecklistContext'); diff --git a/libs/components/src/checklist/checklist-indicator.tsx b/libs/components/src/checklist/checklist-indicator.tsx new file mode 100644 index 00000000..c23eb8d8 --- /dev/null +++ b/libs/components/src/checklist/checklist-indicator.tsx @@ -0,0 +1,14 @@ +import { component$, useContext } from '@builder.io/qwik'; +import { ChecklistContext } from './checklist-context'; + +export const ChecklistItemIndicator = component$((props: { index: number }) => { + const { items, toggleItem } = useContext(ChecklistContext); + + return ( + <input + type="checkbox" + checked={items.value[props.index]} + onClick$={() => toggleItem(props.index)} + /> + ); +}); diff --git a/libs/components/src/checklist/checklist-item.tsx b/libs/components/src/checklist/checklist-item.tsx new file mode 100644 index 00000000..7250ff35 --- /dev/null +++ b/libs/components/src/checklist/checklist-item.tsx @@ -0,0 +1,9 @@ +import { component$, Slot } from '@builder.io/qwik'; + +export const ChecklistItem = component$(() => { + return ( + <li> + <Slot /> + </li> + ); +}); diff --git a/libs/components/src/checklist/checklist-root.tsx b/libs/components/src/checklist/checklist-root.tsx new file mode 100644 index 00000000..40168587 --- /dev/null +++ b/libs/components/src/checklist/checklist-root.tsx @@ -0,0 +1,15 @@ +import { component$, Slot } from '@builder.io/qwik'; +import { useChecklist } from './use-checklist'; + +export const ChecklistRoot = component$( + (props: { initialStates: boolean[] }) => { + const { initialStates } = props; + useChecklist(initialStates); + + return ( + <ul> + <Slot /> + </ul> + ); + } +); diff --git a/libs/components/src/checklist/checklist-selectall.tsx b/libs/components/src/checklist/checklist-selectall.tsx new file mode 100644 index 00000000..c2fc57b8 --- /dev/null +++ b/libs/components/src/checklist/checklist-selectall.tsx @@ -0,0 +1,17 @@ +import { component$, useContext, $ } from '@builder.io/qwik'; +import { ChecklistContext } from './checklist-context'; + +export const ChecklistSelectAll = component$(() => { + const { allSelected, toggleAllSelected } = useContext(ChecklistContext); + return ( + <li> + <input + type="checkbox" + checked={allSelected.value} + onClick$={() => toggleAllSelected()} + /> + {' '} + Select All + </li> + ); +}); diff --git a/libs/components/src/checklist/index.ts b/libs/components/src/checklist/index.ts new file mode 100644 index 00000000..d315123f --- /dev/null +++ b/libs/components/src/checklist/index.ts @@ -0,0 +1,4 @@ +export { ChecklistRoot as Root } from './checklist-root'; +export { ChecklistSelectAll as SelectAll } from './checklist-selectall'; +export { ChecklistItem as Item } from './checklist-item'; +export { ChecklistItemIndicator as ItemIndicator } from './checklist-indicator'; diff --git a/libs/components/src/checklist/use-checklist.tsx b/libs/components/src/checklist/use-checklist.tsx new file mode 100644 index 00000000..76868203 --- /dev/null +++ b/libs/components/src/checklist/use-checklist.tsx @@ -0,0 +1,31 @@ +import { useContextProvider, $, useSignal } from '@builder.io/qwik'; +import { ChecklistContext } from './checklist-context'; + +export const useChecklist = (initialItems: boolean[]) => { + const items = useSignal(initialItems); + const allSelected = useSignal(initialItems.every(Boolean)); + + const setItems = $((newItems: boolean[]) => { + items.value = newItems; + allSelected.value = newItems.every(Boolean); + }); + + const toggleItem = $((index: number) => { + const newItems = [...items.value]; + newItems[index] = !newItems[index]; + setItems(newItems); + }); + + const toggleAllSelected = $(() => { + const newState = !allSelected.value; + allSelected.value = newState; + items.value = items.value.map(() => newState); + }); + + useContextProvider(ChecklistContext, { + items, + toggleItem, + allSelected, + toggleAllSelected, + }); +}; diff --git a/libs/components/src/index.ts b/libs/components/src/index.ts index 8417197c..607892ca 100644 --- a/libs/components/src/index.ts +++ b/libs/components/src/index.ts @@ -1,3 +1,3 @@ export * as Otp from './otp'; export * as Checkbox from './checkbox'; -// export * as Checklist from './checklist'; +export * as Checklist from './checklist'; From 693bce01d5fa774efdfea0be413c7ddfa1fa8cbd Mon Sep 17 00:00:00 2001 From: Jay-Kunaico <jay.brass@kunaico.com> Date: Thu, 29 Aug 2024 13:07:44 -0400 Subject: [PATCH 12/27] Checklist and updated checkbox tests --- .../routes/checkbox/examples/test-default.tsx | 20 ++-- apps/docs/src/routes/checkbox/index.mdx | 2 + .../src/checkbox/checkbox-indicator.tsx | 4 +- .../checkbox/checkbox.driver.ts | 0 .../{legacy-2 => }/checkbox/checkbox.test.ts | 109 ++++++++---------- .../src/checklist/checklist-indicator.tsx | 4 +- 6 files changed, 62 insertions(+), 77 deletions(-) rename libs/components/src/{legacy-2 => }/checkbox/checkbox.driver.ts (100%) rename libs/components/src/{legacy-2 => }/checkbox/checkbox.test.ts (89%) diff --git a/apps/docs/src/routes/checkbox/examples/test-default.tsx b/apps/docs/src/routes/checkbox/examples/test-default.tsx index 78598668..68267a08 100644 --- a/apps/docs/src/routes/checkbox/examples/test-default.tsx +++ b/apps/docs/src/routes/checkbox/examples/test-default.tsx @@ -4,15 +4,15 @@ import { Checkbox } from '@kunai-consulting/qwik-components'; export default component$(() => { const bindChecked = useSignal(false); return ( - <> - <p>default checkbox</p> - <Checkbox.Root class="bg-slate-900 text-white" bindChecked={bindChecked}> - <div class="flex items-center gap-3"> - <Checkbox.Indicator class="w-fit bg-slate-600"> - <p id="indicator">✅</p> - </Checkbox.Indicator> - </div> - </Checkbox.Root> - </> + <Checkbox.Root + bindChecked={bindChecked} + id="test" + class="flex items-center gap-3 border-2 border-black p-2" + > + <Checkbox.Indicator class="flex h-[25px] w-[25px] items-center justify-center bg-slate-600"> + <p id="indicator">✅</p> + </Checkbox.Indicator> + I have read the README + </Checkbox.Root> ); }); diff --git a/apps/docs/src/routes/checkbox/index.mdx b/apps/docs/src/routes/checkbox/index.mdx index eae51d53..ca93820e 100644 --- a/apps/docs/src/routes/checkbox/index.mdx +++ b/apps/docs/src/routes/checkbox/index.mdx @@ -6,6 +6,8 @@ title: Qwik UI | Checkbox Allows users to visually select an option by checking it. +<Showcase name="test-default" /> + <Showcase name="hero" /> ## Anatomy diff --git a/libs/components/src/checkbox/checkbox-indicator.tsx b/libs/components/src/checkbox/checkbox-indicator.tsx index 902e4bab..0774fa54 100644 --- a/libs/components/src/checkbox/checkbox-indicator.tsx +++ b/libs/components/src/checkbox/checkbox-indicator.tsx @@ -6,8 +6,8 @@ export type CheckboxIndicatorProps = PropsOf<'div'>; export const CheckboxIndicator = component$<CheckboxIndicatorProps>((props) => { const checkSig = useContext(CheckboxContext); return ( - <div {...props}> - <div hidden={!checkSig.value} aria-hidden="true"> + <div> + <div hidden={!checkSig.value} aria-hidden="true" {...props}> <Slot /> </div> </div> diff --git a/libs/components/src/legacy-2/checkbox/checkbox.driver.ts b/libs/components/src/checkbox/checkbox.driver.ts similarity index 100% rename from libs/components/src/legacy-2/checkbox/checkbox.driver.ts rename to libs/components/src/checkbox/checkbox.driver.ts diff --git a/libs/components/src/legacy-2/checkbox/checkbox.test.ts b/libs/components/src/checkbox/checkbox.test.ts similarity index 89% rename from libs/components/src/legacy-2/checkbox/checkbox.test.ts rename to libs/components/src/checkbox/checkbox.test.ts index 67d58e8d..5d54bfa1 100644 --- a/libs/components/src/legacy-2/checkbox/checkbox.test.ts +++ b/libs/components/src/checkbox/checkbox.test.ts @@ -1,6 +1,6 @@ import { expect, test, type Page } from '@playwright/test'; import { createTestDriver } from './checkbox.driver'; -import { getTriBool } from '../checklist/checklist-context-wrapper'; +import { getTriBool } from '../legacy-2/checklist/checklist-context-wrapper'; async function setup(page: Page, exampleName: string) { await page.goto(`http://localhost:6174/checkbox/${exampleName}`); @@ -29,97 +29,78 @@ async function setup(page: Page, exampleName: string) { } test.describe('checkbox', () => { - test(`GIVEN a checkbox - WHEN the checkbox renders - It should have aria-checked as false`, async ({ page }) => { - const exampleName = 'hero'; - const { getCheckbox } = await setup(page, exampleName); - await expect(getCheckbox()).toHaveAttribute('aria-checked', 'false'); - }); - - test(`GIVEN a checkbox + // test default state and click from unchecked checkbox + test(`GIVEN a checkbox that is initially unchecked + and its icon is hidden WHEN the checkbox is clicked - It should have aria-checked as true`, async ({ page }) => { - const exampleName = 'hero'; - const { getCheckbox } = await setup(page, exampleName); - await getCheckbox().click(); - await expect(getCheckbox()).toHaveAttribute('aria-checked', 'true'); - }); - - test(`GIVEN a checkbox that is checked - WHEN the checkbox renders - IT should have aria-checked as true`, async ({ page }) => { - const exampleName = 'test-initial-checked'; - const { getCheckbox } = await setup(page, exampleName); - await expect(getCheckbox()).toBeVisible(); - await expect(getCheckbox()).toHaveAttribute('aria-checked', 'true'); - }); - - test(`GIVEN a checkbox that is checked - WHEN the spacebar is pressed - IT should have aria-checked as false`, async ({ page }) => { - const exampleName = 'test-initial-checked'; - const { getCheckbox } = await setup(page, exampleName); - await expect(getCheckbox()).toBeVisible(); - await getCheckbox().press(' '); - await expect(getCheckbox()).toHaveAttribute('aria-checked', 'false'); - }); - - test(`GIVEN a checkbox with a user sig value of true - WHEN the checkbox is focused and the spacebar is pressed - IT should have its icon hidden`, async ({ page }) => { - const exampleName = 'test-initial-checked'; + IT should toggle aria-checked to true + and make its icon visible`, async ({ page }) => { + const exampleName = 'test-default'; const { getCheckbox, getIcon } = await setup(page, exampleName); - await expect(getIcon()).toBeVisible(); - await getCheckbox().focus(); - await getCheckbox().press(' '); await expect(getCheckbox()).toHaveAttribute('aria-checked', 'false'); await expect(getIcon()).toBeHidden(); + await getCheckbox().click(); + await expect(getCheckbox()).toHaveAttribute('aria-checked', 'true'); + await expect(getIcon()).toBeVisible(); }); - test(`GIVEN a default checkbox with a default sig value of false - WHEN the checkbox is focused and the spacebar is pressed - IT should have its icon visible`, async ({ page }) => { + + // test default state and keyboard space from unchecked checkbox + test(`GIVEN a checkbox that is initially unchecked + and its icon is hidden + WHEN the checkbox is focused and the spacebar is pressed + IT should toggle aria-checked to true + and make its icon visible`, async ({ page }) => { const exampleName = 'test-default'; const { getCheckbox, getIcon } = await setup(page, exampleName); + await expect(getCheckbox()).toHaveAttribute('aria-checked', 'false'); await expect(getIcon()).toBeHidden(); await getCheckbox().focus(); await getCheckbox().press(' '); - await expect(getCheckbox()).toHaveAttribute('aria-checked', 'false'); + await expect(getCheckbox()).toHaveAttribute('aria-checked', 'true'); await expect(getIcon()).toBeVisible(); }); - test(`GIVEN a checkbox with a user sig value of true - WHEN the checkbox is clicked - IT should have aria-checked as false`, async ({ page }) => { + // test default state from checked checkbox + test(`GIVEN a checkbox that is checked + WHEN the checkbox renders + IT should have aria-checked as true + and its icon visible`, async ({ page }) => { const exampleName = 'test-initial-checked'; - const { getCheckbox } = await setup(page, exampleName); + const { getCheckbox, getIcon } = await setup(page, exampleName); await expect(getCheckbox()).toBeVisible(); - await getCheckbox().click(); - await expect(getCheckbox()).toHaveAttribute('aria-checked', 'false'); + await expect(getCheckbox()).toHaveAttribute('aria-checked', 'true'); + await expect(getIcon()).toBeVisible(); }); - test(`GIVEN a checkbox with a user sig value of true + // test mouse click + test(`GIVEN a checkbox that is checked WHEN the checkbox is clicked - IT should have its icon hidden`, async ({ page }) => { + IT should have aria-checked as false + and its icon hidden`, async ({ page }) => { const exampleName = 'test-initial-checked'; const { getCheckbox, getIcon } = await setup(page, exampleName); - await expect(getIcon()).toBeVisible(); + await expect(getCheckbox()).toBeVisible(); await getCheckbox().click(); await expect(getCheckbox()).toHaveAttribute('aria-checked', 'false'); await expect(getIcon()).toBeHidden(); }); - test(`GIVEN a default checkbox with a default sig value of false - WHEN the checkbox is clicked - IT should have its icon visible`, async ({ page }) => { - const exampleName = 'test-default'; + + // test keyboard space from checked checkbox + test(`GIVEN a checkbox that is checked + WHEN the checkbox is focused and the spacebar is pressed + IT should have aria-checked as false + and its icon hidden`, async ({ page }) => { + const exampleName = 'test-initial-checked'; const { getCheckbox, getIcon } = await setup(page, exampleName); - await expect(getIcon()).toBeHidden(); - await getCheckbox().click(); + await expect(getCheckbox()).toBeVisible(); + await getCheckbox().focus(); + await getCheckbox().press(' '); await expect(getCheckbox()).toHaveAttribute('aria-checked', 'false'); - await expect(getIcon()).toBeVisible(); + await expect(getIcon()).toBeHidden(); }); }); +// Checklisst tests test.describe('checklist', () => { test(`GIVEN a mixed checklist WHEN the checklist renders @@ -138,7 +119,7 @@ test.describe('checklist', () => { const exampleName = 'test-controlled-list-true'; await setup(page, exampleName); await expect(page.locator('#true-img')).toBeVisible(); - await expect(page.locator('#mixed-img')).toBeHidden(); + // await expect(page.locator('#mixed-img')).toBeHidden(); }); test(`GIVEN an all-unchecked checklist diff --git a/libs/components/src/checklist/checklist-indicator.tsx b/libs/components/src/checklist/checklist-indicator.tsx index c23eb8d8..00340d9a 100644 --- a/libs/components/src/checklist/checklist-indicator.tsx +++ b/libs/components/src/checklist/checklist-indicator.tsx @@ -1,6 +1,8 @@ -import { component$, useContext } from '@builder.io/qwik'; +import { component$, type PropsOf, useContext } from '@builder.io/qwik'; import { ChecklistContext } from './checklist-context'; +export type ChecklistItemIndicatorProps = PropsOf<'div'>; + export const ChecklistItemIndicator = component$((props: { index: number }) => { const { items, toggleItem } = useContext(ChecklistContext); From dae75b2c181a9550ed770d345fee0ebd07e75a1b Mon Sep 17 00:00:00 2001 From: Jay-Kunaico <jay.brass@kunaico.com> Date: Fri, 30 Aug 2024 16:04:33 -0400 Subject: [PATCH 13/27] updated property and data-hidden for checkbox Co-authored-by: Jack Shelton <thejackshelton@users.noreply.github.com> --- .../routes/checkbox/examples/checklist.tsx | 23 +- .../src/routes/checkbox/examples/hero.tsx | 10 +- .../src/routes/checkbox/examples/pizza.tsx | 80 +-- .../examples/test-controlled-list-true.tsx | 59 +-- .../src/checkbox/checkbox-indicator.tsx | 29 +- .../components/src/checkbox/checkbox-root.tsx | 64 +-- libs/components/src/checkbox/checkbox.css | 3 + libs/components/src/checkbox/checkbox.test.ts | 31 ++ .../src/checklist/checklist-indicator.tsx | 36 +- .../src/checklist/checklist-selectall.tsx | 19 +- .../src/checklist/use-checklist.tsx | 6 +- .../src/legacy-2/checkbox/checkbox.tsx | 480 +++++++++--------- .../components/src/legacy-2/checkbox/index.ts | 2 +- .../src/legacy-2/checklist/checklist.tsx | 246 ++++----- .../src/legacy-2/checklist/index.ts | 2 +- 15 files changed, 577 insertions(+), 513 deletions(-) create mode 100644 libs/components/src/checkbox/checkbox.css diff --git a/apps/docs/src/routes/checkbox/examples/checklist.tsx b/apps/docs/src/routes/checkbox/examples/checklist.tsx index c93227dc..06b35f8a 100644 --- a/apps/docs/src/routes/checkbox/examples/checklist.tsx +++ b/apps/docs/src/routes/checkbox/examples/checklist.tsx @@ -5,16 +5,27 @@ import { Checklist } from '@kunai-consulting/qwik-components'; export default component$(() => { return ( <Checklist.Root initialStates={[false, false, false]}> - <Checklist.SelectAll> - <Checklist.Item> - <Checklist.ItemIndicator index={0} /> - </Checklist.Item> + <Checklist.SelectAll class="flex h-[25px] w-[25px] items-center justify-centerbg-slate-600 border-2 border-black p-2"> + <Checklist.ItemIndicator index={0}>✅</Checklist.ItemIndicator> </Checklist.SelectAll> + Select All <Checklist.Item> - <Checklist.ItemIndicator index={1} /> first item + <Checklist.ItemIndicator + index={1} + class="flex h-[25px] w-[25px] items-center justify-centerbg-slate-600 border-2 border-black p-2" + > + ✅ + </Checklist.ItemIndicator>{' '} + first item </Checklist.Item> <Checklist.Item> - <Checklist.ItemIndicator index={2} /> second item + <Checklist.ItemIndicator + index={2} + class="flex h-[25px] w-[25px] items-center justify-centerbg-slate-600 border-2 border-black p-2" + > + ✅ + </Checklist.ItemIndicator>{' '} + second item </Checklist.Item> </Checklist.Root> ); diff --git a/apps/docs/src/routes/checkbox/examples/hero.tsx b/apps/docs/src/routes/checkbox/examples/hero.tsx index d5fe3bba..3f314593 100644 --- a/apps/docs/src/routes/checkbox/examples/hero.tsx +++ b/apps/docs/src/routes/checkbox/examples/hero.tsx @@ -4,14 +4,14 @@ export default component$(() => { const bindChecked = useSignal(false); return ( <Checkbox.Root - bindChecked={bindChecked} + bind:checked={bindChecked} id="test" class="flex items-center gap-3 border-2 border-black p-2" > - <Checkbox.Indicator class="flex h-[25px] w-[25px] items-center justify-center bg-slate-600"> - ✅ - </Checkbox.Indicator> - I have read the README + <div class="flex h-[25px] w-[25px] items-center justify-center bg-slate-600"> + <Checkbox.Indicator>✅</Checkbox.Indicator> + </div> + <p> I have read the README</p> </Checkbox.Root> ); }); diff --git a/apps/docs/src/routes/checkbox/examples/pizza.tsx b/apps/docs/src/routes/checkbox/examples/pizza.tsx index 6034e745..6d877974 100644 --- a/apps/docs/src/routes/checkbox/examples/pizza.tsx +++ b/apps/docs/src/routes/checkbox/examples/pizza.tsx @@ -1,42 +1,42 @@ -import { component$, useSignal, useStyles$ } from '@builder.io/qwik'; -import { Checkbox, Checklist } from '@kunai-consulting/qwik-components'; -const toppingNames = ['hot peppers', 'ham', 'pineaple', 'mushroom']; -const toppingImages = ['🌶️', '🍗', '🍍', '🍄']; -export default component$(() => { - return ( - <> - <h3 id="pizza-toppings">Pizza toppings</h3> - <Checklist.Root - ariaLabeledBy="pizza-toppings" - class="flex flex-col gap-4" - > - <Checkbox.Root - class="flex items-center gap-3 border-2 border-black p-2" - checklist={true} - > - <Checklist.Indicator class="flex h-[25px] w-[25px] items-center justify-center bg-slate-600"> - <div q:slot="true" id="true-img"> - 🍕 - </div> +// import { component$, useSignal, useStyles$ } from '@builder.io/qwik'; +// import { Checkbox, Checklist } from '@kunai-consulting/qwik-components'; +// const toppingNames = ['hot peppers', 'ham', 'pineaple', 'mushroom']; +// const toppingImages = ['🌶️', '🍗', '🍍', '🍄']; +// export default component$(() => { +// return ( +// <> +// <h3 id="pizza-toppings">Pizza toppings</h3> +// <Checklist.Root +// ariaLabeledBy="pizza-toppings" +// class="flex flex-col gap-4" +// > +// <Checkbox.Root +// class="flex items-center gap-3 border-2 border-black p-2" +// checklist={true} +// > +// <Checklist.Indicator class="flex h-[25px] w-[25px] items-center justify-center bg-slate-600"> +// <div q:slot="true" id="true-img"> +// 🍕 +// </div> - <div q:slot="mixed" id="mixed-img"> - ➖ - </div> - </Checklist.Indicator> - Pick all toppings - </Checkbox.Root> +// <div q:slot="mixed" id="mixed-img"> +// ➖ +// </div> +// </Checklist.Indicator> +// Pick all toppings +// </Checkbox.Root> - {toppingNames.map((name, i) => { - return ( - <Checkbox.Root class="ml-8 flex items-center gap-3 border-2 border-black p-2"> - <Checkbox.Indicator class="flex h-[25px] w-[25px] items-center justify-center bg-slate-600"> - {toppingImages[i]} - </Checkbox.Indicator> - {name} - </Checkbox.Root> - ); - })} - </Checklist.Root> - </> - ); -}); +// {toppingNames.map((name, i) => { +// return ( +// <Checkbox.Root class="ml-8 flex items-center gap-3 border-2 border-black p-2"> +// <Checkbox.Indicator class="flex h-[25px] w-[25px] items-center justify-center bg-slate-600"> +// {toppingImages[i]} +// </Checkbox.Indicator> +// {name} +// </Checkbox.Root> +// ); +// })} +// </Checklist.Root> +// </> +// ); +// }); diff --git a/apps/docs/src/routes/checkbox/examples/test-controlled-list-true.tsx b/apps/docs/src/routes/checkbox/examples/test-controlled-list-true.tsx index 1b794a85..ef07feb6 100644 --- a/apps/docs/src/routes/checkbox/examples/test-controlled-list-true.tsx +++ b/apps/docs/src/routes/checkbox/examples/test-controlled-list-true.tsx @@ -2,47 +2,24 @@ import { component$, useSignal, useStyles$ } from '@builder.io/qwik'; import { Checkbox, Checklist } from '@kunai-consulting/qwik-components'; // this test basically ensures that the sig passed to the checklist controlls trumps all its children export default component$(() => { - const checklistSig = useSignal(true); + // const checklistSig = useSignal(true); return ( - <> - <h3 id="test123">Pick a cat</h3> - <Checklist.Root class="flex flex-col gap-3" ariaLabeledBy="test123"> - <Checkbox.Root - class="flex items-center gap-3 bg-slate-900 text-white" - checklist={true} - bind:checked={checklistSig} - id="checklist" - > - <Checklist.Indicator class="w-fit"> - <div q:slot="true" id="true-img"> - ✅ - </div> - - <div q:slot="mixed" id="mixed-img"> - ➖ - </div> - </Checklist.Indicator> - <p>Controlls all</p> - </Checkbox.Root> - <Checkbox.Root - id="child-1" - class="flex items-center gap-3 bg-slate-900 pr-2 text-white" - > - <Checkbox.Indicator class="w-fit bg-slate-600">✅</Checkbox.Indicator> - <p>No other stuff is needed here</p> - </Checkbox.Root> - - <Checkbox.Root id="child-2" class="bg-slate-900 text-white"> - <div class="flex items-center gap-3"> - <Checkbox.Indicator class="w-fit bg-slate-600"> - ✅ - </Checkbox.Indicator> - <p>Im a true.tsx</p> - </div> - </Checkbox.Root> - </Checklist.Root> - <p>You signal is: </p> - <p id="signal-to-text">{`${checklistSig.value}`}</p> - </> + <Checklist.Root initialStates={[true, true, true]}> + <Checklist.SelectAll> + <Checklist.Item> + <Checklist.ItemIndicator index={0}> + <p id="true-img">✅</p> + </Checklist.ItemIndicator> + </Checklist.Item> + </Checklist.SelectAll> + <Checklist.Item> + <Checklist.ItemIndicator index={1}>✅</Checklist.ItemIndicator> first + item + </Checklist.Item> + <Checklist.Item> + <Checklist.ItemIndicator index={2}>✅</Checklist.ItemIndicator> second + item + </Checklist.Item> + </Checklist.Root> ); }); diff --git a/libs/components/src/checkbox/checkbox-indicator.tsx b/libs/components/src/checkbox/checkbox-indicator.tsx index 0774fa54..28c53365 100644 --- a/libs/components/src/checkbox/checkbox-indicator.tsx +++ b/libs/components/src/checkbox/checkbox-indicator.tsx @@ -1,15 +1,34 @@ -import { component$, useContext, type PropsOf, Slot } from '@builder.io/qwik'; +import { + component$, + useContext, + type PropsOf, + Slot, + useTask$, + useStyles$, +} from '@builder.io/qwik'; import { CheckboxContext } from './checkbox-context'; +import './checkbox.css'; +import styles from './checkbox.css?inline'; export type CheckboxIndicatorProps = PropsOf<'div'>; export const CheckboxIndicator = component$<CheckboxIndicatorProps>((props) => { + useStyles$(styles); const checkSig = useContext(CheckboxContext); + + useTask$(({ track }) => { + track(() => checkSig.value); + console.log('checkboxindicator useTask$ ', checkSig.value); + }); + return ( - <div> - <div hidden={!checkSig.value} aria-hidden="true" {...props}> - <Slot /> - </div> + <div + {...props} + data-hidden={!checkSig.value} + data-qds-indicator + aria-hidden="true" + > + <Slot /> </div> ); }); diff --git a/libs/components/src/checkbox/checkbox-root.tsx b/libs/components/src/checkbox/checkbox-root.tsx index 1d8d8b82..e4f7a72e 100644 --- a/libs/components/src/checkbox/checkbox-root.tsx +++ b/libs/components/src/checkbox/checkbox-root.tsx @@ -11,39 +11,41 @@ import { useBoundSignal } from '../../utils/bound-signal'; import { CheckboxContext } from './checkbox-context'; export type CheckboxProps = { - bindChecked: Signal<boolean>; + 'bind:checked': Signal<boolean>; initialValue?: boolean; } & PropsOf<'div'>; -export const CheckboxRoot = component$<CheckboxProps>((props) => { - const checkedSignal = useBoundSignal(props.bindChecked, props.initialValue); - useContextProvider(CheckboxContext, checkedSignal); +export const CheckboxRoot = component$<CheckboxProps>( + ({ 'bind:checked': givenCheckedSig, initialValue, onClick$, ...props }) => { + const checkedSignal = useBoundSignal(givenCheckedSig, initialValue); - const handleKeyDownSync$ = sync$((e: KeyboardEvent) => { - if (e.key === ' ') { - e.preventDefault(); - } - }); - const handleClick = $(() => { - checkedSignal.value = !checkedSignal.value; - }); - const handleKeyDown$ = $((e: KeyboardEvent) => { - if (e.key === ' ') { - props.bindChecked.value = !props.bindChecked.value; - } - }); + useContextProvider(CheckboxContext, checkedSignal); + const handleKeyDownSync$ = sync$((e: KeyboardEvent) => { + if (e.key === ' ') { + e.preventDefault(); + } + }); + const handleClick = $(() => { + checkedSignal.value = !checkedSignal.value; + }); + const handleKeyDown$ = $((e: KeyboardEvent) => { + if (e.key === ' ') { + givenCheckedSig.value = !givenCheckedSig.value; + } + }); - return ( - <div - {...props} - tabIndex={0} - role="checkbox" - aria-checked={checkedSignal.value} - onClick$={handleClick} - onKeyDown$={[handleKeyDownSync$, handleKeyDown$]} - onKeyPress$={handleClick} - > - <Slot /> - </div> - ); -}); + return ( + <div + {...props} + tabIndex={0} + role="checkbox" + aria-checked={checkedSignal.value} + onClick$={onClick$ || handleClick} + onKeyDown$={[handleKeyDownSync$, handleKeyDown$]} + onKeyPress$={handleClick} + > + <Slot /> + </div> + ); + } +); diff --git a/libs/components/src/checkbox/checkbox.css b/libs/components/src/checkbox/checkbox.css new file mode 100644 index 00000000..038a0a6f --- /dev/null +++ b/libs/components/src/checkbox/checkbox.css @@ -0,0 +1,3 @@ +[data-qds-indicator][data-hidden] { + display: none; +} diff --git a/libs/components/src/checkbox/checkbox.test.ts b/libs/components/src/checkbox/checkbox.test.ts index 5d54bfa1..06397819 100644 --- a/libs/components/src/checkbox/checkbox.test.ts +++ b/libs/components/src/checkbox/checkbox.test.ts @@ -28,6 +28,37 @@ async function setup(page: Page, exampleName: string) { }; } +// components -> usually modular, sometimes grouped depending on what it is +// modular -> tests ONE SPECIFIC thing + +// grouped -> tests groups of things + +/** + * + * test.describe('Critical functionality', () => { + * test(`GIVEN a checkbox + When the checkbox is clicked + Then it should be checked`, async ({ page }) => { + await expect() -> aria-checked + await expect() -> data-checked + }); + + // if there's too many of one thing (for example, maybe you need to check 10 different keys! make it a describe block) + test(`GIVEN a checkbox + When the checkbox is pressed with space + Then it should be checked`, async ({ page }) => { + + }); + + Ex: Select + + test.describe('Keyboard Behavior', () => { + + }); + * }) + * + */ + test.describe('checkbox', () => { // test default state and click from unchecked checkbox test(`GIVEN a checkbox that is initially unchecked diff --git a/libs/components/src/checklist/checklist-indicator.tsx b/libs/components/src/checklist/checklist-indicator.tsx index 00340d9a..3a03ffe1 100644 --- a/libs/components/src/checklist/checklist-indicator.tsx +++ b/libs/components/src/checklist/checklist-indicator.tsx @@ -1,16 +1,28 @@ -import { component$, type PropsOf, useContext } from '@builder.io/qwik'; +import { + component$, + type PropsOf, + Slot, + useContext, + useSignal, +} from '@builder.io/qwik'; import { ChecklistContext } from './checklist-context'; +import { CheckboxRoot } from '../checkbox/checkbox-root'; +import { CheckboxIndicator } from '../checkbox/checkbox-indicator'; -export type ChecklistItemIndicatorProps = PropsOf<'div'>; +interface ChecklistItemIndicatorProps extends PropsOf<'div'> { + index: number; +} -export const ChecklistItemIndicator = component$((props: { index: number }) => { - const { items, toggleItem } = useContext(ChecklistContext); +export const ChecklistItemIndicator = component$( + (props: ChecklistItemIndicatorProps) => { + const { items } = useContext(ChecklistContext); + const itemSignal = useSignal(items.value[props.index]); + console.log('checklistitemindicator ', items.value[props.index]); - return ( - <input - type="checkbox" - checked={items.value[props.index]} - onClick$={() => toggleItem(props.index)} - /> - ); -}); + return ( + <CheckboxRoot bind:checked={itemSignal} {...props}> + <CheckboxIndicator /> + </CheckboxRoot> + ); + } +); diff --git a/libs/components/src/checklist/checklist-selectall.tsx b/libs/components/src/checklist/checklist-selectall.tsx index c2fc57b8..033aee46 100644 --- a/libs/components/src/checklist/checklist-selectall.tsx +++ b/libs/components/src/checklist/checklist-selectall.tsx @@ -1,17 +1,22 @@ -import { component$, useContext, $ } from '@builder.io/qwik'; +import { component$, useContext, $, type PropsOf } from '@builder.io/qwik'; import { ChecklistContext } from './checklist-context'; +import { CheckboxRoot } from '../checkbox/checkbox-root'; +import { CheckboxIndicator } from '../checkbox/checkbox-indicator'; -export const ChecklistSelectAll = component$(() => { +export const ChecklistSelectAll = component$((props: PropsOf<'div'>) => { const { allSelected, toggleAllSelected } = useContext(ChecklistContext); return ( <li> - <input - type="checkbox" - checked={allSelected.value} + {/* <CheckboxRoot bind:checked={allSelected} onClick$={toggleAllSelected}> + <CheckboxIndicator /> + </CheckboxRoot> */} + <div + {...props} + tabIndex={0} + role="checkbox" + aria-checked={allSelected.value} onClick$={() => toggleAllSelected()} /> - {' '} - Select All </li> ); }); diff --git a/libs/components/src/checklist/use-checklist.tsx b/libs/components/src/checklist/use-checklist.tsx index 76868203..dabb4986 100644 --- a/libs/components/src/checklist/use-checklist.tsx +++ b/libs/components/src/checklist/use-checklist.tsx @@ -13,13 +13,17 @@ export const useChecklist = (initialItems: boolean[]) => { const toggleItem = $((index: number) => { const newItems = [...items.value]; newItems[index] = !newItems[index]; - setItems(newItems); + items.value = newItems; + allSelected.value = newItems.every(Boolean); }); const toggleAllSelected = $(() => { + console.log('toggleAllSelected'); + const newState = !allSelected.value; allSelected.value = newState; items.value = items.value.map(() => newState); + console.log(items.value); }); useContextProvider(ChecklistContext, { diff --git a/libs/components/src/legacy-2/checkbox/checkbox.tsx b/libs/components/src/legacy-2/checkbox/checkbox.tsx index d9c3e7d8..4f007d75 100644 --- a/libs/components/src/legacy-2/checkbox/checkbox.tsx +++ b/libs/components/src/legacy-2/checkbox/checkbox.tsx @@ -1,240 +1,240 @@ -import { - type PropsOf, - type Signal, - Slot, - component$, - sync$, - useContextProvider, - useContext, - $, - useSignal, - useTask$, -} from '@builder.io/qwik'; -import { CheckListContext, CheckboxContext } from './context-id'; -import { type TriBool, getTriBool } from '../../utils/tri-bool'; -export type MixedStateCheckboxProps = { - 'bind:checked'?: Signal<boolean>; - checklist?: boolean; - _useCheckListContext?: boolean; - _overWriteCheckbox?: boolean; -} & PropsOf<'div'>; -export type TwoStateCheckboxProps = { - 'bind:checked'?: Signal<boolean>; - _useCheckListContext?: boolean; - _overWriteCheckbox?: boolean; -} & PropsOf<'div'>; - -type TwoStateCheckboxBehaviorProps = { - 'bind:checked': Signal<boolean>; -} & PropsOf<'div'>; -export type ChecklistTwoStateCheckboxProps = { - 'bind:checked'?: Signal<boolean>; - _useCheckListContext?: boolean; - _overWriteCheckbox?: boolean; -} & PropsOf<'div'>; -export const CheckboxRoot = component$<MixedStateCheckboxProps>((props) => { - // this is done to avoid consumers dealing with two types checkboxes, could go in different files - if (props.checklist) { - return ( - <MixedStateCheckbox {...props}> - <Slot /> - </MixedStateCheckbox> - ); - } - if (props._useCheckListContext) { - return ( - <ChecklistTwoStateCheckbox {...props}> - <Slot /> - </ChecklistTwoStateCheckbox> - ); - } - return ( - <TwoStateCheckbox {...props}> - <Slot /> - </TwoStateCheckbox> - ); -}); - -function getAriaChecked(triBool: TriBool): 'mixed' | 'true' | 'false' { - if (triBool === 'indeterminate') { - return 'mixed'; - } - return `${triBool === true}`; -} - -export const TwoStateCheckbox = component$<TwoStateCheckboxProps>((props) => { - // all the sig stuff should be refactored into a fancy hook - const defaultSig = useSignal(false); - const appliedSig = props['bind:checked'] ?? defaultSig; - const checklistID = useSignal<string | undefined>(props.id); - useContextProvider(CheckboxContext, appliedSig); - return ( - <TwoStateCheckboxBehavior - bind:checked={appliedSig} - tabIndex={0} - role="checkbox" - aria-checked={getAriaChecked(appliedSig.value)} - {...props} - id={checklistID.value} - > - <Slot /> - </TwoStateCheckboxBehavior> - ); -}); - -export const ChecklistTwoStateCheckbox = - component$<ChecklistTwoStateCheckboxProps>((props) => { - // this code is duplicate bcs you cant use conditionals on hooks (checklistContext could be undefined) - // this has room for improvement: remove most of the code duplivation - // making this a wrapper over the simpler component or using hooks - const checklistContext = useContext(CheckListContext); - const defaultSig = useSignal(false); - const appliedSig = props['bind:checked'] ?? defaultSig; - const checklistID = useSignal<string | undefined>(props.id); - // makes sure that the checklist's value is the same as its child - const syncToChecklist = useSignal<undefined | boolean>( - props._overWriteCheckbox - ); - useContextProvider(CheckboxContext, appliedSig); - useTask$(({ track }) => { - if (syncToChecklist.value !== undefined) { - appliedSig.value = syncToChecklist.value; - syncToChecklist.value = undefined; - } - track(() => { - appliedSig.value; - }); - - // now i can say that there's one good application for object identity - if (!checklistContext.checkboxes.value.some((e) => e === appliedSig)) { - const currIndex = checklistContext.checkboxes.value.length; - - // TODO: refactor id to not run on wrapper but after conditional - if (checklistID.value === undefined) { - checklistID.value = checklistContext.idArr[currIndex]; - } else { - checklistContext.idArr[currIndex] = checklistID.value; - } - checklistContext.checkboxes.value = [ - ...checklistContext.checkboxes.value, - appliedSig, - ]; - } - - const boolArr = checklistContext?.checkboxes.value.map((e) => e.value); - const newVal = getTriBool(boolArr); - checklistContext.checklistSig.value = newVal; - }); - return ( - <TwoStateCheckboxBehavior - {...props} - tabIndex={0} - role="checkbox" - aria-checked={getAriaChecked(appliedSig.value)} - id={checklistID.value} - bind:checked={appliedSig} - > - <Slot /> - </TwoStateCheckboxBehavior> - ); - }); - -export const MixedStateCheckbox = component$<MixedStateCheckboxProps>( - (props) => { - console.log('mixed'); - - // all the sig stuff should be refactored into a fancy hook - const checklistContext = useContext(CheckListContext); - const childCheckboxes = checklistContext.checkboxes; - const appliedSig = props['bind:checked'] ?? checklistContext.checklistSig; - const ariaControlsStrg = - checklistContext.idArr.length === 0 - ? '' - : checklistContext.idArr.reduce( - (previous, current) => `${previous} ${current}` - ); - useContextProvider(CheckboxContext, appliedSig); - - // im not enterily sure why, but the if statement only runs once - if (props['bind:checked'] !== undefined) { - checklistContext.checklistSig = props['bind:checked']; - } - - const changeChecklistSig = $(() => { - if (appliedSig.value !== true) { - appliedSig.value = true; - - for (const checkbox of childCheckboxes.value) { - checkbox.value = true; - } - - return; - } - - appliedSig.value = false; - - for (const checkbox of childCheckboxes.value) { - checkbox.value = false; - } - }); - const handleKeyDownSync$ = sync$((e: KeyboardEvent) => { - if (e.key === ' ') { - e.preventDefault(); - } - }); - const handleKeyDown$ = $((e: KeyboardEvent) => { - if (e.key === ' ') { - changeChecklistSig(); - } - }); - const handleClick$ = $(() => { - changeChecklistSig(); - }); - - return ( - <div - tabIndex={0} - role="checkbox" - aria-checked={getAriaChecked(appliedSig.value)} - onKeyDown$={[handleKeyDownSync$, handleKeyDown$]} - aria-controls={ariaControlsStrg} - onClick$={[handleClick$]} - {...props} - > - <Slot /> - </div> - ); - } -); - -const TwoStateCheckboxBehavior = component$<TwoStateCheckboxBehaviorProps>( - (props) => { - const handleKeyDownSync$ = sync$((e: KeyboardEvent) => { - if (e.key === ' ') { - e.preventDefault(); - } - }); - // this logic is duplicared thrice, make into hook pls - const handleClick = $(() => { - props['bind:checked'].value = !props['bind:checked'].value; - }); - const handleKeyDown$ = $((e: KeyboardEvent) => { - if (e.key === ' ') { - props['bind:checked'].value = !props['bind:checked'].value; - } - }); - // TODO: refactor to usetask code into fancy hook thingy - return ( - <div - tabIndex={0} - role="checkbox" - aria-checked={getAriaChecked(props['bind:checked'].value)} - {...props} - onKeyDown$={[handleKeyDownSync$, handleKeyDown$]} - onClick$={handleClick} - > - <Slot /> - </div> - ); - } -); +// import { +// type PropsOf, +// type Signal, +// Slot, +// component$, +// sync$, +// useContextProvider, +// useContext, +// $, +// useSignal, +// useTask$, +// } from '@builder.io/qwik'; +// import { CheckListContext, CheckboxContext } from './context-id'; +// import { type TriBool, getTriBool } from '../../utils/tri-bool'; +// export type MixedStateCheckboxProps = { +// 'bind:checked'?: Signal<boolean>; +// checklist?: boolean; +// _useCheckListContext?: boolean; +// _overWriteCheckbox?: boolean; +// } & PropsOf<'div'>; +// export type TwoStateCheckboxProps = { +// 'bind:checked'?: Signal<boolean>; +// _useCheckListContext?: boolean; +// _overWriteCheckbox?: boolean; +// } & PropsOf<'div'>; + +// type TwoStateCheckboxBehaviorProps = { +// 'bind:checked': Signal<boolean>; +// } & PropsOf<'div'>; +// export type ChecklistTwoStateCheckboxProps = { +// 'bind:checked'?: Signal<boolean>; +// _useCheckListContext?: boolean; +// _overWriteCheckbox?: boolean; +// } & PropsOf<'div'>; +// export const CheckboxRoot = component$<MixedStateCheckboxProps>((props) => { +// // this is done to avoid consumers dealing with two types checkboxes, could go in different files +// if (props.checklist) { +// return ( +// <MixedStateCheckbox {...props}> +// <Slot /> +// </MixedStateCheckbox> +// ); +// } +// if (props._useCheckListContext) { +// return ( +// <ChecklistTwoStateCheckbox {...props}> +// <Slot /> +// </ChecklistTwoStateCheckbox> +// ); +// } +// return ( +// <TwoStateCheckbox {...props}> +// <Slot /> +// </TwoStateCheckbox> +// ); +// }); + +// function getAriaChecked(triBool: TriBool): 'mixed' | 'true' | 'false' { +// if (triBool === 'indeterminate') { +// return 'mixed'; +// } +// return `${triBool === true}`; +// } + +// export const TwoStateCheckbox = component$<TwoStateCheckboxProps>((props) => { +// // all the sig stuff should be refactored into a fancy hook +// const defaultSig = useSignal(false); +// const appliedSig = props['bind:checked'] ?? defaultSig; +// const checklistID = useSignal<string | undefined>(props.id); +// useContextProvider(CheckboxContext, appliedSig); +// return ( +// <TwoStateCheckboxBehavior +// bind:checked={appliedSig} +// tabIndex={0} +// role="checkbox" +// aria-checked={getAriaChecked(appliedSig.value)} +// {...props} +// id={checklistID.value} +// > +// <Slot /> +// </TwoStateCheckboxBehavior> +// ); +// }); + +// export const ChecklistTwoStateCheckbox = +// component$<ChecklistTwoStateCheckboxProps>((props) => { +// // this code is duplicate bcs you cant use conditionals on hooks (checklistContext could be undefined) +// // this has room for improvement: remove most of the code duplivation +// // making this a wrapper over the simpler component or using hooks +// const checklistContext = useContext(CheckListContext); +// const defaultSig = useSignal(false); +// const appliedSig = props['bind:checked'] ?? defaultSig; +// const checklistID = useSignal<string | undefined>(props.id); +// // makes sure that the checklist's value is the same as its child +// const syncToChecklist = useSignal<undefined | boolean>( +// props._overWriteCheckbox +// ); +// useContextProvider(CheckboxContext, appliedSig); +// useTask$(({ track }) => { +// if (syncToChecklist.value !== undefined) { +// appliedSig.value = syncToChecklist.value; +// syncToChecklist.value = undefined; +// } +// track(() => { +// appliedSig.value; +// }); + +// // now i can say that there's one good application for object identity +// if (!checklistContext.checkboxes.value.some((e) => e === appliedSig)) { +// const currIndex = checklistContext.checkboxes.value.length; + +// // TODO: refactor id to not run on wrapper but after conditional +// if (checklistID.value === undefined) { +// checklistID.value = checklistContext.idArr[currIndex]; +// } else { +// checklistContext.idArr[currIndex] = checklistID.value; +// } +// checklistContext.checkboxes.value = [ +// ...checklistContext.checkboxes.value, +// appliedSig, +// ]; +// } + +// const boolArr = checklistContext?.checkboxes.value.map((e) => e.value); +// const newVal = getTriBool(boolArr); +// checklistContext.checklistSig.value = newVal; +// }); +// return ( +// <TwoStateCheckboxBehavior +// {...props} +// tabIndex={0} +// role="checkbox" +// aria-checked={getAriaChecked(appliedSig.value)} +// id={checklistID.value} +// bind:checked={appliedSig} +// > +// <Slot /> +// </TwoStateCheckboxBehavior> +// ); +// }); + +// export const MixedStateCheckbox = component$<MixedStateCheckboxProps>( +// (props) => { +// console.log('mixed'); + +// // all the sig stuff should be refactored into a fancy hook +// const checklistContext = useContext(CheckListContext); +// const childCheckboxes = checklistContext.checkboxes; +// const appliedSig = props['bind:checked'] ?? checklistContext.checklistSig; +// const ariaControlsStrg = +// checklistContext.idArr.length === 0 +// ? '' +// : checklistContext.idArr.reduce( +// (previous, current) => `${previous} ${current}` +// ); +// useContextProvider(CheckboxContext, appliedSig); + +// // im not enterily sure why, but the if statement only runs once +// if (props['bind:checked'] !== undefined) { +// checklistContext.checklistSig = props['bind:checked']; +// } + +// const changeChecklistSig = $(() => { +// if (appliedSig.value !== true) { +// appliedSig.value = true; + +// for (const checkbox of childCheckboxes.value) { +// checkbox.value = true; +// } + +// return; +// } + +// appliedSig.value = false; + +// for (const checkbox of childCheckboxes.value) { +// checkbox.value = false; +// } +// }); +// const handleKeyDownSync$ = sync$((e: KeyboardEvent) => { +// if (e.key === ' ') { +// e.preventDefault(); +// } +// }); +// const handleKeyDown$ = $((e: KeyboardEvent) => { +// if (e.key === ' ') { +// changeChecklistSig(); +// } +// }); +// const handleClick$ = $(() => { +// changeChecklistSig(); +// }); + +// return ( +// <div +// tabIndex={0} +// role="checkbox" +// aria-checked={getAriaChecked(appliedSig.value)} +// onKeyDown$={[handleKeyDownSync$, handleKeyDown$]} +// aria-controls={ariaControlsStrg} +// onClick$={[handleClick$]} +// {...props} +// > +// <Slot /> +// </div> +// ); +// } +// ); + +// const TwoStateCheckboxBehavior = component$<TwoStateCheckboxBehaviorProps>( +// (props) => { +// const handleKeyDownSync$ = sync$((e: KeyboardEvent) => { +// if (e.key === ' ') { +// e.preventDefault(); +// } +// }); +// // this logic is duplicared thrice, make into hook pls +// const handleClick = $(() => { +// props['bind:checked'].value = !props['bind:checked'].value; +// }); +// const handleKeyDown$ = $((e: KeyboardEvent) => { +// if (e.key === ' ') { +// props['bind:checked'].value = !props['bind:checked'].value; +// } +// }); +// // TODO: refactor to usetask code into fancy hook thingy +// return ( +// <div +// tabIndex={0} +// role="checkbox" +// aria-checked={getAriaChecked(props['bind:checked'].value)} +// {...props} +// onKeyDown$={[handleKeyDownSync$, handleKeyDown$]} +// onClick$={handleClick} +// > +// <Slot /> +// </div> +// ); +// } +// ); diff --git a/libs/components/src/legacy-2/checkbox/index.ts b/libs/components/src/legacy-2/checkbox/index.ts index 12d25986..2a23af3e 100644 --- a/libs/components/src/legacy-2/checkbox/index.ts +++ b/libs/components/src/legacy-2/checkbox/index.ts @@ -1,3 +1,3 @@ -export { CheckboxRoot as Root } from './checkbox'; +// export { CheckboxRoot as Root } from './checkbox'; export { CheckboxIndicator as Indicator } from './checkbox-indicator'; export * from './context-id'; diff --git a/libs/components/src/legacy-2/checklist/checklist.tsx b/libs/components/src/legacy-2/checklist/checklist.tsx index c7e31b0c..7bf07ae5 100644 --- a/libs/components/src/legacy-2/checklist/checklist.tsx +++ b/libs/components/src/legacy-2/checklist/checklist.tsx @@ -1,123 +1,123 @@ -import type { JSXNode, Component, PropsOf } from '@builder.io/qwik'; -import { - CheckboxRoot, - type MixedStateCheckboxProps, -} from '../checkbox/checkbox'; -import { - ChecklistContextWrapper, - getTriBool, -} from './checklist-context-wrapper'; - -import { findComponent, processChildren } from '../../utils/inline-component'; - -function generateUniqueId(): string { - return Date.now().toString(36) + Math.random().toString(36).substr(2, 5); -} - -type CheckListProps = PropsOf<'ul'> & { ariaLabeledBy: string }; -// type CheckBoxes= -/* - This is an inline component. An example use case of an inline component to get the proper indexes with CSR. See issue #4757 - for more information. -*/ -export const Checklist: Component<CheckListProps> = (props: CheckListProps) => { - const checkboxRootsJSXArray: JSXNode[] = []; - const hellSigs = []; - let checklistCheckbox = undefined; - const boolArr: boolean[] = []; - const idArr: Array<false | string> = []; - const checklistChilds: JSXNode[] = []; - const { children } = props; - - const childrenToProcess = ( - Array.isArray(children) ? [...children] : [children] - ) as JSXNode[]; - - while (childrenToProcess.length) { - const child = childrenToProcess.shift(); - - if (!child) { - continue; - } - - if (Array.isArray(child)) { - childrenToProcess.unshift(...child); - continue; - } - - switch (child.type) { - case CheckboxRoot: { - const typedProps = child.props as MixedStateCheckboxProps; - - // FYI: Obj.assign mutates - Object.assign(typedProps, { _useCheckListContext: true }); - - checkboxRootsJSXArray.push(child); - // TODO: fix this if hell by making fn - if (!typedProps.checklist) { - checklistChilds.push(child); - - if (typedProps.id !== undefined) { - idArr.push(typedProps.id as string); - } else { - idArr.push(false); - } - if (typedProps['bind:checked']?.value) { - boolArr.push(typedProps['bind:checked'].value); - hellSigs.push(typedProps['bind:checked']); - } else { - boolArr.push(false); - } - } else { - checklistCheckbox = child; - console.log('checklistCheckbox', checklistCheckbox); - } - - break; - } - - default: { - if (child) { - const anyChildren = Array.isArray(child.children) - ? [...child.children] - : [child.children]; - childrenToProcess.unshift(...(anyChildren as JSXNode[])); - } - - break; - } - } - } - - if (checklistCheckbox === undefined) { - throw Error( - "QWIK UI: checklist doesn't have a checkbox. Did you give the atribute of *checklist* to any of the checkboxes inside the checklist?" - ); - } - - if (checklistCheckbox.props['bind:checked']) { - for (const checkbox of checklistChilds) { - Object.assign(checkbox.props, { _overWriteCheckbox: true }); - } - } - - return ( - <> - <ChecklistContextWrapper - ariaLabeledBy={props.ariaLabeledBy} - arrSize={boolArr.length} - initialTriBool={getTriBool(boolArr)} - idArr={idArr} - > - <ul class={props.class}> - {checkboxRootsJSXArray.map((checkboxRootJSX, i) => { - const uniqueId = generateUniqueId(); - - return <li key={uniqueId}>{checkboxRootJSX}</li>; - })} - </ul> - </ChecklistContextWrapper> - </> - ); -}; -// TODO: deprecate ariaLabelledBy +// import type { JSXNode, Component, PropsOf } from '@builder.io/qwik'; +// import { +// CheckboxRoot, +// type MixedStateCheckboxProps, +// } from '../checkbox/checkbox'; +// import { +// ChecklistContextWrapper, +// getTriBool, +// } from './checklist-context-wrapper'; + +// import { findComponent, processChildren } from '../../utils/inline-component'; + +// function generateUniqueId(): string { +// return Date.now().toString(36) + Math.random().toString(36).substr(2, 5); +// } + +// type CheckListProps = PropsOf<'ul'> & { ariaLabeledBy: string }; +// // type CheckBoxes= +// /* +// This is an inline component. An example use case of an inline component to get the proper indexes with CSR. See issue #4757 +// for more information. +// */ +// export const Checklist: Component<CheckListProps> = (props: CheckListProps) => { +// const checkboxRootsJSXArray: JSXNode[] = []; +// const hellSigs = []; +// let checklistCheckbox = undefined; +// const boolArr: boolean[] = []; +// const idArr: Array<false | string> = []; +// const checklistChilds: JSXNode[] = []; +// const { children } = props; + +// const childrenToProcess = ( +// Array.isArray(children) ? [...children] : [children] +// ) as JSXNode[]; + +// while (childrenToProcess.length) { +// const child = childrenToProcess.shift(); + +// if (!child) { +// continue; +// } + +// if (Array.isArray(child)) { +// childrenToProcess.unshift(...child); +// continue; +// } + +// switch (child.type) { +// case CheckboxRoot: { +// const typedProps = child.props as MixedStateCheckboxProps; + +// // FYI: Obj.assign mutates +// Object.assign(typedProps, { _useCheckListContext: true }); + +// checkboxRootsJSXArray.push(child); +// // TODO: fix this if hell by making fn +// if (!typedProps.checklist) { +// checklistChilds.push(child); + +// if (typedProps.id !== undefined) { +// idArr.push(typedProps.id as string); +// } else { +// idArr.push(false); +// } +// if (typedProps['bind:checked']?.value) { +// boolArr.push(typedProps['bind:checked'].value); +// hellSigs.push(typedProps['bind:checked']); +// } else { +// boolArr.push(false); +// } +// } else { +// checklistCheckbox = child; +// console.log('checklistCheckbox', checklistCheckbox); +// } + +// break; +// } + +// default: { +// if (child) { +// const anyChildren = Array.isArray(child.children) +// ? [...child.children] +// : [child.children]; +// childrenToProcess.unshift(...(anyChildren as JSXNode[])); +// } + +// break; +// } +// } +// } + +// if (checklistCheckbox === undefined) { +// throw Error( +// "QWIK UI: checklist doesn't have a checkbox. Did you give the atribute of *checklist* to any of the checkboxes inside the checklist?" +// ); +// } + +// if (checklistCheckbox.props['bind:checked']) { +// for (const checkbox of checklistChilds) { +// Object.assign(checkbox.props, { _overWriteCheckbox: true }); +// } +// } + +// return ( +// <> +// <ChecklistContextWrapper +// ariaLabeledBy={props.ariaLabeledBy} +// arrSize={boolArr.length} +// initialTriBool={getTriBool(boolArr)} +// idArr={idArr} +// > +// <ul class={props.class}> +// {checkboxRootsJSXArray.map((checkboxRootJSX, i) => { +// const uniqueId = generateUniqueId(); + +// return <li key={uniqueId}>{checkboxRootJSX}</li>; +// })} +// </ul> +// </ChecklistContextWrapper> +// </> +// ); +// }; +// // TODO: deprecate ariaLabelledBy diff --git a/libs/components/src/legacy-2/checklist/index.ts b/libs/components/src/legacy-2/checklist/index.ts index 9db387ec..3eddeaf2 100644 --- a/libs/components/src/legacy-2/checklist/index.ts +++ b/libs/components/src/legacy-2/checklist/index.ts @@ -1,4 +1,4 @@ -export { Checklist as Root } from './checklist'; +// export { Checklist as Root } from './checklist'; export { ChecklistIndicator as Indicator } from './checklist-indicator'; export * from './checklist-context-wrapper'; export * from './context-id'; From d3bbb8fe0d5d243c2f253461338c32faa5c12a8d Mon Sep 17 00:00:00 2001 From: Jay-Kunaico <jay.brass@kunaico.com> Date: Fri, 30 Aug 2024 16:33:29 -0400 Subject: [PATCH 14/27] Update checklistitem, checklistindicator and checkbox root --- .../routes/checkbox/examples/checklist.tsx | 11 +++----- .../src/routes/checkbox/examples/hero.tsx | 5 ++-- .../src/routes/checkbox/examples/reactive.tsx | 27 +++++++++++++++++++ apps/docs/src/routes/checkbox/index.mdx | 8 +++++- .../components/src/checkbox/checkbox-root.tsx | 6 +++-- .../src/checklist/checklist-indicator.tsx | 10 +++---- .../src/checklist/checklist-item.tsx | 16 ++++++++--- 7 files changed, 61 insertions(+), 22 deletions(-) create mode 100644 apps/docs/src/routes/checkbox/examples/reactive.tsx diff --git a/apps/docs/src/routes/checkbox/examples/checklist.tsx b/apps/docs/src/routes/checkbox/examples/checklist.tsx index 06b35f8a..aeeb81f9 100644 --- a/apps/docs/src/routes/checkbox/examples/checklist.tsx +++ b/apps/docs/src/routes/checkbox/examples/checklist.tsx @@ -10,13 +10,10 @@ export default component$(() => { </Checklist.SelectAll> Select All <Checklist.Item> - <Checklist.ItemIndicator - index={1} - class="flex h-[25px] w-[25px] items-center justify-centerbg-slate-600 border-2 border-black p-2" - > - ✅ - </Checklist.ItemIndicator>{' '} - first item + <div class="flex h-[25px] w-[25px] items-center justify-centerbg-slate-600 border-2 border-black p-2"> + <Checklist.ItemIndicator index={1}>✅</Checklist.ItemIndicator> + </div> + first item is this right example? </Checklist.Item> <Checklist.Item> <Checklist.ItemIndicator diff --git a/apps/docs/src/routes/checkbox/examples/hero.tsx b/apps/docs/src/routes/checkbox/examples/hero.tsx index 3f314593..44c31536 100644 --- a/apps/docs/src/routes/checkbox/examples/hero.tsx +++ b/apps/docs/src/routes/checkbox/examples/hero.tsx @@ -1,10 +1,11 @@ import { component$, useSignal, useStyles$ } from '@builder.io/qwik'; import { Checkbox } from '@kunai-consulting/qwik-components'; export default component$(() => { - const bindChecked = useSignal(false); + const isCheckedSig = useSignal(false); + return ( <Checkbox.Root - bind:checked={bindChecked} + bind:checked={isCheckedSig} id="test" class="flex items-center gap-3 border-2 border-black p-2" > diff --git a/apps/docs/src/routes/checkbox/examples/reactive.tsx b/apps/docs/src/routes/checkbox/examples/reactive.tsx new file mode 100644 index 00000000..86361684 --- /dev/null +++ b/apps/docs/src/routes/checkbox/examples/reactive.tsx @@ -0,0 +1,27 @@ +import { component$, useSignal, useStyles$ } from '@builder.io/qwik'; +import { Checkbox } from '@kunai-consulting/qwik-components'; +export default component$(() => { + const isCheckedSig = useSignal(false); + + return ( + <> + <Checkbox.Root + bind:checked={isCheckedSig} + id="test" + class="flex items-center gap-3 border-2 border-black p-2" + > + <div class="flex h-[25px] w-[25px] items-center justify-center bg-slate-600"> + <Checkbox.Indicator>✅</Checkbox.Indicator> + </div> + <p> I have read the README</p> + </Checkbox.Root> + <button + type="button" + // biome-ignore lint/suspicious/noAssignInExpressions: <explanation> + onClick$={() => (isCheckedSig.value = !isCheckedSig.value)} + > + Check the checkbox above + </button> + </> + ); +}); diff --git a/apps/docs/src/routes/checkbox/index.mdx b/apps/docs/src/routes/checkbox/index.mdx index ca93820e..8d983ea4 100644 --- a/apps/docs/src/routes/checkbox/index.mdx +++ b/apps/docs/src/routes/checkbox/index.mdx @@ -6,10 +6,16 @@ title: Qwik UI | Checkbox Allows users to visually select an option by checking it. -<Showcase name="test-default" /> +{/* <Showcase name="test-default" /> */} + +## hero example <Showcase name="hero" /> +## reactive example + +<Showcase name="reactive" /> + ## Anatomy ## Examples diff --git a/libs/components/src/checkbox/checkbox-root.tsx b/libs/components/src/checkbox/checkbox-root.tsx index e4f7a72e..efbf6f29 100644 --- a/libs/components/src/checkbox/checkbox-root.tsx +++ b/libs/components/src/checkbox/checkbox-root.tsx @@ -11,7 +11,7 @@ import { useBoundSignal } from '../../utils/bound-signal'; import { CheckboxContext } from './checkbox-context'; export type CheckboxProps = { - 'bind:checked': Signal<boolean>; + 'bind:checked'?: Signal<boolean>; initialValue?: boolean; } & PropsOf<'div'>; @@ -25,12 +25,14 @@ export const CheckboxRoot = component$<CheckboxProps>( e.preventDefault(); } }); + const handleClick = $(() => { checkedSignal.value = !checkedSignal.value; }); + const handleKeyDown$ = $((e: KeyboardEvent) => { if (e.key === ' ') { - givenCheckedSig.value = !givenCheckedSig.value; + checkedSignal.value = !checkedSignal.value; } }); diff --git a/libs/components/src/checklist/checklist-indicator.tsx b/libs/components/src/checklist/checklist-indicator.tsx index 3a03ffe1..201046c1 100644 --- a/libs/components/src/checklist/checklist-indicator.tsx +++ b/libs/components/src/checklist/checklist-indicator.tsx @@ -15,14 +15,10 @@ interface ChecklistItemIndicatorProps extends PropsOf<'div'> { export const ChecklistItemIndicator = component$( (props: ChecklistItemIndicatorProps) => { - const { items } = useContext(ChecklistContext); - const itemSignal = useSignal(items.value[props.index]); - console.log('checklistitemindicator ', items.value[props.index]); - return ( - <CheckboxRoot bind:checked={itemSignal} {...props}> - <CheckboxIndicator /> - </CheckboxRoot> + <CheckboxIndicator> + <Slot /> + </CheckboxIndicator> ); } ); diff --git a/libs/components/src/checklist/checklist-item.tsx b/libs/components/src/checklist/checklist-item.tsx index 7250ff35..5416d141 100644 --- a/libs/components/src/checklist/checklist-item.tsx +++ b/libs/components/src/checklist/checklist-item.tsx @@ -1,9 +1,19 @@ -import { component$, Slot } from '@builder.io/qwik'; +import { component$, Slot, useContext, useSignal } from '@builder.io/qwik'; +import { CheckboxRoot } from '../checkbox/checkbox-root'; +import { ChecklistContext } from './checklist-context'; export const ChecklistItem = component$(() => { + const { items } = useContext(ChecklistContext); + + // make inline component + // const itemSignal = useSignal(items.value[_index]); + // console.log('checklistitemindicator ', items.value[_index]); + + // If you do pass bind:checked, from the Checklist, you can now control the checkbox + return ( - <li> + <CheckboxRoot> <Slot /> - </li> + </CheckboxRoot> ); }); From 9ee8c48d34e05a3b33e388a0fef0f6a158c996dc Mon Sep 17 00:00:00 2001 From: Jay-Kunaico <jay.brass@kunaico.com> Date: Tue, 3 Sep 2024 14:32:37 -0400 Subject: [PATCH 15/27] Polymorphism Co-authored-by: Jack Shelton <thejackshelton@users.noreply.github.com> --- .../routes/checkbox/examples/checklist.tsx | 19 ++++---- .../src/checkbox/checkbox-indicator.tsx | 3 ++ .../components/src/checkbox/checkbox-root.tsx | 45 +++++++++++++------ .../src/checklist/checklist-indicator.tsx | 4 +- .../src/checklist/checklist-item.tsx | 17 ++++--- .../src/checklist/checklist-selectall.tsx | 4 +- .../src/checklist/use-checklist.tsx | 9 ++-- 7 files changed, 62 insertions(+), 39 deletions(-) diff --git a/apps/docs/src/routes/checkbox/examples/checklist.tsx b/apps/docs/src/routes/checkbox/examples/checklist.tsx index aeeb81f9..e97289f9 100644 --- a/apps/docs/src/routes/checkbox/examples/checklist.tsx +++ b/apps/docs/src/routes/checkbox/examples/checklist.tsx @@ -5,23 +5,22 @@ import { Checklist } from '@kunai-consulting/qwik-components'; export default component$(() => { return ( <Checklist.Root initialStates={[false, false, false]}> - <Checklist.SelectAll class="flex h-[25px] w-[25px] items-center justify-centerbg-slate-600 border-2 border-black p-2"> - <Checklist.ItemIndicator index={0}>✅</Checklist.ItemIndicator> + <Checklist.SelectAll class="flex h-[25px] w-[25px] items-center justify-center bg-slate-600 border-2 border-black p-2"> + <div class="flex h-[25px] w-[25px] items-center justify-centerbg-slate-600 border-2 border-black p-2"> + <Checklist.ItemIndicator>✅</Checklist.ItemIndicator> + </div> </Checklist.SelectAll> Select All <Checklist.Item> <div class="flex h-[25px] w-[25px] items-center justify-centerbg-slate-600 border-2 border-black p-2"> - <Checklist.ItemIndicator index={1}>✅</Checklist.ItemIndicator> + <Checklist.ItemIndicator>✅</Checklist.ItemIndicator> </div> - first item is this right example? + first item </Checklist.Item> <Checklist.Item> - <Checklist.ItemIndicator - index={2} - class="flex h-[25px] w-[25px] items-center justify-centerbg-slate-600 border-2 border-black p-2" - > - ✅ - </Checklist.ItemIndicator>{' '} + <div class="flex h-[25px] w-[25px] items-center justify-centerbg-slate-600 border-2 border-black p-2"> + <Checklist.ItemIndicator>✅</Checklist.ItemIndicator> + </div> second item </Checklist.Item> </Checklist.Root> diff --git a/libs/components/src/checkbox/checkbox-indicator.tsx b/libs/components/src/checkbox/checkbox-indicator.tsx index 28c53365..74904c9c 100644 --- a/libs/components/src/checkbox/checkbox-indicator.tsx +++ b/libs/components/src/checkbox/checkbox-indicator.tsx @@ -14,7 +14,10 @@ export type CheckboxIndicatorProps = PropsOf<'div'>; export const CheckboxIndicator = component$<CheckboxIndicatorProps>((props) => { useStyles$(styles); + // console.log('checkboxindicator props ', props); + const checkSig = useContext(CheckboxContext); + console.log('checkboxindicator checkSig ', checkSig.value); useTask$(({ track }) => { track(() => checkSig.value); diff --git a/libs/components/src/checkbox/checkbox-root.tsx b/libs/components/src/checkbox/checkbox-root.tsx index efbf6f29..0c6c7f38 100644 --- a/libs/components/src/checkbox/checkbox-root.tsx +++ b/libs/components/src/checkbox/checkbox-root.tsx @@ -9,15 +9,30 @@ import { } from '@builder.io/qwik'; import { useBoundSignal } from '../../utils/bound-signal'; import { CheckboxContext } from './checkbox-context'; +import type { QwikIntrinsicElements } from '@builder.io/qwik'; + +type AllowedElements = 'li' | 'div' | 'span'; export type CheckboxProps = { 'bind:checked'?: Signal<boolean>; initialValue?: boolean; } & PropsOf<'div'>; -export const CheckboxRoot = component$<CheckboxProps>( - ({ 'bind:checked': givenCheckedSig, initialValue, onClick$, ...props }) => { +export const CheckboxRoot = component$( + <C extends AllowedElements = 'div'>( + props: QwikIntrinsicElements[C] & { as?: C } & CheckboxProps + ) => { + const { + 'bind:checked': givenCheckedSig, + initialValue, + onClick$, + as, + ...rest + } = props; + const Comp = as ?? 'div'; + const checkedSignal = useBoundSignal(givenCheckedSig, initialValue); + console.log('checkboxROOTcheckedSignal ', checkedSignal.value); useContextProvider(CheckboxContext, checkedSignal); const handleKeyDownSync$ = sync$((e: KeyboardEvent) => { @@ -37,17 +52,21 @@ export const CheckboxRoot = component$<CheckboxProps>( }); return ( - <div - {...props} - tabIndex={0} - role="checkbox" - aria-checked={checkedSignal.value} - onClick$={onClick$ || handleClick} - onKeyDown$={[handleKeyDownSync$, handleKeyDown$]} - onKeyPress$={handleClick} - > - <Slot /> - </div> + <> + {/* @ts-ignore */} + <Comp + {...rest} + tabIndex={0} + role="checkbox" + aria-checked={checkedSignal.value} + onClick$={handleClick} + onKeyDown$={[handleKeyDownSync$, handleKeyDown$]} + onKeyPress$={handleClick} + data-qds-checkbox-root + > + <Slot /> + </Comp> + </> ); } ); diff --git a/libs/components/src/checklist/checklist-indicator.tsx b/libs/components/src/checklist/checklist-indicator.tsx index 201046c1..1426eecc 100644 --- a/libs/components/src/checklist/checklist-indicator.tsx +++ b/libs/components/src/checklist/checklist-indicator.tsx @@ -9,9 +9,7 @@ import { ChecklistContext } from './checklist-context'; import { CheckboxRoot } from '../checkbox/checkbox-root'; import { CheckboxIndicator } from '../checkbox/checkbox-indicator'; -interface ChecklistItemIndicatorProps extends PropsOf<'div'> { - index: number; -} +interface ChecklistItemIndicatorProps extends PropsOf<'div'> {} export const ChecklistItemIndicator = component$( (props: ChecklistItemIndicatorProps) => { diff --git a/libs/components/src/checklist/checklist-item.tsx b/libs/components/src/checklist/checklist-item.tsx index 5416d141..c7d4c04f 100644 --- a/libs/components/src/checklist/checklist-item.tsx +++ b/libs/components/src/checklist/checklist-item.tsx @@ -1,18 +1,21 @@ -import { component$, Slot, useContext, useSignal } from '@builder.io/qwik'; +import { + component$, + type Signal, + Slot, + useContext, + useSignal, + useTask$, +} from '@builder.io/qwik'; import { CheckboxRoot } from '../checkbox/checkbox-root'; import { ChecklistContext } from './checklist-context'; export const ChecklistItem = component$(() => { const { items } = useContext(ChecklistContext); - // make inline component - // const itemSignal = useSignal(items.value[_index]); - // console.log('checklistitemindicator ', items.value[_index]); - - // If you do pass bind:checked, from the Checklist, you can now control the checkbox + const isCheckedSig = useSignal(false); return ( - <CheckboxRoot> + <CheckboxRoot as="li" bind:checked={isCheckedSig}> <Slot /> </CheckboxRoot> ); diff --git a/libs/components/src/checklist/checklist-selectall.tsx b/libs/components/src/checklist/checklist-selectall.tsx index 033aee46..79f486eb 100644 --- a/libs/components/src/checklist/checklist-selectall.tsx +++ b/libs/components/src/checklist/checklist-selectall.tsx @@ -2,14 +2,12 @@ import { component$, useContext, $, type PropsOf } from '@builder.io/qwik'; import { ChecklistContext } from './checklist-context'; import { CheckboxRoot } from '../checkbox/checkbox-root'; import { CheckboxIndicator } from '../checkbox/checkbox-indicator'; +import { ChecklistItem } from './checklist-item'; export const ChecklistSelectAll = component$((props: PropsOf<'div'>) => { const { allSelected, toggleAllSelected } = useContext(ChecklistContext); return ( <li> - {/* <CheckboxRoot bind:checked={allSelected} onClick$={toggleAllSelected}> - <CheckboxIndicator /> - </CheckboxRoot> */} <div {...props} tabIndex={0} diff --git a/libs/components/src/checklist/use-checklist.tsx b/libs/components/src/checklist/use-checklist.tsx index dabb4986..2087f50c 100644 --- a/libs/components/src/checklist/use-checklist.tsx +++ b/libs/components/src/checklist/use-checklist.tsx @@ -1,9 +1,13 @@ import { useContextProvider, $, useSignal } from '@builder.io/qwik'; import { ChecklistContext } from './checklist-context'; +import { CheckboxContext } from '../checkbox/checkbox-context'; export const useChecklist = (initialItems: boolean[]) => { const items = useSignal(initialItems); const allSelected = useSignal(initialItems.every(Boolean)); + useContextProvider(CheckboxContext, allSelected); + console.log('use-checklist items ', items.value); + console.log('use-checklist allSelected ', allSelected.value); const setItems = $((newItems: boolean[]) => { items.value = newItems; @@ -15,15 +19,14 @@ export const useChecklist = (initialItems: boolean[]) => { newItems[index] = !newItems[index]; items.value = newItems; allSelected.value = newItems.every(Boolean); + setItems(newItems); }); const toggleAllSelected = $(() => { - console.log('toggleAllSelected'); - const newState = !allSelected.value; allSelected.value = newState; items.value = items.value.map(() => newState); - console.log(items.value); + setItems(items.value); }); useContextProvider(ChecklistContext, { From 876b7934f1d1bded68fbfb20d75ae23d2a3348c2 Mon Sep 17 00:00:00 2001 From: Jay-Kunaico <jay.brass@kunaico.com> Date: Tue, 3 Sep 2024 15:52:02 -0400 Subject: [PATCH 16/27] update selectAll --- .../routes/checkbox/examples/checklist.tsx | 13 +++++----- .../src/checkbox/checkbox-indicator.tsx | 4 ++-- .../components/src/checkbox/checkbox-root.tsx | 8 +++---- .../src/checklist/checklist-selectall.tsx | 24 +++++++++++-------- 4 files changed, 27 insertions(+), 22 deletions(-) diff --git a/apps/docs/src/routes/checkbox/examples/checklist.tsx b/apps/docs/src/routes/checkbox/examples/checklist.tsx index e97289f9..b7819df8 100644 --- a/apps/docs/src/routes/checkbox/examples/checklist.tsx +++ b/apps/docs/src/routes/checkbox/examples/checklist.tsx @@ -5,20 +5,21 @@ import { Checklist } from '@kunai-consulting/qwik-components'; export default component$(() => { return ( <Checklist.Root initialStates={[false, false, false]}> - <Checklist.SelectAll class="flex h-[25px] w-[25px] items-center justify-center bg-slate-600 border-2 border-black p-2"> - <div class="flex h-[25px] w-[25px] items-center justify-centerbg-slate-600 border-2 border-black p-2"> + <Checklist.SelectAll> + <div class="flex h-[25px] w-[25px] items-center justify-center border-2 border-black p-2"> <Checklist.ItemIndicator>✅</Checklist.ItemIndicator> - </div> + </div>{' '} + Select All </Checklist.SelectAll> - Select All + <Checklist.Item> - <div class="flex h-[25px] w-[25px] items-center justify-centerbg-slate-600 border-2 border-black p-2"> + <div class="flex h-[25px] w-[25px] items-center justify-center border-2 border-black p-2"> <Checklist.ItemIndicator>✅</Checklist.ItemIndicator> </div> first item </Checklist.Item> <Checklist.Item> - <div class="flex h-[25px] w-[25px] items-center justify-centerbg-slate-600 border-2 border-black p-2"> + <div class="flex h-[25px] w-[25px] items-center justify-center border-2 border-black p-2"> <Checklist.ItemIndicator>✅</Checklist.ItemIndicator> </div> second item diff --git a/libs/components/src/checkbox/checkbox-indicator.tsx b/libs/components/src/checkbox/checkbox-indicator.tsx index 74904c9c..47cf4ace 100644 --- a/libs/components/src/checkbox/checkbox-indicator.tsx +++ b/libs/components/src/checkbox/checkbox-indicator.tsx @@ -14,10 +14,10 @@ export type CheckboxIndicatorProps = PropsOf<'div'>; export const CheckboxIndicator = component$<CheckboxIndicatorProps>((props) => { useStyles$(styles); - // console.log('checkboxindicator props ', props); const checkSig = useContext(CheckboxContext); - console.log('checkboxindicator checkSig ', checkSig.value); + + console.log('checkboxindicator checkSig', checkSig.value); useTask$(({ track }) => { track(() => checkSig.value); diff --git a/libs/components/src/checkbox/checkbox-root.tsx b/libs/components/src/checkbox/checkbox-root.tsx index 0c6c7f38..a4cf4908 100644 --- a/libs/components/src/checkbox/checkbox-root.tsx +++ b/libs/components/src/checkbox/checkbox-root.tsx @@ -32,7 +32,7 @@ export const CheckboxRoot = component$( const Comp = as ?? 'div'; const checkedSignal = useBoundSignal(givenCheckedSig, initialValue); - console.log('checkboxROOTcheckedSignal ', checkedSignal.value); + console.log('checkboxROOT checkedSignal ', checkedSignal.value); useContextProvider(CheckboxContext, checkedSignal); const handleKeyDownSync$ = sync$((e: KeyboardEvent) => { @@ -41,7 +41,7 @@ export const CheckboxRoot = component$( } }); - const handleClick = $(() => { + const handleClick$ = $(() => { checkedSignal.value = !checkedSignal.value; }); @@ -59,9 +59,9 @@ export const CheckboxRoot = component$( tabIndex={0} role="checkbox" aria-checked={checkedSignal.value} - onClick$={handleClick} + onClick$={onClick$ || handleClick$} onKeyDown$={[handleKeyDownSync$, handleKeyDown$]} - onKeyPress$={handleClick} + onKeyPress$={onClick$ || handleClick$} data-qds-checkbox-root > <Slot /> diff --git a/libs/components/src/checklist/checklist-selectall.tsx b/libs/components/src/checklist/checklist-selectall.tsx index 79f486eb..20f76e37 100644 --- a/libs/components/src/checklist/checklist-selectall.tsx +++ b/libs/components/src/checklist/checklist-selectall.tsx @@ -1,4 +1,10 @@ -import { component$, useContext, $, type PropsOf } from '@builder.io/qwik'; +import { + component$, + useContext, + $, + type PropsOf, + Slot, +} from '@builder.io/qwik'; import { ChecklistContext } from './checklist-context'; import { CheckboxRoot } from '../checkbox/checkbox-root'; import { CheckboxIndicator } from '../checkbox/checkbox-indicator'; @@ -7,14 +13,12 @@ import { ChecklistItem } from './checklist-item'; export const ChecklistSelectAll = component$((props: PropsOf<'div'>) => { const { allSelected, toggleAllSelected } = useContext(ChecklistContext); return ( - <li> - <div - {...props} - tabIndex={0} - role="checkbox" - aria-checked={allSelected.value} - onClick$={() => toggleAllSelected()} - /> - </li> + <CheckboxRoot + as="li" + bind:checked={allSelected} + onClick$={() => toggleAllSelected()} + > + <Slot /> + </CheckboxRoot> ); }); From d15e1ac6878f8e0a1463b1069396bdb99230b225 Mon Sep 17 00:00:00 2001 From: Jay-Kunaico <jay.brass@kunaico.com> Date: Wed, 4 Sep 2024 14:20:22 -0400 Subject: [PATCH 17/27] Update checklistItem, update checkbox tests --- .../examples/test-controlled-list-mixed.tsx | 90 +++++++++---------- .../routes/checkbox/examples/test-default.tsx | 2 +- .../examples/test-initial-checked.tsx | 2 +- .../routes/checkbox/examples/test-list.tsx | 58 +++++------- apps/docs/src/routes/checkbox/index.mdx | 2 + .../src/checkbox/checkbox-indicator.tsx | 3 - .../components/src/checkbox/checkbox-root.tsx | 5 +- .../src/checkbox/checkbox.driver.ts | 2 +- libs/components/src/checkbox/checkbox.test.ts | 18 ++-- .../src/checklist/checklist-item.tsx | 27 +++++- .../src/checklist/checklist-selectall.tsx | 4 + .../src/checklist/use-checklist.tsx | 11 ++- 12 files changed, 124 insertions(+), 100 deletions(-) diff --git a/apps/docs/src/routes/checkbox/examples/test-controlled-list-mixed.tsx b/apps/docs/src/routes/checkbox/examples/test-controlled-list-mixed.tsx index e122796a..dca6dbdb 100644 --- a/apps/docs/src/routes/checkbox/examples/test-controlled-list-mixed.tsx +++ b/apps/docs/src/routes/checkbox/examples/test-controlled-list-mixed.tsx @@ -1,46 +1,46 @@ -import { component$, useSignal, useStyles$ } from '@builder.io/qwik'; -import { Checkbox, Checklist } from '@kunai-consulting/qwik-components'; -export default component$(() => { - const firstUserSig = useSignal(false); - const secondUserSig = useSignal(true); - return ( - <> - <h3 id="test123">Pick a cat</h3> - <Checklist.Root class="flex flex-col gap-3" ariaLabeledBy="test123"> - <Checkbox.Root - class="flex items-center gap-3 bg-slate-900 p-2 text-white" - checklist={true} - > - <Checklist.Indicator class="w-fit"> - <div q:slot="true" id="true-img"> - ✅ - </div> - <div q:slot="mixed" id="mixed-img"> - ➖ - </div> - </Checklist.Indicator> - <p>Controlls all</p> - </Checkbox.Root> - <Checkbox.Root - bind:checked={firstUserSig} - class="flex items-center gap-3 bg-slate-900 pr-2 text-white" - > - <Checkbox.Indicator class="w-fit bg-slate-600">✅</Checkbox.Indicator> - <p>No other stuff is needed here</p> - </Checkbox.Root> +// import { component$, useSignal, useStyles$ } from '@builder.io/qwik'; +// import { Checkbox, Checklist } from '@kunai-consulting/qwik-components'; +// export default component$(() => { +// const firstUserSig = useSignal(false); +// const secondUserSig = useSignal(true); +// return ( +// <> +// <h3 id="test123">Pick a cat</h3> +// <Checklist.Root class="flex flex-col gap-3" ariaLabeledBy="test123"> +// <Checkbox.Root +// class="flex items-center gap-3 bg-slate-900 p-2 text-white" +// checklist={true} +// > +// <Checklist.Indicator class="w-fit"> +// <div q:slot="true" id="true-img"> +// ✅ +// </div> +// <div q:slot="mixed" id="mixed-img"> +// ➖ +// </div> +// </Checklist.Indicator> +// <p>Controlls all</p> +// </Checkbox.Root> +// <Checkbox.Root +// bind:checked={firstUserSig} +// class="flex items-center gap-3 bg-slate-900 pr-2 text-white" +// > +// <Checkbox.Indicator class="w-fit bg-slate-600">✅</Checkbox.Indicator> +// <p>No other stuff is needed here</p> +// </Checkbox.Root> - <Checkbox.Root - bind:checked={secondUserSig} - class="bg-slate-900 text-white" - > - <div class="flex items-center gap-3"> - <Checkbox.Indicator class="w-fit bg-slate-600"> - ✅ - </Checkbox.Indicator> - <p>No other stuff is needed here</p> - </div> - </Checkbox.Root> - </Checklist.Root> - </> - ); -}); +// <Checkbox.Root +// bind:checked={secondUserSig} +// class="bg-slate-900 text-white" +// > +// <div class="flex items-center gap-3"> +// <Checkbox.Indicator class="w-fit bg-slate-600"> +// ✅ +// </Checkbox.Indicator> +// <p>No other stuff is needed here</p> +// </div> +// </Checkbox.Root> +// </Checklist.Root> +// </> +// ); +// }); diff --git a/apps/docs/src/routes/checkbox/examples/test-default.tsx b/apps/docs/src/routes/checkbox/examples/test-default.tsx index 68267a08..361f7574 100644 --- a/apps/docs/src/routes/checkbox/examples/test-default.tsx +++ b/apps/docs/src/routes/checkbox/examples/test-default.tsx @@ -5,7 +5,7 @@ export default component$(() => { const bindChecked = useSignal(false); return ( <Checkbox.Root - bindChecked={bindChecked} + bind:checked={bindChecked} id="test" class="flex items-center gap-3 border-2 border-black p-2" > diff --git a/apps/docs/src/routes/checkbox/examples/test-initial-checked.tsx b/apps/docs/src/routes/checkbox/examples/test-initial-checked.tsx index 30d7bfd5..366c8841 100644 --- a/apps/docs/src/routes/checkbox/examples/test-initial-checked.tsx +++ b/apps/docs/src/routes/checkbox/examples/test-initial-checked.tsx @@ -5,7 +5,7 @@ export default component$(() => { const bindChecked = useSignal(true); return ( <> - <Checkbox.Root class="bg-slate-900 text-white" bindChecked={bindChecked}> + <Checkbox.Root class="bg-slate-900 text-white" bind:checked={bindChecked}> <div class="flex items-center gap-3"> <Checkbox.Indicator class="w-fit bg-slate-600"> <p id="indicator">✅</p> diff --git a/apps/docs/src/routes/checkbox/examples/test-list.tsx b/apps/docs/src/routes/checkbox/examples/test-list.tsx index 081478e9..a7a8dc66 100644 --- a/apps/docs/src/routes/checkbox/examples/test-list.tsx +++ b/apps/docs/src/routes/checkbox/examples/test-list.tsx @@ -1,41 +1,29 @@ import { component$ } from '@builder.io/qwik'; -import { Checkbox, Checklist } from '@kunai-consulting/qwik-components'; -export default component$(() => { - return ( - <> - <h3 id="test123">Pick a cat</h3> - <Checklist.Root class="flex flex-col gap-3" ariaLabeledBy="test123"> - <Checkbox.Root - class="flex items-center gap-3 bg-slate-900 p-2 text-white" - checklist={true} - > - <Checklist.Indicator class="w-fit bg-black"> - <div q:slot="true" id="true-img"> - ✅ - </div> - - <div q:slot="mixed" id="mixed-img"> - ➖ - </div> - </Checklist.Indicator> - <p>Get All</p> - </Checkbox.Root> +import { Checklist } from '@kunai-consulting/qwik-components'; - <Checkbox.Root class="flex items-center gap-3 bg-slate-900 pr-2 text-white"> - <Checkbox.Indicator class="w-fit bg-slate-600">✅</Checkbox.Indicator> - <p>Cat One</p> - </Checkbox.Root> +export default component$(() => { + return ( + <Checklist.Root initialStates={[true, true, true]}> + <Checklist.SelectAll role="group"> + <div class="flex h-[25px] w-[25px] items-center justify-center border-2 border-black p-2"> + <Checklist.ItemIndicator>✅</Checklist.ItemIndicator> + </div>{' '} + Select All + </Checklist.SelectAll> - <Checkbox.Root class="bg-slate-900 text-white"> - <div class="flex items-center gap-3"> - <Checkbox.Indicator class="w-fit bg-slate-600"> - ✅ - </Checkbox.Indicator> - <p>Cat Two</p> - </div> - </Checkbox.Root> - </Checklist.Root> - </> + <Checklist.Item> + <div class="flex h-[25px] w-[25px] items-center justify-center border-2 border-black p-2"> + <Checklist.ItemIndicator>✅</Checklist.ItemIndicator> + </div> + first item + </Checklist.Item> + <Checklist.Item> + <div class="flex h-[25px] w-[25px] items-center justify-center border-2 border-black p-2"> + <Checklist.ItemIndicator>✅</Checklist.ItemIndicator> + </div> + second item + </Checklist.Item> + </Checklist.Root> ); }); diff --git a/apps/docs/src/routes/checkbox/index.mdx b/apps/docs/src/routes/checkbox/index.mdx index 8d983ea4..89c8f6a5 100644 --- a/apps/docs/src/routes/checkbox/index.mdx +++ b/apps/docs/src/routes/checkbox/index.mdx @@ -49,3 +49,5 @@ Text tags lose their semantic meaning. For example, an h1 tag is treated the sam {/* <Showcase name="pizza" /> */} <Showcase name="checklist" /> + +<Showcase name="test-list" /> diff --git a/libs/components/src/checkbox/checkbox-indicator.tsx b/libs/components/src/checkbox/checkbox-indicator.tsx index 47cf4ace..9cad05ac 100644 --- a/libs/components/src/checkbox/checkbox-indicator.tsx +++ b/libs/components/src/checkbox/checkbox-indicator.tsx @@ -17,11 +17,8 @@ export const CheckboxIndicator = component$<CheckboxIndicatorProps>((props) => { const checkSig = useContext(CheckboxContext); - console.log('checkboxindicator checkSig', checkSig.value); - useTask$(({ track }) => { track(() => checkSig.value); - console.log('checkboxindicator useTask$ ', checkSig.value); }); return ( diff --git a/libs/components/src/checkbox/checkbox-root.tsx b/libs/components/src/checkbox/checkbox-root.tsx index a4cf4908..2922a593 100644 --- a/libs/components/src/checkbox/checkbox-root.tsx +++ b/libs/components/src/checkbox/checkbox-root.tsx @@ -59,9 +59,10 @@ export const CheckboxRoot = component$( tabIndex={0} role="checkbox" aria-checked={checkedSignal.value} - onClick$={onClick$ || handleClick$} + aria-labelledby={props['aria-labelledby']} + onClick$={handleClick$} onKeyDown$={[handleKeyDownSync$, handleKeyDown$]} - onKeyPress$={onClick$ || handleClick$} + onKeyPress$={handleClick$} data-qds-checkbox-root > <Slot /> diff --git a/libs/components/src/checkbox/checkbox.driver.ts b/libs/components/src/checkbox/checkbox.driver.ts index 9e9f3162..f669d956 100644 --- a/libs/components/src/checkbox/checkbox.driver.ts +++ b/libs/components/src/checkbox/checkbox.driver.ts @@ -10,7 +10,7 @@ export function createTestDriver<T extends DriverLocator>(rootLocator: T) { return getRoot().locator('#indicator'); }; const getCheckList = () => { - return getRoot().getByRole('group'); + return getRoot().getByRole('checkbox'); }; const getChecklistUL = () => { // note: filter method is always relative to the original locator not document root despite using root diff --git a/libs/components/src/checkbox/checkbox.test.ts b/libs/components/src/checkbox/checkbox.test.ts index 06397819..d5d5745d 100644 --- a/libs/components/src/checkbox/checkbox.test.ts +++ b/libs/components/src/checkbox/checkbox.test.ts @@ -133,15 +133,15 @@ test.describe('checkbox', () => { // Checklisst tests test.describe('checklist', () => { - test(`GIVEN a mixed checklist - WHEN the checklist renders - IT should render the mixed img - AND not the true img`, async ({ page }) => { - const exampleName = 'test-controlled-list-mixed'; - await setup(page, exampleName); - await expect(page.locator('#mixed-img')).toBeVisible(); - await expect(page.locator('#true-img')).toBeHidden(); - }); + // test(`GIVEN a mixed checklist + // WHEN the checklist renders + // IT should render the mixed img + // AND not the true img`, async ({ page }) => { + // const exampleName = 'test-controlled-list-mixed'; + // await setup(page, exampleName); + // await expect(page.locator('#mixed-img')).toBeVisible(); + // await expect(page.locator('#true-img')).toBeHidden(); + // }); test(`GIVEN an all-checked checklist WHEN the checklist renders diff --git a/libs/components/src/checklist/checklist-item.tsx b/libs/components/src/checklist/checklist-item.tsx index c7d4c04f..dffd8bbc 100644 --- a/libs/components/src/checklist/checklist-item.tsx +++ b/libs/components/src/checklist/checklist-item.tsx @@ -11,12 +11,35 @@ import { ChecklistContext } from './checklist-context'; export const ChecklistItem = component$(() => { const { items } = useContext(ChecklistContext); - const isCheckedSig = useSignal(false); + const initialLoadSig = useSignal(false); + const context = useContext(ChecklistContext); + + useTask$(({ track }) => { + track(() => context.allSelected.value); + + if (initialLoadSig.value) { + return; + } + + if (context.allSelected.value) { + isCheckedSig.value = true; + } else { + isCheckedSig.value = false; + } + }); + + useTask$(({ track }) => { + initialLoadSig.value = false; + }); return ( - <CheckboxRoot as="li" bind:checked={isCheckedSig}> + <CheckboxRoot as="li" bind:checked={isCheckedSig} aria-labelledby="test123"> <Slot /> </CheckboxRoot> ); }); + +// 1) GET TESTS WORKING FOR CHECKBOX + +// 2) GET FAILING TEST FOR SELECT ALL ON CHECKLIST. WHAT IS THE IDEAL BEHAVIOR? diff --git a/libs/components/src/checklist/checklist-selectall.tsx b/libs/components/src/checklist/checklist-selectall.tsx index 20f76e37..406c88e3 100644 --- a/libs/components/src/checklist/checklist-selectall.tsx +++ b/libs/components/src/checklist/checklist-selectall.tsx @@ -4,6 +4,7 @@ import { $, type PropsOf, Slot, + useContextProvider, } from '@builder.io/qwik'; import { ChecklistContext } from './checklist-context'; import { CheckboxRoot } from '../checkbox/checkbox-root'; @@ -12,11 +13,14 @@ import { ChecklistItem } from './checklist-item'; export const ChecklistSelectAll = component$((props: PropsOf<'div'>) => { const { allSelected, toggleAllSelected } = useContext(ChecklistContext); + return ( <CheckboxRoot as="li" bind:checked={allSelected} + aria-labelledby="test123" onClick$={() => toggleAllSelected()} + role="group" > <Slot /> </CheckboxRoot> diff --git a/libs/components/src/checklist/use-checklist.tsx b/libs/components/src/checklist/use-checklist.tsx index 2087f50c..cfc3b74c 100644 --- a/libs/components/src/checklist/use-checklist.tsx +++ b/libs/components/src/checklist/use-checklist.tsx @@ -1,4 +1,9 @@ -import { useContextProvider, $, useSignal } from '@builder.io/qwik'; +import { + useContextProvider, + $, + useSignal, + useVisibleTask$, +} from '@builder.io/qwik'; import { ChecklistContext } from './checklist-context'; import { CheckboxContext } from '../checkbox/checkbox-context'; @@ -29,6 +34,10 @@ export const useChecklist = (initialItems: boolean[]) => { setItems(items.value); }); + useVisibleTask$(() => { + toggleAllSelected(); + }); + useContextProvider(ChecklistContext, { items, toggleItem, From e9c113e466af501c719fa11e7684e20aff039695 Mon Sep 17 00:00:00 2001 From: Jay-Kunaico <jay.brass@kunaico.com> Date: Thu, 5 Sep 2024 14:10:38 -0400 Subject: [PATCH 18/27] Interim changes for index --- .../routes/checkbox/examples/checklist.tsx | 25 ++++---- .../routes/checkbox/examples/test-list.tsx | 4 +- apps/docs/src/routes/checkbox/index.mdx | 4 ++ libs/components/src/checkbox/checkbox.test.ts | 51 ++++++++-------- .../src/checklist/checklist-context.tsx | 2 + .../src/checklist/checklist-item.tsx | 24 ++++++-- .../src/checklist/checklist-selectall.tsx | 13 ++--- .../src/checklist/use-checklist.tsx | 58 ++++++++++++++----- 8 files changed, 112 insertions(+), 69 deletions(-) diff --git a/apps/docs/src/routes/checkbox/examples/checklist.tsx b/apps/docs/src/routes/checkbox/examples/checklist.tsx index b7819df8..c45edcf6 100644 --- a/apps/docs/src/routes/checkbox/examples/checklist.tsx +++ b/apps/docs/src/routes/checkbox/examples/checklist.tsx @@ -5,25 +5,24 @@ import { Checklist } from '@kunai-consulting/qwik-components'; export default component$(() => { return ( <Checklist.Root initialStates={[false, false, false]}> - <Checklist.SelectAll> + <Checklist.SelectAll class="flex h-[25px] w-[25px] items-center justify-center bg-slate-600 border-2 border-black p-2"> <div class="flex h-[25px] w-[25px] items-center justify-center border-2 border-black p-2"> <Checklist.ItemIndicator>✅</Checklist.ItemIndicator> </div>{' '} Select All </Checklist.SelectAll> - <Checklist.Item> - <div class="flex h-[25px] w-[25px] items-center justify-center border-2 border-black p-2"> - <Checklist.ItemIndicator>✅</Checklist.ItemIndicator> - </div> - first item - </Checklist.Item> - <Checklist.Item> - <div class="flex h-[25px] w-[25px] items-center justify-center border-2 border-black p-2"> - <Checklist.ItemIndicator>✅</Checklist.ItemIndicator> - </div> - second item - </Checklist.Item> + {Array.from({ length: 2 }, (_, index) => { + const uniqueKey = `otp-${index}-${Date.now()}`; + return ( + <Checklist.Item key={uniqueKey} index={index}> + <div class="flex h-[25px] w-[25px] items-center justify-center border-2 border-black p-2"> + <Checklist.ItemIndicator>✅</Checklist.ItemIndicator> + </div> + {`item ${index}`} + </Checklist.Item> + ); + })} </Checklist.Root> ); }); diff --git a/apps/docs/src/routes/checkbox/examples/test-list.tsx b/apps/docs/src/routes/checkbox/examples/test-list.tsx index a7a8dc66..f360e3c8 100644 --- a/apps/docs/src/routes/checkbox/examples/test-list.tsx +++ b/apps/docs/src/routes/checkbox/examples/test-list.tsx @@ -4,8 +4,8 @@ import { Checklist } from '@kunai-consulting/qwik-components'; export default component$(() => { return ( - <Checklist.Root initialStates={[true, true, true]}> - <Checklist.SelectAll role="group"> + <Checklist.Root initialStates={[false, false, false]}> + <Checklist.SelectAll aria-labelledby="test123"> <div class="flex h-[25px] w-[25px] items-center justify-center border-2 border-black p-2"> <Checklist.ItemIndicator>✅</Checklist.ItemIndicator> </div>{' '} diff --git a/apps/docs/src/routes/checkbox/index.mdx b/apps/docs/src/routes/checkbox/index.mdx index 89c8f6a5..65dfcfa8 100644 --- a/apps/docs/src/routes/checkbox/index.mdx +++ b/apps/docs/src/routes/checkbox/index.mdx @@ -48,6 +48,10 @@ Text tags lose their semantic meaning. For example, an h1 tag is treated the sam {/* <Showcase name="pizza" /> */} +### Checklist + <Showcase name="checklist" /> +### Checklist Test List + <Showcase name="test-list" /> diff --git a/libs/components/src/checkbox/checkbox.test.ts b/libs/components/src/checkbox/checkbox.test.ts index d5d5745d..928086d2 100644 --- a/libs/components/src/checkbox/checkbox.test.ts +++ b/libs/components/src/checkbox/checkbox.test.ts @@ -162,33 +162,32 @@ test.describe('checklist', () => { await expect(page.locator('#mixed-img')).toBeHidden(); }); - test(`GIVEN a checklist with checkboxes - WHEN the elements render - THEN the checklist should be a <ul> with <li>s of checkboxes, all wrapped around a div with a role and aria-labeledby attributes`, async ({ - page, - }) => { - const { getCheckList, getChecklistUL, getChecklistLIs } = await setup( - page, - 'test-list' - ); - await expect(getCheckList()).toBeVisible(); - await expect(getCheckList()).toHaveAttribute('aria-labelledby', 'test123'); - await expect(getChecklistUL()).toBeVisible(); - await expect(getChecklistLIs()).toBeVisible(); - }); + // test(`GIVEN a checklist with checkboxes + // WHEN the elements render + // THEN the checklist should be a <ul> with <li>s of checkboxes, all wrapped around a div with a role and aria-labeledby attributes`, async ({ + // page, + // }) => { + // const { getRoot, getCheckList, getChecklistUL, getChecklistLIs } = + // await setup(page, 'test-list'); + // await expect(getCheckList()).toBeVisible(); + // await expect(getCheckList()).toHaveAttribute('aria-labelledby', 'test123'); + // await expect(getChecklistUL()).toBeVisible(); + // await expect(getChecklistLIs()).toBeVisible(); + // }); - test(`GIVEN a tri boolean function - WHEN it recieves an array of booleans - IT should return the correct tri bool`, async () => { - const indeterminateArr = [true, true, false]; - const trueArr = [true, true, true]; - const falseArr = [false, false, false]; - const emptyArr: boolean[] = []; - expect(getTriBool(indeterminateArr)).toBe('indeterminate'); - expect(getTriBool(trueArr)).toBe(true); - expect(getTriBool(falseArr)).toBe(false); - expect(getTriBool(emptyArr)).toBe('indeterminate'); - }); + // not using triboolean + // test(`GIVEN a tri boolean function + // WHEN it recieves an array of booleans + // IT should return the correct tri bool`, async () => { + // const indeterminateArr = [true, true, false]; + // const trueArr = [true, true, true]; + // const falseArr = [false, false, false]; + // const emptyArr: boolean[] = []; + // expect(getTriBool(indeterminateArr)).toBe('indeterminate'); + // expect(getTriBool(trueArr)).toBe(true); + // expect(getTriBool(falseArr)).toBe(false); + // expect(getTriBool(emptyArr)).toBe('indeterminate'); + // }); test(`GIVEN checklist with all unchecked checkboxes WHEN it renders diff --git a/libs/components/src/checklist/checklist-context.tsx b/libs/components/src/checklist/checklist-context.tsx index 6b8fcceb..a811cc18 100644 --- a/libs/components/src/checklist/checklist-context.tsx +++ b/libs/components/src/checklist/checklist-context.tsx @@ -5,6 +5,8 @@ export interface ChecklistState { toggleItem: (index: number) => void; allSelected: Signal<boolean>; toggleAllSelected: () => void; + indeterminate: Signal<boolean>; + activeIndex: Signal<number>; } export const ChecklistContext = diff --git a/libs/components/src/checklist/checklist-item.tsx b/libs/components/src/checklist/checklist-item.tsx index dffd8bbc..fba8f148 100644 --- a/libs/components/src/checklist/checklist-item.tsx +++ b/libs/components/src/checklist/checklist-item.tsx @@ -1,19 +1,32 @@ import { component$, + type PropsOf, type Signal, Slot, useContext, + useContextProvider, useSignal, useTask$, + useVisibleTask$, } from '@builder.io/qwik'; import { CheckboxRoot } from '../checkbox/checkbox-root'; import { ChecklistContext } from './checklist-context'; -export const ChecklistItem = component$(() => { +interface ChecklistItemProps extends PropsOf<'div'> { + _index?: number; +} + +export const ChecklistItem = component$((props: ChecklistItemProps) => { const { items } = useContext(ChecklistContext); const isCheckedSig = useSignal(false); const initialLoadSig = useSignal(false); const context = useContext(ChecklistContext); + const { _index = 0, ...rest } = props; + console.log('ChecklistItem items ', items.value); + + if (_index === undefined) { + throw new Error('Checklist Item must have an index.'); + } useTask$(({ track }) => { track(() => context.allSelected.value); @@ -33,13 +46,16 @@ export const ChecklistItem = component$(() => { initialLoadSig.value = false; }); + useVisibleTask$(({ track }) => { + track(() => context.items.value); + console.log('ChecklistItem useVisibleTask items ', context.items.value); + }); + return ( - <CheckboxRoot as="li" bind:checked={isCheckedSig} aria-labelledby="test123"> + <CheckboxRoot as="li" bind:checked={isCheckedSig} index={_index}> <Slot /> </CheckboxRoot> ); }); -// 1) GET TESTS WORKING FOR CHECKBOX - // 2) GET FAILING TEST FOR SELECT ALL ON CHECKLIST. WHAT IS THE IDEAL BEHAVIOR? diff --git a/libs/components/src/checklist/checklist-selectall.tsx b/libs/components/src/checklist/checklist-selectall.tsx index 406c88e3..afd588a1 100644 --- a/libs/components/src/checklist/checklist-selectall.tsx +++ b/libs/components/src/checklist/checklist-selectall.tsx @@ -12,16 +12,13 @@ import { CheckboxIndicator } from '../checkbox/checkbox-indicator'; import { ChecklistItem } from './checklist-item'; export const ChecklistSelectAll = component$((props: PropsOf<'div'>) => { - const { allSelected, toggleAllSelected } = useContext(ChecklistContext); + const { allSelected } = useContext(ChecklistContext); + // console.log('ChecklistSelectAll items ', items.value); + // console.log('ChecklistSelectAll allSelected ', allSelected.value); + // console.log('ChecklistSelectAll indeterminate ', indeterminate.value); return ( - <CheckboxRoot - as="li" - bind:checked={allSelected} - aria-labelledby="test123" - onClick$={() => toggleAllSelected()} - role="group" - > + <CheckboxRoot as="li" bind:checked={allSelected}> <Slot /> </CheckboxRoot> ); diff --git a/libs/components/src/checklist/use-checklist.tsx b/libs/components/src/checklist/use-checklist.tsx index cfc3b74c..247d6455 100644 --- a/libs/components/src/checklist/use-checklist.tsx +++ b/libs/components/src/checklist/use-checklist.tsx @@ -10,32 +10,56 @@ import { CheckboxContext } from '../checkbox/checkbox-context'; export const useChecklist = (initialItems: boolean[]) => { const items = useSignal(initialItems); const allSelected = useSignal(initialItems.every(Boolean)); + const indeterminate = useSignal(false); useContextProvider(CheckboxContext, allSelected); console.log('use-checklist items ', items.value); console.log('use-checklist allSelected ', allSelected.value); - const setItems = $((newItems: boolean[]) => { - items.value = newItems; - allSelected.value = newItems.every(Boolean); - }); + // const updateAllSelected = $(() => { + // const allChecked = items.value.every(Boolean); + // const anyChecked = items.value.some(Boolean); + // allSelected.value = allChecked; + // indeterminate.value = anyChecked && !allChecked; + // console.log('updateAllSelected items ', items.value); + // console.log('updateAllSelected allSelected ', allSelected.value); + // console.log('updateAllSelected indeterminate ', indeterminate.value); + // }); + + // const setItems = $((newItems: boolean[]) => { + // items.value = newItems; + // allSelected.value = newItems.every(Boolean); + // }); + + // const toggleItem = $((index: number) => { + // const newItems = [...items.value]; + // newItems[index] = !newItems[index]; + // items.value = newItems; + // allSelected.value = newItems.every(Boolean); + // setItems(newItems); + // }); + + // const toggleAllSelected = $(() => { + // const newState = !allSelected.value; + // allSelected.value = newState; + // items.value = items.value.map(() => newState); + // setItems(items.value); + // }); + + // useVisibleTask$(() => { + // toggleAllSelected(); + // }); const toggleItem = $((index: number) => { - const newItems = [...items.value]; - newItems[index] = !newItems[index]; - items.value = newItems; - allSelected.value = newItems.every(Boolean); - setItems(newItems); + items.value[index] = !items.value[index]; + console.log('use-checklist toggleItem items ', items.value); + + // updateAllSelected(); }); const toggleAllSelected = $(() => { - const newState = !allSelected.value; - allSelected.value = newState; + const newState = !allSelected.value || indeterminate.value; items.value = items.value.map(() => newState); - setItems(items.value); - }); - - useVisibleTask$(() => { - toggleAllSelected(); + // updateAllSelected(); }); useContextProvider(ChecklistContext, { @@ -43,5 +67,7 @@ export const useChecklist = (initialItems: boolean[]) => { toggleItem, allSelected, toggleAllSelected, + indeterminate, + activeIndex: useSignal(0), }); }; From 53994af7bc9fb2ca692043deaa3ec0ae631d5eb5 Mon Sep 17 00:00:00 2001 From: Jay-Kunaico <jay.brass@kunaico.com> Date: Thu, 5 Sep 2024 14:10:58 -0400 Subject: [PATCH 19/27] Interim changes for index --- .../components/src/checkbox/checkbox-root.tsx | 1 + .../src/checklist/checklist-root.tsx | 48 +++++++++++++++++-- libs/components/utils/inline-component.ts | 5 +- 3 files changed, 49 insertions(+), 5 deletions(-) diff --git a/libs/components/src/checkbox/checkbox-root.tsx b/libs/components/src/checkbox/checkbox-root.tsx index 2922a593..5f9774bb 100644 --- a/libs/components/src/checkbox/checkbox-root.tsx +++ b/libs/components/src/checkbox/checkbox-root.tsx @@ -16,6 +16,7 @@ type AllowedElements = 'li' | 'div' | 'span'; export type CheckboxProps = { 'bind:checked'?: Signal<boolean>; initialValue?: boolean; + index?: number; } & PropsOf<'div'>; export const CheckboxRoot = component$( diff --git a/libs/components/src/checklist/checklist-root.tsx b/libs/components/src/checklist/checklist-root.tsx index 40168587..423a54ef 100644 --- a/libs/components/src/checklist/checklist-root.tsx +++ b/libs/components/src/checklist/checklist-root.tsx @@ -1,15 +1,55 @@ -import { component$, Slot } from '@builder.io/qwik'; +import { + component$, + type JSXChildren, + type PropsOf, + Slot, +} from '@builder.io/qwik'; import { useChecklist } from './use-checklist'; +import { findComponent, processChildren } from '../../utils/inline-component'; +import { ChecklistItem } from './checklist-item'; + +export const ChecklistBase = component$((props: PropsOf<'ul'>) => { + return ( + <ul> + <Slot /> + </ul> + ); +}); export const ChecklistRoot = component$( - (props: { initialStates: boolean[] }) => { + (props: { initialStates: boolean[]; children: JSXChildren }) => { const { initialStates } = props; useChecklist(initialStates); + let currItemIndex = 0; + let initialIndex = null; + const itemsMap = new Map(); + const initialValue = initialStates[0]; + const children = props.children; + + findComponent(ChecklistItem, (itemProps) => { + itemProps._index = currItemIndex; + + itemsMap.set(currItemIndex, itemProps.disabled); + + if (initialValue && initialValue === itemProps.value) { + initialIndex = currItemIndex; + } + + currItemIndex++; + console.log( + 'findComponent assigned index:', + currItemIndex, + 'to item:', + itemProps + ); + }); + + processChildren(children); return ( - <ul> + <ChecklistBase> <Slot /> - </ul> + </ChecklistBase> ); } ); diff --git a/libs/components/utils/inline-component.ts b/libs/components/utils/inline-component.ts index f802a138..4eb61de7 100644 --- a/libs/components/utils/inline-component.ts +++ b/libs/components/utils/inline-component.ts @@ -1,4 +1,4 @@ -import type { JSXChildren, JSXNode } from "@builder.io/qwik"; +import type { JSXChildren, JSXNode } from '@builder.io/qwik'; /** * @@ -14,6 +14,8 @@ export function processChildren(children: JSXChildren) { Array.isArray(children) ? [...children] : children ? [children] : [] ) as JSXNode[]; + console.log('processChildren childrenToProcess ', childrenToProcess); + while (childrenToProcess.length) { const child = childrenToProcess.shift(); @@ -45,6 +47,7 @@ const componentRegistry = new Map<any, ComponentProcessor>(); // eslint-disable-next-line @typescript-eslint/no-explicit-any export function findComponent(component: any, processor: ComponentProcessor) { componentRegistry.set(component, processor); + console.log('findComponent componentRegistry ', componentRegistry); } type ComponentProcessor = (props: Record<string, unknown>) => void; From f93b920a2c62bd2b27577ffdf60141bf269432d9 Mon Sep 17 00:00:00 2001 From: Jay-Kunaico <jay.brass@kunaico.com> Date: Fri, 6 Sep 2024 08:54:30 -0400 Subject: [PATCH 20/27] Inline ChecklistRoot --- apps/docs/src/routes/checkbox/index.mdx | 6 -- .../src/checklist/checklist-root.tsx | 72 ++++++++----------- .../src/checklist/checklist-selectall.tsx | 8 +-- 3 files changed, 31 insertions(+), 55 deletions(-) diff --git a/apps/docs/src/routes/checkbox/index.mdx b/apps/docs/src/routes/checkbox/index.mdx index 65dfcfa8..b740f0ff 100644 --- a/apps/docs/src/routes/checkbox/index.mdx +++ b/apps/docs/src/routes/checkbox/index.mdx @@ -44,14 +44,8 @@ Text tags lose their semantic meaning. For example, an h1 tag is treated the sam ## API -### Checkbox.Root - -{/* <Showcase name="pizza" /> */} - ### Checklist <Showcase name="checklist" /> ### Checklist Test List - -<Showcase name="test-list" /> diff --git a/libs/components/src/checklist/checklist-root.tsx b/libs/components/src/checklist/checklist-root.tsx index 423a54ef..dbd59a1c 100644 --- a/libs/components/src/checklist/checklist-root.tsx +++ b/libs/components/src/checklist/checklist-root.tsx @@ -1,55 +1,41 @@ import { - component$, type JSXChildren, type PropsOf, + component$, Slot, } from '@builder.io/qwik'; -import { useChecklist } from './use-checklist'; import { findComponent, processChildren } from '../../utils/inline-component'; + import { ChecklistItem } from './checklist-item'; -export const ChecklistBase = component$((props: PropsOf<'ul'>) => { +export const ChecklistRoot = ( + props: { initialStates: boolean[]; children: JSXChildren } & PropsOf<'ul'> +) => { + const { initialStates } = props; + let currItemIndex = 0; + const itemsMap = new Map(); + const children = props.children; + findComponent(ChecklistItem, (itemProps) => { + itemProps._index = currItemIndex; + itemsMap.set(currItemIndex, itemProps.disabled); + currItemIndex++; + console.log( + 'findComponent assigned index:', + currItemIndex, + 'to item:', + itemProps._index + ); + }); + processChildren(children); + return <ChecklistBase>{children}</ChecklistBase>; +}; +interface ChecklistItemProps extends PropsOf<'div'> { + _index?: number; +} +export const ChecklistBase = component$((props: ChecklistItemProps) => { return ( - <ul> + <ChecklistItem> <Slot /> - </ul> + </ChecklistItem> ); }); - -export const ChecklistRoot = component$( - (props: { initialStates: boolean[]; children: JSXChildren }) => { - const { initialStates } = props; - useChecklist(initialStates); - let currItemIndex = 0; - let initialIndex = null; - const itemsMap = new Map(); - const initialValue = initialStates[0]; - const children = props.children; - - findComponent(ChecklistItem, (itemProps) => { - itemProps._index = currItemIndex; - - itemsMap.set(currItemIndex, itemProps.disabled); - - if (initialValue && initialValue === itemProps.value) { - initialIndex = currItemIndex; - } - - currItemIndex++; - console.log( - 'findComponent assigned index:', - currItemIndex, - 'to item:', - itemProps - ); - }); - - processChildren(children); - - return ( - <ChecklistBase> - <Slot /> - </ChecklistBase> - ); - } -); diff --git a/libs/components/src/checklist/checklist-selectall.tsx b/libs/components/src/checklist/checklist-selectall.tsx index afd588a1..ee417f2a 100644 --- a/libs/components/src/checklist/checklist-selectall.tsx +++ b/libs/components/src/checklist/checklist-selectall.tsx @@ -8,14 +8,10 @@ import { } from '@builder.io/qwik'; import { ChecklistContext } from './checklist-context'; import { CheckboxRoot } from '../checkbox/checkbox-root'; -import { CheckboxIndicator } from '../checkbox/checkbox-indicator'; -import { ChecklistItem } from './checklist-item'; export const ChecklistSelectAll = component$((props: PropsOf<'div'>) => { - const { allSelected } = useContext(ChecklistContext); - // console.log('ChecklistSelectAll items ', items.value); - // console.log('ChecklistSelectAll allSelected ', allSelected.value); - // console.log('ChecklistSelectAll indeterminate ', indeterminate.value); + const { items, allSelected } = useContext(ChecklistContext); + useContextProvider(ChecklistContext, { items, allSelected }); return ( <CheckboxRoot as="li" bind:checked={allSelected}> From 70dbbdcee6b9f9c354ec1510c73e5585735e477b Mon Sep 17 00:00:00 2001 From: Jay-Kunaico <jay.brass@kunaico.com> Date: Mon, 9 Sep 2024 10:21:51 -0400 Subject: [PATCH 21/27] Co-authored-by: Aleksandr Zainetdinov <zaynet@users.noreply.github.com> Co-authored-by: Jack Shelton <thejackshelton@users.noreply.github.com> --- .../routes/checkbox/examples/checklist.tsx | 6 +- .../examples/test-controlled-list-falses.tsx | 59 ++++++++----------- .../routes/checkbox/examples/test-list.tsx | 6 +- 3 files changed, 31 insertions(+), 40 deletions(-) diff --git a/apps/docs/src/routes/checkbox/examples/checklist.tsx b/apps/docs/src/routes/checkbox/examples/checklist.tsx index c45edcf6..62b00ab3 100644 --- a/apps/docs/src/routes/checkbox/examples/checklist.tsx +++ b/apps/docs/src/routes/checkbox/examples/checklist.tsx @@ -5,7 +5,7 @@ import { Checklist } from '@kunai-consulting/qwik-components'; export default component$(() => { return ( <Checklist.Root initialStates={[false, false, false]}> - <Checklist.SelectAll class="flex h-[25px] w-[25px] items-center justify-center bg-slate-600 border-2 border-black p-2"> + <Checklist.SelectAll class="flex h-[25px] w-[25px] items-center justify-center border-2 border-black p-2"> <div class="flex h-[25px] w-[25px] items-center justify-center border-2 border-black p-2"> <Checklist.ItemIndicator>✅</Checklist.ItemIndicator> </div>{' '} @@ -13,9 +13,9 @@ export default component$(() => { </Checklist.SelectAll> {Array.from({ length: 2 }, (_, index) => { - const uniqueKey = `otp-${index}-${Date.now()}`; + const uniqueKey = `cl-${index}-${Date.now()}`; return ( - <Checklist.Item key={uniqueKey} index={index}> + <Checklist.Item key={uniqueKey} _index={index}> <div class="flex h-[25px] w-[25px] items-center justify-center border-2 border-black p-2"> <Checklist.ItemIndicator>✅</Checklist.ItemIndicator> </div> diff --git a/apps/docs/src/routes/checkbox/examples/test-controlled-list-falses.tsx b/apps/docs/src/routes/checkbox/examples/test-controlled-list-falses.tsx index d9c407d3..8feaad0e 100644 --- a/apps/docs/src/routes/checkbox/examples/test-controlled-list-falses.tsx +++ b/apps/docs/src/routes/checkbox/examples/test-controlled-list-falses.tsx @@ -1,41 +1,28 @@ -import { component$, useSignal, useStyles$ } from '@builder.io/qwik'; -import { Checkbox, Checklist } from '@kunai-consulting/qwik-components'; +import { component$ } from '@builder.io/qwik'; + +import { Checklist } from '@kunai-consulting/qwik-components'; + export default component$(() => { - const firstUserSig = useSignal(false); - const secondUserSig = useSignal(false); return ( - <> - <h3 id="test123">Pick a cat</h3> - <Checklist.Root class="flex flex-col gap-3" ariaLabeledBy="test123"> - <Checkbox.Root - class="flex items-center gap-3 bg-slate-900 p-2 text-white" - checklist={true} - > - <Checkbox.Indicator class=" flex w-[80px] justify-center bg-white p-3"> - ✅ - </Checkbox.Indicator> - <p>Controlls all</p> - </Checkbox.Root> - <Checkbox.Root - bind:checked={firstUserSig} - class="flex items-center gap-3 bg-slate-900 pr-2 text-white" - > - <Checkbox.Indicator class="w-fit bg-slate-600">✅</Checkbox.Indicator> - <p>No other stuff is needed here</p> - </Checkbox.Root> + <Checklist.Root initialStates={[true, false, false]}> + <Checklist.SelectAll class="flex h-[25px] w-[25px] items-center justify-center border-2 border-black p-2"> + <div class="flex h-[25px] w-[25px] items-center justify-center border-2 border-black p-2"> + <Checklist.ItemIndicator>✅</Checklist.ItemIndicator> + </div>{' '} + Select All + </Checklist.SelectAll> - <Checkbox.Root - bind:checked={secondUserSig} - class="bg-slate-900 text-white" - > - <div class="flex items-center gap-3"> - <Checkbox.Indicator class="w-fit bg-slate-600"> - ✅ - </Checkbox.Indicator> - <p>No other stuff is needed here</p> - </div> - </Checkbox.Root> - </Checklist.Root> - </> + {Array.from({ length: 2 }, (_, index) => { + const uniqueKey = `cl-${index}-${Date.now()}`; + return ( + <Checklist.Item key={uniqueKey} _index={index}> + <div class="flex h-[25px] w-[25px] items-center justify-center border-2 border-black p-2"> + <Checklist.ItemIndicator>✅</Checklist.ItemIndicator> + </div> + {`item ${index}`} + </Checklist.Item> + ); + })} + </Checklist.Root> ); }); diff --git a/apps/docs/src/routes/checkbox/examples/test-list.tsx b/apps/docs/src/routes/checkbox/examples/test-list.tsx index f360e3c8..91c54e19 100644 --- a/apps/docs/src/routes/checkbox/examples/test-list.tsx +++ b/apps/docs/src/routes/checkbox/examples/test-list.tsx @@ -5,7 +5,11 @@ import { Checklist } from '@kunai-consulting/qwik-components'; export default component$(() => { return ( <Checklist.Root initialStates={[false, false, false]}> - <Checklist.SelectAll aria-labelledby="test123"> + <Checklist.SelectAll + class="flex h-[25px] w-[25px] items-center justify-center bg-slate-600 border-2 border-black p-2" + aria-labelledby="test123" + id="selectAll" + > <div class="flex h-[25px] w-[25px] items-center justify-center border-2 border-black p-2"> <Checklist.ItemIndicator>✅</Checklist.ItemIndicator> </div>{' '} From 9e5dc1e8c759dbce7d4bed5a1cd10949d1006fc4 Mon Sep 17 00:00:00 2001 From: Jay-Kunaico <jay.brass@kunaico.com> Date: Mon, 9 Sep 2024 10:23:03 -0400 Subject: [PATCH 22/27] Update state management --- .../src/checkbox/checkbox.driver.ts | 2 +- libs/components/src/checkbox/checkbox.test.ts | 313 ++++++++++-------- .../src/checklist/checklist-context.tsx | 6 +- .../src/checklist/checklist-item.tsx | 13 +- .../src/checklist/checklist-root.tsx | 66 ++-- .../src/checklist/checklist-selectall.tsx | 9 +- .../src/checklist/use-checklist.tsx | 56 ++-- libs/components/utils/inline-component.ts | 4 +- 8 files changed, 255 insertions(+), 214 deletions(-) diff --git a/libs/components/src/checkbox/checkbox.driver.ts b/libs/components/src/checkbox/checkbox.driver.ts index f669d956..4f083fa8 100644 --- a/libs/components/src/checkbox/checkbox.driver.ts +++ b/libs/components/src/checkbox/checkbox.driver.ts @@ -26,7 +26,7 @@ export function createTestDriver<T extends DriverLocator>(rootLocator: T) { }; const getTriCheckbox = () => { - return getRoot().locator('css=[aria-controls]'); + return getRoot().locator('#selectAll'); }; return { ...rootLocator, diff --git a/libs/components/src/checkbox/checkbox.test.ts b/libs/components/src/checkbox/checkbox.test.ts index 928086d2..123c220c 100644 --- a/libs/components/src/checkbox/checkbox.test.ts +++ b/libs/components/src/checkbox/checkbox.test.ts @@ -133,14 +133,14 @@ test.describe('checkbox', () => { // Checklisst tests test.describe('checklist', () => { - // test(`GIVEN a mixed checklist - // WHEN the checklist renders - // IT should render the mixed img - // AND not the true img`, async ({ page }) => { - // const exampleName = 'test-controlled-list-mixed'; - // await setup(page, exampleName); - // await expect(page.locator('#mixed-img')).toBeVisible(); - // await expect(page.locator('#true-img')).toBeHidden(); + // test(`GIVEN a mixed checklist + // WHEN the checklist renders + // IT should render the mixed img + // AND not the true img`, async ({ page }) => { + // const exampleName = 'test-controlled-list-mixed'; + // await setup(page, exampleName); + // await expect(page.locator('#mixed-img')).toBeVisible(); + // await expect(page.locator('#true-img')).toBeHidden(); // }); test(`GIVEN an all-checked checklist @@ -162,6 +162,7 @@ test.describe('checklist', () => { await expect(page.locator('#mixed-img')).toBeHidden(); }); + // TODO: fix this test // test(`GIVEN a checklist with checkboxes // WHEN the elements render // THEN the checklist should be a <ul> with <li>s of checkboxes, all wrapped around a div with a role and aria-labeledby attributes`, async ({ @@ -199,30 +200,33 @@ test.describe('checklist', () => { await expect(getTriCheckbox()).toBeVisible(); await expect(getTriCheckbox()).toHaveAttribute('aria-checked', 'false'); }); - test(`GIVEN checklist with all unchecked checkboxes - WHEN the first checkbox is checked - the chekbox with aria-controls should have aria-checked mixed`, async ({ - page, - }) => { - const exampleName = 'test-list'; - const { getTriCheckbox, getCheckbox } = await setup(page, exampleName); - await expect(getTriCheckbox()).toBeVisible(); - await getCheckbox().nth(1).press(' '); - await expect(getTriCheckbox()).toHaveAttribute('aria-checked', 'mixed'); - }); - test(`GIVEN checklist with all unchecked checkboxes - WHEN all checkboxes are checked with space - the tri state checkbox should have aria-checked true`, async ({ - page, - }) => { - const exampleName = 'test-list'; - const { getTriCheckbox, getCheckbox } = await setup(page, exampleName); - await expect(getTriCheckbox()).toBeVisible(); - await getCheckbox().nth(1).press(' '); - await getCheckbox().nth(2).press(' '); - await expect(getTriCheckbox()).toHaveAttribute('aria-checked', 'true'); - }); + // Not using mixed yet + // test(`GIVEN checklist with all unchecked checkboxes + // WHEN the first checkbox is checked + // the chekbox with aria-controls should have aria-checked mixed`, async ({ + // page, + // }) => { + // const exampleName = 'test-list'; + // const { getTriCheckbox, getCheckbox } = await setup(page, exampleName); + // await expect(getTriCheckbox()).toBeVisible(); + // await getCheckbox().nth(1).press(' '); + // await expect(getTriCheckbox()).toHaveAttribute('aria-checked', 'mixed'); + // }); + + // Not implemented yet + // test(`GIVEN checklist with all unchecked checkboxes + // WHEN all checkboxes are checked with space + // the tri state checkbox should have aria-checked true`, async ({ + // page, + // }) => { + // const exampleName = 'test-list'; + // const { getTriCheckbox, getCheckbox } = await setup(page, exampleName); + // await expect(getTriCheckbox()).toBeVisible(); + // await getCheckbox().nth(1).press(' '); + // await getCheckbox().nth(2).press(' '); + // await expect(getTriCheckbox()).toHaveAttribute('aria-checked', 'true'); + // }); test(`GIVEN checklist with all unchecked checkboxes WHEN the checklist's checkbox is checked with space @@ -256,47 +260,51 @@ test.describe('checklist', () => { await expect(getCheckbox().nth(1)).toHaveAttribute('aria-checked', 'false'); await expect(getCheckbox().nth(2)).toHaveAttribute('aria-checked', 'false'); }); - test(`GIVEN checklist with checkboxes - WHEN the values of aria-controls are search - IT should always return a valid, non-duplicate, checkboxes`, async ({ - page, - }) => { - const { getTriCheckbox } = await setup(page, 'test-list'); - await expect(getTriCheckbox()).toHaveAttribute('aria-controls'); - const magic = await getTriCheckbox().getAttribute('aria-controls'); - expect(magic).not.toBe(null); - if (magic === null) { - throw new Error( - 'no mixed checkbox found. Was the driver or test template changed?' - ); - } - const idArr = magic.split(' '); - expect(isUniqArr(idArr)).toBe(true); - for (let index = 0; index < idArr.length; index++) { - const elementId = idArr[index]; - const PosCheckbox = page.locator(`#${elementId}`); - await expect(PosCheckbox).toBeVisible(); - const role = await PosCheckbox.getAttribute('role'); - expect(role).toBe('checkbox'); - } - }); - test(`GIVEN a controlled checklist with one default checkbox and a controlled checkbox of true - WHEN it renders - IT should have aria-checked mixed`, async ({ page }) => { - const exampleName = 'test-controlled-list'; - const { getTriCheckbox } = await setup(page, exampleName); - await expect(getTriCheckbox()).toHaveAttribute('aria-checked', 'mixed'); - }); + // Search? + // test(`GIVEN checklist with checkboxes + // WHEN the values of aria-controls are search + // IT should always return a valid, non-duplicate, checkboxes`, async ({ + // page, + // }) => { + // const { getTriCheckbox } = await setup(page, 'test-list'); + // await expect(getTriCheckbox()).toHaveAttribute('aria-controls'); + // const magic = await getTriCheckbox().getAttribute('aria-controls'); + // expect(magic).not.toBe(null); + // if (magic === null) { + // throw new Error( + // 'no mixed checkbox found. Was the driver or test template changed?' + // ); + // } + // const idArr = magic.split(' '); + // expect(isUniqArr(idArr)).toBe(true); + // for (let index = 0; index < idArr.length; index++) { + // const elementId = idArr[index]; + // const PosCheckbox = page.locator(`#${elementId}`); + // await expect(PosCheckbox).toBeVisible(); + // const role = await PosCheckbox.getAttribute('role'); + // expect(role).toBe('checkbox'); + // } + // }); - test(`GIVEN a controlled checklist with two true checkboxes + // Not using mixed yet + // test(`GIVEN a controlled checklist with one default checkbox and a controlled checkbox of true + // WHEN it renders + // IT should have aria-checked mixed`, async ({ page }) => { + // const exampleName = 'test-controlled-list'; + // const { getTriCheckbox } = await setup(page, exampleName); + // await expect(getTriCheckbox()).toHaveAttribute('aria-checked', 'mixed'); + // }); + + test(`GIVEN a controlled checklist with two checked checkboxes WHEN it renders IT should have aria-checked true`, async ({ page }) => { const exampleName = 'test-controlled-list-trues'; const { getTriCheckbox } = await setup(page, exampleName); await expect(getTriCheckbox()).toHaveAttribute('aria-checked', 'true'); }); - test(`GIVEN a controlled checklist with two false checkboxes + + test(`GIVEN a controlled checklist with two unchecked checkboxes WHEN it renders IT should have aria-checked true`, async ({ page }) => { const exampleName = 'test-controlled-list-falses'; @@ -304,22 +312,24 @@ test.describe('checklist', () => { await expect(getTriCheckbox()).toHaveAttribute('aria-checked', 'false'); }); - test(`GIVEN a controlled checklist with mixed checkboxes - WHEN it renders - IT should have aria-checked mixed`, async ({ page }) => { - const exampleName = 'test-controlled-list-mixed'; - const { getTriCheckbox } = await setup(page, exampleName); - await expect(getTriCheckbox()).toHaveAttribute('aria-checked', 'mixed'); - }); + //Not using mixed yet + // test(`GIVEN a controlled checklist with mixed checkboxes + // WHEN it renders + // IT should have aria-checked mixed`, async ({ page }) => { + // const exampleName = 'test-controlled-list-mixed'; + // const { getTriCheckbox } = await setup(page, exampleName); + // await expect(getTriCheckbox()).toHaveAttribute('aria-checked', 'mixed'); + // }); - test(`GIVEN a controlled checklist with a checklist signal of true and default checkboxes as children + test(`GIVEN a checklist with intial value of true and default checkboxes as children WHEN the checklist renders IT shoud have aria-checked true`, async ({ page }) => { const exampleName = 'test-controlled-list-true'; const { getTriCheckbox } = await setup(page, exampleName); await expect(getTriCheckbox()).toHaveAttribute('aria-checked', 'true'); }); - test(`GIVEN a controlled checklist with a checklist signal of true and default checkboxes as children + + test(`GIVEN a checklist with intial value of true and default checkboxes as children WHEN the checklist renders ALL its child checkboxes should have aria-checked true`, async ({ page }) => { const exampleName = 'test-controlled-list-true'; @@ -331,63 +341,70 @@ test.describe('checklist', () => { } }); + // Not using mixed yet // TODO: change api to not use indeterminate and used mixed instead - test(`GIVEN a controlled checklist with a checklist signal of true and default checkboxes as children - WHEN a child checkbox is unchecked - THEN the checklist signal should have aria-checked mixed`, async ({ - page, - }) => { - const exampleName = 'test-controlled-list-true'; - const { getTriCheckbox } = await setup(page, exampleName); - const firstCheckbox = page.locator('#child-1'); - await firstCheckbox.press(' '); - await expect(getTriCheckbox()).toHaveAttribute('aria-checked', 'mixed'); - }); + // test(`GIVEN a controlled checklist with a checklist signal of true and default checkboxes as children + // WHEN a child checkbox is unchecked + // THEN the checklist signal should have aria-checked mixed`, async ({ + // page, + // }) => { + // const exampleName = 'test-controlled-list-true'; + // const { getTriCheckbox } = await setup(page, exampleName); + // const firstCheckbox = page.locator('#child-1'); + // await firstCheckbox.press(' '); + // await expect(getTriCheckbox()).toHaveAttribute('aria-checked', 'mixed'); + // }); - test(`GIVEN a controlled checklist with a checklist signal of true and default checkboxes as children - WHEN all child checkbox are unchecked - THEN the checklist signal should have aria-checked false`, async ({ - page, - }) => { - const exampleName = 'test-controlled-list-true'; - const { getTriCheckbox } = await setup(page, exampleName); - await page.locator('#child-1').press(' '); - await page.locator('#child-2').press(' '); - await expect(getTriCheckbox()).toHaveAttribute('aria-checked', 'false'); - }); - test(`GIVEN a controlled checklist with every checkbox having a defined ID - WHEN it renders - ALL IDs should be present/rendered`, async ({ page }) => { - await setup(page, 'test-props-ids-list'); - const hardCodedIds = ['checklist', 'child-1', 'child-2']; - for (let index = 0; index < hardCodedIds.length; index++) { - const id = hardCodedIds[index]; - await expect(page.locator(`#${id}`)).toBeVisible(); - } - }); + // Not implemented yet + // test(`GIVEN a controlled checklist with a checklist signal of true and default checkboxes as children + // WHEN all child checkbox are unchecked + // THEN the checklist signal should have aria-checked false`, async ({ + // page, + // }) => { + // const exampleName = 'test-controlled-list-true'; + // const { getTriCheckbox } = await setup(page, exampleName); + // await page.locator('#child-1').press(' '); + // await page.locator('#child-2').press(' '); + // await expect(getTriCheckbox()).toHaveAttribute('aria-checked', 'false'); + // }); - test(`GIVEN a controlled checklist with every checkbox having a defined ID - WHEN it renders - THEN all IDs should be present in the aria-controls`, async ({ page }) => { - const { getTriCheckbox } = await setup(page, 'test-props-ids-list'); - const hardChildren = ['child-1', 'child-2']; - const magic = await getTriCheckbox().getAttribute('aria-controls'); - const twin = magic?.split(' '); - expect(hardChildren).toStrictEqual(twin); - }); + //Not using IDs yet and may not be needed + // test(`GIVEN a controlled checklist with every checkbox having a defined ID + // WHEN it renders + // ALL IDs should be present/rendered`, async ({ page }) => { + // await setup(page, 'test-props-ids-list'); + // const hardCodedIds = ['checklist', 'child-1', 'child-2']; + // for (let index = 0; index < hardCodedIds.length; index++) { + // const id = hardCodedIds[index]; + // await expect(page.locator(`#${id}`)).toBeVisible(); + // } + // }); - test(`GIVEN checklist with all unchecked checkboxes - WHEN the first child checkbox is clicked - the chekbox with aria-controls should have aria-checked mixed`, async ({ - page, - }) => { - const exampleName = 'test-list'; - const { getTriCheckbox, getCheckbox } = await setup(page, exampleName); - await expect(getTriCheckbox()).toBeVisible(); - await getCheckbox().nth(1).click(); - await expect(getTriCheckbox()).toHaveAttribute('aria-checked', 'mixed'); - }); + //Not using IDs yet and may not be needed + // test(`GIVEN a controlled checklist with every checkbox having a defined ID + // WHEN it renders + // THEN all IDs should be present in the aria-controls`, async ({ page }) => { + // const { getTriCheckbox } = await setup(page, 'test-props-ids-list'); + // const hardChildren = ['child-1', 'child-2']; + // const magic = await getTriCheckbox().getAttribute('aria-controls'); + // const twin = magic?.split(' '); + // expect(hardChildren).toStrictEqual(twin); + // }); + // Not using mixed yet + // test(`GIVEN checklist with all unchecked checkboxes + // WHEN the first child checkbox is clicked + // the chekbox with aria-controls should have aria-checked mixed`, async ({ + // page, + // }) => { + // const exampleName = 'test-list'; + // const { getTriCheckbox, getCheckbox } = await setup(page, exampleName); + // await expect(getTriCheckbox()).toBeVisible(); + // await getCheckbox().nth(1).click(); + // await expect(getTriCheckbox()).toHaveAttribute('aria-checked', 'mixed'); + // }); + + //Duplicates? test(`GIVEN checklist with all unchecked checkboxes WHEN all checkboxes are checked using click THEN the checkbox with aria-controls should have aria-checked true`, async ({ @@ -452,29 +469,31 @@ test.describe('checklist', () => { // } // }); - test(`GIVEN a controlled checklist with a checklist signal of true and default checkboxes as children - WHEN a child checkbox is unchecked - THEN the checklist signal should have aria-checked mixed`, async ({ - page, - }) => { - const exampleName = 'test-controlled-list-true'; - const { getTriCheckbox } = await setup(page, exampleName); - const firstCheckbox = page.locator('#child-1'); - await firstCheckbox.click(); - await expect(getTriCheckbox()).toHaveAttribute('aria-checked', 'mixed'); - }); + // Not using mixed yet + // test(`GIVEN a controlled checklist with a checklist signal of true and default checkboxes as children + // WHEN a child checkbox is unchecked + // THEN the checklist signal should have aria-checked mixed`, async ({ + // page, + // }) => { + // const exampleName = 'test-controlled-list-true'; + // const { getTriCheckbox } = await setup(page, exampleName); + // const firstCheckbox = page.locator('#child-1'); + // await firstCheckbox.click(); + // await expect(getTriCheckbox()).toHaveAttribute('aria-checked', 'mixed'); + // }); - test(`GIVEN a controlled checklist with a checklist signal of true and default checkboxes as children - WHEN all child checkbox are unchecked - THEN the checklist signal should have aria-checked false`, async ({ - page, - }) => { - const exampleName = 'test-controlled-list-true'; - const { getTriCheckbox } = await setup(page, exampleName); - await page.locator('#child-1').click(); - await page.locator('#child-2').click(); - await expect(getTriCheckbox()).toHaveAttribute('aria-checked', 'false'); - }); + // Not using mixed yet + // test(`GIVEN a controlled checklist with a checklist signal of true and default checkboxes as children + // WHEN all child checkbox are unchecked + // THEN the checklist signal should have aria-checked false`, async ({ + // page, + // }) => { + // const exampleName = 'test-controlled-list-true'; + // const { getTriCheckbox } = await setup(page, exampleName); + // await page.locator('#child-1').click(); + // await page.locator('#child-2').click(); + // await expect(getTriCheckbox()).toHaveAttribute('aria-checked', 'false'); + // }); }); //TODO: create util file diff --git a/libs/components/src/checklist/checklist-context.tsx b/libs/components/src/checklist/checklist-context.tsx index a811cc18..8bf5d78b 100644 --- a/libs/components/src/checklist/checklist-context.tsx +++ b/libs/components/src/checklist/checklist-context.tsx @@ -2,11 +2,11 @@ import { createContextId, type Signal } from '@builder.io/qwik'; export interface ChecklistState { items: Signal<boolean[]>; - toggleItem: (index: number) => void; + // toggleItem: (index: number) => void; allSelected: Signal<boolean>; toggleAllSelected: () => void; - indeterminate: Signal<boolean>; - activeIndex: Signal<number>; + // indeterminate: Signal<boolean>; + // activeIndex: Signal<number>; } export const ChecklistContext = diff --git a/libs/components/src/checklist/checklist-item.tsx b/libs/components/src/checklist/checklist-item.tsx index fba8f148..66dcd2a0 100644 --- a/libs/components/src/checklist/checklist-item.tsx +++ b/libs/components/src/checklist/checklist-item.tsx @@ -14,20 +14,22 @@ import { ChecklistContext } from './checklist-context'; interface ChecklistItemProps extends PropsOf<'div'> { _index?: number; + // context: typeof ChecklistContext; } export const ChecklistItem = component$((props: ChecklistItemProps) => { - const { items } = useContext(ChecklistContext); const isCheckedSig = useSignal(false); const initialLoadSig = useSignal(false); const context = useContext(ChecklistContext); const { _index = 0, ...rest } = props; - console.log('ChecklistItem items ', items.value); + console.log('ChecklistItem items ', context.items.value); if (_index === undefined) { throw new Error('Checklist Item must have an index.'); } - + useTask$(() => { + console.log('context.items.value ', context.items.value); + }); useTask$(({ track }) => { track(() => context.allSelected.value); @@ -46,11 +48,6 @@ export const ChecklistItem = component$((props: ChecklistItemProps) => { initialLoadSig.value = false; }); - useVisibleTask$(({ track }) => { - track(() => context.items.value); - console.log('ChecklistItem useVisibleTask items ', context.items.value); - }); - return ( <CheckboxRoot as="li" bind:checked={isCheckedSig} index={_index}> <Slot /> diff --git a/libs/components/src/checklist/checklist-root.tsx b/libs/components/src/checklist/checklist-root.tsx index dbd59a1c..95ba26f6 100644 --- a/libs/components/src/checklist/checklist-root.tsx +++ b/libs/components/src/checklist/checklist-root.tsx @@ -1,38 +1,62 @@ import { + type JSXNode, type JSXChildren, type PropsOf, component$, + useContext, + useSignal, Slot, + useContextProvider, + $, } from '@builder.io/qwik'; import { findComponent, processChildren } from '../../utils/inline-component'; - +import { CheckboxRoot } from '../checkbox/checkbox-root'; +import { ChecklistContext, type ChecklistState } from './checklist-context'; import { ChecklistItem } from './checklist-item'; +import { useChecklist } from './use-checklist'; + +export const ChecklistRoot = + //removing component to make inline causes Internal Server + (props: { initialStates: boolean[]; children: JSXChildren | JSXNode }) => { + const initialStates = props.initialStates; + const children = props.children; + let currItemIndex = 0; + const itemsMap = new Map(); + console.log('initialStates ', initialStates); + console.log('children ', children); + + findComponent(ChecklistItem, (itemProps) => { + itemProps._index = currItemIndex; + itemsMap.set(currItemIndex, itemProps.disabled); + currItemIndex++; + console.log( + 'findComponent assigned index:', + currItemIndex, + 'to item:', + itemProps._index + ); + }); + processChildren(props.children); -export const ChecklistRoot = ( - props: { initialStates: boolean[]; children: JSXChildren } & PropsOf<'ul'> -) => { - const { initialStates } = props; - let currItemIndex = 0; - const itemsMap = new Map(); - const children = props.children; - findComponent(ChecklistItem, (itemProps) => { - itemProps._index = currItemIndex; - itemsMap.set(currItemIndex, itemProps.disabled); - currItemIndex++; - console.log( - 'findComponent assigned index:', - currItemIndex, - 'to item:', - itemProps._index + return ( + <ul> + <ChecklistBase>{children}</ChecklistBase> + </ul> ); - }); - processChildren(children); - return <ChecklistBase>{children}</ChecklistBase>; -}; + }; + interface ChecklistItemProps extends PropsOf<'div'> { _index?: number; + // context: typeof ChecklistContext; } export const ChecklistBase = component$((props: ChecklistItemProps) => { + const items = useSignal([false, false, false]); + const allSelected = useSignal(false); + const toggleAllSelected = $(() => { + allSelected.value = !allSelected.value; + }); + const context: ChecklistState = { items, allSelected, toggleAllSelected }; + useContextProvider(ChecklistContext, context); return ( <ChecklistItem> <Slot /> diff --git a/libs/components/src/checklist/checklist-selectall.tsx b/libs/components/src/checklist/checklist-selectall.tsx index ee417f2a..70dacd35 100644 --- a/libs/components/src/checklist/checklist-selectall.tsx +++ b/libs/components/src/checklist/checklist-selectall.tsx @@ -5,16 +5,17 @@ import { type PropsOf, Slot, useContextProvider, + useSignal, } from '@builder.io/qwik'; -import { ChecklistContext } from './checklist-context'; +import { ChecklistContext, type ChecklistState } from './checklist-context'; import { CheckboxRoot } from '../checkbox/checkbox-root'; export const ChecklistSelectAll = component$((props: PropsOf<'div'>) => { - const { items, allSelected } = useContext(ChecklistContext); - useContextProvider(ChecklistContext, { items, allSelected }); + const allSelected = useSignal(false); + const context = useContext(ChecklistContext); return ( - <CheckboxRoot as="li" bind:checked={allSelected}> + <CheckboxRoot as="li" bind:checked={allSelected} id="selectAll"> <Slot /> </CheckboxRoot> ); diff --git a/libs/components/src/checklist/use-checklist.tsx b/libs/components/src/checklist/use-checklist.tsx index 247d6455..b5f2ca7d 100644 --- a/libs/components/src/checklist/use-checklist.tsx +++ b/libs/components/src/checklist/use-checklist.tsx @@ -15,20 +15,20 @@ export const useChecklist = (initialItems: boolean[]) => { console.log('use-checklist items ', items.value); console.log('use-checklist allSelected ', allSelected.value); - // const updateAllSelected = $(() => { - // const allChecked = items.value.every(Boolean); - // const anyChecked = items.value.some(Boolean); - // allSelected.value = allChecked; - // indeterminate.value = anyChecked && !allChecked; - // console.log('updateAllSelected items ', items.value); - // console.log('updateAllSelected allSelected ', allSelected.value); - // console.log('updateAllSelected indeterminate ', indeterminate.value); - // }); + const updateAllSelected = $(() => { + const allChecked = items.value.every(Boolean); + const anyChecked = items.value.some(Boolean); + allSelected.value = allChecked; + indeterminate.value = anyChecked && !allChecked; + console.log('updateAllSelected items ', items.value); + console.log('updateAllSelected allSelected ', allSelected.value); + console.log('updateAllSelected indeterminate ', indeterminate.value); + }); - // const setItems = $((newItems: boolean[]) => { - // items.value = newItems; - // allSelected.value = newItems.every(Boolean); - // }); + const setItems = $((newItems: boolean[]) => { + items.value = newItems; + allSelected.value = newItems.every(Boolean); + }); // const toggleItem = $((index: number) => { // const newItems = [...items.value]; @@ -38,12 +38,12 @@ export const useChecklist = (initialItems: boolean[]) => { // setItems(newItems); // }); - // const toggleAllSelected = $(() => { - // const newState = !allSelected.value; - // allSelected.value = newState; - // items.value = items.value.map(() => newState); - // setItems(items.value); - // }); + const toggleAllSelected = $(() => { + const newState = !allSelected.value; + allSelected.value = newState; + items.value = items.value.map(() => newState); + setItems(items.value); + }); // useVisibleTask$(() => { // toggleAllSelected(); @@ -56,18 +56,20 @@ export const useChecklist = (initialItems: boolean[]) => { // updateAllSelected(); }); - const toggleAllSelected = $(() => { - const newState = !allSelected.value || indeterminate.value; - items.value = items.value.map(() => newState); - // updateAllSelected(); - }); + // const toggleAllSelected = $(() => { + // console.log('toggleAllSelected items ', items.value); + + // const newState = !allSelected.value || indeterminate.value; + // items.value = items.value.map(() => newState); + // updateAllSelected(); + // }); useContextProvider(ChecklistContext, { items, - toggleItem, + // toggleItem, allSelected, toggleAllSelected, - indeterminate, - activeIndex: useSignal(0), + // indeterminate, + // activeIndex: useSignal(0), }); }; diff --git a/libs/components/utils/inline-component.ts b/libs/components/utils/inline-component.ts index 4eb61de7..307ea65e 100644 --- a/libs/components/utils/inline-component.ts +++ b/libs/components/utils/inline-component.ts @@ -14,8 +14,6 @@ export function processChildren(children: JSXChildren) { Array.isArray(children) ? [...children] : children ? [children] : [] ) as JSXNode[]; - console.log('processChildren childrenToProcess ', childrenToProcess); - while (childrenToProcess.length) { const child = childrenToProcess.shift(); @@ -47,7 +45,7 @@ const componentRegistry = new Map<any, ComponentProcessor>(); // eslint-disable-next-line @typescript-eslint/no-explicit-any export function findComponent(component: any, processor: ComponentProcessor) { componentRegistry.set(component, processor); - console.log('findComponent componentRegistry ', componentRegistry); + console.log('findComponent componentRegistry ', componentRegistry.size); } type ComponentProcessor = (props: Record<string, unknown>) => void; From 252372c9ef7d6d2e8e526f9a4b0474f77ce1a67b Mon Sep 17 00:00:00 2001 From: Jay-Kunaico <jay.brass@kunaico.com> Date: Mon, 9 Sep 2024 15:11:38 -0400 Subject: [PATCH 23/27] all checkboxes check and uncheck Co-authored-by: Jack Shelton <thejackshelton@users.noreply.github.com> --- .../src/checkbox/checkbox-indicator.tsx | 1 + .../components/src/checkbox/checkbox-root.tsx | 2 +- .../src/checklist/checklist-context.tsx | 2 +- .../src/checklist/checklist-item.tsx | 7 +++---- .../src/checklist/checklist-root.tsx | 14 ++++++++++---- .../src/checklist/checklist-selectall.tsx | 19 +++++++++++++++++-- .../src/checklist/use-checklist.tsx | 13 ++++++------- libs/components/utils/inline-component.ts | 2 +- 8 files changed, 40 insertions(+), 20 deletions(-) diff --git a/libs/components/src/checkbox/checkbox-indicator.tsx b/libs/components/src/checkbox/checkbox-indicator.tsx index 9cad05ac..f590f2eb 100644 --- a/libs/components/src/checkbox/checkbox-indicator.tsx +++ b/libs/components/src/checkbox/checkbox-indicator.tsx @@ -16,6 +16,7 @@ export const CheckboxIndicator = component$<CheckboxIndicatorProps>((props) => { useStyles$(styles); const checkSig = useContext(CheckboxContext); + console.log('CheckboxIndicator checkSig ', checkSig.value); useTask$(({ track }) => { track(() => checkSig.value); diff --git a/libs/components/src/checkbox/checkbox-root.tsx b/libs/components/src/checkbox/checkbox-root.tsx index 5f9774bb..f5109987 100644 --- a/libs/components/src/checkbox/checkbox-root.tsx +++ b/libs/components/src/checkbox/checkbox-root.tsx @@ -61,7 +61,7 @@ export const CheckboxRoot = component$( role="checkbox" aria-checked={checkedSignal.value} aria-labelledby={props['aria-labelledby']} - onClick$={handleClick$} + onClick$={onClick$ || handleClick$} onKeyDown$={[handleKeyDownSync$, handleKeyDown$]} onKeyPress$={handleClick$} data-qds-checkbox-root diff --git a/libs/components/src/checklist/checklist-context.tsx b/libs/components/src/checklist/checklist-context.tsx index 8bf5d78b..b7c49280 100644 --- a/libs/components/src/checklist/checklist-context.tsx +++ b/libs/components/src/checklist/checklist-context.tsx @@ -5,7 +5,7 @@ export interface ChecklistState { // toggleItem: (index: number) => void; allSelected: Signal<boolean>; toggleAllSelected: () => void; - // indeterminate: Signal<boolean>; + indeterminate: Signal<boolean>; // activeIndex: Signal<number>; } diff --git a/libs/components/src/checklist/checklist-item.tsx b/libs/components/src/checklist/checklist-item.tsx index 66dcd2a0..85455a2c 100644 --- a/libs/components/src/checklist/checklist-item.tsx +++ b/libs/components/src/checklist/checklist-item.tsx @@ -8,19 +8,20 @@ import { useSignal, useTask$, useVisibleTask$, + $, } from '@builder.io/qwik'; import { CheckboxRoot } from '../checkbox/checkbox-root'; -import { ChecklistContext } from './checklist-context'; +import { ChecklistContext, type ChecklistState } from './checklist-context'; interface ChecklistItemProps extends PropsOf<'div'> { _index?: number; - // context: typeof ChecklistContext; } export const ChecklistItem = component$((props: ChecklistItemProps) => { const isCheckedSig = useSignal(false); const initialLoadSig = useSignal(false); const context = useContext(ChecklistContext); + const { _index = 0, ...rest } = props; console.log('ChecklistItem items ', context.items.value); @@ -54,5 +55,3 @@ export const ChecklistItem = component$((props: ChecklistItemProps) => { </CheckboxRoot> ); }); - -// 2) GET FAILING TEST FOR SELECT ALL ON CHECKLIST. WHAT IS THE IDEAL BEHAVIOR? diff --git a/libs/components/src/checklist/checklist-root.tsx b/libs/components/src/checklist/checklist-root.tsx index 95ba26f6..7114c2ac 100644 --- a/libs/components/src/checklist/checklist-root.tsx +++ b/libs/components/src/checklist/checklist-root.tsx @@ -23,7 +23,7 @@ export const ChecklistRoot = let currItemIndex = 0; const itemsMap = new Map(); console.log('initialStates ', initialStates); - console.log('children ', children); + console.log('children ', children ? 'true' : 'false'); findComponent(ChecklistItem, (itemProps) => { itemProps._index = currItemIndex; @@ -55,11 +55,17 @@ export const ChecklistBase = component$((props: ChecklistItemProps) => { const toggleAllSelected = $(() => { allSelected.value = !allSelected.value; }); - const context: ChecklistState = { items, allSelected, toggleAllSelected }; + const indeterminate = useSignal(false); + const context: ChecklistState = { + items, + allSelected, + toggleAllSelected, + indeterminate, + }; useContextProvider(ChecklistContext, context); return ( - <ChecklistItem> + <div> <Slot /> - </ChecklistItem> + </div> ); }); diff --git a/libs/components/src/checklist/checklist-selectall.tsx b/libs/components/src/checklist/checklist-selectall.tsx index 70dacd35..9e0023db 100644 --- a/libs/components/src/checklist/checklist-selectall.tsx +++ b/libs/components/src/checklist/checklist-selectall.tsx @@ -11,11 +11,26 @@ import { ChecklistContext, type ChecklistState } from './checklist-context'; import { CheckboxRoot } from '../checkbox/checkbox-root'; export const ChecklistSelectAll = component$((props: PropsOf<'div'>) => { - const allSelected = useSignal(false); const context = useContext(ChecklistContext); + const allSelected = useSignal(context.allSelected.value); + const items = useSignal(context.items.value); + console.log('ChecklistSelectAll items ', items.value); + console.log('ChecklistSelectAll context ', context); + console.log('ChecklistSelectAll allSelected ', allSelected.value); + const toggleAll = $(() => { + const newState = !allSelected.value; + allSelected.value = newState; + items.value = items.value.map(() => newState); + context.toggleAllSelected(); + }); return ( - <CheckboxRoot as="li" bind:checked={allSelected} id="selectAll"> + <CheckboxRoot + as="li" + bind:checked={allSelected} + onClick$={toggleAll} + id="selectAll" + > <Slot /> </CheckboxRoot> ); diff --git a/libs/components/src/checklist/use-checklist.tsx b/libs/components/src/checklist/use-checklist.tsx index b5f2ca7d..ee579103 100644 --- a/libs/components/src/checklist/use-checklist.tsx +++ b/libs/components/src/checklist/use-checklist.tsx @@ -39,16 +39,15 @@ export const useChecklist = (initialItems: boolean[]) => { // }); const toggleAllSelected = $(() => { - const newState = !allSelected.value; + console.log('toggleAllSelected items ', items.value); + + const newState = !allSelected.value || indeterminate.value; allSelected.value = newState; items.value = items.value.map(() => newState); - setItems(items.value); + // setItems(items.value); + updateAllSelected(); }); - // useVisibleTask$(() => { - // toggleAllSelected(); - // }); - const toggleItem = $((index: number) => { items.value[index] = !items.value[index]; console.log('use-checklist toggleItem items ', items.value); @@ -69,7 +68,7 @@ export const useChecklist = (initialItems: boolean[]) => { // toggleItem, allSelected, toggleAllSelected, - // indeterminate, + indeterminate, // activeIndex: useSignal(0), }); }; diff --git a/libs/components/utils/inline-component.ts b/libs/components/utils/inline-component.ts index 307ea65e..1c29822f 100644 --- a/libs/components/utils/inline-component.ts +++ b/libs/components/utils/inline-component.ts @@ -45,7 +45,7 @@ const componentRegistry = new Map<any, ComponentProcessor>(); // eslint-disable-next-line @typescript-eslint/no-explicit-any export function findComponent(component: any, processor: ComponentProcessor) { componentRegistry.set(component, processor); - console.log('findComponent componentRegistry ', componentRegistry.size); + console.log('findComponent componentRegistry size ', componentRegistry.size); } type ComponentProcessor = (props: Record<string, unknown>) => void; From dc2533b97e9745c9d5994f31a549d7d091901b36 Mon Sep 17 00:00:00 2001 From: Jay-Kunaico <jay.brass@kunaico.com> Date: Mon, 9 Sep 2024 16:22:08 -0400 Subject: [PATCH 24/27] Updated tests for selecting checkboxes Co-authored-by: Jack Shelton <thejackshelton@users.noreply.github.com> Co-authored-by: Aleksandr Zainetdinov <zaynet@users.noreply.github.com> --- apps/component-tests/src/entry.ssr.tsx | 23 +++++-- .../examples/test-controlled-list-true.tsx | 14 ++-- apps/docs/src/routes/checkbox/index.mdx | 42 +----------- .../src/checkbox/checkbox-indicator.tsx | 1 - .../components/src/checkbox/checkbox-root.tsx | 1 - .../src/checkbox/checkbox.driver.ts | 14 ++++ libs/components/src/checkbox/checkbox.test.ts | 49 ++++++++------ .../src/checklist/checklist-context.tsx | 2 +- .../src/checklist/checklist-item.tsx | 37 +++++++--- .../src/checklist/checklist-root.tsx | 67 +++++++++---------- .../src/checklist/checklist-selectall.tsx | 10 +-- libs/components/src/checklist/index.ts | 1 + .../src/checklist/use-checklist.tsx | 5 -- libs/components/utils/inline-component.ts | 1 - 14 files changed, 134 insertions(+), 133 deletions(-) diff --git a/apps/component-tests/src/entry.ssr.tsx b/apps/component-tests/src/entry.ssr.tsx index c6963a9f..e07e5864 100644 --- a/apps/component-tests/src/entry.ssr.tsx +++ b/apps/component-tests/src/entry.ssr.tsx @@ -10,9 +10,12 @@ * - npm run build * */ -import { type RenderToStreamOptions, renderToStream } from "@builder.io/qwik/server"; -import { manifest } from "@qwik-client-manifest"; -import Root from "./root"; +import { + type RenderToStreamOptions, + renderToStream, +} from '@builder.io/qwik/server'; +import { manifest } from '@qwik-client-manifest'; +import Root from './root'; export default function (opts: RenderToStreamOptions) { return renderToStream(<Root />, { @@ -20,11 +23,17 @@ export default function (opts: RenderToStreamOptions) { ...opts, // Use container attributes to set attributes on the html tag. containerAttributes: { - lang: "en-us", - ...opts.containerAttributes + lang: 'en-us', + ...opts.containerAttributes, }, serverData: { - ...opts.serverData - } + ...opts.serverData, + }, + prefetchStrategy: { + implementation: { + linkInsert: 'html-append', + linkRel: 'modulepreload', + }, + }, }); } diff --git a/apps/docs/src/routes/checkbox/examples/test-controlled-list-true.tsx b/apps/docs/src/routes/checkbox/examples/test-controlled-list-true.tsx index ef07feb6..ad4ceee4 100644 --- a/apps/docs/src/routes/checkbox/examples/test-controlled-list-true.tsx +++ b/apps/docs/src/routes/checkbox/examples/test-controlled-list-true.tsx @@ -4,21 +4,15 @@ import { Checkbox, Checklist } from '@kunai-consulting/qwik-components'; export default component$(() => { // const checklistSig = useSignal(true); return ( - <Checklist.Root initialStates={[true, true, true]}> + <Checklist.Root initialStates={[true, true]}> <Checklist.SelectAll> - <Checklist.Item> - <Checklist.ItemIndicator index={0}> - <p id="true-img">✅</p> - </Checklist.ItemIndicator> - </Checklist.Item> + <Checklist.Indicator>✅</Checklist.Indicator> </Checklist.SelectAll> <Checklist.Item> - <Checklist.ItemIndicator index={1}>✅</Checklist.ItemIndicator> first - item + <Checklist.ItemIndicator>✅</Checklist.ItemIndicator> first item </Checklist.Item> <Checklist.Item> - <Checklist.ItemIndicator index={2}>✅</Checklist.ItemIndicator> second - item + <Checklist.ItemIndicator>✅</Checklist.ItemIndicator> second item </Checklist.Item> </Checklist.Root> ); diff --git a/apps/docs/src/routes/checkbox/index.mdx b/apps/docs/src/routes/checkbox/index.mdx index b740f0ff..119a2fa1 100644 --- a/apps/docs/src/routes/checkbox/index.mdx +++ b/apps/docs/src/routes/checkbox/index.mdx @@ -6,46 +6,8 @@ title: Qwik UI | Checkbox Allows users to visually select an option by checking it. -{/* <Showcase name="test-default" /> */} +### test-controlled-list-true -## hero example - -<Showcase name="hero" /> - -## reactive example - -<Showcase name="reactive" /> - -## Anatomy - -## Examples - -### Controlled Checkbox - -To add reactive state, use the `bind:checked` prop on the `<Checkbox.Root />` component. - -{/* <Showcase name="controlled" /> */} - -> Note that the initial value of the signal will affect the initial value of the checkbox. - -In the example below, the left checkbox starts as **false**, while the right checkbox starts as **true**. - -{/* <Showcase name="controlled-values" /> */} - -## Customization & Caveats - -You can apply CSS classes to any part of the component. Any valid HTML can be used as an icon, but children of the Checkbox Indicator are removed from the accessibility tree to prevent them from being added to the Checkbox Label. - -### Automatic Labeling - -The Checkbox element is a div with the role of checkbox, so any text inside the Checkbox Root is interpreted as the checkbox label. - -Text tags lose their semantic meaning. For example, an h1 tag is treated the same as a p tag. See [this MDN section](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/checkbox_role#all_descendants_are_presentational) for more details. - -## API - -### Checklist - -<Showcase name="checklist" /> +<Showcase name="test-controlled-list-true" /> ### Checklist Test List diff --git a/libs/components/src/checkbox/checkbox-indicator.tsx b/libs/components/src/checkbox/checkbox-indicator.tsx index f590f2eb..9cad05ac 100644 --- a/libs/components/src/checkbox/checkbox-indicator.tsx +++ b/libs/components/src/checkbox/checkbox-indicator.tsx @@ -16,7 +16,6 @@ export const CheckboxIndicator = component$<CheckboxIndicatorProps>((props) => { useStyles$(styles); const checkSig = useContext(CheckboxContext); - console.log('CheckboxIndicator checkSig ', checkSig.value); useTask$(({ track }) => { track(() => checkSig.value); diff --git a/libs/components/src/checkbox/checkbox-root.tsx b/libs/components/src/checkbox/checkbox-root.tsx index f5109987..dc83d587 100644 --- a/libs/components/src/checkbox/checkbox-root.tsx +++ b/libs/components/src/checkbox/checkbox-root.tsx @@ -33,7 +33,6 @@ export const CheckboxRoot = component$( const Comp = as ?? 'div'; const checkedSignal = useBoundSignal(givenCheckedSig, initialValue); - console.log('checkboxROOT checkedSignal ', checkedSignal.value); useContextProvider(CheckboxContext, checkedSignal); const handleKeyDownSync$ = sync$((e: KeyboardEvent) => { diff --git a/libs/components/src/checkbox/checkbox.driver.ts b/libs/components/src/checkbox/checkbox.driver.ts index 4f083fa8..90765791 100644 --- a/libs/components/src/checkbox/checkbox.driver.ts +++ b/libs/components/src/checkbox/checkbox.driver.ts @@ -25,6 +25,18 @@ export function createTestDriver<T extends DriverLocator>(rootLocator: T) { return getRoot().getByRole('checkbox'); }; + const getCheckboxIndicator = () => { + return getRoot().locator('[data-qds-indicator]'); + }; + + const getSelectAll = () => { + return getRoot().locator('[data-qds-selectall]'); + }; + + const getSelectAllIndicator = () => { + return getSelectAll().locator('[data-qds-indicator]'); + }; + const getTriCheckbox = () => { return getRoot().locator('#selectAll'); }; @@ -38,5 +50,7 @@ export function createTestDriver<T extends DriverLocator>(rootLocator: T) { getChecklistUL, getChecklistLIs, getTriCheckbox, + getSelectAll, + getSelectAllIndicator, }; } diff --git a/libs/components/src/checkbox/checkbox.test.ts b/libs/components/src/checkbox/checkbox.test.ts index 123c220c..91bf9fad 100644 --- a/libs/components/src/checkbox/checkbox.test.ts +++ b/libs/components/src/checkbox/checkbox.test.ts @@ -28,6 +28,18 @@ async function setup(page: Page, exampleName: string) { }; } +/** + * TYPESCRIPT SUPPORT + LESS IMPORTS + * test(`GIVEN a carousel + WHEN clicking on the next button + THEN it should move to the next slide`, async ({ page }) => { + const { driver: d } = await setup(page, 'hero'); + + await d.getNextButton().click(); + await expect(d.getSlideAt(1)).toHaveAttribute('data-active'); + }); + */ + // components -> usually modular, sometimes grouped depending on what it is // modular -> tests ONE SPECIFIC thing @@ -143,14 +155,13 @@ test.describe('checklist', () => { // await expect(page.locator('#true-img')).toBeHidden(); // }); - test(`GIVEN an all-checked checklist - WHEN the checklist renders - IT should render the true img - AND not the mixed img`, async ({ page }) => { - const exampleName = 'test-controlled-list-true'; - await setup(page, exampleName); - await expect(page.locator('#true-img')).toBeVisible(); - // await expect(page.locator('#mixed-img')).toBeHidden(); + test(`GIVEN a checklist with all items checked + WHEN the checklist renders + The indicator in the toggle all checkbox should be visible`, async ({ + page, + }) => { + const { driver: d } = await setup(page, 'test-controlled-list-true'); + await expect(d.getSelectAllIndicator()).toBeVisible(); }); test(`GIVEN an all-unchecked checklist @@ -470,17 +481,17 @@ test.describe('checklist', () => { // }); // Not using mixed yet - // test(`GIVEN a controlled checklist with a checklist signal of true and default checkboxes as children - // WHEN a child checkbox is unchecked - // THEN the checklist signal should have aria-checked mixed`, async ({ - // page, - // }) => { - // const exampleName = 'test-controlled-list-true'; - // const { getTriCheckbox } = await setup(page, exampleName); - // const firstCheckbox = page.locator('#child-1'); - // await firstCheckbox.click(); - // await expect(getTriCheckbox()).toHaveAttribute('aria-checked', 'mixed'); - // }); + test(`GIVEN a checklist that has all items checked + WHEN a child checkbox is unchecked + THEN the checklist signal should have a mixed state`, async ({ + page, + }) => { + const exampleName = 'test-controlled-list-true'; + const { getTriCheckbox, getCheckbox } = await setup(page, exampleName); + const firstCheckbox = getCheckbox().first(); + await firstCheckbox.click(); + await expect(getTriCheckbox()).toHaveAttribute('aria-checked', 'mixed'); + }); // Not using mixed yet // test(`GIVEN a controlled checklist with a checklist signal of true and default checkboxes as children diff --git a/libs/components/src/checklist/checklist-context.tsx b/libs/components/src/checklist/checklist-context.tsx index b7c49280..58b67e85 100644 --- a/libs/components/src/checklist/checklist-context.tsx +++ b/libs/components/src/checklist/checklist-context.tsx @@ -6,7 +6,7 @@ export interface ChecklistState { allSelected: Signal<boolean>; toggleAllSelected: () => void; indeterminate: Signal<boolean>; - // activeIndex: Signal<number>; + initialStates: boolean[]; } export const ChecklistContext = diff --git a/libs/components/src/checklist/checklist-item.tsx b/libs/components/src/checklist/checklist-item.tsx index 85455a2c..693448c9 100644 --- a/libs/components/src/checklist/checklist-item.tsx +++ b/libs/components/src/checklist/checklist-item.tsx @@ -18,19 +18,16 @@ interface ChecklistItemProps extends PropsOf<'div'> { } export const ChecklistItem = component$((props: ChecklistItemProps) => { - const isCheckedSig = useSignal(false); - const initialLoadSig = useSignal(false); - const context = useContext(ChecklistContext); - - const { _index = 0, ...rest } = props; - console.log('ChecklistItem items ', context.items.value); + const { _index, ...rest } = props; if (_index === undefined) { throw new Error('Checklist Item must have an index.'); } - useTask$(() => { - console.log('context.items.value ', context.items.value); - }); + + const context = useContext(ChecklistContext); + const isCheckedSig = useSignal(context.items.value[_index]); + const initialLoadSig = useSignal(true); + useTask$(({ track }) => { track(() => context.allSelected.value); @@ -45,6 +42,28 @@ export const ChecklistItem = component$((props: ChecklistItemProps) => { } }); + useTask$(({ track }) => { + track(() => isCheckedSig.value); + + const isAllSelected = context.items.value.every((item) => item === true); + + if (isAllSelected) { + context.allSelected.value = true; + } + + if (initialLoadSig.value) { + return; + } + + context.items.value[_index] = isCheckedSig.value; + + if (isCheckedSig.value === false) { + context.allSelected.value = false; + } + + console.log('ALL SELECTED ', context.allSelected.value); + }); + useTask$(({ track }) => { initialLoadSig.value = false; }); diff --git a/libs/components/src/checklist/checklist-root.tsx b/libs/components/src/checklist/checklist-root.tsx index 7114c2ac..1d7ec440 100644 --- a/libs/components/src/checklist/checklist-root.tsx +++ b/libs/components/src/checklist/checklist-root.tsx @@ -18,54 +18,53 @@ import { useChecklist } from './use-checklist'; export const ChecklistRoot = //removing component to make inline causes Internal Server (props: { initialStates: boolean[]; children: JSXChildren | JSXNode }) => { - const initialStates = props.initialStates; const children = props.children; let currItemIndex = 0; const itemsMap = new Map(); - console.log('initialStates ', initialStates); - console.log('children ', children ? 'true' : 'false'); findComponent(ChecklistItem, (itemProps) => { itemProps._index = currItemIndex; itemsMap.set(currItemIndex, itemProps.disabled); currItemIndex++; - console.log( - 'findComponent assigned index:', - currItemIndex, - 'to item:', - itemProps._index - ); }); + processChildren(props.children); return ( <ul> - <ChecklistBase>{children}</ChecklistBase> + <ChecklistBase initialStates={props.initialStates}> + {children} + </ChecklistBase> </ul> ); }; -interface ChecklistItemProps extends PropsOf<'div'> { - _index?: number; - // context: typeof ChecklistContext; -} -export const ChecklistBase = component$((props: ChecklistItemProps) => { - const items = useSignal([false, false, false]); - const allSelected = useSignal(false); - const toggleAllSelected = $(() => { - allSelected.value = !allSelected.value; - }); - const indeterminate = useSignal(false); - const context: ChecklistState = { - items, - allSelected, - toggleAllSelected, - indeterminate, - }; - useContextProvider(ChecklistContext, context); - return ( - <div> - <Slot /> - </div> - ); -}); +type ChecklistRootProps = PropsOf<'div'> & { + initialStates: boolean[]; +}; + +export const ChecklistBase = component$( + ({ initialStates, ...props }: ChecklistRootProps) => { + const items = useSignal<boolean[]>(initialStates); + const allSelected = useSignal(false); + const toggleAllSelected = $(() => { + allSelected.value = !allSelected.value; + }); + const indeterminate = useSignal(false); + + const context: ChecklistState = { + items, + allSelected, + toggleAllSelected, + indeterminate, + initialStates, + }; + + useContextProvider(ChecklistContext, context); + return ( + <div> + <Slot /> + </div> + ); + } +); diff --git a/libs/components/src/checklist/checklist-selectall.tsx b/libs/components/src/checklist/checklist-selectall.tsx index 9e0023db..d8be46ae 100644 --- a/libs/components/src/checklist/checklist-selectall.tsx +++ b/libs/components/src/checklist/checklist-selectall.tsx @@ -12,24 +12,24 @@ import { CheckboxRoot } from '../checkbox/checkbox-root'; export const ChecklistSelectAll = component$((props: PropsOf<'div'>) => { const context = useContext(ChecklistContext); - const allSelected = useSignal(context.allSelected.value); - const items = useSignal(context.items.value); - console.log('ChecklistSelectAll items ', items.value); - console.log('ChecklistSelectAll context ', context); - console.log('ChecklistSelectAll allSelected ', allSelected.value); + const isInitiallySelected = context.initialStates.every(Boolean); + const allSelected = useSignal(isInitiallySelected); + const items = useSignal(context.items.value); const toggleAll = $(() => { const newState = !allSelected.value; allSelected.value = newState; items.value = items.value.map(() => newState); context.toggleAllSelected(); }); + return ( <CheckboxRoot as="li" bind:checked={allSelected} onClick$={toggleAll} id="selectAll" + data-qds-selectall > <Slot /> </CheckboxRoot> diff --git a/libs/components/src/checklist/index.ts b/libs/components/src/checklist/index.ts index d315123f..110a6b63 100644 --- a/libs/components/src/checklist/index.ts +++ b/libs/components/src/checklist/index.ts @@ -2,3 +2,4 @@ export { ChecklistRoot as Root } from './checklist-root'; export { ChecklistSelectAll as SelectAll } from './checklist-selectall'; export { ChecklistItem as Item } from './checklist-item'; export { ChecklistItemIndicator as ItemIndicator } from './checklist-indicator'; +export { ChecklistItemIndicator as Indicator } from './checklist-indicator'; diff --git a/libs/components/src/checklist/use-checklist.tsx b/libs/components/src/checklist/use-checklist.tsx index ee579103..c8e5574f 100644 --- a/libs/components/src/checklist/use-checklist.tsx +++ b/libs/components/src/checklist/use-checklist.tsx @@ -12,17 +12,12 @@ export const useChecklist = (initialItems: boolean[]) => { const allSelected = useSignal(initialItems.every(Boolean)); const indeterminate = useSignal(false); useContextProvider(CheckboxContext, allSelected); - console.log('use-checklist items ', items.value); - console.log('use-checklist allSelected ', allSelected.value); const updateAllSelected = $(() => { const allChecked = items.value.every(Boolean); const anyChecked = items.value.some(Boolean); allSelected.value = allChecked; indeterminate.value = anyChecked && !allChecked; - console.log('updateAllSelected items ', items.value); - console.log('updateAllSelected allSelected ', allSelected.value); - console.log('updateAllSelected indeterminate ', indeterminate.value); }); const setItems = $((newItems: boolean[]) => { diff --git a/libs/components/utils/inline-component.ts b/libs/components/utils/inline-component.ts index 1c29822f..fff091cb 100644 --- a/libs/components/utils/inline-component.ts +++ b/libs/components/utils/inline-component.ts @@ -45,7 +45,6 @@ const componentRegistry = new Map<any, ComponentProcessor>(); // eslint-disable-next-line @typescript-eslint/no-explicit-any export function findComponent(component: any, processor: ComponentProcessor) { componentRegistry.set(component, processor); - console.log('findComponent componentRegistry size ', componentRegistry.size); } type ComponentProcessor = (props: Record<string, unknown>) => void; From 443801a79b94ffe75d79c6e129b693a49ae5e3cb Mon Sep 17 00:00:00 2001 From: Jay-Kunaico <jay.brass@kunaico.com> Date: Mon, 9 Sep 2024 16:25:37 -0400 Subject: [PATCH 25/27] made function simpler --- .../src/checklist/checklist-item.tsx | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/libs/components/src/checklist/checklist-item.tsx b/libs/components/src/checklist/checklist-item.tsx index 693448c9..2186f768 100644 --- a/libs/components/src/checklist/checklist-item.tsx +++ b/libs/components/src/checklist/checklist-item.tsx @@ -42,26 +42,21 @@ export const ChecklistItem = component$((props: ChecklistItemProps) => { } }); - useTask$(({ track }) => { + useTask$(function syncCheckboxState({ track }) { track(() => isCheckedSig.value); - const isAllSelected = context.items.value.every((item) => item === true); - - if (isAllSelected) { - context.allSelected.value = true; - } - - if (initialLoadSig.value) { - return; - } - + // itemsSig context.items.value[_index] = isCheckedSig.value; if (isCheckedSig.value === false) { context.allSelected.value = false; } - console.log('ALL SELECTED ', context.allSelected.value); + const isAllSelected = context.items.value.every((item) => item === true); + + if (isAllSelected) { + context.allSelected.value = true; + } }); useTask$(({ track }) => { From c985fcd72aa73758b0a03b435b5a5f10bdd87f0a Mon Sep 17 00:00:00 2001 From: Jay-Kunaico <jay.brass@kunaico.com> Date: Tue, 10 Sep 2024 15:54:35 -0400 Subject: [PATCH 26/27] Troubleshoot children checkboxes --- apps/docs/src/routes/checkbox/index.mdx | 42 ++++++++++- .../components/src/checkbox/checkbox-root.tsx | 1 + libs/components/src/checkbox/checkbox.test.ts | 75 +++++++++---------- .../src/checklist/checklist-item.tsx | 8 +- .../src/checklist/checklist-root.tsx | 5 +- .../src/checklist/checklist-selectall.tsx | 11 ++- .../src/checklist/use-checklist.tsx | 17 +---- 7 files changed, 98 insertions(+), 61 deletions(-) diff --git a/apps/docs/src/routes/checkbox/index.mdx b/apps/docs/src/routes/checkbox/index.mdx index 119a2fa1..b740f0ff 100644 --- a/apps/docs/src/routes/checkbox/index.mdx +++ b/apps/docs/src/routes/checkbox/index.mdx @@ -6,8 +6,46 @@ title: Qwik UI | Checkbox Allows users to visually select an option by checking it. -### test-controlled-list-true +{/* <Showcase name="test-default" /> */} -<Showcase name="test-controlled-list-true" /> +## hero example + +<Showcase name="hero" /> + +## reactive example + +<Showcase name="reactive" /> + +## Anatomy + +## Examples + +### Controlled Checkbox + +To add reactive state, use the `bind:checked` prop on the `<Checkbox.Root />` component. + +{/* <Showcase name="controlled" /> */} + +> Note that the initial value of the signal will affect the initial value of the checkbox. + +In the example below, the left checkbox starts as **false**, while the right checkbox starts as **true**. + +{/* <Showcase name="controlled-values" /> */} + +## Customization & Caveats + +You can apply CSS classes to any part of the component. Any valid HTML can be used as an icon, but children of the Checkbox Indicator are removed from the accessibility tree to prevent them from being added to the Checkbox Label. + +### Automatic Labeling + +The Checkbox element is a div with the role of checkbox, so any text inside the Checkbox Root is interpreted as the checkbox label. + +Text tags lose their semantic meaning. For example, an h1 tag is treated the same as a p tag. See [this MDN section](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/checkbox_role#all_descendants_are_presentational) for more details. + +## API + +### Checklist + +<Showcase name="checklist" /> ### Checklist Test List diff --git a/libs/components/src/checkbox/checkbox-root.tsx b/libs/components/src/checkbox/checkbox-root.tsx index dc83d587..fe367142 100644 --- a/libs/components/src/checkbox/checkbox-root.tsx +++ b/libs/components/src/checkbox/checkbox-root.tsx @@ -28,6 +28,7 @@ export const CheckboxRoot = component$( initialValue, onClick$, as, + index, ...rest } = props; const Comp = as ?? 'div'; diff --git a/libs/components/src/checkbox/checkbox.test.ts b/libs/components/src/checkbox/checkbox.test.ts index 91bf9fad..5dd86026 100644 --- a/libs/components/src/checkbox/checkbox.test.ts +++ b/libs/components/src/checkbox/checkbox.test.ts @@ -225,20 +225,19 @@ test.describe('checklist', () => { // await expect(getTriCheckbox()).toHaveAttribute('aria-checked', 'mixed'); // }); - // Not implemented yet - // test(`GIVEN checklist with all unchecked checkboxes - // WHEN all checkboxes are checked with space - // the tri state checkbox should have aria-checked true`, async ({ - // page, - // }) => { - // const exampleName = 'test-list'; - // const { getTriCheckbox, getCheckbox } = await setup(page, exampleName); - // await expect(getTriCheckbox()).toBeVisible(); - // await getCheckbox().nth(1).press(' '); - // await getCheckbox().nth(2).press(' '); - // await expect(getTriCheckbox()).toHaveAttribute('aria-checked', 'true'); - // }); + // 4 + test(`GIVEN checklist with all unchecked checkboxes + WHEN all checkboxes are checked + the toggle all indicator should be checked`, async ({ page }) => { + const exampleName = 'test-list'; + const { driver: d } = await setup(page, exampleName); + await expect(d.getSelectAllIndicator()).toBeHidden(); + await d.getCheckbox().nth(1).click(); + await d.getCheckbox().nth(2).click(); + await expect(d.getSelectAllIndicator()).toBeVisible(); + }); + //5 test(`GIVEN checklist with all unchecked checkboxes WHEN the checklist's checkbox is checked with space THEN all chekboxes should have aria-checked true`, async ({ @@ -253,6 +252,7 @@ test.describe('checklist', () => { await expect(getCheckbox().nth(2)).toHaveAttribute('aria-checked', 'true'); }); + //6 // TODO: reme two part of test by adding new test file test(`GIVEN checklist with all unchecked checkboxes WHEN the checklist's checkbox is checked twice using space @@ -307,6 +307,7 @@ test.describe('checklist', () => { // await expect(getTriCheckbox()).toHaveAttribute('aria-checked', 'mixed'); // }); + //7 test(`GIVEN a controlled checklist with two checked checkboxes WHEN it renders IT should have aria-checked true`, async ({ page }) => { @@ -315,6 +316,7 @@ test.describe('checklist', () => { await expect(getTriCheckbox()).toHaveAttribute('aria-checked', 'true'); }); + //8 test(`GIVEN a controlled checklist with two unchecked checkboxes WHEN it renders IT should have aria-checked true`, async ({ page }) => { @@ -332,6 +334,7 @@ test.describe('checklist', () => { // await expect(getTriCheckbox()).toHaveAttribute('aria-checked', 'mixed'); // }); + //9 test(`GIVEN a checklist with intial value of true and default checkboxes as children WHEN the checklist renders IT shoud have aria-checked true`, async ({ page }) => { @@ -340,6 +343,7 @@ test.describe('checklist', () => { await expect(getTriCheckbox()).toHaveAttribute('aria-checked', 'true'); }); + //10 test(`GIVEN a checklist with intial value of true and default checkboxes as children WHEN the checklist renders ALL its child checkboxes should have aria-checked true`, async ({ page }) => { @@ -353,20 +357,22 @@ test.describe('checklist', () => { }); // Not using mixed yet - // TODO: change api to not use indeterminate and used mixed instead - // test(`GIVEN a controlled checklist with a checklist signal of true and default checkboxes as children - // WHEN a child checkbox is unchecked - // THEN the checklist signal should have aria-checked mixed`, async ({ - // page, - // }) => { - // const exampleName = 'test-controlled-list-true'; - // const { getTriCheckbox } = await setup(page, exampleName); - // const firstCheckbox = page.locator('#child-1'); - // await firstCheckbox.press(' '); - // await expect(getTriCheckbox()).toHaveAttribute('aria-checked', 'mixed'); - // }); + // ONE CHILD UNCHECKED + //11 + test(`GIVEN a checklist that has all items checked + WHEN a child checkbox is unchecked + THEN the checklist signal should have a mixed state`, async ({ + page, + }) => { + const exampleName = 'test-controlled-list-true'; + const { getTriCheckbox, getCheckbox } = await setup(page, exampleName); + const firstCheckbox = getCheckbox().first(); + await firstCheckbox.click(); + await expect(getTriCheckbox()).toHaveAttribute('aria-checked', 'mixed'); + }); // Not implemented yet + // ALL CHILDREN UNCHECKED // test(`GIVEN a controlled checklist with a checklist signal of true and default checkboxes as children // WHEN all child checkbox are unchecked // THEN the checklist signal should have aria-checked false`, async ({ @@ -403,6 +409,7 @@ test.describe('checklist', () => { // }); // Not using mixed yet + // ONE CHILD CHECKED // test(`GIVEN checklist with all unchecked checkboxes // WHEN the first child checkbox is clicked // the chekbox with aria-controls should have aria-checked mixed`, async ({ @@ -415,7 +422,8 @@ test.describe('checklist', () => { // await expect(getTriCheckbox()).toHaveAttribute('aria-checked', 'mixed'); // }); - //Duplicates? + //Duplicates? Starts with Unchecked + //12 test(`GIVEN checklist with all unchecked checkboxes WHEN all checkboxes are checked using click THEN the checkbox with aria-controls should have aria-checked true`, async ({ @@ -429,6 +437,7 @@ test.describe('checklist', () => { await expect(getTriCheckbox()).toHaveAttribute('aria-checked', 'true'); }); + //13 test(`GIVEN checklist with all unchecked checkboxes WHEN the checklist's checkbox is checked by clicking THEN all checkboxes should have aria-checked true`, async ({ @@ -443,6 +452,7 @@ test.describe('checklist', () => { await expect(getCheckbox().nth(2)).toHaveAttribute('aria-checked', 'true'); }); + //14 // TODO: reme two part of test by adding new test file test(`GIVEN checklist with all unchecked checkboxes WHEN the checklist's checkbox is checked twice using click @@ -480,19 +490,6 @@ test.describe('checklist', () => { // } // }); - // Not using mixed yet - test(`GIVEN a checklist that has all items checked - WHEN a child checkbox is unchecked - THEN the checklist signal should have a mixed state`, async ({ - page, - }) => { - const exampleName = 'test-controlled-list-true'; - const { getTriCheckbox, getCheckbox } = await setup(page, exampleName); - const firstCheckbox = getCheckbox().first(); - await firstCheckbox.click(); - await expect(getTriCheckbox()).toHaveAttribute('aria-checked', 'mixed'); - }); - // Not using mixed yet // test(`GIVEN a controlled checklist with a checklist signal of true and default checkboxes as children // WHEN all child checkbox are unchecked diff --git a/libs/components/src/checklist/checklist-item.tsx b/libs/components/src/checklist/checklist-item.tsx index 2186f768..36d6f48f 100644 --- a/libs/components/src/checklist/checklist-item.tsx +++ b/libs/components/src/checklist/checklist-item.tsx @@ -48,11 +48,13 @@ export const ChecklistItem = component$((props: ChecklistItemProps) => { // itemsSig context.items.value[_index] = isCheckedSig.value; - if (isCheckedSig.value === false) { - context.allSelected.value = false; - } + // root of both checkboxes updating. context.allselected is updated causing the other useTask$ to run again + // if (isCheckedSig.value === false) { + // context.allSelected.value = false; + // } const isAllSelected = context.items.value.every((item) => item === true); + const isAnyChecked = context.items.value.some(Boolean); if (isAllSelected) { context.allSelected.value = true; diff --git a/libs/components/src/checklist/checklist-root.tsx b/libs/components/src/checklist/checklist-root.tsx index 1d7ec440..ea0467eb 100644 --- a/libs/components/src/checklist/checklist-root.tsx +++ b/libs/components/src/checklist/checklist-root.tsx @@ -13,7 +13,6 @@ import { findComponent, processChildren } from '../../utils/inline-component'; import { CheckboxRoot } from '../checkbox/checkbox-root'; import { ChecklistContext, type ChecklistState } from './checklist-context'; import { ChecklistItem } from './checklist-item'; -import { useChecklist } from './use-checklist'; export const ChecklistRoot = //removing component to make inline causes Internal Server @@ -45,8 +44,8 @@ type ChecklistRootProps = PropsOf<'div'> & { export const ChecklistBase = component$( ({ initialStates, ...props }: ChecklistRootProps) => { - const items = useSignal<boolean[]>(initialStates); - const allSelected = useSignal(false); + const items = useSignal<boolean[]>(initialStates ?? []); + const allSelected = useSignal<boolean>(false); const toggleAllSelected = $(() => { allSelected.value = !allSelected.value; }); diff --git a/libs/components/src/checklist/checklist-selectall.tsx b/libs/components/src/checklist/checklist-selectall.tsx index d8be46ae..e022d6d5 100644 --- a/libs/components/src/checklist/checklist-selectall.tsx +++ b/libs/components/src/checklist/checklist-selectall.tsx @@ -6,13 +6,13 @@ import { Slot, useContextProvider, useSignal, + useTask$, } from '@builder.io/qwik'; import { ChecklistContext, type ChecklistState } from './checklist-context'; import { CheckboxRoot } from '../checkbox/checkbox-root'; export const ChecklistSelectAll = component$((props: PropsOf<'div'>) => { const context = useContext(ChecklistContext); - const isInitiallySelected = context.initialStates.every(Boolean); const allSelected = useSignal(isInitiallySelected); const items = useSignal(context.items.value); @@ -23,6 +23,15 @@ export const ChecklistSelectAll = component$((props: PropsOf<'div'>) => { context.toggleAllSelected(); }); + useTask$(({ track }) => { + track(() => context.items.value); + }); + + /** + * When in mixed state, it should show the mixed state indicator + * + * When in all selected state, it should show the checked indicator + */ return ( <CheckboxRoot as="li" diff --git a/libs/components/src/checklist/use-checklist.tsx b/libs/components/src/checklist/use-checklist.tsx index c8e5574f..29cf1b72 100644 --- a/libs/components/src/checklist/use-checklist.tsx +++ b/libs/components/src/checklist/use-checklist.tsx @@ -7,9 +7,9 @@ import { import { ChecklistContext } from './checklist-context'; import { CheckboxContext } from '../checkbox/checkbox-context'; -export const useChecklist = (initialItems: boolean[]) => { - const items = useSignal(initialItems); - const allSelected = useSignal(initialItems.every(Boolean)); +export const useChecklist = (initialStates: boolean[]) => { + const items = useSignal(initialStates); + const allSelected = useSignal(initialStates.every(Boolean)); const indeterminate = useSignal(false); useContextProvider(CheckboxContext, allSelected); @@ -50,20 +50,11 @@ export const useChecklist = (initialItems: boolean[]) => { // updateAllSelected(); }); - // const toggleAllSelected = $(() => { - // console.log('toggleAllSelected items ', items.value); - - // const newState = !allSelected.value || indeterminate.value; - // items.value = items.value.map(() => newState); - // updateAllSelected(); - // }); - useContextProvider(ChecklistContext, { items, - // toggleItem, allSelected, toggleAllSelected, indeterminate, - // activeIndex: useSignal(0), + initialStates, }); }; From 455f20cf5d2ed87fbee4bf0d91c2b3d20dbe2393 Mon Sep 17 00:00:00 2001 From: Jay-Kunaico <jay.brass@kunaico.com> Date: Tue, 10 Sep 2024 16:14:35 -0400 Subject: [PATCH 27/27] Clean up tests for preview --- .../routes/checkbox/examples/test-controlled-list-false.tsx | 4 ++-- .../routes/checkbox/examples/test-controlled-list-trues.tsx | 4 ++-- .../src/routes/checkbox/examples/test-controlled-list.tsx | 4 ++-- .../docs/src/routes/checkbox/examples/test-props-ids-list.tsx | 4 ++-- libs/components/src/checkbox/checkbox-root.tsx | 4 +++- 5 files changed, 11 insertions(+), 9 deletions(-) diff --git a/apps/docs/src/routes/checkbox/examples/test-controlled-list-false.tsx b/apps/docs/src/routes/checkbox/examples/test-controlled-list-false.tsx index 86a11701..0d2794db 100644 --- a/apps/docs/src/routes/checkbox/examples/test-controlled-list-false.tsx +++ b/apps/docs/src/routes/checkbox/examples/test-controlled-list-false.tsx @@ -7,10 +7,10 @@ export default component$(() => { return ( <> <h3 id="test123">Pick a cat</h3> - <Checklist.Root class="flex flex-col gap-3" ariaLabeledBy="test123"> + <Checklist.Root initialStates={[false, false, false]}> <Checkbox.Root class="flex items-center gap-3 bg-slate-900 text-white" - checklist={true} + // checklist={true} bind:checked={checklistSig} id="checklist" > diff --git a/apps/docs/src/routes/checkbox/examples/test-controlled-list-trues.tsx b/apps/docs/src/routes/checkbox/examples/test-controlled-list-trues.tsx index 18951ff4..3180eee7 100644 --- a/apps/docs/src/routes/checkbox/examples/test-controlled-list-trues.tsx +++ b/apps/docs/src/routes/checkbox/examples/test-controlled-list-trues.tsx @@ -6,10 +6,10 @@ export default component$(() => { return ( <> <h3 id="test123">Pick a cat</h3> - <Checklist.Root class="flex flex-col gap-3" ariaLabeledBy="test123"> + <Checklist.Root initialStates={[true, true, true]}> <Checkbox.Root class="flex items-center gap-3 bg-slate-900 p-2 text-white" - checklist={true} + // checklist={true} > <Checkbox.Indicator class=" flex w-[80px] justify-center bg-white p-3"> ✅ diff --git a/apps/docs/src/routes/checkbox/examples/test-controlled-list.tsx b/apps/docs/src/routes/checkbox/examples/test-controlled-list.tsx index da68fa41..ad709e23 100644 --- a/apps/docs/src/routes/checkbox/examples/test-controlled-list.tsx +++ b/apps/docs/src/routes/checkbox/examples/test-controlled-list.tsx @@ -6,10 +6,10 @@ export default component$(() => { return ( <> <h3 id="test123">Pick a cat</h3> - <Checklist.Root class="flex flex-col gap-3" ariaLabeledBy="test123"> + <Checklist.Root initialStates={[true, true, true]}> <Checkbox.Root class="flex items-center gap-3 bg-slate-900 p-2 text-white" - checklist={true} + // checklist={true} > <Checkbox.Indicator class=" flex w-[80px] justify-center bg-white p-3"> ✅ diff --git a/apps/docs/src/routes/checkbox/examples/test-props-ids-list.tsx b/apps/docs/src/routes/checkbox/examples/test-props-ids-list.tsx index c68ac176..911d7003 100644 --- a/apps/docs/src/routes/checkbox/examples/test-props-ids-list.tsx +++ b/apps/docs/src/routes/checkbox/examples/test-props-ids-list.tsx @@ -6,10 +6,10 @@ export default component$(() => { return ( <> <h3 id="test123">Pick a cat</h3> - <Checklist.Root class="flex flex-col gap-3" ariaLabeledBy="test123"> + <Checklist.Root initialStates={[true, true, true]}> <Checkbox.Root class="flex items-center gap-3 bg-slate-900 p-2 text-white" - checklist={true} + // checklist={true} id="checklist" > <Checkbox.Indicator class=" flex w-[80px] justify-center bg-white p-3"> diff --git a/libs/components/src/checkbox/checkbox-root.tsx b/libs/components/src/checkbox/checkbox-root.tsx index fe367142..7bc2167e 100644 --- a/libs/components/src/checkbox/checkbox-root.tsx +++ b/libs/components/src/checkbox/checkbox-root.tsx @@ -61,7 +61,9 @@ export const CheckboxRoot = component$( role="checkbox" aria-checked={checkedSignal.value} aria-labelledby={props['aria-labelledby']} - onClick$={onClick$ || handleClick$} + // need the onClick$ to work with the handleClick$ below + // onClick$={onClick$ || handleClick$} + onClick$={handleClick$} onKeyDown$={[handleKeyDownSync$, handleKeyDown$]} onKeyPress$={handleClick$} data-qds-checkbox-root