Skip to content

Commit

Permalink
Added reactivity to examples, and correctly labelling field components (
Browse files Browse the repository at this point in the history
  • Loading branch information
sureshjoshi authored Dec 9, 2024
1 parent 3cfc67e commit 0f7e9d6
Show file tree
Hide file tree
Showing 18 changed files with 339 additions and 126 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ This can manifest in strange, but intentional, ways:

## Progress

The target compatibility for the components below is based on [HeadlessUI React d71fb9c](https://github.com/tailwindlabs/headlessui/tree/d71fb9cd2e12f5a48617b26e6bb3db90b3e07965). A component is considered completed when most of the mapped-over tests pass, and the component functions similarly to [headlessui.com](headlessui.com).

### Forms

- [ ] Button
Expand Down
22 changes: 11 additions & 11 deletions src/lib/button/Button.svelte
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<script lang="ts">
import { type Component, type Snippet } from "svelte";
import { useDisabled } from "$lib/internal/DisabledProvider.svelte";
type Props = {
/** The element or component the button should render as. */
Expand Down Expand Up @@ -30,7 +31,7 @@
let {
as = "button",
autofocus = false,
disabled = false,
disabled = useDisabled() || false,
type = "button",
children,
...theirProps
Expand All @@ -48,27 +49,26 @@
type,
};
let snippetProps: SnippetProps = {
let snippetProps: SnippetProps = $derived({
active,
autofocus,
disabled,
focus,
hover,
};
});
// TODO: Utility function to create this
let dataAttributes: DataAttributes<SnippetProps> = {
"data-active": active,
"data-autofocus": autofocus,
"data-disabled": disabled,
"data-focus": focus,
"data-hover": hover,
};
let dataAttributes: DataAttributes<SnippetProps> = $derived({
"data-active": active || undefined,
"data-autofocus": autofocus || undefined,
"data-disabled": disabled || undefined,
"data-focus": focus || undefined,
"data-hover": hover || undefined,
});
</script>

{#if typeof as === "string"}
<svelte:element this={as} {...theirProps} {...ourProps} {...dataAttributes}>
<!-- <svelte:element this={as} role="button" {type} data-autofocus={autofocus}> -->
{@render children?.({ active, autofocus, disabled, focus, hover })}
</svelte:element>
{:else}
Expand Down
5 changes: 2 additions & 3 deletions src/lib/button/button.dom.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ describe("Rendering", async () => {
</Button>
`);
render(component);
// screen.debug();

expect(screen.getByRole("button").textContent).toEqual(
JSON.stringify({
Expand All @@ -61,7 +60,7 @@ describe("Rendering", async () => {
<script>
import Button from "$lib/button/Button.svelte";
</script>
<Button autoFocus>My Button</Button>
<Button autofocus>My Button</Button>
`);
render(component);

Expand All @@ -79,7 +78,7 @@ describe("Rendering", async () => {
</Button>
`);
render(component);
screen.debug();

expect(screen.getByRole("button")).toHaveAttribute("type");
});
});
Expand Down
37 changes: 26 additions & 11 deletions src/lib/checkbox/Checkbox.svelte
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<script lang="ts">
import type { Component, Snippet } from "svelte";
import { useId } from "../../hooks/use-id";
import { useDisabled } from "$lib/internal/DisabledProvider.svelte";
type Props = {
/** The element or component the checkbox should render as. */
Expand Down Expand Up @@ -53,36 +54,50 @@
id = `headlessui-checkbox-${useId()}`,
as = "span",
autofocus = false,
disabled = false,
checked = false,
disabled = useDisabled() || false,
indeterminate = false,
children,
...theirProps
}: Props & Record<string, any> = $props();
let ourProps = {
function toggle() {
checked = !checked;
}
function onclick(e: MouseEvent) {
e.preventDefault();
toggle();
}
let ourProps = $derived({
id,
autofocus,
disabled,
role: "checkbox",
"aria-checked": checked,
// "aria-invalid": invalid, // ? "" : undefined,
// "aria-labelledby": labelledBy,
// "aria-describedby": describedBy,
};
onclick,
});
let snippetProps: SnippetProps = {
let snippetProps: SnippetProps = $derived({
autofocus,
checked,
disabled,
focus: false,
hover: false,
};
});
// TODO: Utility function to create this
let dataAttributes: DataAttributes<SnippetProps> = {
"data-autofocus": autofocus,
"data-disabled": disabled,
"data-focus": false,
"data-hover": false,
};
let dataAttributes: DataAttributes<SnippetProps> = $derived({
"data-autofocus": autofocus || undefined,
"data-checked": checked || undefined,
"data-disabled": disabled || undefined,
"data-focus": false || undefined,
"data-hover": false || undefined,
});
</script>

{#if typeof as === "string"}
Expand Down
61 changes: 32 additions & 29 deletions src/lib/field/Field.svelte
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
<script lang="ts">
import type { Component, Snippet } from "svelte";
import { type Component, getContext, setContext, type Snippet } from "svelte";
import { useId } from "../../hooks/use-id";
import { getAllContexts, getContext, setContext } from "svelte";
// const contexts = getAllContexts();
// console.log(contexts);
import LabelProvider from "$lib/label/LabelProvider.svelte";
import DisabledProvider, { useDisabled } from "$lib/internal/DisabledProvider.svelte";
type Props = {
/** The element or component the checkbox should render as. */
Expand All @@ -20,8 +17,7 @@
disabled?: boolean;
};
let providedDisabled = getContext<boolean>("headlessui-disabled-context");
console.log("Field: Incoming disabled context:", providedDisabled);
let providedDisabled = useDisabled();
let {
id = `headlessui-control-${useId()}`,
Expand All @@ -31,30 +27,37 @@
...theirProps
}: Props & Record<string, any> = $props();
let ourProps = {
disabled,
"aria-disabled": disabled,
};
let ourProps = $derived({
disabled: disabled || undefined,
"aria-disabled": disabled || undefined,
});
let snippetProps: SnippetProps = {
let snippetProps: SnippetProps = $derived({
disabled,
};
});
// TODO: Utility function to create this
let dataAttributes: DataAttributes<SnippetProps> = {
"data-disabled": disabled,
};
setContext("headlessui-disabled-context", disabled);
let dataAttributes: DataAttributes<SnippetProps> = $derived({
"data-disabled": disabled || undefined,
});
</script>

{#if typeof as === "string"}
<svelte:element this={as} {...theirProps} {...ourProps} {...dataAttributes}>
{@render children?.(snippetProps)}
</svelte:element>
{:else}
{@const AsComponent = as}
<AsComponent {...theirProps} {...ourProps} {...dataAttributes}>
{@render children?.(snippetProps)}
</AsComponent>
{/if}
<DisabledProvider {disabled}>
<LabelProvider name="FieldLabel">
{#if typeof as === "string"}
<svelte:element
this={as}
{...theirProps}
{...ourProps}
{...dataAttributes}
>
{@render children?.(snippetProps)}
</svelte:element>
{:else}
{@const AsComponent = as}
<AsComponent {...theirProps} {...ourProps} {...dataAttributes}>
{@render children?.(snippetProps)}
</AsComponent>
{/if}
</LabelProvider>
</DisabledProvider>
75 changes: 42 additions & 33 deletions src/lib/fieldset/Fieldset.svelte
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
<script lang="ts">
import { type Component, getContext, setContext, type Snippet } from "svelte";
import LabelProvider from "$lib/label/LabelProvider.svelte";
import DisabledProvider, { useDisabled } from "$lib/internal/DisabledProvider.svelte";
type Props = {
/** The element or component the fieldset should render as. */
Expand All @@ -14,48 +16,55 @@
disabled?: boolean;
};
let providedDisabled = getContext<boolean>("headlessui-disabled-context");
console.log("Fieldset: Incoming disabled context:", providedDisabled);
let providedDisabled = useDisabled();
let {
as = "fieldset",
disabled = providedDisabled || false,
children,
...theirProps
}: Props & Record<string, any> = $props();
let ourProps = as === "fieldset"
? {
disabled,
// 'aria-labelledby': labelledBy
}
: {
role: "group",
"aria-disabled": disabled,
// 'aria-labelledby': labelledBy
};
let snippetProps: SnippetProps = {
let ourProps = $derived({
disabled: disabled || undefined,
role: as !== "fieldset" ? "group" : undefined,
"aria-disabled": as !== "fieldset" ? disabled : undefined,
});
let snippetProps: SnippetProps = $derived({
disabled,
};
});
// TODO: Utility function to create this
let dataAttributes: DataAttributes<SnippetProps> = {
"data-disabled": disabled,
};
setContext("headlessui-disabled-context", disabled);
let value = $state({ count: 0 });
setContext("counter", value);
let dataAttributes: DataAttributes<SnippetProps> = $derived({
"data-disabled": disabled || undefined,
});
</script>

{#if typeof as === "string"}
<svelte:element this={as} {...theirProps} {...ourProps} {...dataAttributes}>
{@render children?.(snippetProps)}
</svelte:element>
{:else}
{@const AsComponent = as}
<AsComponent {...theirProps} {...ourProps} {...dataAttributes}>
{@render children?.(snippetProps)}
</AsComponent>
{/if}
<DisabledProvider {disabled}>
<LabelProvider name="FieldsetLabel">
{#snippet labelledBy({ labelIds })}
{#if typeof as === "string"}
<svelte:element
this={as}
{...theirProps}
{...ourProps}
{...dataAttributes}
aria-labelledby={labelIds[0]}
>
{@render children?.(snippetProps)}
</svelte:element>
{:else}
{@const AsComponent = as}
<AsComponent
{...theirProps}
{...ourProps}
{...dataAttributes}
aria-labelledby={labelIds}
>
{@render children?.(snippetProps)}
</AsComponent>
{/if}
{/snippet}
</LabelProvider>
</DisabledProvider>
14 changes: 9 additions & 5 deletions src/lib/fieldset/fieldset.dom.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ describe("Rendering", () => {
expect(fieldset?.firstElementChild).toBeDisabled();
});

it.skip("should link a `Fieldset` to a nested `Legend`", async () => {
it("should link a `Fieldset` to a nested `Legend`", async () => {
const component = await sveltify(`
<script>
import Fieldset from "$lib/fieldset/Fieldset.svelte";
Expand All @@ -106,11 +106,13 @@ describe("Rendering", () => {
assertLinkedWithLabel(fieldset, getLabels());
});

it.skip("should not link a `Label` inside a `Field` to the `Fieldset`", async () => {
it("should not link a `Label` inside a `Field` to the `Fieldset`", async () => {
const component = await sveltify(`
<script>
import Field from "$lib/field/Field.svelte";
import Fieldset from "$lib/fieldset/Fieldset.svelte";
import Input from "$lib/input/Input.svelte";
import Label from "$lib/label/Label.svelte";
import Legend from "$lib/legend/Legend.svelte";
</script>
<Fieldset>
Expand All @@ -135,13 +137,15 @@ describe("Rendering", () => {
assertLinkedWithLabel(fieldset, legend);

// The input/control should be linked with the label
assertLinkedWithLabel(input, label);
// TODO: This expects that an IdProvider in Field will have re-named the input to "headlessui-control"
// assertLinkedWithLabel(input, label);

// The fieldset should not be linked with the label
assertNotLinkedWithLabel(fieldset, label);

// The input/control should not be linked with the legend
assertNotLinkedWithLabel(input, legend);
// // The input/control should not be linked with the legend
// TODO: This expects that an IdProvider in Field will have re-named the input to "headlessui-control"
// assertNotLinkedWithLabel(input, legend);

// The field should not be linked with anything
assertNotLinkedWithLabel(field, legend);
Expand Down
9 changes: 9 additions & 0 deletions src/lib/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export * from "./button/Button.svelte";
export * from "./checkbox/Checkbox.svelte";
export * from "./field/Field.svelte";
export * from "./fieldset/Fieldset.svelte";
export * from "./fragment/Fragment.svelte";
export * from "./input/Input.svelte";
export * from "./label/Label.svelte";
export * from "./legend/Legend.svelte";
export * from "./switch/Switch.svelte";
Loading

0 comments on commit 0f7e9d6

Please sign in to comment.