Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added Description and a DescriptionProvider #17

Merged
merged 1 commit into from
Dec 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions src/lib/checkbox/Checkbox.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
import type { Component, Snippet } from "svelte";
import { useId } from "$lib/internal/hooks/use-id";
import { useDisabled } from "$lib/internal/DisabledProvider.svelte";
import { useDescribedBy } from "$lib/description/DescriptionProvider.svelte";
import { useLabelledBy } from "$lib/label/LabelProvider.svelte";

type Props = {
/** The element or component the checkbox should render as. */
Expand Down Expand Up @@ -70,15 +72,18 @@
toggle();
}

let describedBy = $derived(useDescribedBy());
let labelledBy = $derived(useLabelledBy());

let ourProps = $derived({
id,
autofocus,
disabled,
role: "checkbox",
"aria-checked": checked,
// "aria-invalid": invalid, // ? "" : undefined,
// "aria-labelledby": labelledBy,
// "aria-describedby": describedBy,
"aria-labelledby": labelledBy,
"aria-describedby": describedBy,
onclick,
});

Expand Down
63 changes: 63 additions & 0 deletions src/lib/description/Description.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<script lang="ts">
import { type Component, onMount, type Snippet } from "svelte";
import { useId } from "$lib/internal/hooks/use-id";
// import { useLabelContext } from "./LabelProvider.svelte";
import { useDisabled } from "$lib/internal/DisabledProvider.svelte";
import { useDescriptionContext } from "./DescriptionProvider.svelte";

type Props = {
/** The element or component the input should render as. */
as?: string | Component;
/** TODO: Fix naming */
htmlFor?: string;
children?: Snippet<[SnippetProps]>;
};

type SnippetProps = {
/** Whether or not the parent `Field` is disabled. */
disabled?: boolean;
};

let {
id = `headlessui-description-${useId()}`,
as = "p",
htmlFor,
passive = false,
children,
...theirProps
}: Props & Record<string, any> = $props();

function onclick(e: MouseEvent) {
e.preventDefault;
}

let context = useDescriptionContext();
onMount(() => context.register(id));

let ourProps = $derived({
id,
for: htmlFor,
onclick,
});

let disabled = useDisabled() || false;

let snippetProps: SnippetProps = $derived({
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}
71 changes: 71 additions & 0 deletions src/lib/description/DescriptionProvider.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<script module lang="ts">
import { getContext, type Snippet } from "svelte";

export type DescriptionContext = {
name?: string;
props?: object;
descriptionIds?: string[];
register: (value: string) => void;
};

export const DESCRIPTION_CONTEXT_NAME = Symbol(
"headlessui-description-context",
);
export function useDescriptionContext(): DescriptionContext {
const context = getContext<DescriptionContext>(DESCRIPTION_CONTEXT_NAME);
if (!context) {
throw new Error(
"You used a <Description /> component, but it is not inside a relevant parent.",
);
}
return context;
}

export function useDescribedBy(
alwaysAvailableIds?: (string | undefined | null)[],
): string | undefined {
const context = getContext<DescriptionContext>(DESCRIPTION_CONTEXT_NAME);
// if ((alwaysAvailableIds?.length ?? 0) > 0) {
// return [labelIds, ...alwaysAvailableIds!].filter(Boolean).join(" ");
// }
return context?.descriptionIds?.join(" ");
}
</script>

<script lang="ts">
import { setContext } from "svelte";

type Props = {
/** A name associated with this LabelProvider */
name?: string;
children?: Snippet;
describedBy?: Snippet<[SnippetProps]>;
};

type SnippetProps = {
descriptionIds: string[];
};

let descriptionIds: string[] = $state([]);

let { name, children, describedBy, ...theirProps }: Props = $props();

let context: DescriptionContext = {
name,
props: theirProps,
descriptionIds,
register,
};
setContext(DESCRIPTION_CONTEXT_NAME, context);

function register(value: string) {
descriptionIds.push(value); // = [...labelIds, value];
return () => {
descriptionIds.filter((descriptionId) => descriptionId !== value);
};
}
</script>

<!-- TODO: I hate this snippet children vs non-children naming convention so much -->
{@render children?.()}
{@render describedBy?.({ descriptionIds })}
85 changes: 85 additions & 0 deletions src/lib/description/description.dom.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import Description from "./Description.svelte";
import type { SvelteComponent } from "svelte";
import { render, screen } from "@testing-library/svelte";
import {
assertActiveElement,
assertSwitch,
getByText,
getSwitch,
getSwitchLabel,
SwitchState,
} from "../../test-utils/accessibility-assertions";
import { click, focus, Keys, mouseEnter, press } from "../../test-utils/interactions";

vi.mock("../../hooks/use-id");

it("empty", () => {
});

// it('should be possible to use a DescriptionProvider without using a Description', async () => {
// function Component(props: { children: ReactNode }) {
// let [describedby, DescriptionProvider] = useDescriptions()

// return (
// <DescriptionProvider>
// <div aria-describedby={describedby}>{props.children}</div>
// </DescriptionProvider>
// )
// }

// function Example() {
// return <Component>No description</Component>
// }

// let { container } = render(<Example />)
// expect(container.firstChild).toMatchSnapshot()
// })

// it('should be possible to use a DescriptionProvider and a single Description, and have them linked', async () => {
// function Component(props: { children: ReactNode }) {
// let [describedby, DescriptionProvider] = useDescriptions()

// return (
// <DescriptionProvider>
// <div aria-describedby={describedby}>{props.children}</div>
// </DescriptionProvider>
// )
// }

// function Example() {
// return (
// <Component>
// <Description>I am a description</Description>
// <span>Contents</span>
// </Component>
// )
// }

// let { container } = render(<Example />)
// expect(container.firstChild).toMatchSnapshot()
// })

// it('should be possible to use a DescriptionProvider and multiple Description components, and have them linked', async () => {
// function Component(props: { children: ReactNode }) {
// let [describedby, DescriptionProvider] = useDescriptions()

// return (
// <DescriptionProvider>
// <div aria-describedby={describedby}>{props.children}</div>
// </DescriptionProvider>
// )
// }

// function Example() {
// return (
// <Component>
// <Description>I am a description</Description>
// <span>Contents</span>
// <Description>I am also a description</Description>
// </Component>
// )
// }

// let { container } = render(<Example />)
// expect(container.firstChild).toMatchSnapshot()
// })
33 changes: 18 additions & 15 deletions src/lib/field/Field.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { useId } from "$lib/internal/hooks/use-id";
import LabelProvider from "$lib/label/LabelProvider.svelte";
import DisabledProvider, { useDisabled } from "$lib/internal/DisabledProvider.svelte";
import DescriptionProvider from "$lib/description/DescriptionProvider.svelte";

type Props = {
/** The element or component the checkbox should render as. */
Expand Down Expand Up @@ -44,20 +45,22 @@

<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}
<DescriptionProvider name="FieldDescription">
{#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}
</DescriptionProvider>
</LabelProvider>
</DisabledProvider>
10 changes: 6 additions & 4 deletions src/lib/input/Input.svelte
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<script lang="ts">
import { type Component, type Snippet } from "svelte";
import { useId } from "$lib/internal/hooks/use-id";
import { useDescribedBy } from "$lib/description/DescriptionProvider.svelte";
import { useLabelledBy } from "$lib/label/LabelProvider.svelte";
import { useDisabled } from "$lib/internal/DisabledProvider.svelte";

Expand Down Expand Up @@ -40,15 +41,16 @@
}: Props & Record<string, any> = $props();

// TODO: This feels soooo janky, this can't be the runes-way...
let describedBy = $derived(useDescribedBy());
let labelledBy = $derived(useLabelledBy());

let ourProps = $derived({
id,
autofocus,
disabled,
"aria-describedby": describedBy,
"aria-invalid": invalid, // ? "" : undefined,
"aria-labelledby": labelledBy?.[0],
// "aria-describedby": describedBy,
"aria-labelledby": labelledBy,
});

let snippetProps: SnippetProps = $derived({
Expand All @@ -63,8 +65,8 @@
let dataAttributes: DataAttributes<SnippetProps> = $derived({
"data-autofocus": autofocus || undefined,
"data-disabled": disabled || undefined,
"data-focus": invalid || undefined,
"data-hover": invalid || undefined,
"data-focus": invalid || undefined, // TODO
"data-hover": invalid || undefined, // TODO
"data-invalid": invalid || undefined,
});
</script>
Expand Down
Loading
Loading