Initial docs (#61)
Add initial documentation site. Co-authored-by: ambiguous48 <33713262+ambiguous48@users.noreply.github.com>
This commit is contained in:
6
src/routes/__error.svelte
Normal file
6
src/routes/__error.svelte
Normal file
@@ -0,0 +1,6 @@
|
||||
<div
|
||||
class="h-screen -mt-[61px] text-center flex flex-col items-center justify-center text-stone-600"
|
||||
>
|
||||
<h1 class="text-3xl">Error</h1>
|
||||
<p class="font-light">This page could not be loaded.</p>
|
||||
</div>
|
||||
@@ -2,4 +2,23 @@
|
||||
import "../app.css";
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="sticky top-0 px-6 text-xl text-stone-500 w-full backdrop-blur border-b z-20"
|
||||
>
|
||||
<div class="py-4 pr-4 flex justify-between">
|
||||
<a href="/docs">
|
||||
<span class="text-amber-600">Svelte</span> Headless UI
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/rgossiaux/svelte-headlessui"
|
||||
class="hover:text-black"
|
||||
>
|
||||
<svg viewBox="0 0 16 16" fill="currentColor" class="w-5 h-5">
|
||||
<path
|
||||
d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<slot />
|
||||
|
||||
11
src/routes/docs/_Preview.svelte
Normal file
11
src/routes/docs/_Preview.svelte
Normal file
@@ -0,0 +1,11 @@
|
||||
<script lang="ts">
|
||||
export let url: string;
|
||||
export let code: string;
|
||||
let _class = "";
|
||||
export { _class as class };
|
||||
</script>
|
||||
|
||||
<div class:rounded-xl={true} class={_class}>
|
||||
<iframe src={url} class="w-full h-full" />
|
||||
</div>
|
||||
<p>{code}</p>
|
||||
129
src/routes/docs/_TableOfContents.svelte
Normal file
129
src/routes/docs/_TableOfContents.svelte
Normal file
@@ -0,0 +1,129 @@
|
||||
<script lang="ts">
|
||||
import { createRunWithCleanup } from "$lib/utils/run-with-cleanup";
|
||||
import { onMount } from "svelte";
|
||||
import TocItems, { type TocItem } from "./_TocItems.svelte";
|
||||
|
||||
export let el: HTMLElement | null;
|
||||
|
||||
let runWithCleanup = createRunWithCleanup();
|
||||
let activeId: string | undefined;
|
||||
let items: TocItem[] = [];
|
||||
let idToItem: { [id: string]: TocItem } = {};
|
||||
let visibleIds = new Set<string>();
|
||||
|
||||
$: flatItems = flattenItems(items);
|
||||
|
||||
function offsetItemById(id: string, offset: number): TocItem | undefined {
|
||||
let item = idToItem[id];
|
||||
if (item) {
|
||||
return flatItems[item.index + offset];
|
||||
}
|
||||
}
|
||||
|
||||
function computeActiveId() {
|
||||
if (visibleIds.size === 0) {
|
||||
activeId = undefined;
|
||||
return;
|
||||
}
|
||||
activeId = Array.from(visibleIds)
|
||||
.map((id) => idToItem[id]!)
|
||||
.reduce((a, b) => (a.index < b.index ? a : b))?.id;
|
||||
}
|
||||
|
||||
let observer: IntersectionObserver | null = null;
|
||||
onMount(() => {
|
||||
observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
visibleIds.add(entry.target.id);
|
||||
computeActiveId();
|
||||
} else if (visibleIds.has(entry.target.id)) {
|
||||
visibleIds.delete(entry.target.id);
|
||||
if (visibleIds.size > 0) {
|
||||
computeActiveId();
|
||||
} else {
|
||||
let scrollingDown =
|
||||
entry.boundingClientRect.y < (entry.rootBounds?.y ?? 0);
|
||||
// If scrolling down, this one should remain active: we're still in the contents of it
|
||||
// until we get to the next item below.
|
||||
// If scrolling up, we should go to the previous item.
|
||||
if (!scrollingDown) {
|
||||
activeId = offsetItemById(entry.target.id, -1)?.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
{ rootMargin: `0% 0% -80% 0%` }
|
||||
);
|
||||
});
|
||||
|
||||
function generateItems(root: HTMLElement, ignoreH1 = true) {
|
||||
let headings = Array.from(
|
||||
root.querySelectorAll(`${ignoreH1 ? "" : "h1, "}h2, h3, h4, h5, h6`)
|
||||
);
|
||||
let newItems: TocItem[] = [];
|
||||
let index = 0;
|
||||
for (const heading of headings) {
|
||||
let headingLevel = parseInt(heading.tagName[1]);
|
||||
const newItem = {
|
||||
headingLevel,
|
||||
index: index++,
|
||||
id: heading.id,
|
||||
url: "#" + heading.id,
|
||||
title: heading.textContent || "",
|
||||
};
|
||||
idToItem[newItem.id] = newItem;
|
||||
let parentItems: TocItem[] = newItems;
|
||||
while (
|
||||
parentItems.length > 0 &&
|
||||
parentItems[parentItems.length - 1].headingLevel < headingLevel
|
||||
) {
|
||||
let child = parentItems[parentItems.length - 1];
|
||||
child.items = child.items || [];
|
||||
parentItems = child.items;
|
||||
}
|
||||
parentItems.push(newItem);
|
||||
}
|
||||
return newItems;
|
||||
}
|
||||
|
||||
$: items = el ? generateItems(el) : [];
|
||||
|
||||
function flattenItems(itemList?: TocItem[]) {
|
||||
let result: TocItem[] = [];
|
||||
if (!itemList) {
|
||||
return result;
|
||||
}
|
||||
for (let item of itemList) {
|
||||
result = result.concat([item], flattenItems(item.items));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
$: runWithCleanup(() => {
|
||||
if (!observer) {
|
||||
return;
|
||||
}
|
||||
let ids = flatItems.map((item) => item.id);
|
||||
ids.forEach((id) => {
|
||||
let element = document.getElementById(id);
|
||||
if (element) {
|
||||
observer!.observe(element);
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
ids.forEach((id) => {
|
||||
let element = document.getElementById(id);
|
||||
if (element) {
|
||||
observer!.unobserve(element);
|
||||
}
|
||||
});
|
||||
};
|
||||
}, "observer");
|
||||
</script>
|
||||
|
||||
<nav>
|
||||
<TocItems {items} {activeId} />
|
||||
</nav>
|
||||
38
src/routes/docs/_TocItems.svelte
Normal file
38
src/routes/docs/_TocItems.svelte
Normal file
@@ -0,0 +1,38 @@
|
||||
<script lang="ts" context="module">
|
||||
export interface TocItem {
|
||||
id: string;
|
||||
url: string;
|
||||
title: string;
|
||||
headingLevel: number;
|
||||
index: number;
|
||||
items?: TocItem[];
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export let items: TocItem[] = [];
|
||||
export let activeId: string | undefined;
|
||||
</script>
|
||||
|
||||
<ol class="pl-4">
|
||||
{#each items as item (item.url)}
|
||||
<li>
|
||||
<a href={item.url} class:active={item.id === activeId}>{item.title}</a>
|
||||
{#if item.items}
|
||||
<svelte:self items={item.items} {activeId} />
|
||||
{/if}
|
||||
</li>
|
||||
{/each}
|
||||
</ol>
|
||||
|
||||
<style lang="postcss">
|
||||
.active {
|
||||
@apply font-bold;
|
||||
}
|
||||
li {
|
||||
@apply pt-3;
|
||||
}
|
||||
a:hover {
|
||||
@apply underline;
|
||||
}
|
||||
</style>
|
||||
83
src/routes/docs/__layout.svelte
Normal file
83
src/routes/docs/__layout.svelte
Normal file
@@ -0,0 +1,83 @@
|
||||
<script lang="ts">
|
||||
import TableOfContents from "./_TableOfContents.svelte";
|
||||
import { page } from "$app/stores";
|
||||
|
||||
let el: HTMLElement | null = null;
|
||||
|
||||
$: isHome = $page.path.endsWith("docs");
|
||||
$: base = isHome ? "docs/" : "";
|
||||
|
||||
$: pages = [
|
||||
{ url: "../docs", text: "Home" },
|
||||
{ url: `${base}general-concepts`, text: "General concepts" },
|
||||
{ url: `${base}tailwind-ui`, text: "Use with Tailwind UI" },
|
||||
{ url: `${base}version-history`, text: "Version history" },
|
||||
];
|
||||
|
||||
$: components = [
|
||||
{ url: `${base}dialog`, text: "Dialog" },
|
||||
{ url: `${base}disclosure`, text: "Disclosure" },
|
||||
{ url: `${base}listbox`, text: "Listbox" },
|
||||
{ url: `${base}menu`, text: "Menu" },
|
||||
{ url: `${base}popover`, text: "Popover" },
|
||||
{ url: `${base}radio-group`, text: "Radio Group" },
|
||||
{ url: `${base}switch`, text: "Switch" },
|
||||
{ url: `${base}tabs`, text: "Tabs" },
|
||||
{ url: `${base}transition`, text: "Transition" },
|
||||
];
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<link rel="stylesheet" href="/prism-one-light.css" />
|
||||
</svelte:head>
|
||||
|
||||
<div class="flex">
|
||||
<div class="w-52 min-w-fit hidden md:block flex-shrink-0">
|
||||
<nav title="Pages" class="sticky top-20 ml-6 flex flex-col">
|
||||
{#each pages as p (p.url)}
|
||||
<a
|
||||
href={p.url}
|
||||
class:font-bold={$page.path.includes(p.url)}
|
||||
class="py-1 hover:decoration-stone-400 hover:underline">{p.text}</a
|
||||
>
|
||||
{/each}
|
||||
<hr class="w-24 my-4" />
|
||||
{#each components as component (component.url)}
|
||||
<a
|
||||
href={component.url}
|
||||
class:font-bold={$page.path.includes(component.url)}
|
||||
class="py-1 hover:decoration-stone-400 hover:underline"
|
||||
>{component.text}</a
|
||||
>
|
||||
{/each}
|
||||
</nav>
|
||||
</div>
|
||||
<article class="prose max-w-3xl min-w-0 mt-5 px-6 pb-8" bind:this={el}>
|
||||
<slot />
|
||||
</article>
|
||||
<div class="w-64 text-sm hidden flex-shrink-0 lg:block">
|
||||
<nav
|
||||
title="Table of contents"
|
||||
class="sticky top-0 pt-20 pb-4 -mt-[61px] max-h-screen overflow-y-auto"
|
||||
>
|
||||
{#key $page}
|
||||
<TableOfContents {el} />
|
||||
{/key}
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="postcss">
|
||||
article {
|
||||
:global(h1),
|
||||
:global(h2),
|
||||
:global(h3),
|
||||
:global(h4) {
|
||||
@apply before:h-20 before:-mt-20 before:block before:content-[""];
|
||||
|
||||
:global(a) {
|
||||
@apply no-underline hover:underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
393
src/routes/docs/dialog.svx
Normal file
393
src/routes/docs/dialog.svx
Normal file
@@ -0,0 +1,393 @@
|
||||
# Dialog
|
||||
|
||||
<script>
|
||||
import Preview from "./_Preview.svelte";
|
||||
</script>
|
||||
|
||||
<Preview url="examples/dialog" code="" class="h-[410px] bg-indigo-300"/>
|
||||
|
||||
## Basic example
|
||||
|
||||
Dialogs are built using the `Dialog`, `DialogOverlay`, `DialogTitle`, and `DialogDescription` components.
|
||||
|
||||
When the dialog's `open` prop is `true`, the contents of the dialog will render. Focus will be moved inside the dialog and trapped there as the user cycles through the focusable elements. Scrolling is locked, the rest of your application UI is hidden from screen readers, and clicking outside the dialog or pressing the Escape key will fire the `close` event and close the dialog.
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import {
|
||||
Dialog,
|
||||
DialogOverlay,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
} from "@rgossiaux/svelte-headlessui";
|
||||
let isOpen = true;
|
||||
</script>
|
||||
|
||||
<Dialog open={isOpen} on:close={() => (isOpen = false)}>
|
||||
<DialogOverlay />
|
||||
|
||||
<DialogTitle>Deactivate account</DialogTitle>
|
||||
<DialogDescription>
|
||||
This will permanently deactivate your account
|
||||
</DialogDescription>
|
||||
|
||||
<p>
|
||||
Are you sure you want to deactivate your account? All of your data will be
|
||||
permanently removed. This action cannot be undone.
|
||||
</p>
|
||||
|
||||
<button on:click={() => (isOpen = false)}>Deactivate</button>
|
||||
<button on:click={() => (isOpen = false)}>Cancel</button>
|
||||
</Dialog>
|
||||
```
|
||||
|
||||
## Showing and hiding your dialog
|
||||
|
||||
`Dialog` has no automatic management of its open/closed state. To show and hide your dialog, pass in a boolean value to the `open` prop. When `open` is true the dialog will render, and when it's false the dialog will unmount.
|
||||
|
||||
The `close` event is fired when an open dialog is dismissed, which happens when the user clicks outside of the contents of your dialog or presses the Escape key. You can use this callback to set `open` back to false and close your dialog.
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import {
|
||||
Dialog,
|
||||
DialogOverlay,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
} from "@rgossiaux/svelte-headlessui";
|
||||
let isOpen = true;
|
||||
</script>
|
||||
|
||||
<!-- Pass `isOpen` to the `open` prop and use the `on:close` handler to set it back to false when the user clicks outside of the dialog or presses the escape key. -->
|
||||
<Dialog open={isOpen} on:close={() => (isOpen = false)}>
|
||||
<DialogOverlay />
|
||||
|
||||
<DialogTitle>Deactivate account</DialogTitle>
|
||||
<DialogDescription>
|
||||
This will permanently deactivate your account
|
||||
</DialogDescription>
|
||||
|
||||
<p>
|
||||
Are you sure you want to deactivate your account? All of your data will be
|
||||
permanently removed. This action cannot be undone.
|
||||
</p>
|
||||
|
||||
<!-- You can render additional buttons to dismiss your dialog by setting `isOpen` to `false`. -->
|
||||
<button on:click={() => (isOpen = false)}>Deactivate</button>
|
||||
<button on:click={() => (isOpen = false)}>Cancel</button>
|
||||
</Dialog>
|
||||
```
|
||||
|
||||
## Managing focus within your dialog
|
||||
|
||||
For accessibility reasons, your dialog should contain at least one focusable element. By default, the `Dialog` component will focus the first focusable element (by DOM order) when it is rendered, and pressing the Tab key will cycle through all additional focusable elements within the contents.
|
||||
|
||||
Focus is trapped within the dialog as long as it is rendered, so tabbing to the end will start cycling back through the beginning again. All other application elements outside of the dialog will be marked as inert and thus not focusable.
|
||||
|
||||
If you'd like something other than the first focusable element to receive initial focus when your dialog is initially rendered, you can use the `initialFocus` prop:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import {
|
||||
Dialog,
|
||||
DialogOverlay,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
} from "@rgossiaux/svelte-headlessui";
|
||||
let isOpen = true;
|
||||
let completeButton = null;
|
||||
|
||||
function completeOrder() {
|
||||
// ...
|
||||
}
|
||||
</script>
|
||||
|
||||
<Dialog
|
||||
open={isOpen}
|
||||
on:close={() => (isOpen = false)}
|
||||
initialFocus={completeButton}
|
||||
>
|
||||
<DialogOverlay />
|
||||
|
||||
<DialogTitle>Complete your order</DialogTitle>
|
||||
|
||||
<p>Your order is all ready!</p>
|
||||
|
||||
<button on:click={() => (isOpen = false)}> Cancel </button>
|
||||
<button on:click={completeOrder} bind:this={completeButton}>
|
||||
Complete order
|
||||
</button>
|
||||
</Dialog>
|
||||
```
|
||||
|
||||
## Styling
|
||||
|
||||
[See here](general-concepts#component-styling) for some general notes on styling the components in this library.
|
||||
|
||||
### Styling the overlay
|
||||
|
||||
Typically modal dialogs will be rendered on top of a transparent dark background. You can style the `DialogOverlay` component to achieve this look.
|
||||
|
||||
The overlay component accepts normal styling props like `style` and `class`, so you can style it using any technique you like. Be sure to place it before the rest of your dialog's contents in the DOM so that it doesn't obscure the dialog's interactive elements.
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import {
|
||||
Dialog,
|
||||
DialogOverlay,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
} from "@rgossiaux/svelte-headlessui";
|
||||
let isOpen = true;
|
||||
</script>
|
||||
|
||||
<Dialog open={isOpen} on:close={() => (isOpen = false)}>
|
||||
<DialogOverlay
|
||||
style={"position: fixed; top: 0; left: 0; background-color: rgb(0 0 0); opacity: 0.3;"}
|
||||
/>
|
||||
<!-- ... -->
|
||||
</Dialog>
|
||||
```
|
||||
|
||||
### Styling the dialog
|
||||
|
||||
There's nothing special about the contents of your dialog--you can use whatever HTML and CSS you please. Typical dialogs will have a max width and be centered in the screen, but fullscreen treatments on smaller screens are also common.
|
||||
|
||||
## Using the Title and Description components
|
||||
|
||||
For accessibility reasons, you should use the `DialogTitle` and `DialogDescription` components when rendering content that labels and describes your dialog contents. They will be automatically linked to the root `Dialog` component via the `aria-labelledby` and `aria-describedby` attributes, and their contents will be announced to users using screenreaders.
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import {
|
||||
Dialog,
|
||||
DialogOverlay,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
} from "@rgossiaux/svelte-headlessui";
|
||||
let isOpen = true;
|
||||
</script>
|
||||
|
||||
<Dialog open={isOpen} on:close={() => (isOpen = false)}>
|
||||
<DialogOverlay />
|
||||
<!-- Use the Title and Description components when appropriate to improve the accessibility of your custom dialogs. -->
|
||||
<DialogTitle>Deactivate account</DialogTitle>
|
||||
<DialogDescription>
|
||||
This will permanently deactivate your account
|
||||
</DialogDescription>
|
||||
|
||||
<!-- ... -->
|
||||
</Dialog>
|
||||
```
|
||||
|
||||
## Rendering to a Portal
|
||||
|
||||
If you've ever implemented a dialog before in another framework, you may have come across the concept of portals. Portals let you insert components from one place in the component tree (for instance, deep within your application UI), but have them actually render to another place in the DOM entirely.
|
||||
|
||||
Since dialogs and their overlays take up the full page, you typically want to render them as a sibling to the root node of the page. That way, you can rely on natural DOM ordering to ensure that their content is rendered on top of your existing application UI. This also makes it easy to apply scroll locking to the rest of your application, as well as ensure that your Dialog's contents and overlay are unobstructed to receive focus and click events.
|
||||
|
||||
Because of these accessibility concerns, Svelte Headless UI's `Dialog` actually uses a portal under the hood. This way we can provide features like unobstructed event handling and making the rest of your application inert. That means that when using this `Dialog`, there's no need to use a portal yourself--it's already taken care of. You can put the `<Dialog>` anywhere in your component tree and it will render to the portal.
|
||||
|
||||
## Transitions
|
||||
|
||||
To animate the opening and closing of your dialog, you can use [this library's Transition component](/docs/transition) or Svelte's built-in transition engine. See that page for a comparison.
|
||||
|
||||
### Using the `Transition` component
|
||||
|
||||
To use the `Transition` component, all you need to do is wrap the `Dialog` in a `<Transition>`, and the dialog will transition automatically based on the value of the `show` prop on the `<Transition>`. You can remove the `open` prop on the `<Dialog>`, as the dialog will read the `show` value from the `<Transition>` automatically.
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import {
|
||||
Dialog,
|
||||
DialogOverlay,
|
||||
DialogTitle,
|
||||
Transition,
|
||||
} from "@rgossiaux/svelte-headlessui";
|
||||
let isOpen = true;
|
||||
</script>
|
||||
|
||||
<!-- This example uses Tailwind's transition classes -->
|
||||
<Transition
|
||||
show={isOpen}
|
||||
enter="transition duration-100 ease-out"
|
||||
enterFrom="transform scale-95 opacity-0"
|
||||
enterTo="transform scale-100 opacity-100"
|
||||
leave="transition duration-75 ease-out"
|
||||
leaveFrom="transform scale-100 opacity-100"
|
||||
leaveTo="transform scale-95 opacity-0"
|
||||
>
|
||||
<Dialog on:close={() => (isOpen = false)}>
|
||||
<DialogOverlay />
|
||||
<DialogTitle>Deactivate account</DialogTitle>
|
||||
|
||||
<!-- ... -->
|
||||
</Dialog>
|
||||
</Transition>
|
||||
```
|
||||
|
||||
To animate the dialog's overlay and contents separately, use `Transition` and `TransitionChild`:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import {
|
||||
Dialog,
|
||||
DialogOverlay,
|
||||
DialogTitle,
|
||||
Transition,
|
||||
} from "@rgossiaux/svelte-headlessui";
|
||||
let isOpen = true;
|
||||
</script>
|
||||
|
||||
<!-- This example uses Tailwind's transition classes -->
|
||||
<!-- Use the `Transition` component at the root level -->
|
||||
<Transition show={isOpen}>
|
||||
<Dialog on:close={() => (isOpen = false)}>
|
||||
<!-- Use one `TransitionChild` to apply one transition to the overlay... -->
|
||||
<TransitionChild
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<DialogOverlay />
|
||||
</TransitionChild>
|
||||
|
||||
<!-- ...and another `TransitionChild` to apply a separate transition to the contents -->
|
||||
<TransitionChild
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 scale-95"
|
||||
enterTo="opacity-100 scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
<DialogTitle>Deactivate account</DialogTitle>
|
||||
|
||||
<!-- ... -->
|
||||
</TransitionChild>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
```
|
||||
|
||||
### Using Svelte transitions
|
||||
|
||||
If you wish to animate your dialogs using another technique (like Svelte's built-in transitions), you can use the `static` prop to tell the component to not manage rendering itself, so you can control it manually in another way.
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import {
|
||||
Dialog,
|
||||
DialogOverlay,
|
||||
DialogTitle,
|
||||
Transition,
|
||||
} from "@rgossiaux/svelte-headlessui";
|
||||
import { fade } from "svelte/transition";
|
||||
let isOpen = true;
|
||||
</script>
|
||||
|
||||
{#if isOpen}
|
||||
<div transition:fade>
|
||||
<Dialog open={isOpen} on:close={() => (isOpen = false)} static>
|
||||
<DialogOverlay />
|
||||
<DialogTitle>Deactivate account</DialogTitle>
|
||||
|
||||
<!-- ... -->
|
||||
</Dialog>
|
||||
</div>
|
||||
{/if}
|
||||
```
|
||||
|
||||
The `open` prop is still used for managing scroll locking and focus trapping, but as long as `static` is present, the actual element will always be rendered regardless of the `open` value, which allows you to control it yourself externally. Without `static`, the exit transitions won't work correctly.
|
||||
|
||||
## Accessibility notes
|
||||
|
||||
### Focus management
|
||||
|
||||
When the dialog's `open` prop is `true`, the contents of the dialog will render and focus will be moved inside the dialog and trapped there. The first focusable element according to DOM order will receive focus, although you can use the `initialFocus` prop to control which element receives initial focus. Pressing Tab on an open dialog cycles through all the focusable elements.
|
||||
|
||||
### Mouse interaction
|
||||
|
||||
When a `Dialog` is rendered, clicking the `DialogOverlay` will close the `Dialog`.
|
||||
|
||||
No mouse interaction to open the `Dialog` is included out-of-the-box, though typically you will wire a `<button />` element up with an `on:click` handler that toggles the dialog's `open` prop to `true`.
|
||||
|
||||
### Keyboard interaction
|
||||
|
||||
| Command | Description |
|
||||
| ------------------- | -------------------------------------------------- |
|
||||
| `<Esc>` | Closes any open dialogs |
|
||||
| `<Tab>` | Cycles through an open dialog's contents |
|
||||
| `<Shift>` + `<Tab>` | Cycles backwards through an open dialog's contents |
|
||||
|
||||
### Other
|
||||
|
||||
When a Dialog is open, scroll is locked and the rest of your application UI is hidden from screen readers.
|
||||
|
||||
All relevant ARIA attributes are automatically managed.
|
||||
|
||||
For a full reference on all accessibility features implemented in `Dialog`, see <a href="https://www.w3.org/TR/wai-aria-practices-1.2/#dialog_modal">the ARIA spec on Dialogs</a>.
|
||||
|
||||
## Component API
|
||||
|
||||
### Dialog
|
||||
|
||||
The main dialog component.
|
||||
|
||||
| Prop | Default | Type | Description |
|
||||
| -------------- | ------- | ------------ | ----------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- |
|
||||
| `open` | -- | `boolean` | Whether the `Dialog` is open |
|
||||
| `initialFocus` | `null` | `HTMLElement` \| `null` | The element that should receive focus when the `Dialog` is first opened |
|
||||
| `as` | `div` | `string` | The element the `Dialog` should render as |
|
||||
| `static` | `false` | `boolean` | Whether the element should ignore the internally managed open/closed state |
|
||||
| `unmount` | `true` | `boolean` | Whether the element should be unmounted, instead of just hidden, based on the open/closed state |
|
||||
|
||||
Note that `static` and `unmount` cannot be used together.
|
||||
|
||||
| Slot prop | Type | Description |
|
||||
| --------- | --------- | --------------------------------- |
|
||||
| `open` | `boolean` | Whether or not the dialog is open |
|
||||
|
||||
This component also dispatches a custom event, which is listened to using the Svelte `on:` directive:
|
||||
|
||||
| Event name | Type of event `.detail` | Description |
|
||||
| ---------- | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `close` | `null` | Emitted when the `Dialog` is dismissed (via the overlay or Escape key). Typically used to close the dialog by setting `open` to false. |
|
||||
|
||||
### DialogOverlay
|
||||
|
||||
This can be used to create an overlay for your dialog. Clicking on the overlay will close the dialog.
|
||||
|
||||
| Prop | Default | Type | Description |
|
||||
| ---- | ------- | -------- | ------------------------------------------------ |
|
||||
| `as` | `div` | `string` | The element the `DialogOverlay` should render as |
|
||||
|
||||
| Slot prop | Type | Description |
|
||||
| --------- | --------- | --------------------------------- |
|
||||
| `open` | `boolean` | Whether or not the dialog is open |
|
||||
|
||||
### DialogTitle
|
||||
|
||||
This is the title for your dialog. When this is used, it will set the `aria-labelledby` on the dialog.
|
||||
|
||||
| Prop | Default | Type | Description |
|
||||
| ---- | ------- | -------- | ---------------------------------------------- |
|
||||
| `as` | `h2` | `string` | The element the `DialogTitle` should render as |
|
||||
|
||||
| Slot prop | Type | Description |
|
||||
| --------- | --------- | --------------------------------- |
|
||||
| `open` | `boolean` | Whether or not the dialog is open |
|
||||
|
||||
### DialogDescription
|
||||
|
||||
This is the description for your dialog. When this is used, it will set the `aria-describedby` on the dialog.
|
||||
|
||||
| Prop | Default | Type | Description |
|
||||
| ---- | ------- | -------- | ---------------------------------------------------- |
|
||||
| `as` | `p` | `string` | The element the `DialogDescription` should render as |
|
||||
|
||||
| Slot prop | Type | Description |
|
||||
| --------- | --------- | --------------------------------- |
|
||||
| `open` | `boolean` | Whether or not the dialog is open |
|
||||
308
src/routes/docs/disclosure.svx
Normal file
308
src/routes/docs/disclosure.svx
Normal file
@@ -0,0 +1,308 @@
|
||||
# Disclosure
|
||||
|
||||
<script>
|
||||
import Preview from "./_Preview.svelte";
|
||||
</script>
|
||||
|
||||
<Preview url="examples/disclosure" code="" class="h-[370px] bg-fuchsia-300"/>
|
||||
|
||||
## Basic example
|
||||
|
||||
Disclosures are built using the `Disclosure`, `DisclosureButton`, and `DisclosurePanel` components.
|
||||
|
||||
Clicking the `DisclosureButton` will automatically open/close the `DisclosurePanel`, and all components will receive the appropriate `aria-*` attributes like `aria-expanded` and `aria-controls`.
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import {
|
||||
Disclosure,
|
||||
DisclosureButton,
|
||||
DisclosurePanel,
|
||||
} from "@rgossiaux/svelte-headlessui";
|
||||
</script>
|
||||
|
||||
<Disclosure>
|
||||
<DisclosureButton>Is team pricing available?</DisclosureButton>
|
||||
|
||||
<DisclosurePanel>
|
||||
Yes! You can purchase a license that you can share with your entire team.
|
||||
</DisclosurePanel>
|
||||
</Disclosure>
|
||||
```
|
||||
|
||||
## Styling
|
||||
|
||||
[See here](general-concepts#component-styling) for some general notes on styling the components in this library.
|
||||
|
||||
### Open panels
|
||||
|
||||
`Disclosure` and its related components expose a slot prop containing the `open` state of the panel. You can use this to apply whatever styles you wish.
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import {
|
||||
Disclosure,
|
||||
DisclosureButton,
|
||||
DisclosurePanel,
|
||||
} from "@rgossiaux/svelte-headlessui";
|
||||
import { ChevronRightIcon } from "@rgossiaux/svelte-heroicons/solid";
|
||||
</script>
|
||||
|
||||
<Disclosure let:open>
|
||||
<DisclosureButton>
|
||||
<span>Is team pricing available?</span>
|
||||
<!-- Use the `open` slot prop to rotate the icon when the panel is open -->
|
||||
<ChevronRightIcon style={open ? "transform: rotate(90deg);" : ""} />
|
||||
</DisclosureButton>
|
||||
|
||||
<DisclosurePanel>
|
||||
Yes! You can purchase a license that you can share with your entire team.
|
||||
</DisclosurePanel>
|
||||
</Disclosure>
|
||||
```
|
||||
|
||||
## Showing/hiding the panel
|
||||
|
||||
By default, your `DisclosurePanel` will be shown/hidden automatically based on the internal open state tracked within the `Disclosure` component itself.
|
||||
|
||||
If you'd rather handle this yourself (perhaps because you need to add an extra wrapper element for one reason or another), you can add a `static` prop to the `DisclosurePanel` component to tell it to always render, and use the `open` slot prop to show or hide the panel yourself.
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import {
|
||||
Disclosure,
|
||||
DisclosureButton,
|
||||
DisclosurePanel,
|
||||
} from "@rgossiaux/svelte-headlessui";
|
||||
</script>
|
||||
|
||||
<Disclosure let:open>
|
||||
<DisclosureButton>Is team pricing available?</DisclosureButton>
|
||||
|
||||
{#if open}
|
||||
<div>
|
||||
<!-- Using `static`, `DisclosurePanel` is always rendered,
|
||||
and ignores the `open` state -->
|
||||
<DisclosurePanel static>
|
||||
Yes! You can purchase a license that you can share with your entire
|
||||
team.
|
||||
</DisclosurePanel>
|
||||
</div>
|
||||
{/if}
|
||||
</Disclosure>
|
||||
```
|
||||
|
||||
## Closing disclosures manually
|
||||
|
||||
To close a disclosure manually when clicking a child of its panel, render that child as a `DisclosureButton`. You can use the `as` prop to customize which element is being rendered.
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import {
|
||||
Disclosure,
|
||||
DisclosureButton,
|
||||
DisclosurePanel,
|
||||
} from "@rgossiaux/svelte-headlessui";
|
||||
</script>
|
||||
|
||||
<Disclosure>
|
||||
<DisclosureButton>Open mobile menu</DisclosureButton>
|
||||
<DisclosurePanel>
|
||||
<DisclosureButton as="a" href="/home">Home</DisclosureButton>
|
||||
<!-- ... -->
|
||||
</DisclosurePanel>
|
||||
</Disclosure>
|
||||
```
|
||||
|
||||
This is especially useful when using disclosures for things like mobile menus that contain links, where you want the disclosure to close when navigating to the next page.
|
||||
|
||||
Alternatively, `Disclosure` and `DisclosurePanel` expose a `close()` slot prop which you can use to imperatively close the panel:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import {
|
||||
Disclosure,
|
||||
DisclosureButton,
|
||||
DisclosurePanel,
|
||||
} from "@rgossiaux/svelte-headlessui";
|
||||
</script>
|
||||
|
||||
<Disclosure>
|
||||
<DisclosureButton>Solutions</DisclosureButton>
|
||||
<DisclosurePanel let:close>
|
||||
<button
|
||||
on:click={async () => {
|
||||
await fetch("/accept-terms", { method: "POST" });
|
||||
close();
|
||||
}}
|
||||
>
|
||||
Read and accept
|
||||
</button>
|
||||
<!-- ... -->
|
||||
</DisclosurePanel>
|
||||
</Disclosure>
|
||||
```
|
||||
|
||||
By default the `DisclosureButton` receives focus after calling `close()`, but you can change this by passing an element into `close(el)`.
|
||||
|
||||
## Transitions
|
||||
|
||||
To animate the opening and closing of the disclosure panel, you can use [this library's Transition component](/docs/transition) or Svelte's built-in transition engine. See that page for a comparison.
|
||||
|
||||
### Using the `Transition` component
|
||||
|
||||
To use the `Transition` component, all you need to do is wrap the `DisclosurePanel` in a `<Transition>` and the panel will transition automatically.
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import {
|
||||
Disclosure,
|
||||
DisclosureButton,
|
||||
DisclosurePanel,
|
||||
Transition,
|
||||
} from "@rgossiaux/svelte-headlessui";
|
||||
</script>
|
||||
|
||||
<Disclosure>
|
||||
<DisclosureButton>Is team pricing available?</DisclosureButton>
|
||||
<!-- This example uses Tailwind's transition classes -->
|
||||
<Transition
|
||||
enter="transition duration-100 ease-out"
|
||||
enterFrom="transform scale-95 opacity-0"
|
||||
enterTo="transform scale-100 opacity-100"
|
||||
leave="transition duration-75 ease-out"
|
||||
leaveFrom="transform scale-100 opacity-100"
|
||||
leaveTo="transform scale-95 opacity-0"
|
||||
>
|
||||
<DisclosurePanel>
|
||||
<!-- ... -->
|
||||
</DisclosurePanel>
|
||||
</Transition>
|
||||
</Disclosure>
|
||||
```
|
||||
|
||||
The components in this library communicate with each other, so the Transition will be managed automatically when the Listbox is opened/closed. If you require more control over this behavior, you may use a more explicit version:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import {
|
||||
Disclosure,
|
||||
DisclosureButton,
|
||||
DisclosurePanel,
|
||||
Transition,
|
||||
} from "@rgossiaux/svelte-headlessui";
|
||||
</script>
|
||||
|
||||
<Disclosure let:open>
|
||||
<DisclosureButton>Is team pricing available?</DisclosureButton>
|
||||
<!-- This example uses Tailwind's transition classes -->
|
||||
<Transition
|
||||
show={open}
|
||||
enter="transition duration-100 ease-out"
|
||||
enterFrom="transform scale-95 opacity-0"
|
||||
enterTo="transform scale-100 opacity-100"
|
||||
leave="transition duration-75 ease-out"
|
||||
leaveFrom="transform scale-100 opacity-100"
|
||||
leaveTo="transform scale-95 opacity-0"
|
||||
>
|
||||
<!-- When controlling the transition manually, make sure to use `static` -->
|
||||
<DisclosurePanel static>
|
||||
<!-- ... -->
|
||||
</DisclosurePanel>
|
||||
</Transition>
|
||||
</Disclosure>
|
||||
```
|
||||
|
||||
### Using Svelte transitions
|
||||
|
||||
The last example above also provides a blueprint for using Svelte transitions:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import {
|
||||
Disclosure,
|
||||
DisclosureButton,
|
||||
DisclosurePanel,
|
||||
} from "@rgossiaux/svelte-headlessui";
|
||||
import { fade } from "svelte/transition";
|
||||
</script>
|
||||
|
||||
<Disclosure let:open>
|
||||
<DisclosureButton>Is team pricing available?</DisclosureButton>
|
||||
{#if open}
|
||||
<div transition:fade>
|
||||
<!-- When controlling the transition manually, make sure to use `static` -->
|
||||
<DisclosurePanel static>
|
||||
<!-- ... -->
|
||||
</DisclosurePanel>
|
||||
</div>
|
||||
{/if}
|
||||
</Disclosure>
|
||||
```
|
||||
|
||||
Make sure to use the `static` prop, or else the exit transitions won't work correctly.
|
||||
|
||||
## Accessibility notes
|
||||
|
||||
### Mouse interaction
|
||||
|
||||
Clicking a `DisclosureButton` toggles the disclosure's panel open and closed.
|
||||
|
||||
### Keyboard interaction
|
||||
|
||||
| Command | Description |
|
||||
| ---------------------------------------------------------- | ------------- |
|
||||
| `<Enter>` / `<Space>` when a `DisclosureButton` is focused | Toggles panel |
|
||||
|
||||
### Other
|
||||
|
||||
All relevant ARIA attributes are automatically managed.
|
||||
|
||||
For a full reference on all accessibility features implemented in `Disclosure`, see <a href="https://www.w3.org/TR/wai-aria-practices-1.2/#Disclosure">the ARIA spec on Disclosure</a>.
|
||||
|
||||
## Component API
|
||||
|
||||
### Disclosure
|
||||
|
||||
The main disclosure component.
|
||||
|
||||
| Prop | Default | Type | Description |
|
||||
| ------------- | ------- | --------- | -------------------------------------------------- |
|
||||
| `as` | `div` | `string` | The element the `Disclosure` should render as |
|
||||
| `defaultOpen` | `false` | `boolean` | Whether the `Disclosure` should be open by default |
|
||||
|
||||
| Slot prop | Type | Description |
|
||||
| --------- | ---------------------------- | ----------------------------------------------------------------------------------- |
|
||||
| `open` | `boolean` | Whether the disclosure is open |
|
||||
| `close` | `(el?: HTMLElement) => void` | Closes the disclosure and focuses `el`, if passed, or the `DisclosureButton` if not |
|
||||
|
||||
### DisclosureButton
|
||||
|
||||
This is the trigger button to toggle a disclosure.
|
||||
|
||||
You can also use this `DisclosureButton` component inside a `DisclosurePanel`. If you do, it will behave as a close button and have the appropriate `aria-*` attributes.
|
||||
|
||||
| Prop | Default | Type | Description |
|
||||
| ---- | -------- | -------- | --------------------------------------------------- |
|
||||
| `as` | `button` | `string` | The element the `DisclosureButton` should render as |
|
||||
|
||||
| Slot prop | Type | Description |
|
||||
| --------- | --------- | ------------------------------------- |
|
||||
| `open` | `boolean` | Whether or not the disclosure is open |
|
||||
|
||||
### DisclosurePanel
|
||||
|
||||
This component contains the contents of your disclosure.
|
||||
|
||||
| Prop | Default | Type | Description |
|
||||
| --------- | ------- | --------- | ----------------------------------------------------------------------------------------------- |
|
||||
| `as` | `div` | `string` | The element the `DisclosurePanel` should render as |
|
||||
| `static` | `false` | `boolean` | Whether the element should ignore the internally managed open/closed state |
|
||||
| `unmount` | `true` | `boolean` | Whether the element should be unmounted, instead of just hidden, based on the open/closed state |
|
||||
|
||||
Note that `static` and `unmount` cannot be used together.
|
||||
|
||||
| Slot prop | Type | Description |
|
||||
| --------- | ---------------------------- | ----------------------------------------------------------------------------------- |
|
||||
| `open` | `boolean` | Whether or not the disclosure is open |
|
||||
| `close` | `(el?: HTMLElement) => void` | Closes the disclosure and focuses `el`, if passed, or the `DisclosureButton` if not |
|
||||
28
src/routes/docs/examples/_PopoverIconOne.svelte
Normal file
28
src/routes/docs/examples/_PopoverIconOne.svelte
Normal file
@@ -0,0 +1,28 @@
|
||||
<svg
|
||||
width="48"
|
||||
height="48"
|
||||
viewBox="0 0 48 48"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<rect width="48" height="48" rx="8" fill="#FFEDD5" />
|
||||
<path
|
||||
d="M24 11L35.2583 17.5V30.5L24 37L12.7417 30.5V17.5L24 11Z"
|
||||
stroke="#FB923C"
|
||||
stroke-width="2"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M16.7417 19.8094V28.1906L24 32.3812L31.2584 28.1906V19.8094L24 15.6188L16.7417 19.8094Z"
|
||||
stroke="#FDBA74"
|
||||
stroke-width="2"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M20.7417 22.1196V25.882L24 27.7632L27.2584 25.882V22.1196L24 20.2384L20.7417 22.1196Z"
|
||||
stroke="#FDBA74"
|
||||
stroke-width="2"
|
||||
/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 685 B |
15
src/routes/docs/examples/_PopoverIconThree.svelte
Normal file
15
src/routes/docs/examples/_PopoverIconThree.svelte
Normal file
@@ -0,0 +1,15 @@
|
||||
<svg
|
||||
width="48"
|
||||
height="48"
|
||||
viewBox="0 0 48 48"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<rect width="48" height="48" rx="8" fill="#FFEDD5" />
|
||||
<rect x="13" y="32" width="2" height="4" fill="#FDBA74" />
|
||||
<rect x="17" y="28" width="2" height="8" fill="#FDBA74" />
|
||||
<rect x="21" y="24" width="2" height="12" fill="#FDBA74" />
|
||||
<rect x="25" y="20" width="2" height="16" fill="#FDBA74" />
|
||||
<rect x="29" y="16" width="2" height="20" fill="#FB923C" />
|
||||
<rect x="33" y="12" width="2" height="24" fill="#FB923C" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 540 B |
21
src/routes/docs/examples/_PopoverIconTwo.svelte
Normal file
21
src/routes/docs/examples/_PopoverIconTwo.svelte
Normal file
@@ -0,0 +1,21 @@
|
||||
<svg
|
||||
width="48"
|
||||
height="48"
|
||||
viewBox="0 0 48 48"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<rect width="48" height="48" rx="8" fill="#FFEDD5" />
|
||||
<path
|
||||
d="M28.0413 20L23.9998 13L19.9585 20M32.0828 27.0001L36.1242 34H28.0415M19.9585 34H11.8755L15.9171 27"
|
||||
stroke="#FB923C"
|
||||
stroke-width="2"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M18.804 30H29.1963L24.0001 21L18.804 30Z"
|
||||
stroke="#FDBA74"
|
||||
stroke-width="2"
|
||||
/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 484 B |
3
src/routes/docs/examples/__layout.reset.svelte
Normal file
3
src/routes/docs/examples/__layout.reset.svelte
Normal file
@@ -0,0 +1,3 @@
|
||||
<div class="flex flex-col items-center justify-center">
|
||||
<slot />
|
||||
</div>
|
||||
90
src/routes/docs/examples/dialog.svelte
Normal file
90
src/routes/docs/examples/dialog.svelte
Normal file
@@ -0,0 +1,90 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
Dialog,
|
||||
DialogOverlay,
|
||||
DialogTitle,
|
||||
Transition,
|
||||
TransitionChild,
|
||||
} from "$lib";
|
||||
|
||||
let isOpen = true;
|
||||
|
||||
function closeModal() {
|
||||
isOpen = false;
|
||||
}
|
||||
|
||||
function openModal() {
|
||||
isOpen = true;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="fixed inset-0 flex items-center justify-center">
|
||||
<button
|
||||
type="button"
|
||||
on:click={openModal}
|
||||
class="px-4 py-2 text-sm font-medium text-white bg-black rounded-md bg-opacity-20 hover:bg-opacity-30 focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75"
|
||||
>
|
||||
Open dialog
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Transition appear show={isOpen}>
|
||||
<Dialog
|
||||
as="div"
|
||||
class="fixed inset-0 z-10 overflow-y-auto"
|
||||
on:close={closeModal}
|
||||
>
|
||||
<div class="min-h-screen px-4 text-center">
|
||||
<TransitionChild
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<DialogOverlay class="fixed inset-0" />
|
||||
</TransitionChild>
|
||||
|
||||
<TransitionChild
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 scale-95"
|
||||
enterTo="opacity-100 scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
<!-- This element is to trick the browser into centering the modal contents. -->
|
||||
<span class="inline-block h-screen align-middle" aria-hidden="true">
|
||||
​
|
||||
</span>
|
||||
<div
|
||||
class="inline-block w-full max-w-md p-6 my-8 overflow-hidden text-left align-middle transition-all transform bg-white shadow-xl rounded-2xl"
|
||||
>
|
||||
<DialogTitle
|
||||
as="h3"
|
||||
class="text-lg font-medium leading-6 text-gray-900"
|
||||
>
|
||||
Payment successful
|
||||
</DialogTitle>
|
||||
<div class="mt-2">
|
||||
<p class="text-sm text-gray-500">
|
||||
Your payment has been successfully submitted. We’ve sent you an
|
||||
email with all of the details of your order.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex justify-center px-4 py-2 text-sm font-medium text-blue-900 bg-blue-100 border border-transparent rounded-md hover:bg-blue-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-blue-500"
|
||||
on:click={closeModal}
|
||||
>
|
||||
Got it, thanks!
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</TransitionChild>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
40
src/routes/docs/examples/disclosure.svelte
Normal file
40
src/routes/docs/examples/disclosure.svelte
Normal file
@@ -0,0 +1,40 @@
|
||||
<script lang="ts">
|
||||
import { Disclosure, DisclosureButton, DisclosurePanel } from "$lib";
|
||||
import { ChevronUpIcon } from "@rgossiaux/svelte-heroicons/solid";
|
||||
</script>
|
||||
|
||||
<div class="w-full px-4 pt-16">
|
||||
<div class="w-full max-w-md p-2 mx-auto bg-white rounded-2xl">
|
||||
<Disclosure let:open>
|
||||
<DisclosureButton
|
||||
class="flex justify-between w-full px-4 py-2 text-sm font-medium text-left text-purple-900 bg-purple-100 rounded-lg hover:bg-purple-200 focus:outline-none focus-visible:ring focus-visible:ring-purple-500 focus-visible:ring-opacity-75"
|
||||
>
|
||||
<span>What is your refund policy?</span>
|
||||
<ChevronUpIcon
|
||||
class={`${
|
||||
open ? "transform rotate-180" : ""
|
||||
} w-5 h-5 text-purple-500`}
|
||||
/>
|
||||
</DisclosureButton>
|
||||
<DisclosurePanel class="px-4 pt-4 pb-2 text-sm text-gray-500">
|
||||
If you're unhappy with your purchase for any reason, email us within 90
|
||||
days and we'll refund you in full, no questions asked.
|
||||
</DisclosurePanel>
|
||||
</Disclosure>
|
||||
<Disclosure as="div" class="mt-2" let:open>
|
||||
<DisclosureButton
|
||||
class="flex justify-between w-full px-4 py-2 text-sm font-medium text-left text-purple-900 bg-purple-100 rounded-lg hover:bg-purple-200 focus:outline-none focus-visible:ring focus-visible:ring-purple-500 focus-visible:ring-opacity-75"
|
||||
>
|
||||
<span>Do you offer technical support?</span>
|
||||
<ChevronUpIcon
|
||||
class={`${
|
||||
open ? "transform rotate-180" : ""
|
||||
} w-5 h-5 text-purple-500`}
|
||||
/>
|
||||
</DisclosureButton>
|
||||
<DisclosurePanel class="px-4 pt-4 pb-2 text-sm text-gray-500">
|
||||
No.
|
||||
</DisclosurePanel>
|
||||
</Disclosure>
|
||||
</div>
|
||||
</div>
|
||||
73
src/routes/docs/examples/listbox.svelte
Normal file
73
src/routes/docs/examples/listbox.svelte
Normal file
@@ -0,0 +1,73 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
Listbox,
|
||||
ListboxButton,
|
||||
ListboxOption,
|
||||
ListboxOptions,
|
||||
Transition,
|
||||
} from "$lib";
|
||||
import { CheckIcon, SelectorIcon } from "@rgossiaux/svelte-heroicons/solid";
|
||||
|
||||
const people = [
|
||||
{ name: "Wade Cooper" },
|
||||
{ name: "Arlene Mccoy" },
|
||||
{ name: "Devon Webb" },
|
||||
{ name: "Tom Cook" },
|
||||
{ name: "Tanya Fox" },
|
||||
{ name: "Hellen Schmidt" },
|
||||
];
|
||||
|
||||
let selected = people[0];
|
||||
</script>
|
||||
|
||||
<div class="w-72 fixed top-16">
|
||||
<Listbox value={selected} on:change={(e) => (selected = e.detail)}>
|
||||
<div class="relative mt-1">
|
||||
<ListboxButton
|
||||
class="relative w-full py-2 pl-3 pr-10 text-left bg-white rounded-lg shadow-md cursor-default focus:outline-none focus-visible:ring-2 focus-visible:ring-opacity-75 focus-visible:ring-white focus-visible:ring-offset-orange-300 focus-visible:ring-offset-2 focus-visible:border-indigo-500 sm:text-sm"
|
||||
>
|
||||
<span class="block truncate">{selected.name}</span>
|
||||
<span
|
||||
class="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none"
|
||||
>
|
||||
<SelectorIcon class="w-5 h-5 text-gray-400" aria-hidden="true" />
|
||||
</span>
|
||||
</ListboxButton>
|
||||
<Transition
|
||||
leave="transition ease-in duration-100"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<ListboxOptions
|
||||
class="absolute w-full py-1 mt-1 overflow-auto text-base bg-white rounded-md shadow-lg max-h-60 ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm"
|
||||
>
|
||||
{#each people as person, personIdx (personIdx)}
|
||||
<ListboxOption
|
||||
class={({ active }) =>
|
||||
`cursor-default select-none relative py-2 pl-10 pr-4 ${
|
||||
active ? "text-amber-900 bg-amber-100" : "text-gray-900"
|
||||
}`}
|
||||
value={person}
|
||||
let:selected
|
||||
>
|
||||
<span
|
||||
class={`block truncate ${
|
||||
selected ? "font-medium" : "font-normal"
|
||||
}`}
|
||||
>
|
||||
{person.name}
|
||||
</span>
|
||||
{#if selected}
|
||||
<span
|
||||
class="absolute inset-y-0 left-0 flex items-center pl-3 text-amber-600"
|
||||
>
|
||||
<CheckIcon class="w-5 h-5" aria-hidden="true" />
|
||||
</span>
|
||||
{/if}
|
||||
</ListboxOption>
|
||||
{/each}
|
||||
</ListboxOptions>
|
||||
</Transition>
|
||||
</div>
|
||||
</Listbox>
|
||||
</div>
|
||||
202
src/routes/docs/examples/menu.svelte
Normal file
202
src/routes/docs/examples/menu.svelte
Normal file
@@ -0,0 +1,202 @@
|
||||
<script lang="ts">
|
||||
import { Menu, MenuButton, MenuItem, MenuItems, Transition } from "$lib";
|
||||
import { ChevronDownIcon } from "@rgossiaux/svelte-heroicons/solid";
|
||||
</script>
|
||||
|
||||
<div class="w-56 text-right fixed top-16">
|
||||
<Menu as="div" class="relative inline-block text-left">
|
||||
<div>
|
||||
<MenuButton
|
||||
class="inline-flex justify-center w-full px-4 py-2 text-sm font-medium text-white bg-black rounded-md bg-opacity-20 hover:bg-opacity-30 focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75"
|
||||
>
|
||||
Options
|
||||
<ChevronDownIcon
|
||||
class="w-5 h-5 ml-2 -mr-1 text-violet-200 hover:text-violet-100"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</MenuButton>
|
||||
</div>
|
||||
<Transition
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<MenuItems
|
||||
class="absolute right-0 w-56 mt-2 origin-top-right bg-white divide-y divide-gray-100 rounded-md shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none"
|
||||
>
|
||||
<div class="px-1 py-1 ">
|
||||
<MenuItem let:active>
|
||||
<button
|
||||
class={`${
|
||||
active ? "bg-violet-500 text-white" : "text-gray-900"
|
||||
} group flex rounded-md items-center w-full px-2 py-2 text-sm`}
|
||||
>
|
||||
<!-- Edit icon -->
|
||||
<svg
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="w-5 h-5 mr-2"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M4 13V16H7L16 7L13 4L4 13Z"
|
||||
fill={active ? "#8B5CF6" : "#EDE9FE"}
|
||||
stroke={active ? "#C4B5FD" : "#A78BFA"}
|
||||
stroke-width="2"
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem let:active>
|
||||
<button
|
||||
class={`${
|
||||
active ? "bg-violet-500 text-white" : "text-gray-900"
|
||||
} group flex rounded-md items-center w-full px-2 py-2 text-sm`}
|
||||
>
|
||||
<!-- Duplicate icon -->
|
||||
<svg
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="w-5 h-5 mr-2"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M4 4H12V12H4V4Z"
|
||||
fill={active ? "#8B5CF6" : "#EDE9FE"}
|
||||
stroke={active ? "#C4B5FD" : "#A78BFA"}
|
||||
stroke-width="2"
|
||||
/>
|
||||
<path
|
||||
d="M8 8H16V16H8V8Z"
|
||||
fill={active ? "#8B5CF6" : "#EDE9FE"}
|
||||
stroke={active ? "#C4B5FD" : "#A78BFA"}
|
||||
stroke-width="2"
|
||||
/>
|
||||
</svg>
|
||||
Duplicate
|
||||
</button>
|
||||
</MenuItem>
|
||||
</div>
|
||||
|
||||
<div class="px-1 py-1">
|
||||
<MenuItem let:active>
|
||||
<button
|
||||
class={`${
|
||||
active ? "bg-violet-500 text-white" : "text-gray-900"
|
||||
} group flex rounded-md items-center w-full px-2 py-2 text-sm`}
|
||||
>
|
||||
<!-- Archive icon -->
|
||||
<svg
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="w-5 h-5 mr-2"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<rect
|
||||
x="5"
|
||||
y="8"
|
||||
width="10"
|
||||
height="8"
|
||||
fill={active ? "#8B5CF6" : "#EDE9FE"}
|
||||
stroke={active ? "#C4B5FD" : "#A78BFA"}
|
||||
stroke-width="2"
|
||||
/>
|
||||
<rect
|
||||
x="4"
|
||||
y="4"
|
||||
width="12"
|
||||
height="4"
|
||||
fill={active ? "#8B5CF6" : "#EDE9FE"}
|
||||
stroke={active ? "#C4B5FD" : "#A78BFA"}
|
||||
stroke-width="2"
|
||||
/>
|
||||
<path d="M8 12H12" stroke="#A78BFA" stroke-width="2" />
|
||||
</svg>
|
||||
Archive
|
||||
</button>
|
||||
</MenuItem>
|
||||
<MenuItem let:active>
|
||||
<button
|
||||
class={`${
|
||||
active ? "bg-violet-500 text-white" : "text-gray-900"
|
||||
} group flex rounded-md items-center w-full px-2 py-2 text-sm`}
|
||||
>
|
||||
<!-- Move icon -->
|
||||
<svg
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="w-5 h-5 mr-2"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M10 4H16V10"
|
||||
stroke={active ? "#C4B5FD" : "#A78BFA"}
|
||||
stroke-width="2"
|
||||
/>
|
||||
<path
|
||||
d="M16 4L8 12"
|
||||
stroke={active ? "#C4B5FD" : "#A78BFA"}
|
||||
stroke-width="2"
|
||||
/>
|
||||
<path
|
||||
d="M8 6H4V16H14V12"
|
||||
stroke={active ? "#C4B5FD" : "#A78BFA"}
|
||||
stroke-width="2"
|
||||
/>
|
||||
</svg>
|
||||
Move
|
||||
</button>
|
||||
</MenuItem>
|
||||
</div>
|
||||
<div class="px-1 py-1">
|
||||
<MenuItem let:active>
|
||||
<button
|
||||
class={`${
|
||||
active ? "bg-violet-500 text-white" : "text-gray-900"
|
||||
} group flex rounded-md items-center w-full px-2 py-2 text-sm`}
|
||||
>
|
||||
<!-- Delete icon -->
|
||||
<svg
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-hidden="true"
|
||||
class="w-5 h-5 mr-2 text-violet-400"
|
||||
>
|
||||
<rect
|
||||
x="5"
|
||||
y="6"
|
||||
width="10"
|
||||
height="10"
|
||||
fill={active ? "#8B5CF6" : "#EDE9FE"}
|
||||
stroke={active ? "#C4B5FD" : "#A78BFA"}
|
||||
stroke-width="2"
|
||||
/>
|
||||
<path
|
||||
d="M3 6H17"
|
||||
stroke={active ? "#C4B5FD" : "#A78BFA"}
|
||||
stroke-width="2"
|
||||
/>
|
||||
<path
|
||||
d="M8 6V4H12V6"
|
||||
stroke={active ? "#C4B5FD" : "#A78BFA"}
|
||||
stroke-width="2"
|
||||
/>
|
||||
</svg>
|
||||
Delete
|
||||
</button>
|
||||
</MenuItem>
|
||||
</div>
|
||||
</MenuItems>
|
||||
</Transition>
|
||||
</Menu>
|
||||
</div>
|
||||
99
src/routes/docs/examples/popover.svelte
Normal file
99
src/routes/docs/examples/popover.svelte
Normal file
@@ -0,0 +1,99 @@
|
||||
<script lang="ts">
|
||||
import { Popover, PopoverButton, PopoverPanel, Transition } from "$lib";
|
||||
import { ChevronDownIcon } from "@rgossiaux/svelte-heroicons/solid";
|
||||
import PopoverIconOne from "./_PopoverIconOne.svelte";
|
||||
import PopoverIconTwo from "./_PopoverIconTwo.svelte";
|
||||
import PopoverIconThree from "./_PopoverIconThree.svelte";
|
||||
|
||||
const solutions = [
|
||||
{
|
||||
name: "Insights",
|
||||
description: "Measure actions your users take",
|
||||
href: "##",
|
||||
icon: PopoverIconOne,
|
||||
},
|
||||
{
|
||||
name: "Automations",
|
||||
description: "Create your own targeted content",
|
||||
href: "##",
|
||||
icon: PopoverIconTwo,
|
||||
},
|
||||
{
|
||||
name: "Reports",
|
||||
description: "Keep track of your growth",
|
||||
href: "##",
|
||||
icon: PopoverIconThree,
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<div class="w-full max-w-sm px-4 fixed top-16">
|
||||
<Popover class="relative" let:open>
|
||||
<PopoverButton
|
||||
class={`
|
||||
${open ? "" : "text-opacity-90"}
|
||||
text-white group bg-orange-700 px-3 py-2 rounded-md inline-flex items-center text-base font-medium hover:text-opacity-100 focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75`}
|
||||
>
|
||||
<span>Solutions</span>
|
||||
<ChevronDownIcon
|
||||
class={`${open ? "" : "text-opacity-70"}
|
||||
ml-2 h-5 w-5 text-orange-300 group-hover:text-opacity-80 transition ease-in-out duration-150`}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</PopoverButton>
|
||||
<Transition
|
||||
enter="transition ease-out duration-200"
|
||||
enterFrom="opacity-0 translate-y-1"
|
||||
enterTo="opacity-100 translate-y-0"
|
||||
leave="transition ease-in duration-150"
|
||||
leaveFrom="opacity-100 translate-y-0"
|
||||
leaveTo="opacity-0 translate-y-1"
|
||||
>
|
||||
<PopoverPanel
|
||||
class="absolute z-10 w-screen max-w-sm px-4 mt-3 transform -translate-x-1/2 left-1/2 sm:px-0 lg:max-w-3xl"
|
||||
>
|
||||
<div
|
||||
class="overflow-hidden rounded-lg shadow-lg ring-1 ring-black ring-opacity-5"
|
||||
>
|
||||
<div class="relative grid gap-8 bg-white p-7 lg:grid-cols-2">
|
||||
{#each solutions as item (item.name)}
|
||||
<a
|
||||
href={item.href}
|
||||
class="flex items-center p-2 -m-3 transition duration-150 ease-in-out rounded-lg hover:bg-gray-50 focus:outline-none focus-visible:ring focus-visible:ring-orange-500 focus-visible:ring-opacity-50"
|
||||
>
|
||||
<div
|
||||
class="flex items-center justify-center flex-shrink-0 w-10 h-10 text-white sm:h-12 sm:w-12"
|
||||
>
|
||||
<svelte:component this={item.icon} aria-hidden="true" />
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<p class="text-sm font-medium text-gray-900">
|
||||
{item.name}
|
||||
</p>
|
||||
<p class="text-sm text-gray-500">
|
||||
{item.description}
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="p-4 bg-gray-50">
|
||||
<a
|
||||
href="##"
|
||||
class="flow-root px-2 py-2 transition duration-150 ease-in-out rounded-md hover:bg-gray-100 focus:outline-none focus-visible:ring focus-visible:ring-orange-500 focus-visible:ring-opacity-50"
|
||||
>
|
||||
<span class="flex items-center">
|
||||
<span class="text-sm font-medium text-gray-900">
|
||||
Documentation
|
||||
</span>
|
||||
</span>
|
||||
<span class="block text-sm text-gray-500">
|
||||
Start integrating products and tools
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</PopoverPanel>
|
||||
</Transition>
|
||||
</Popover>
|
||||
</div>
|
||||
98
src/routes/docs/examples/radio-group.svelte
Normal file
98
src/routes/docs/examples/radio-group.svelte
Normal file
@@ -0,0 +1,98 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
RadioGroup,
|
||||
RadioGroupDescription,
|
||||
RadioGroupLabel,
|
||||
RadioGroupOption,
|
||||
} from "$lib";
|
||||
const plans = [
|
||||
{
|
||||
name: "Startup",
|
||||
ram: "12GB",
|
||||
cpus: "6 CPUs",
|
||||
disk: "160 GB SSD disk",
|
||||
},
|
||||
{
|
||||
name: "Business",
|
||||
ram: "16GB",
|
||||
cpus: "8 CPUs",
|
||||
disk: "512 GB SSD disk",
|
||||
},
|
||||
{
|
||||
name: "Enterprise",
|
||||
ram: "32GB",
|
||||
cpus: "12 CPUs",
|
||||
disk: "1024 GB SSD disk",
|
||||
},
|
||||
];
|
||||
|
||||
let selected = plans[0];
|
||||
</script>
|
||||
|
||||
<div class="w-full px-4 py-16">
|
||||
<div class="w-full max-w-md mx-auto">
|
||||
<RadioGroup value={selected} on:change={(e) => (selected = e.detail)}>
|
||||
<RadioGroupLabel class="sr-only">Server size</RadioGroupLabel>
|
||||
<div class="space-y-2">
|
||||
{#each plans as plan (plan.name)}
|
||||
<RadioGroupOption
|
||||
key={plan.name}
|
||||
value={plan}
|
||||
class={({ active, checked }) =>
|
||||
`${
|
||||
active
|
||||
? "ring-2 ring-offset-2 ring-offset-sky-300 ring-white ring-opacity-60"
|
||||
: ""
|
||||
}
|
||||
${
|
||||
checked ? "bg-sky-900 bg-opacity-75 text-white" : "bg-white"
|
||||
}
|
||||
relative rounded-lg shadow-md px-5 py-4 cursor-pointer flex focus:outline-none`}
|
||||
let:checked
|
||||
>
|
||||
<div class="flex items-center justify-between w-full">
|
||||
<div class="flex items-center">
|
||||
<div class="text-sm">
|
||||
<RadioGroupLabel
|
||||
as="p"
|
||||
class={`font-medium ${
|
||||
checked ? "text-white" : "text-gray-900"
|
||||
}`}
|
||||
>
|
||||
{plan.name}
|
||||
</RadioGroupLabel>
|
||||
<RadioGroupDescription
|
||||
as="span"
|
||||
class={`inline ${
|
||||
checked ? "text-sky-100" : "text-gray-500"
|
||||
}`}
|
||||
>
|
||||
<span>
|
||||
{plan.ram}/{plan.cpus}
|
||||
</span>{" "}
|
||||
<span aria-hidden="true">·</span>{" "}
|
||||
<span>{plan.disk}</span>
|
||||
</RadioGroupDescription>
|
||||
</div>
|
||||
</div>
|
||||
{#if checked}
|
||||
<div class="flex-shrink-0 text-white">
|
||||
<svg viewBox="0 0 24 24" fill="none" class="w-6 h-6">
|
||||
<circle cx={12} cy={12} r={12} fill="#fff" opacity="0.2" />
|
||||
<path
|
||||
d="M7 13l3 3 7-7"
|
||||
stroke="#fff"
|
||||
stroke-width={1.5}
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</RadioGroupOption>
|
||||
{/each}
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
</div>
|
||||
21
src/routes/docs/examples/switch.svelte
Normal file
21
src/routes/docs/examples/switch.svelte
Normal file
@@ -0,0 +1,21 @@
|
||||
<script lang="ts">
|
||||
import { Switch } from "$lib";
|
||||
|
||||
let enabled = false;
|
||||
</script>
|
||||
|
||||
<div class="py-16">
|
||||
<Switch
|
||||
checked={enabled}
|
||||
on:change={(e) => (enabled = e.detail)}
|
||||
class={`${enabled ? "bg-teal-900" : "bg-teal-700"}
|
||||
relative inline-flex flex-shrink-0 h-[38px] w-[74px] border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75`}
|
||||
>
|
||||
<span class="sr-only">Use setting</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class={`${enabled ? "translate-x-9" : "translate-x-0"}
|
||||
pointer-events-none inline-block h-[34px] w-[34px] rounded-full bg-white shadow-lg transform ring-0 transition ease-in-out duration-200`}
|
||||
/>
|
||||
</Switch>
|
||||
</div>
|
||||
117
src/routes/docs/examples/tabs.svelte
Normal file
117
src/routes/docs/examples/tabs.svelte
Normal file
@@ -0,0 +1,117 @@
|
||||
<script lang="ts">
|
||||
import { Tab, TabGroup, TabList, TabPanel, TabPanels } from "$lib";
|
||||
|
||||
function classNames(...classes) {
|
||||
return classes.filter(Boolean).join(" ");
|
||||
}
|
||||
|
||||
let categories = {
|
||||
Recent: [
|
||||
{
|
||||
id: 1,
|
||||
title: "Does drinking coffee make you smarter?",
|
||||
date: "5h ago",
|
||||
commentCount: 5,
|
||||
shareCount: 2,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "So you've bought coffee... now what?",
|
||||
date: "2h ago",
|
||||
commentCount: 3,
|
||||
shareCount: 2,
|
||||
},
|
||||
],
|
||||
Popular: [
|
||||
{
|
||||
id: 1,
|
||||
title: "Is tech making coffee better or worse?",
|
||||
date: "Jan 7",
|
||||
commentCount: 29,
|
||||
shareCount: 16,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "The most innovative things happening in coffee",
|
||||
date: "Mar 19",
|
||||
commentCount: 24,
|
||||
shareCount: 12,
|
||||
},
|
||||
],
|
||||
Trending: [
|
||||
{
|
||||
id: 1,
|
||||
title: "Ask Me Anything: 10 answers to your questions about coffee",
|
||||
date: "2d ago",
|
||||
commentCount: 9,
|
||||
shareCount: 5,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "The worst advice we've ever heard about coffee",
|
||||
date: "4d ago",
|
||||
commentCount: 1,
|
||||
shareCount: 2,
|
||||
},
|
||||
],
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="w-full max-w-md px-2 py-16 sm:px-0">
|
||||
<TabGroup>
|
||||
<TabList class="flex p-1 space-x-1 bg-blue-900/20 rounded-xl">
|
||||
{#each Object.keys(categories) as category (category)}
|
||||
<Tab
|
||||
class={({ selected }) =>
|
||||
classNames(
|
||||
"w-full py-2.5 text-sm leading-5 font-medium text-blue-700 rounded-lg",
|
||||
"focus:outline-none focus:ring-2 ring-offset-2 ring-offset-blue-400 ring-white ring-opacity-60",
|
||||
selected
|
||||
? "bg-white shadow"
|
||||
: "text-blue-100 hover:bg-white/[0.12] hover:text-white"
|
||||
)}
|
||||
>
|
||||
{category}
|
||||
</Tab>
|
||||
{/each}
|
||||
</TabList>
|
||||
<TabPanels class="mt-2">
|
||||
{#each Object.values(categories) as posts, idx (idx)}
|
||||
<TabPanel
|
||||
class={classNames(
|
||||
"bg-white rounded-xl p-3",
|
||||
"focus:outline-none focus:ring-2 ring-offset-2 ring-offset-blue-400 ring-white ring-opacity-60"
|
||||
)}
|
||||
>
|
||||
<ul>
|
||||
{#each posts as post (post.id)}
|
||||
<li class="relative p-3 rounded-md hover:bg-gray-100">
|
||||
<h3 class="text-sm font-medium leading-5">
|
||||
{post.title}
|
||||
</h3>
|
||||
|
||||
<ul
|
||||
class="flex mt-1 space-x-1 text-xs font-normal leading-4 text-gray-500"
|
||||
>
|
||||
<li>{post.date}</li>
|
||||
<li>·</li>
|
||||
<li>{post.commentCount} comments</li>
|
||||
<li>·</li>
|
||||
<li>{post.shareCount} shares</li>
|
||||
</ul>
|
||||
|
||||
<a
|
||||
href="#"
|
||||
class={classNames(
|
||||
"absolute inset-0 rounded-md",
|
||||
"focus:z-10 focus:outline-none focus:ring-2 ring-blue-400"
|
||||
)}
|
||||
/>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</TabPanel>
|
||||
{/each}
|
||||
</TabPanels>
|
||||
</TabGroup>
|
||||
</div>
|
||||
39
src/routes/docs/examples/transition.svelte
Normal file
39
src/routes/docs/examples/transition.svelte
Normal file
@@ -0,0 +1,39 @@
|
||||
<script lang="ts">
|
||||
import { Transition } from "$lib";
|
||||
|
||||
let isShowing = true;
|
||||
function resetIsShowing() {
|
||||
isShowing = false;
|
||||
setTimeout(() => (isShowing = true), 500);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col items-center py-16">
|
||||
<div class="w-32 h-32">
|
||||
<Transition
|
||||
show={isShowing}
|
||||
enter="transform transition duration-[400ms]"
|
||||
enterFrom="opacity-0 rotate-[-120deg] scale-50"
|
||||
enterTo="opacity-100 rotate-0 scale-100"
|
||||
leave="transform duration-200 transition ease-in-out"
|
||||
leaveFrom="opacity-100 rotate-0 scale-100 "
|
||||
leaveTo="opacity-0 scale-95 "
|
||||
class="w-full h-full bg-white rounded-md shadow-lg"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
on:click={resetIsShowing}
|
||||
class="flex items-center px-3 py-2 mt-8 text-sm font-medium text-white transition transform bg-black rounded-full backface-visibility-hidden active:bg-opacity-40 hover:scale-105 hover:bg-opacity-30 focus:outline-none bg-opacity-20"
|
||||
>
|
||||
<svg viewBox="0 0 20 20" fill="none" class="w-5 h-5 opacity-70">
|
||||
<path
|
||||
d="M14.9497 14.9498C12.2161 17.6835 7.78392 17.6835 5.05025 14.9498C2.31658 12.2162 2.31658 7.784 5.05025 5.05033C7.78392 2.31666 12.2161 2.31666 14.9497 5.05033C15.5333 5.63385 15.9922 6.29475 16.3266 7M16.9497 2L17 7H16.3266M12 7L16.3266 7"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<span class="ml-3">Click to transition</span>
|
||||
</button>
|
||||
</div>
|
||||
425
src/routes/docs/general-concepts.svx
Normal file
425
src/routes/docs/general-concepts.svx
Normal file
@@ -0,0 +1,425 @@
|
||||
# General concepts
|
||||
|
||||
## Component styling
|
||||
|
||||
Out of the box, these components are completely unstyled. This has some big advantages: you're free to make them look however you want, without having to fight against any pre-existing styles that don't fit in with the look and feel of your site. Instead of being constrained by an existing design system like Material Design or Carbon Components, you can fully customize your components' appearance--but letting the library take care of implementing all the keyboard and mouse interactions, accessibility features, and so on.
|
||||
|
||||
The flipside of unstyled components, however, is that you need to, well, style them. There are a number of ways of doing this.
|
||||
|
||||
### Styling children: slot props
|
||||
|
||||
You can style the descendants of components from this library with the help of <a href="https://svelte.dev/tutorial/slot-props">slot props</a>:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import {
|
||||
RadioGroup,
|
||||
RadioGroupLabel,
|
||||
RadioGroupOption,
|
||||
RadioGroupDescription,
|
||||
} from "@rgossiaux/svelte-headlessui";
|
||||
let plan = "startup";
|
||||
</script>
|
||||
|
||||
<RadioGroup value={plan} on:change={(e) => (plan = e.detail)}>
|
||||
<RadioGroupLabel>Plan</RadioGroupLabel>
|
||||
<RadioGroupOption value="startup" let:checked>
|
||||
<span class:checked>Startup</span>
|
||||
</RadioGroupOption>
|
||||
<RadioGroupOption value="business" let:checked>
|
||||
<span class:checked>Business</span>
|
||||
</RadioGroupOption>
|
||||
<RadioGroupOption value="enterprise" let:checked>
|
||||
<span class:checked>Enterprise</span>
|
||||
</RadioGroupOption>
|
||||
</RadioGroup>
|
||||
|
||||
<style>
|
||||
/* Note that using global styles this way is bad practice in larger applications; see below for more */
|
||||
:global(.checked) {
|
||||
background-color: rgb(191 219 254);
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
### Styling the components themselves: `class` and `style`
|
||||
|
||||
Slot props can be used to style any descendent components, but they cannot be used to style the same component that defines them. To work around this, any component that defines slot props will also optionally accept a **function** for its `class` and/or `style` props. These functions will be **called with the slot props as an argument**:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import {
|
||||
RadioGroup,
|
||||
RadioGroupLabel,
|
||||
RadioGroupOption,
|
||||
RadioGroupDescription,
|
||||
} from "@rgossiaux/svelte-headlessui";
|
||||
let plan = "startup";
|
||||
</script>
|
||||
|
||||
<RadioGroup value={plan} on:change={(e) => (plan = e.detail)}>
|
||||
<RadioGroupLabel>Plan</RadioGroupLabel>
|
||||
<RadioGroupOption
|
||||
value="startup"
|
||||
class={({ checked }) => (checked ? "checked" : undefined)}
|
||||
>
|
||||
<span class:checked>Startup</span>
|
||||
</RadioGroupOption>
|
||||
<RadioGroupOption
|
||||
value="business"
|
||||
class={({ checked }) => (checked ? "checked" : undefined)}
|
||||
>
|
||||
<span class:checked>Business</span>
|
||||
</RadioGroupOption>
|
||||
<RadioGroupOption
|
||||
value="enterprise"
|
||||
class={({ checked }) => (checked ? "checked" : undefined)}
|
||||
>
|
||||
<span class:checked>Enterprise</span>
|
||||
</RadioGroupOption>
|
||||
</RadioGroup>
|
||||
|
||||
<style>
|
||||
/* Note that using global styles this way is bad practice in larger applications; see below for more */
|
||||
:global(.checked) {
|
||||
background-color: rgb(191 219 254);
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
See the individual component documentation to learn about the slot props for each component.
|
||||
|
||||
You may also pass a string for `class` or `style`, or omit them entirely, of course.
|
||||
|
||||
What follows below is some guidance on the different ways to use the `class` and `style` props to style your components.
|
||||
|
||||
#### Using Svelte's `<style>` tags
|
||||
|
||||
Svelte comes with an out-of-the-box solution for component styling: using the `<style>` tag inside your components. This generates CSS that is scoped to an individual component instance, letting you do things like this without worrying about your CSS selectors conflicting with other components:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import {
|
||||
RadioGroup,
|
||||
RadioGroupLabel,
|
||||
RadioGroupOption,
|
||||
RadioGroupDescription,
|
||||
} from "@rgossiaux/svelte-headlessui";
|
||||
let plan = "startup";
|
||||
</script>
|
||||
|
||||
<RadioGroup value={plan} on:change={(e) => (plan = e.detail)}>
|
||||
<RadioGroupLabel>Plan</RadioGroupLabel>
|
||||
<RadioGroupOption value="startup" let:checked>
|
||||
<span class:checked>Startup</span>
|
||||
</RadioGroupOption>
|
||||
<RadioGroupOption value="business" let:checked>
|
||||
<span class:checked>Business</span>
|
||||
</RadioGroupOption>
|
||||
<RadioGroupOption value="enterprise" let:checked>
|
||||
<span class:checked>Enterprise</span>
|
||||
</RadioGroupOption>
|
||||
</RadioGroup>
|
||||
|
||||
<style>
|
||||
.checked {
|
||||
background-color: rgb(191 219 254);
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
This works great for styling HTML elements. However, it (unfortunately) does **not** work for styling _components_. If you try to create the same example without the `<span>`s, it won't work:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import {
|
||||
RadioGroup,
|
||||
RadioGroupLabel,
|
||||
RadioGroupOption,
|
||||
RadioGroupDescription,
|
||||
} from "@rgossiaux/svelte-headlessui";
|
||||
let plan = "startup";
|
||||
</script>
|
||||
|
||||
<RadioGroup value={plan} on:change={(e) => (plan = e.detail)}>
|
||||
<RadioGroupLabel>Plan</RadioGroupLabel>
|
||||
<RadioGroupOption
|
||||
value="startup"
|
||||
class={({ checked }) => (checked ? "checked" : "")}
|
||||
>
|
||||
Startup
|
||||
</RadioGroupOption>
|
||||
<RadioGroupOption
|
||||
value="business"
|
||||
class={({ checked }) => (checked ? "checked" : "")}
|
||||
>
|
||||
Business
|
||||
</RadioGroupOption>
|
||||
<RadioGroupOption
|
||||
value="enterprise"
|
||||
class={({ checked }) => (checked ? "checked" : "")}
|
||||
>
|
||||
Enterprise
|
||||
</RadioGroupOption>
|
||||
</RadioGroup>
|
||||
|
||||
<!-- This doesn't work! -->
|
||||
<style>
|
||||
.checked {
|
||||
background-color: rgb(191 219 254);
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
(Note that we also can no longer use the `class:checked` shorthand on a component)
|
||||
|
||||
Why doesn't this work? When Svelte compiles our component, it adds in a special unique class to the CSS rules and to the HTML elements rendered by this component that are affected. But it does _not_ add this special class to any child _components_. The result is that the `.checked` CSS rule will be generated with a second class that the `<RadioGroupOption>` components do not have, and it will not match.
|
||||
|
||||
Unfortunately there is no way in Svelte right now to pass this special class down to the `<RadioGroupOption>` components, and the framework does not have a good solution in general for this. Hopefully one day the Svelte maintainers will add some way of solving this problem. Until then, you can still style your components using one of the approaches below.
|
||||
|
||||
#### The `:global()` modifier
|
||||
|
||||
The `:global()` modifier opts your rule out of the default component style scoping. CSS rules inside `:global()` are treated as "normal" CSS, so the following will work:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import {
|
||||
RadioGroup,
|
||||
RadioGroupLabel,
|
||||
RadioGroupOption,
|
||||
RadioGroupDescription,
|
||||
} from "@rgossiaux/svelte-headlessui";
|
||||
let plan = "startup";
|
||||
</script>
|
||||
|
||||
<RadioGroup value={plan} on:change={(e) => (plan = e.detail)}>
|
||||
<RadioGroupLabel>Plan</RadioGroupLabel>
|
||||
<RadioGroupOption
|
||||
value="startup"
|
||||
class={({ checked }) => (checked ? "checked" : "")}
|
||||
>
|
||||
Startup
|
||||
</RadioGroupOption>
|
||||
<RadioGroupOption
|
||||
value="business"
|
||||
class={({ checked }) => (checked ? "checked" : "")}
|
||||
>
|
||||
Business
|
||||
</RadioGroupOption>
|
||||
<RadioGroupOption
|
||||
value="enterprise"
|
||||
class={({ checked }) => (checked ? "checked" : "")}
|
||||
>
|
||||
Enterprise
|
||||
</RadioGroupOption>
|
||||
</RadioGroup>
|
||||
|
||||
<!-- WARNING: This works, but may not be desirable; see below -->
|
||||
<style>
|
||||
:global(.checked) {
|
||||
background-color: rgb(191 219 254);
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
However, global CSS has one critical downside: it's global! The `.checked` rule above will now apply to _any_ `.checked` CSS class in your application, even if it's in other components. In a larger application, this can get difficult to maintain.
|
||||
|
||||
You can scope this more narrowly if you have a wrapper element:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import {
|
||||
RadioGroup,
|
||||
RadioGroupLabel,
|
||||
RadioGroupOption,
|
||||
RadioGroupDescription,
|
||||
} from "@rgossiaux/svelte-headlessui";
|
||||
let plan = "startup";
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<RadioGroup value={plan} on:change={(e) => (plan = e.detail)}>
|
||||
<RadioGroupLabel>Plan</RadioGroupLabel>
|
||||
<RadioGroupOption
|
||||
value="startup"
|
||||
class={({ checked }) => (checked ? "checked" : "")}
|
||||
>
|
||||
Startup
|
||||
</RadioGroupOption>
|
||||
<RadioGroupOption
|
||||
value="business"
|
||||
class={({ checked }) => (checked ? "checked" : "")}
|
||||
>
|
||||
Business
|
||||
</RadioGroupOption>
|
||||
<RadioGroupOption
|
||||
value="enterprise"
|
||||
class={({ checked }) => (checked ? "checked" : "")}
|
||||
>
|
||||
Enterprise
|
||||
</RadioGroupOption>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
|
||||
<!-- This will only apply to .checked elements that descend from this component -->
|
||||
<style>
|
||||
* > :global(.checked) {
|
||||
background-color: rgb(191 219 254);
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
In this case, this works perfectly.
|
||||
|
||||
When using this scoped `* > :global()` approach, keep in mind these principles:
|
||||
|
||||
- Your Headless UI components must be contained inside some element **that is rendered by your component**, like the wrapper `<div>` above. You need an element for the Svelte scoped class to be attached to.
|
||||
- Your rule will be scoped to your component **and any descendant**. In the example above this is not a problem, since the component has no `<slot>`s. But if your component _does_ have `<slot>`s, you will still need to be careful for collisions.
|
||||
- This rule won't work properly inside a portal, which means that it won't work properly for a `<Dialog>`. To style the `<Dialog>`, you will need to either 1) use global styles without the `* >` scoping, 2) put a wrapper element inside the `<Dialog>` and style that instead, or 3) use one of the other styling approaches.
|
||||
|
||||
#### Inline styles
|
||||
|
||||
You can style any component using inline styles using the `style=...` prop:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import {
|
||||
RadioGroup,
|
||||
RadioGroupLabel,
|
||||
RadioGroupOption,
|
||||
RadioGroupDescription,
|
||||
} from "@rgossiaux/svelte-headlessui";
|
||||
let plan = "startup";
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<RadioGroup value={plan} on:change={(e) => (plan = e.detail)}>
|
||||
<RadioGroupLabel>Plan</RadioGroupLabel>
|
||||
<RadioGroupOption
|
||||
value="startup"
|
||||
style={({ checked }) =>
|
||||
checked ? "background-color: rgb(191 219 254)" : undefined}
|
||||
>
|
||||
Startup
|
||||
</RadioGroupOption>
|
||||
<RadioGroupOption
|
||||
value="business"
|
||||
style={({ checked }) =>
|
||||
checked ? "background-color: rgb(191 219 254)" : undefined}
|
||||
>
|
||||
Business
|
||||
</RadioGroupOption>
|
||||
<RadioGroupOption
|
||||
value="enterprise"
|
||||
style={({ checked }) =>
|
||||
checked ? "background-color: rgb(191 219 254)" : undefined}
|
||||
>
|
||||
Enterprise
|
||||
</RadioGroupOption>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
```
|
||||
|
||||
As mentioned above, this prop accepts a function whose input will be the component's slot props, allowing you to apply conditional styles.
|
||||
|
||||
One downside of inline styles is that you cannot use them to style CSS psuedoselectors, like `:focus` and `:hover`. In general this library will provide you with states that make these psuedoselectors unnecessary, however.
|
||||
|
||||
#### Using a CSS framework
|
||||
|
||||
You can always use the `class` prop as normal alongside a CSS framework like Bootstrap, Bulma, Tailwind, etc. The original version of this component library was written by the same team that maintains Tailwind CSS.
|
||||
|
||||
## Event forwarding
|
||||
|
||||
This library will forward all events to the underlying elements, so you can add your own event handlers if necessary:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import { Switch } from "@rgossiaux/svelte-headlessui";
|
||||
let enabled = false;
|
||||
</script>
|
||||
|
||||
<Switch
|
||||
checked={enabled}
|
||||
on:change={(e) => (enabled = e.detail)}
|
||||
on:click={() => console.log("Clicked!")}
|
||||
>
|
||||
Toggle me!
|
||||
</Switch>
|
||||
```
|
||||
|
||||
Note that the library already handles all of the keyboard and mouse events you'd need to implement the component functionality. You don't need to add a handler to e.g. open or close a popover when the popover button is clicked.
|
||||
|
||||
You can also use event modifiers, but they must be separated by ! instead of the normal |, for technical reasons:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import { Switch } from "@rgossiaux/svelte-headlessui";
|
||||
let enabled = false;
|
||||
</script>
|
||||
|
||||
<Switch
|
||||
checked={enabled}
|
||||
on:change={(e) => (enabled = e.detail)}
|
||||
on:click!once={() => console.log("Clicked!")}
|
||||
>
|
||||
Toggle me!
|
||||
</Switch>
|
||||
```
|
||||
|
||||
## Using Svelte actions
|
||||
|
||||
You can use [actions](https://svelte.dev/tutorial/actions) with this library as well. The Svelte `use:` directive is not supported for Components, so you need to use the `use` prop instead:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import { Switch } from "@rgossiaux/svelte-headlessui";
|
||||
import { action1, action2, action3 } from "./my-library";
|
||||
let action1options = {};
|
||||
let action3options = { foo: true };
|
||||
|
||||
let enabled = false;
|
||||
</script>
|
||||
|
||||
<Switch
|
||||
checked={enabled}
|
||||
on:change={(e) => (enabled = e.detail)}
|
||||
use={[[action1, action1options], [action2], [action3, action3options]]}
|
||||
>
|
||||
Toggle me!
|
||||
</Switch>
|
||||
```
|
||||
|
||||
The `use` prop must be a list of `[action]` or `[action, options]` items. These actions will be applied to the underlying element that the component eventually renders.
|
||||
|
||||
## Customizing the elements rendered
|
||||
|
||||
Each component in this library renders as some specific HTML element. By default, this element will be something that makes sense for that particular component: a `Label` component renders as `<label>`, a `Button` component renders as a `<button>`, etc. These defaults can be found on each component's page.
|
||||
|
||||
You're free to change the element that any component renders as using the `as` prop, which is available on every component. This prop can take a string referring to an HTML element to use instead:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import {
|
||||
Menu,
|
||||
MenuButton,
|
||||
MenuItems,
|
||||
MenuItem,
|
||||
} from "@rgossiaux/svelte-headlessui";
|
||||
</script>
|
||||
|
||||
<Menu>
|
||||
<MenuButton>More</MenuButton>
|
||||
<!-- Render a `ul` instead of a `div` -->
|
||||
<MenuItems as="ul">
|
||||
<!-- Render a `li` instead of an `a` -->
|
||||
<MenuItem as="li" let:active>
|
||||
<a href="/account-settings" class={`${active ? "active" : "inactive"}`}
|
||||
>Account settings</a
|
||||
>
|
||||
</MenuItem>
|
||||
<!-- ... -->
|
||||
</MenuItems>
|
||||
</Menu>
|
||||
```
|
||||
|
||||
Due to technical limitations in Svelte, each one of these elements must be handled individually by the library. Most elements should be supported, but some uncommon ones might not be. If you find yourself wanting to use an element that is _not_ currently supported, please [file a GitHub issue](https://github.com/rgossiaux/svelte-headlessui/issues).
|
||||
17
src/routes/docs/index.svx
Normal file
17
src/routes/docs/index.svx
Normal file
@@ -0,0 +1,17 @@
|
||||
Svelte Headless UI is an unofficial, complete Svelte port of the [Headless UI](https://headlessui.dev/) component library.
|
||||
|
||||
These are high-quality, unstyled Svelte components which are:
|
||||
|
||||
* Completely **unstyled**
|
||||
* Fully **accessible**
|
||||
* Extensively **tested**
|
||||
* Fully **typed** with TypeScript
|
||||
* SvelteKit compatible
|
||||
|
||||
These components can serve as the basis for your own components or design system. They handle accessibility, keyboard and mouse interactions, focus management, and other things you would normally need to worry about when building your own components. You can simply add your own CSS and the rest will be managed for you.
|
||||
|
||||
The original Headless UI library was written by Tailwind Labs and is maintained by them. This project is a direct port, but it is not affiliated with Tailwind Labs and is neither endorsed by, nor supported by, them; it is a community port.
|
||||
|
||||
Headless UI was written originally to support the paid component library called [Tailwind UI](https://tailwindui.com/). This port was done with this use case in mind, and you can [use this with Tailwind UI](docs/tailwind-ui).
|
||||
|
||||
This library works very well with [Tailwind CSS](https://tailwindcss.com/) as a styling system, but it is not a requirement & there is nothing Tailwind-specific in the library. [See here](docs/general-concepts#component-styling) for different methods to style these components with Svelte.
|
||||
561
src/routes/docs/listbox.svx
Normal file
561
src/routes/docs/listbox.svx
Normal file
@@ -0,0 +1,561 @@
|
||||
# Listbox
|
||||
|
||||
<script>
|
||||
import Preview from "./_Preview.svelte";
|
||||
</script>
|
||||
|
||||
<Preview url="examples/listbox" code="" class="h-[410px] bg-orange-300"/>
|
||||
|
||||
## Basic example
|
||||
|
||||
Listboxes are built using a combination of the `Listbox`, `ListboxButton`, `ListboxOptions`, `ListboxOption`, and `ListboxLabel` components.
|
||||
|
||||
The `ListboxButton` will automatically open/close the `ListboxOptions` when clicked, and when the listbox is open, the list of items receives focus and is automatically navigable via the keyboard.
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import {
|
||||
Listbox,
|
||||
ListboxButton,
|
||||
ListboxOptions,
|
||||
ListboxOption,
|
||||
} from "@rgossiaux/svelte-headlessui";
|
||||
|
||||
const people = [
|
||||
{ id: 1, name: "Durward Reynolds", unavailable: false },
|
||||
{ id: 2, name: "Kenton Towne", unavailable: false },
|
||||
{ id: 3, name: "Therese Wunsch", unavailable: false },
|
||||
{ id: 4, name: "Benedict Kessler", unavailable: true },
|
||||
{ id: 5, name: "Katelyn Rohan", unavailable: false },
|
||||
];
|
||||
|
||||
let selectedPerson = people[0];
|
||||
</script>
|
||||
|
||||
<Listbox value={selectedPerson} on:change={(e) => (selectedPerson = e.detail)}>
|
||||
<ListboxButton>{selectedPerson.name}</ListboxButton>
|
||||
<ListboxOptions>
|
||||
{#each people as person (person.id)}
|
||||
<ListboxOption value={person} disabled={person.unavailable}>
|
||||
{person.name}
|
||||
</ListboxOption>
|
||||
{/each}
|
||||
</ListboxOptions>
|
||||
</Listbox>
|
||||
```
|
||||
|
||||
## Styling
|
||||
|
||||
[See here](general-concepts#component-styling) for some general notes on styling the components in this library.
|
||||
|
||||
### Active and selected items
|
||||
|
||||
To style the active `ListboxOption`, you can use the `active` slot prop that it provides, which tells you whether or not that option is currently focused via the mouse or keyboard.
|
||||
|
||||
To style the selected `ListboxOption`, you can use the `selected` slot prop that it provides, which tells you whether or not that option is the selected option (the one passed to the `value` prop of the `<Listbox>`)
|
||||
|
||||
You can use these states to conditionally apply whatever active/focus styles you wish.
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import {
|
||||
Listbox,
|
||||
ListboxButton,
|
||||
ListboxOptions,
|
||||
ListboxOption,
|
||||
} from "@rgossiaux/svelte-headlessui";
|
||||
import { CheckIcon } from "@rgossiaux/svelte-heroicons/solid";
|
||||
|
||||
const people = [
|
||||
{ id: 1, name: "Durward Reynolds" },
|
||||
{ id: 2, name: "Kenton Towne" },
|
||||
{ id: 3, name: "Therese Wunsch" },
|
||||
{ id: 4, name: "Benedict Kessler" },
|
||||
{ id: 5, name: "Katelyn Rohan" },
|
||||
];
|
||||
|
||||
let selectedPerson = people[0];
|
||||
</script>
|
||||
|
||||
<Listbox value={selectedPerson} on:change={(e) => (selectedPerson = e.detail)}>
|
||||
<ListboxButton>{selectedPerson.name}</ListboxButton>
|
||||
<ListboxOptions>
|
||||
{#each people as person (person.id)}
|
||||
<!-- Use the `active` state to conditionally style the active (focused) option -->
|
||||
<!-- Use the `selected` state to conditionally style the selected option -->
|
||||
<ListboxOption
|
||||
value={person}
|
||||
disabled={person.unavailable}
|
||||
class={({ active }) => (active ? "active" : "")}
|
||||
let:selected
|
||||
>
|
||||
{#if selected}
|
||||
<CheckIcon />
|
||||
{/if}
|
||||
{person.name}
|
||||
</ListboxOption>
|
||||
{/each}
|
||||
</ListboxOptions>
|
||||
</Listbox>
|
||||
```
|
||||
|
||||
## Using a custom label
|
||||
|
||||
By default, the `Listbox` will use the `<ListboxButton>` contents as the label for screenreaders. If you'd like more control over what is announced to assistive technologies, use the `ListboxLabel` component:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import {
|
||||
Listbox,
|
||||
ListboxButton,
|
||||
ListboxLabel,
|
||||
ListboxOptions,
|
||||
ListboxOption,
|
||||
} from "@rgossiaux/svelte-headlessui";
|
||||
|
||||
const people = [
|
||||
{ id: 1, name: "Durward Reynolds" },
|
||||
{ id: 2, name: "Kenton Towne" },
|
||||
{ id: 3, name: "Therese Wunsch" },
|
||||
{ id: 4, name: "Benedict Kessler" },
|
||||
{ id: 5, name: "Katelyn Rohan" },
|
||||
];
|
||||
|
||||
let selectedPerson = people[0];
|
||||
</script>
|
||||
|
||||
<Listbox value={selectedPerson} on:change={(e) => (selectedPerson = e.detail)}>
|
||||
<ListboxLabel>Assignee:</ListboxLabel>
|
||||
<ListboxButton>{selectedPerson.name}</ListboxButton>
|
||||
<ListboxOptions>
|
||||
{#each people as person (person.id)}
|
||||
<ListboxOption value={person}>
|
||||
{person.name}
|
||||
</ListboxOption>
|
||||
{/each}
|
||||
</ListboxOptions>
|
||||
</Listbox>
|
||||
```
|
||||
|
||||
## Showing/hiding the listbox
|
||||
|
||||
By default, the `ListboxOptions` instance will be shown and hidden automatically based on the internal `open` state tracked by the `Listbox` component itself.
|
||||
|
||||
If you'd rather handle this yourself (perhaps because you need to add an extra wrapper element for one reason or another), you can add a `static` prop to the `ListboxOptions` component to tell it to always render, and use the `open` slot prop provided by the `Listbox` to show or hide the listbox yourself.
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import {
|
||||
Listbox,
|
||||
ListboxButton,
|
||||
ListboxOptions,
|
||||
ListboxOption,
|
||||
} from "@rgossiaux/svelte-headlessui";
|
||||
|
||||
const people = [
|
||||
{ id: 1, name: "Durward Reynolds" },
|
||||
{ id: 2, name: "Kenton Towne" },
|
||||
{ id: 3, name: "Therese Wunsch" },
|
||||
{ id: 4, name: "Benedict Kessler" },
|
||||
{ id: 5, name: "Katelyn Rohan" },
|
||||
];
|
||||
|
||||
let selectedPerson = people[0];
|
||||
</script>
|
||||
|
||||
<Listbox
|
||||
value={selectedPerson}
|
||||
on:change={(e) => (selectedPerson = e.detail)}
|
||||
let:open
|
||||
>
|
||||
<ListboxButton>{selectedPerson.name}</ListboxButton>
|
||||
{#if open}
|
||||
<div>
|
||||
<!-- Using `static`, the `ListboxOptions` is always rendered
|
||||
and ignores the internal `open` state -->
|
||||
<ListboxOptions static>
|
||||
{#each people as person (person.id)}
|
||||
<ListboxOption value={person}>
|
||||
{person.name}
|
||||
</ListboxOption>
|
||||
{/each}
|
||||
</ListboxOptions>
|
||||
</div>
|
||||
{/if}
|
||||
</Listbox>
|
||||
```
|
||||
|
||||
You can also choose to have the `Listbox` merely hide the `ListboxOptions` when the `Listbox` is closed, instead of removing it from the DOM entirely, by using the `unmount` prop. This may be useful for performance reasons if rendering the `ListboxOptions` is expensive.
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import {
|
||||
Listbox,
|
||||
ListboxButton,
|
||||
ListboxOptions,
|
||||
ListboxOption,
|
||||
} from "@rgossiaux/svelte-headlessui";
|
||||
|
||||
const people = [
|
||||
{ id: 1, name: "Durward Reynolds" },
|
||||
{ id: 2, name: "Kenton Towne" },
|
||||
{ id: 3, name: "Therese Wunsch" },
|
||||
{ id: 4, name: "Benedict Kessler" },
|
||||
{ id: 5, name: "Katelyn Rohan" },
|
||||
];
|
||||
|
||||
let selectedPerson = people[0];
|
||||
</script>
|
||||
|
||||
<Listbox value={selectedPerson} on:change={(e) => (selectedPerson = e.detail)}>
|
||||
<ListboxButton>{selectedPerson.name}</ListboxButton>
|
||||
<!-- Using `unmount={false}`, the `ListboxOptions` is kept in the DOM
|
||||
when the `Listbox` is closed -->
|
||||
<ListboxOptions unmount={false}>
|
||||
{#each people as person (person.id)}
|
||||
<ListboxOption value={person}>
|
||||
{person.name}
|
||||
</ListboxOption>
|
||||
{/each}
|
||||
</ListboxOptions>
|
||||
</Listbox>
|
||||
```
|
||||
|
||||
## Disabling an item
|
||||
|
||||
Use the `disabled` prop to disable a `ListboxOption`. This will make it unselectable via mouse and keyboard navigation, and it will be skipped when pressing the up/down arrows.
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import {
|
||||
Listbox,
|
||||
ListboxButton,
|
||||
ListboxOptions,
|
||||
ListboxOption,
|
||||
} from "@rgossiaux/svelte-headlessui";
|
||||
|
||||
const people = [
|
||||
{ id: 1, name: "Durward Reynolds", unavailable: false },
|
||||
{ id: 2, name: "Kenton Towne", unavailable: false },
|
||||
{ id: 3, name: "Therese Wunsch", unavailable: false },
|
||||
{ id: 4, name: "Benedict Kessler", unavailable: true },
|
||||
{ id: 5, name: "Katelyn Rohan", unavailable: false },
|
||||
];
|
||||
|
||||
let selectedPerson = people[0];
|
||||
</script>
|
||||
|
||||
<Listbox value={selectedPerson} on:change={(e) => (selectedPerson = e.detail)}>
|
||||
<ListboxButton>{selectedPerson.name}</ListboxButton>
|
||||
<ListboxOptions>
|
||||
{#each people as person (person.id)}
|
||||
<ListboxOption value={person} disabled={person.unavailable}>
|
||||
<span class:unavailable={person.unavailable}>
|
||||
{person.name}
|
||||
</span>
|
||||
</ListboxOption>
|
||||
{/each}
|
||||
</ListboxOptions>
|
||||
</Listbox>
|
||||
```
|
||||
|
||||
## Transitions
|
||||
|
||||
To animate the opening and closing of the listbox, you can use [this library's Transition component](/docs/transition) or Svelte's built-in transition engine. See that page for a comparison.
|
||||
|
||||
### Using the `Transition` component
|
||||
|
||||
To use the `Transition` component, all you need to do is wrap the `ListboxOptions` in a `<Transition>`, and the transition will be applied automatically.
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import {
|
||||
Listbox,
|
||||
ListboxButton,
|
||||
ListboxOptions,
|
||||
ListboxOption,
|
||||
Transition,
|
||||
} from "@rgossiaux/svelte-headlessui";
|
||||
|
||||
const people = [
|
||||
{ id: 1, name: "Durward Reynolds" },
|
||||
{ id: 2, name: "Kenton Towne" },
|
||||
{ id: 3, name: "Therese Wunsch" },
|
||||
{ id: 4, name: "Benedict Kessler" },
|
||||
{ id: 5, name: "Katelyn Rohan" },
|
||||
];
|
||||
|
||||
let selectedPerson = people[0];
|
||||
</script>
|
||||
|
||||
<Listbox value={selectedPerson} on:change={(e) => (selectedPerson = e.detail)}>
|
||||
<ListboxButton>{selectedPerson.name}</ListboxButton>
|
||||
<!-- Example using Tailwind CSS transition classes -->
|
||||
<Transition
|
||||
enter="transition duration-100 ease-out"
|
||||
enterFrom="transform scale-95 opacity-0"
|
||||
enterTo="transform scale-100 opacity-100"
|
||||
leave="transition duration-75 ease-out"
|
||||
leaveFrom="transform scale-100 opacity-100"
|
||||
leaveTo="transform scale-95 opacity-0"
|
||||
>
|
||||
<ListboxOptions>
|
||||
{#each people as person (person.id)}
|
||||
<ListboxOption value={person}>
|
||||
{person.name}
|
||||
</ListboxOption>
|
||||
{/each}
|
||||
</ListboxOptions>
|
||||
</Transition>
|
||||
</Listbox>
|
||||
```
|
||||
|
||||
The components in this library communicate with each other, so the Transition will be managed automatically when the Listbox is opened/closed. If you require more control over this behavior, you may use a more explicit version:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import {
|
||||
Listbox,
|
||||
ListboxButton,
|
||||
ListboxOptions,
|
||||
ListboxOption,
|
||||
Transition,
|
||||
} from "@rgossiaux/svelte-headlessui";
|
||||
|
||||
const people = [
|
||||
{ id: 1, name: "Durward Reynolds" },
|
||||
{ id: 2, name: "Kenton Towne" },
|
||||
{ id: 3, name: "Therese Wunsch" },
|
||||
{ id: 4, name: "Benedict Kessler" },
|
||||
{ id: 5, name: "Katelyn Rohan" },
|
||||
];
|
||||
|
||||
let selectedPerson = people[0];
|
||||
</script>
|
||||
|
||||
<Listbox
|
||||
value={selectedPerson}
|
||||
on:change={(e) => (selectedPerson = e.detail)}
|
||||
let:open
|
||||
>
|
||||
<ListboxButton>{selectedPerson.name}</ListboxButton>
|
||||
<!-- Example using Tailwind CSS transition classes -->
|
||||
<Transition
|
||||
show={open}
|
||||
enter="transition duration-100 ease-out"
|
||||
enterFrom="transform scale-95 opacity-0"
|
||||
enterTo="transform scale-100 opacity-100"
|
||||
leave="transition duration-75 ease-out"
|
||||
leaveFrom="transform scale-100 opacity-100"
|
||||
leaveTo="transform scale-95 opacity-0"
|
||||
>
|
||||
<!-- When controlling the transition manually, make sure to use `static` -->
|
||||
<ListboxOptions static>
|
||||
{#each people as person (person.id)}
|
||||
<ListboxOption value={person}>
|
||||
{person.name}
|
||||
</ListboxOption>
|
||||
{/each}
|
||||
</ListboxOptions>
|
||||
</Transition>
|
||||
</Listbox>
|
||||
```
|
||||
|
||||
### Using Svelte transitions
|
||||
|
||||
The last example above also provides a blueprint for using Svelte transitions:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import {
|
||||
Listbox,
|
||||
ListboxButton,
|
||||
ListboxOptions,
|
||||
ListboxOption,
|
||||
} from "@rgossiaux/svelte-headlessui";
|
||||
import { fade } from "svelte/transition";
|
||||
|
||||
const people = [
|
||||
{ id: 1, name: "Durward Reynolds" },
|
||||
{ id: 2, name: "Kenton Towne" },
|
||||
{ id: 3, name: "Therese Wunsch" },
|
||||
{ id: 4, name: "Benedict Kessler" },
|
||||
{ id: 5, name: "Katelyn Rohan" },
|
||||
];
|
||||
|
||||
let selectedPerson = people[0];
|
||||
</script>
|
||||
|
||||
<Listbox
|
||||
value={selectedPerson}
|
||||
on:change={(e) => (selectedPerson = e.detail)}
|
||||
let:open
|
||||
>
|
||||
<ListboxButton>{selectedPerson.name}</ListboxButton>
|
||||
{#if open}
|
||||
<div transition:fade>
|
||||
<!-- When controlling the transition manually, make sure to use `static` -->
|
||||
<ListboxOptions static>
|
||||
{#each people as person (person.id)}
|
||||
<ListboxOption value={person}>
|
||||
{person.name}
|
||||
</ListboxOption>
|
||||
{/each}
|
||||
</ListboxOptions>
|
||||
</div>
|
||||
{/if}
|
||||
</Listbox>
|
||||
```
|
||||
|
||||
Make sure to use the `static` prop, or else the exit transitions won't work correctly.
|
||||
|
||||
## Horizontal options
|
||||
|
||||
If you've styled your `ListboxOptions` to appear horizontally, use the `horizontal` prop on the `Listbox` component to enable navigating the items with the left and right arrow keys instead of up and down, and to update the `aria-orientation` attribute for assistive technologies.
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import {
|
||||
Listbox,
|
||||
ListboxButton,
|
||||
ListboxOptions,
|
||||
ListboxOption,
|
||||
} from "@rgossiaux/svelte-headlessui";
|
||||
|
||||
const people = [
|
||||
{ id: 1, name: "Durward Reynolds" },
|
||||
{ id: 2, name: "Kenton Towne" },
|
||||
{ id: 3, name: "Therese Wunsch" },
|
||||
{ id: 4, name: "Benedict Kessler" },
|
||||
{ id: 5, name: "Katelyn Rohan" },
|
||||
];
|
||||
|
||||
let selectedPerson = people[0];
|
||||
</script>
|
||||
|
||||
<Listbox
|
||||
value={selectedPerson}
|
||||
on:change={(e) => (selectedPerson = e.detail)}
|
||||
horizontal
|
||||
>
|
||||
<ListboxButton>{selectedPerson.name}</ListboxButton>
|
||||
<ListboxOptions>
|
||||
{#each people as person (person.id)}
|
||||
<ListboxOption value={person}>
|
||||
{person.name}
|
||||
</ListboxOption>
|
||||
{/each}
|
||||
</ListboxOptions>
|
||||
</Listbox>
|
||||
```
|
||||
|
||||
## Accessibility notes
|
||||
|
||||
### Focus management
|
||||
|
||||
When a Listbox is toggled open, the `ListboxOptions` receives focus. Focus is trapped within the list of items until Escape is pressed or the user clicks outside the options. Closing the `Listbox` returns focus to the `ListboxButton`.
|
||||
|
||||
### Mouse interaction
|
||||
|
||||
Clicking a `ListboxButton` toggles the options list open and closed. Clicking anywhere outside of the options list will close the listbox.
|
||||
|
||||
### Keyboard interaction
|
||||
|
||||
When the `horizontal` prop is set, the `<ArrowUp>` and `<ArrowDown>` below become `<ArrowLeft>` and `<ArrowRight>`:
|
||||
|
||||
| Command | Description |
|
||||
| ----------------------------------------------------------------------------------- | ----------------------------------------------- |
|
||||
| `<Enter>` / `<Space>` / `<ArrowUp>` / `<ArrowDown>` when `ListboxButton` is focused | Opens listbox and focuses the selected item |
|
||||
| `<Esc>` when listbox is open | Closes listbox |
|
||||
| `<ArrowDown>` / `<ArrowUp>` when listbox is open | Focuses next/previous non-disabled option |
|
||||
| `<Home>` / `<End>` when listbox is open | Focuses first/last non-disabled option |
|
||||
| `<Enter>` / `<Space>` when listbox is open | Selects the focused option |
|
||||
| `<A-Za-z>` when listbox is open | Focuses next option that matches keyboard input |
|
||||
|
||||
### Other
|
||||
|
||||
All relevant ARIA attributes are automatically managed.
|
||||
|
||||
For a full reference on all accessibility features implemented in `Listbox`, see <a href="https://www.w3.org/TR/wai-aria-practices-1.2/#Listbox">the ARIA spec on Listbox</a>.
|
||||
|
||||
## Component API
|
||||
|
||||
### Listbox
|
||||
|
||||
The main listbox component.
|
||||
|
||||
| Prop | Default | Type | Description |
|
||||
| ------------ | ------- | --------- | ---------------------------------------------------------------------------------- |
|
||||
| `as` | `div` | `string` | The element the `Listbox` should render as |
|
||||
| `disabled` | `false` | `boolean` | Whether the entire `Listbox` and its children should be disabled |
|
||||
| `horizontal` | `false` | `boolean` | Whether the entire `Listbox` should be oriented horizontally instead of vertically |
|
||||
| `value` | -- | `T` | The selected value |
|
||||
|
||||
| Slot prop | Type | Description |
|
||||
| ---------- | --------- | -------------------------------------- |
|
||||
| `disabled` | `boolean` | Whether or not the listbox is disabled |
|
||||
| `open` | `boolean` | Whether or not the listbox is open |
|
||||
|
||||
This component also dispatches a custom event, which is listened to using the Svelte `on:` directive:
|
||||
|
||||
| Event name | Type of event `.detail` | Description |
|
||||
| ---------- | ----------------------- | ------------------------------------------------------------------------------------------------------------- |
|
||||
| `change` | `T` | Dispatched when a `ListboxOption` is selected; the event `detail` contains the `value` of the selected option |
|
||||
|
||||
### ListboxButton
|
||||
|
||||
The listbox's button.
|
||||
|
||||
| Prop | Default | Type | Description |
|
||||
| ---- | -------- | -------- | ------------------------------------------------ |
|
||||
| `as` | `button` | `string` | The element the `ListboxButton` should render as |
|
||||
|
||||
| Slot prop | Type | Description |
|
||||
| ---------- | --------- | -------------------------------------- |
|
||||
| `disabled` | `boolean` | Whether or not the listbox is disabled |
|
||||
| `open` | `boolean` | Whether or not the listbox is open |
|
||||
|
||||
### ListboxLabel
|
||||
|
||||
A label that can be used for more control over the text your listbox will announce to screenreaders. Renders an element that is linked to the root `Listbox` via the `aria-labelledby` attribute and an autogenerated id.
|
||||
|
||||
| Prop | Default | Type | Description |
|
||||
| ---- | ------- | -------- | ----------------------------------------------- |
|
||||
| `as` | `label` | `string` | The element the `ListboxLabel` should render as |
|
||||
|
||||
| Slot prop | Type | Description |
|
||||
| ---------- | --------- | -------------------------------------- |
|
||||
| `disabled` | `boolean` | Whether or not the listbox is disabled |
|
||||
| `open` | `boolean` | Whether or not the listbox is open |
|
||||
|
||||
### ListboxOptions
|
||||
|
||||
The component that directly wraps the list of options in your custom listbox.
|
||||
|
||||
| Prop | Default | Type | Description |
|
||||
| --------- | ------- | --------- | ----------------------------------------------------------------------------------------------- |
|
||||
| `as` | `ul` | `string` | The element the `ListboxOptions` should render as |
|
||||
| `static` | `false` | `boolean` | Whether the element should ignore the internally managed open/closed state |
|
||||
| `unmount` | `true` | `boolean` | Whether the element should be unmounted, instead of just hidden, based on the open/closed state |
|
||||
|
||||
Note that `static` and `unmount` cannot be used together.
|
||||
|
||||
| Slot prop | Type | Description |
|
||||
| --------- | --------- | ---------------------------------- |
|
||||
| `open` | `boolean` | Whether or not the listbox is open |
|
||||
|
||||
### ListboxOption
|
||||
|
||||
Used to wrap each item within your listbox.
|
||||
|
||||
| Prop | Default | Type | Description |
|
||||
| ---------- | ------- | --------- | ------------------------------------------------------------------------------- |
|
||||
| `value` | -- | `T` | The option value |
|
||||
| `as` | `li` | `string` | The element the `ListboxOption` should render as |
|
||||
| `disabled` | `false` | `boolean` | Whether the option should be disabled for keyboard navigation and ARIA purposes |
|
||||
|
||||
| Slot prop | Type | Description |
|
||||
| ---------- | --------- | -------------------------------------- |
|
||||
| `active` | `boolean` | Whether the option is active (focused) |
|
||||
| `selected` | `boolean` | Whether the option is selected |
|
||||
| `disabled` | `boolean` | Whether the option is disabled |
|
||||
399
src/routes/docs/menu.svx
Normal file
399
src/routes/docs/menu.svx
Normal file
@@ -0,0 +1,399 @@
|
||||
# Menu
|
||||
|
||||
<script>
|
||||
import Preview from "./_Preview.svelte";
|
||||
</script>
|
||||
|
||||
<Preview url="examples/menu" code="" class="h-[410px] bg-violet-300"/>
|
||||
|
||||
## Basic example
|
||||
|
||||
Menus are built using a combination of the `Menu`, `MenuButton`, `MenuItems`, and `MenuItem` components:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import {
|
||||
Menu,
|
||||
MenuButton,
|
||||
MenuItems,
|
||||
MenuItem,
|
||||
} from "@rgossiaux/svelte-headlessui";
|
||||
</script>
|
||||
|
||||
<Menu>
|
||||
<MenuButton>More</MenuButton>
|
||||
<MenuItems>
|
||||
<MenuItem let:active>
|
||||
<a href="/account-settings" class:active>Account settings</a>
|
||||
</MenuItem>
|
||||
<MenuItem let:active>
|
||||
<a href="/documentation" class:active>Documentation</a>
|
||||
</MenuItem>
|
||||
<MenuItem disabled>
|
||||
<span class="disabled">Invite a friend (coming soon!)</span>
|
||||
</MenuItem>
|
||||
</MenuItems>
|
||||
</Menu>
|
||||
```
|
||||
|
||||
## Styling
|
||||
|
||||
[See here](general-concepts#component-styling) for some general notes on styling the components in this library.
|
||||
|
||||
### Active item
|
||||
|
||||
To style the active `MenuItem`, you can use the `active` slot prop that it provides, which tells you whether or not that menu item is currently focused via the mouse or keyboard. You can use this state to conditionally apply whatever active/focus styles you wish.
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import {
|
||||
Menu,
|
||||
MenuButton,
|
||||
MenuItems,
|
||||
MenuItem,
|
||||
} from "@rgossiaux/svelte-headlessui";
|
||||
</script>
|
||||
|
||||
<Menu>
|
||||
<MenuButton>More</MenuButton>
|
||||
<MenuItems>
|
||||
<!--Use the `active` slot prop to conditionally style the active item.-->
|
||||
<MenuItem let:active>
|
||||
<a href="/account-settings" class={`${active ? "active" : "inactive"}`}
|
||||
>Account settings</a
|
||||
>
|
||||
</MenuItem>
|
||||
<!-- ... -->
|
||||
</MenuItems>
|
||||
</Menu>
|
||||
```
|
||||
|
||||
If necessary, you can also style the `MenuItem` itself by passing a function to `class`:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import {
|
||||
Menu,
|
||||
MenuButton,
|
||||
MenuItems,
|
||||
MenuItem,
|
||||
} from "@rgossiaux/svelte-headlessui";
|
||||
</script>
|
||||
|
||||
<Menu>
|
||||
<MenuButton>More</MenuButton>
|
||||
<MenuItems>
|
||||
<!--Use the `active` slot prop to conditionally style the active item.-->
|
||||
<MenuItem class={({ active }) => (active ? "active" : "inactive")}>
|
||||
Account settings
|
||||
</MenuItem>
|
||||
<!-- ... -->
|
||||
</MenuItems>
|
||||
</Menu>
|
||||
```
|
||||
|
||||
## Showing/hiding the menu
|
||||
|
||||
By default, the `MenuItems` instance will be shown and hidden automatically based on the internal `open` state tracked by the `Menu` component itself.
|
||||
|
||||
If you'd rather handle this yourself (perhaps because you need to add an extra wrapper element for one reason or another), you can add a `static` prop to the `MenuItems` component to tell it to always render, and use the `open` slot prop provided by the `Menu` to show or hide the menu yourself.
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import {
|
||||
Menu,
|
||||
MenuButton,
|
||||
MenuItems,
|
||||
MenuItem,
|
||||
} from "@rgossiaux/svelte-headlessui";
|
||||
</script>
|
||||
|
||||
<Menu let:open>
|
||||
<MenuButton>More</MenuButton>
|
||||
|
||||
{#if open}
|
||||
<!-- Using `static`, `MenuItems` is always rendered and ignores the `open` state. -->
|
||||
<MenuItems static>
|
||||
<MenuItem>
|
||||
<!-- ... -->
|
||||
</MenuItem>
|
||||
<!-- ... -->
|
||||
</MenuItems>
|
||||
{/if}
|
||||
</Menu>
|
||||
```
|
||||
|
||||
You can also choose to have the `Menu` merely hide the `MenuItems` when the `Menu` is closed, instead of removing it from the DOM entirely, by using the `unmount` prop. This may be useful for performance reasons if rendering the `MenuItems` is expensive.
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import {
|
||||
Menu,
|
||||
MenuButton,
|
||||
MenuItems,
|
||||
MenuItem,
|
||||
} from "@rgossiaux/svelte-headlessui";
|
||||
</script>
|
||||
|
||||
<Menu>
|
||||
<MenuButton>More</MenuButton>
|
||||
<!-- Using `unmount={false}`, the `MenuItems` is kept in the DOM when the `Menu` is closed -->
|
||||
<MenuItems unmount={false}>
|
||||
<MenuItem>
|
||||
<!-- ... -->
|
||||
</MenuItem>
|
||||
<!-- ... -->
|
||||
</MenuItems>
|
||||
</Menu>
|
||||
```
|
||||
|
||||
## Disabling an item
|
||||
|
||||
Use the `disabled` prop to disable a `MenuItem`. This will make it unselectable via keyboard navigation, and it will be skipped when pressing the up/down arrows.
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import {
|
||||
Menu,
|
||||
MenuButton,
|
||||
MenuItems,
|
||||
MenuItem,
|
||||
} from "@rgossiaux/svelte-headlessui";
|
||||
</script>
|
||||
|
||||
<Menu>
|
||||
<MenuButton>More</MenuButton>
|
||||
<MenuItems>
|
||||
<!-- ... -->
|
||||
|
||||
<!-- This item will be skipped by keyboard navigation -->
|
||||
<MenuItem disabled>
|
||||
<span class="disabled">Invite a friend (coming soon!)</span>
|
||||
</MenuItem>
|
||||
|
||||
<!-- ... -->
|
||||
</MenuItems>
|
||||
</Menu>
|
||||
```
|
||||
|
||||
## Transitions
|
||||
|
||||
To animate the opening and closing of the menu, you can use [this library's Transition component](/docs/transition) or Svelte's built-in transition engine. See that page for a comparison.
|
||||
|
||||
### Using the `Transition` component
|
||||
|
||||
To use the `Transition` component, all you need to do is wrap the `MenuItems` in a `<Transition>`, and the transition will be applied automatically.
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import {
|
||||
Menu,
|
||||
MenuButton,
|
||||
MenuItems,
|
||||
MenuItem,
|
||||
Transition,
|
||||
} from "@rgossiaux/svelte-headlessui";
|
||||
</script>
|
||||
|
||||
<Menu>
|
||||
<MenuButton>More</MenuButton>
|
||||
<!-- Example using Tailwind CSS transition classes -->
|
||||
<Transition
|
||||
enter="transition duration-100 ease-out"
|
||||
enterFrom="transform scale-95 opacity-0"
|
||||
enterTo="transform scale-100 opacity-100"
|
||||
leave="transition duration-75 ease-out"
|
||||
leaveFrom="transform scale-100 opacity-100"
|
||||
leaveTo="transform scale-95 opacity-0"
|
||||
>
|
||||
<MenuItems>
|
||||
<MenuItem>
|
||||
<!-- ... -->
|
||||
</MenuItem>
|
||||
<!-- ... -->
|
||||
</MenuItems>
|
||||
</Transition>
|
||||
</Menu>
|
||||
```
|
||||
|
||||
The components in this library communicate with each other, so the Transition
|
||||
will be managed automatically when the Menu is opened/closed. If you require
|
||||
more control over this behavior, you may use a more explicit version:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import {
|
||||
Menu,
|
||||
MenuButton,
|
||||
MenuItems,
|
||||
MenuItem,
|
||||
Transition,
|
||||
} from "@rgossiaux/svelte-headlessui";
|
||||
</script>
|
||||
|
||||
<!-- Use the open slot prop -->
|
||||
<Menu let:open>
|
||||
<MenuButton>More</MenuButton>
|
||||
<!-- Example using Tailwind CSS transition classes -->
|
||||
<Transition
|
||||
show={open}
|
||||
enter="transition duration-100 ease-out"
|
||||
enterFrom="transform scale-95 opacity-0"
|
||||
enterTo="transform scale-100 opacity-100"
|
||||
leave="transition duration-75 ease-out"
|
||||
leaveFrom="transform scale-100 opacity-100"
|
||||
leaveTo="transform scale-95 opacity-0"
|
||||
>
|
||||
<!-- Mark this as static -->
|
||||
<MenuItems static>
|
||||
<MenuItem>
|
||||
<!-- ... -->
|
||||
</MenuItem>
|
||||
<!-- ... -->
|
||||
</MenuItems>
|
||||
</Transition>
|
||||
</Menu>
|
||||
```
|
||||
|
||||
### Using Svelte transitions
|
||||
|
||||
The last example above also provides a blueprint for using Svelte transitions:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import {
|
||||
Menu,
|
||||
MenuButton,
|
||||
MenuItems,
|
||||
MenuItem,
|
||||
Transition,
|
||||
} from "@rgossiaux/svelte-headlessui";
|
||||
import { fade } from "svelte/transition";
|
||||
</script>
|
||||
|
||||
<!-- Use the open slot prop -->
|
||||
<Menu let:open>
|
||||
<MenuButton>More</MenuButton>
|
||||
<!-- Example using Tailwind CSS transition classes -->
|
||||
{#if open}
|
||||
<div transition:fade>
|
||||
<!-- Mark this as static -->
|
||||
<MenuItems static>
|
||||
<MenuItem>
|
||||
<!-- ... -->
|
||||
</MenuItem>
|
||||
<!-- ... -->
|
||||
</MenuItems>
|
||||
</div>
|
||||
{/if}
|
||||
</Menu>
|
||||
```
|
||||
|
||||
Make sure to use the `static` prop, or else the exit transitions won't work correctly.
|
||||
|
||||
## Rendering additional content
|
||||
|
||||
The accessibility semantics of role="menu" are fairly strict, and any children of a Menu besides MenuItem components will be automatically hidden from assistive technology to make sure the menu works the way screen reader users expect.
|
||||
|
||||
For this reason, rendering any children other than MenuItem components is discouraged, as that content will be inaccessible to people using assistive technology.
|
||||
|
||||
If you want to build a dropdown with more flexible content, consider using <a href="popover">Popover</a> instead.
|
||||
|
||||
## When to use a `Menu`
|
||||
|
||||
Menus are best for UI elements that resemble things like the menus you'd find in the title bar of most operating systems. They have specific accessibility semantics, and their content should be restricted to a list of links or buttons. Focus is trapped in an open menu, so you cannot tab through the content or away from the menu. Instead, the arrow keys navigate through a `Menu`'s items.
|
||||
|
||||
Here's when you might use other similar components from Headless UI:
|
||||
|
||||
- <a href="popover">Popover</a>: Popovers are general-purpose floating menus.
|
||||
They appear near the button that triggers them, and you can put arbitrary
|
||||
markup in them like images or non-clickable content. The Tab key navigates the
|
||||
contents of a Popover like it would any other normal markup. They're great for
|
||||
building header nav items with expandable content and flyout panels.
|
||||
|
||||
- <a href="disclosure">Disclosure</a>: Disclosures are useful for elements that
|
||||
expand to reveal additional information, like a toggleable FAQ section. They
|
||||
are typically rendered inline and reflow the document when they're shown or
|
||||
hidden.
|
||||
|
||||
- <a href="dialog">Dialog</a>: Dialogs are meant to grab the user's full
|
||||
attention. They typically render a floating panel in the center of the screen,
|
||||
and use a backdrop to dim the rest of the application's contents. They also
|
||||
capture focus and prevent tabbing away from the Dialog's contents until the
|
||||
Dialog is dismissed.
|
||||
|
||||
## Accessibility notes
|
||||
|
||||
### Focus management
|
||||
|
||||
Clicking the `MenuButton` toggles the menu and focuses the `MenuItems` component. Focus is trapped within the open menu until Escape is pressed or the user clicks outside the menu. Closing the menu returns focus to the `MenuButton`.
|
||||
|
||||
### Mouse interaction
|
||||
|
||||
Clicking a `MenuButton` toggles the menu. Clicking anywhere outside of an open menu will close that menu.
|
||||
|
||||
### Keyboard interaction
|
||||
|
||||
| Command | Description |
|
||||
| -------------------------------------------------------- | --------------------------------------------------- |
|
||||
| `<Enter>` / `<Space>` when `MenuButton` is focused | Opens menu and focuses first non-disabled item |
|
||||
| `<ArrowDown>` / `<ArrowUp>` when `MenuButton` is focused | Opens menu and focuses first/last non-disabled item |
|
||||
| `<Esc>` when menu is open | Closes any open Menus |
|
||||
| `<ArrowDown>` / `<ArrowUp>` when menu is open | Focuses next/previous non-disabled item |
|
||||
| `<Home>` / `<End>` when menu is open | Focuses first/last non-disabled item |
|
||||
| `<Enter>` / `<Space>` when menu is open | Activates/clicks the current menu item |
|
||||
| `<A-Za-z>` when menu is open | Focuses next item that matches keyboard input |
|
||||
|
||||
### Other
|
||||
|
||||
All relevant ARIA attributes are automatically managed.
|
||||
|
||||
For a full reference on all accessibility features implemented in `Menu`, see <a href="https://www.w3.org/TR/wai-aria-practices-1.2/#menu">the ARIA spec on Menus</a>.
|
||||
|
||||
## Component API
|
||||
|
||||
### Menu
|
||||
|
||||
| Prop | Default | Type | Description |
|
||||
| ---- | ------- | -------- | --------------------------------------- |
|
||||
| `as` | `div` | `string` | The element the `Menu` should render as |
|
||||
|
||||
| Slot prop | Type | Description |
|
||||
| --------- | --------- | ------------------------------- |
|
||||
| `open` | `boolean` | Whether or not the menu is open |
|
||||
|
||||
### MenuButton
|
||||
|
||||
| Prop | Default | Type | Description |
|
||||
| ---- | -------- | -------- | --------------------------------------------- |
|
||||
| `as` | `button` | `string` | The element the `MenuButton` should render as |
|
||||
|
||||
| Slot prop | Type | Description |
|
||||
| --------- | --------- | ------------------------------- |
|
||||
| `open` | `boolean` | Whether or not the menu is open |
|
||||
|
||||
### MenuItems
|
||||
|
||||
| Prop | Default | Type | Description |
|
||||
| --------- | ------- | --------- | ----------------------------------------------------------------------------------------------- |
|
||||
| `as` | `div` | `string` | The element the `MenuItems` should render as |
|
||||
| `static` | `false` | `boolean` | Whether the element should ignore the internally managed open/closed state |
|
||||
| `unmount` | `true` | `boolean` | Whether the element should be unmounted, instead of just hidden, based on the open/closed state |
|
||||
|
||||
Note that `static` and `unmount` cannot be used together.
|
||||
|
||||
| Slot prop | Type | Description |
|
||||
| --------- | --------- | ------------------------------- |
|
||||
| `open` | `boolean` | Whether or not the menu is open |
|
||||
|
||||
### MenuItem
|
||||
|
||||
| Prop | Default | Type | Description |
|
||||
| ---------- | ------- | --------- | ----------------------------------------------------------------------------- |
|
||||
| `as` | `a` | `string` | The element the `MenuItem` should render as |
|
||||
| `disabled` | `false` | `boolean` | Whether the item should be disabled for keyboard navigation and ARIA purposes |
|
||||
|
||||
| Slot prop | Type | Description |
|
||||
| ---------- | --------- | ---------------------------------------------------------------------- |
|
||||
| `open` | `boolean` | Whether or not the menu is open |
|
||||
| `disabled` | `boolean` | Whether the item is disabled for keyboard navigation and ARIA purposes |
|
||||
428
src/routes/docs/popover.svx
Normal file
428
src/routes/docs/popover.svx
Normal file
@@ -0,0 +1,428 @@
|
||||
# Popover
|
||||
|
||||
<script>
|
||||
import Preview from "./_Preview.svelte";
|
||||
</script>
|
||||
|
||||
<Preview url="examples/popover" code="" class="h-[520px] bg-orange-500"/>
|
||||
|
||||
## Basic example
|
||||
|
||||
Popovers are built using the `Popover`, `PopoverButton`, and `PopverPanel` components.
|
||||
|
||||
Clicking the `PopoverButton` will automatically open/close the `PopoverPanel`. When the panel is open, clicking anywhere outside of its contents, pressing the Escape key, or tabbing away from it will close the `Popover`.
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import {
|
||||
Popover,
|
||||
PopoverButton,
|
||||
PopoverPanel,
|
||||
} from "@rgossiaux/svelte-headlessui";
|
||||
</script>
|
||||
|
||||
<Popover style="position: relative;">
|
||||
<PopoverButton>Solutions</PopoverButton>
|
||||
|
||||
<PopoverPanel style="position: absolute; z-index: 10;">
|
||||
<div class="panel-contents">
|
||||
<a href="/analytics">Analytics</a>
|
||||
<a href="/engagement">Engagement</a>
|
||||
<a href="/security">Security</a>
|
||||
<a href="/integrations">Integrations</a>
|
||||
</div>
|
||||
|
||||
<img src="/solutions.jpg" alt="" />
|
||||
</PopoverPanel>
|
||||
</Popover>
|
||||
|
||||
<style>
|
||||
.panel-contents {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
## Positioning the panel
|
||||
|
||||
To get your popover to actually render a floating panel near your button, you'll need to use some styling technique that relies on CSS, JS, or both. In the previous example, we used CSS absolute and relative positioning so that the panel renders near the button that opened it.
|
||||
|
||||
For more sophisticated approaches, you might use a library like Popper JS. We recommend using the [svelte-popperjs](https://github.com/bryanmylee/svelte-popperjs) wrapper and forwarding the actions to our components:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import {
|
||||
Popover,
|
||||
PopoverButton,
|
||||
PopoverPanel,
|
||||
} from "@rgossiaux/svelte-headlessui";
|
||||
import { createPopperActions } from "svelte-popperjs";
|
||||
|
||||
const [popperRef, popperContent] = createPopperActions();
|
||||
|
||||
// Example Popper configuration
|
||||
const popperOptions = {
|
||||
placement: "bottom-end",
|
||||
strategy: "fixed",
|
||||
modifiers: [{ name: "offset", options: { offset: [0, 10] } }],
|
||||
};
|
||||
</script>
|
||||
|
||||
<Popover>
|
||||
<PopoverButton use={[popperRef]}>Solutions</PopoverButton>
|
||||
|
||||
<PopoverPanel use={[[popperContent, popperOptions]]}>
|
||||
<a href="/analytics">Analytics</a>
|
||||
<a href="/engagement">Engagement</a>
|
||||
<a href="/security">Security</a>
|
||||
<a href="/integrations">Integrations</a>
|
||||
|
||||
<img src="/solutions.jpg" alt="" />
|
||||
</PopoverPanel>
|
||||
</Popover>
|
||||
```
|
||||
|
||||
## Styling
|
||||
|
||||
[See here](general-concepts#component-styling) for some general notes on styling the components in this library.
|
||||
|
||||
## Showing/hiding the popover
|
||||
|
||||
By default, your `PopoverPanel` will be shown/hidden automatically based on the internal open state tracked within the `Popover` component itself.
|
||||
|
||||
If you'd rather handle this yourself (perhaps because you need to add an extra wrapper element for one reason or another), you can add a `static` prop to the `PopoverPanel` component to tell it to always render, and use the `open` slot prop provided by the `Popover` to show or hide the popover yourself.
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import {
|
||||
Popover,
|
||||
PopoverButton,
|
||||
PopoverPanel,
|
||||
} from "@rgossiaux/svelte-headlessui";
|
||||
</script>
|
||||
|
||||
<Popover let:open>
|
||||
<PopoverButton>Solutions</PopoverButton>
|
||||
{#if open}
|
||||
<div>
|
||||
<!-- Using `static`, `PopoverPanel` is always rendered
|
||||
and ignores the `open` state. -->
|
||||
<PopoverPanel static>
|
||||
<!-- ... -->
|
||||
</PopoverPanel>
|
||||
</div>
|
||||
{/if}
|
||||
</Popover>
|
||||
```
|
||||
|
||||
## Closing popovers manually
|
||||
|
||||
Since popovers can contain interactive content like form controls, we can't automatically close them when you click something inside of them like we can with `Menu` components.
|
||||
|
||||
To close a popover manually when clicking a child of its panel, render that child as a `PopoverButton`. You can use the `as` prop to customize which element is being rendered.
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import {
|
||||
Popover,
|
||||
PopoverButton,
|
||||
PopoverPanel,
|
||||
} from "@rgossiaux/svelte-headlessui";
|
||||
</script>
|
||||
|
||||
<Popover>
|
||||
<PopoverButton>Solutions</PopoverButton>
|
||||
<PopoverPanel>
|
||||
<PopoverButton as="a" href="/insights">Insights</PopoverButton>
|
||||
<!-- ... -->
|
||||
</PopoverPanel>
|
||||
</Popover>
|
||||
```
|
||||
|
||||
Alternatively, `Popover` and `PopoverPanel` expose a `close()` slot prop which you can use to imperatively close the panel:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import {
|
||||
Popover,
|
||||
PopoverButton,
|
||||
PopoverPanel,
|
||||
} from "@rgossiaux/svelte-headlessui";
|
||||
</script>
|
||||
|
||||
<Popover>
|
||||
<PopoverButton>Solutions</PopoverButton>
|
||||
<PopoverPanel let:close>
|
||||
<button
|
||||
on:click={async () => {
|
||||
await fetch("/accept-terms", { method: "POST" });
|
||||
close();
|
||||
}}
|
||||
>
|
||||
Read and accept
|
||||
</button>
|
||||
<!-- ... -->
|
||||
</PopoverPanel>
|
||||
</Popover>
|
||||
```
|
||||
|
||||
By default the `PopoverButton` receives focus after calling `close()`, but you can change this by passing an element into `close(el)`.
|
||||
|
||||
## Adding an overlay
|
||||
|
||||
If you'd like to style a backdrop over your application UI whenever you open a `Popover`, use the `PopoverOverlay` component:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import {
|
||||
Popover,
|
||||
PopoverButton,
|
||||
PopoverOverlay,
|
||||
PopoverPanel,
|
||||
} from "@rgossiaux/svelte-headlessui";
|
||||
</script>
|
||||
|
||||
<Popover let:open>
|
||||
<PopoverButton>Solutions</PopoverButton>
|
||||
<PopoverOverlay
|
||||
class={open ? "popover-overlay-open" : "popover-overlay-closed"}
|
||||
/>
|
||||
<PopoverPanel>
|
||||
<!-- ... -->
|
||||
</PopoverPanel>
|
||||
</Popover>
|
||||
|
||||
<style>
|
||||
/* WARNING: This is just for demonstration.
|
||||
Using :global() in this way can be risky. */
|
||||
:global(.popover-overlay-open) {
|
||||
background-color: rgb(0 0 0);
|
||||
opacity: 0.3;
|
||||
position: fixed;
|
||||
top: 0px;
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
/* Your styles here */
|
||||
}
|
||||
|
||||
:global(.popover-overlay-closed) {
|
||||
background-color: rgb(0 0 0);
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
Like all the other components, PopoverOverlay is completely unstyled, so how it appears is up to you. In this example, we put the `PopoverOverlay` before the `PopoverPanel` in the DOM so that it doesn't cover up the panel's contents. Also, since the `PopoverOverlay` is always rendered (even when the panel is closed), we use the `open` slot prop to only make it full-screen when the panel is open.
|
||||
|
||||
## Transitions
|
||||
|
||||
To animate the opening and closing of the popover panel, you can use [this library's Transition component](/docs/transition) or Svelte's built-in transition engine. See that page for a comparison.
|
||||
|
||||
### Using the `Transition` component
|
||||
|
||||
To use the `Transition` component, all you need to do is wrap the `PopoverPanel` in a `<Transition>` and the panel will transition automatically.
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import {
|
||||
Popover,
|
||||
PopoverButton,
|
||||
PopoverPanel,
|
||||
Transition,
|
||||
} from "@rgossiaux/svelte-headlessui";
|
||||
</script>
|
||||
|
||||
<Popover>
|
||||
<PopoverButton>Solutions</PopoverButton>
|
||||
<!-- This example uses Tailwind's transition classes -->
|
||||
<Transition
|
||||
enter="transition duration-100 ease-out"
|
||||
enterFrom="transform scale-95 opacity-0"
|
||||
enterTo="transform scale-100 opacity-100"
|
||||
leave="transition duration-75 ease-out"
|
||||
leaveFrom="transform scale-100 opacity-100"
|
||||
leaveTo="transform scale-95 opacity-0"
|
||||
>
|
||||
<PopoverPanel>
|
||||
<!-- ... -->
|
||||
</PopoverPanel>
|
||||
</Transition>
|
||||
</Popover>
|
||||
```
|
||||
|
||||
### Using Svelte transitions
|
||||
|
||||
If you wish to animate your popovers using another technique (like Svelte's built-in transitions), you can use the `static` prop to tell the component to not manage rendering itself, so you can control it manually in another way.
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import {
|
||||
Popover,
|
||||
PopoverButton,
|
||||
PopoverPanel,
|
||||
} from "@rgossiaux/svelte-headlessui";
|
||||
</script>
|
||||
|
||||
<Popover let:open>
|
||||
<PopoverButton>Solutions</PopoverButton>
|
||||
{#if open}
|
||||
<div transition:fade>
|
||||
<PopoverPanel static>
|
||||
<!-- ... -->
|
||||
</PopoverPanel>
|
||||
</div>
|
||||
{/if}
|
||||
</Popover>
|
||||
```
|
||||
|
||||
Without the `static` prop, the exit transitions won't work correctly.
|
||||
|
||||
## Grouping related popovers
|
||||
|
||||
When rendering several related `Popover`s, for example in a site's header navigation, use the `PopoverGroup` component. This ensures panels stay open while users are tabbing between `Popover`s within a group, but closes any open panel once the user tabs outside of the group:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import {
|
||||
Popover,
|
||||
PopoverButton,
|
||||
PopoverGroup,
|
||||
PopoverPanel,
|
||||
} from "@rgossiaux/svelte-headlessui";
|
||||
</script>
|
||||
|
||||
<PopoverGroup>
|
||||
<Popover>
|
||||
<PopoverButton>Solutions</PopoverButton>
|
||||
<PopoverPanel>
|
||||
<!-- ... -->
|
||||
</PopoverPanel>
|
||||
</Popover>
|
||||
|
||||
<Popover>
|
||||
<PopoverButton>Solutions</PopoverButton>
|
||||
<PopoverPanel>
|
||||
<!-- ... -->
|
||||
</PopoverPanel>
|
||||
</Popover>
|
||||
</PopoverGroup>
|
||||
```
|
||||
|
||||
## When to use a `Popover`
|
||||
|
||||
Here's how `Popover` compares to other components from Headless UI:
|
||||
|
||||
- <a href="menu">Menu</a>: `Popover`s are more general-purpose than `Menu`s.
|
||||
`Menu`s only support very restricted content and have specific accessibility
|
||||
semantics. Arrow keys also navigate a `Menu`'s items, unlike a `Popover`.
|
||||
`Menu`s are best for UI elements that resemble things like the menus you'd
|
||||
find in the title bar of most operating systems. If your floating panel has
|
||||
images or more markup than simple links, use a Popover.
|
||||
|
||||
- <a href="disclosure">Disclosure</a>: `Disclosure`s are useful for things that
|
||||
typically reflow the document, like an accordion. `Popover`s also have extra
|
||||
behavior on top of `Disclosure`s: they can render overlays, and are closed
|
||||
when the user either clicks the overlay (by clicking outside of the
|
||||
`Popover`'s content) or presses the Escape key. If your UI element needs this
|
||||
behavior, use a `Popover` instead of a `Disclosure`.
|
||||
|
||||
- <a href="dialog">Dialog</a>: `Dialog`s are meant to grab the user's full
|
||||
attention. They typically render a floating panel in the center of the screen,
|
||||
and use a backdrop to dim the rest of the application's contents. They also
|
||||
capture focus and prevent tabbing away from the `Dialog`'s contents until the
|
||||
`Dialog` is dismissed. Popovers are more contextual, and are usually
|
||||
positioned near the element that triggered them.
|
||||
|
||||
## Accessibility notes
|
||||
|
||||
### Focus management
|
||||
|
||||
Pressing Tab on an open panel will focus the first focusable element within the panel's contents. If a `PopoverGroup` is being used, Tab cycles from the end of an open panel's content to the next `Popover`'s button.
|
||||
|
||||
### Mouse interaction
|
||||
|
||||
Clicking a `PopoverButton` toggles a panel open and closed. Clicking anywhere outside of an open panel will close that panel.
|
||||
|
||||
### Keyboard interaction
|
||||
|
||||
| Command | Description |
|
||||
| ------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `<Enter>` / `<Space>` when a `PopoverButton` is focused | Toggles panel |
|
||||
| `<Esc>` | Closes any open popovers |
|
||||
| `<Tab>` | Cycles through an open panel's contents. Tabbing out of an open panel will close that panel, and tabbing from one open panel to a sibling popover's button (within a `PopoverGroup`) closes the first panel |
|
||||
| `<Shift>` + `<Tab>` | Cycles backwards through an open panel's contents |
|
||||
|
||||
### Other
|
||||
|
||||
Nested `Popover`s are supported, and all panels will close correctly whenever the root panel is closed.
|
||||
|
||||
All relevant ARIA attributes are automatically managed.
|
||||
|
||||
## Component API
|
||||
|
||||
### Popover
|
||||
|
||||
The main popover component.
|
||||
|
||||
| Prop | Default | Type | Description |
|
||||
| ---- | ------- | -------- | ------------------------------------------ |
|
||||
| `as` | `div` | `string` | The element the `Popover` should render as |
|
||||
|
||||
| Slot prop | Type | Description |
|
||||
| --------- | ---------------------------- | ----------------------------------------------------------------------------- |
|
||||
| `open` | `boolean` | Whether the popover is open |
|
||||
| `close` | `(el?: HTMLElement) => void` | Closes the popover and focuses `el`, if passed, or the `PopoverButton` if not |
|
||||
|
||||
### PopoverOverlay
|
||||
|
||||
This can be used to create an overlay for your popover. Clicking on the overlay will close the popover.
|
||||
|
||||
| Prop | Default | Type | Description |
|
||||
| ---- | ------- | -------- | ------------------------------------------------- |
|
||||
| `as` | `div` | `string` | The element the `PopoverOverlay` should render as |
|
||||
|
||||
| Slot prop | Type | Description |
|
||||
| --------- | --------- | ---------------------------------- |
|
||||
| `open` | `boolean` | Whether or not the popover is open |
|
||||
|
||||
### PopoverButton
|
||||
|
||||
This is the trigger button to toggle a popover.
|
||||
|
||||
You can also use this `PopoverButton` component inside a `PopoverPanel`. If you do, it will behave as a close button and have the appropriate `aria-*` attributes.
|
||||
|
||||
| Prop | Default | Type | Description |
|
||||
| ---- | -------- | -------- | ------------------------------------------------ |
|
||||
| `as` | `button` | `string` | The element the `PopoverButton` should render as |
|
||||
|
||||
| Slot prop | Type | Description |
|
||||
| --------- | --------- | ---------------------------------- |
|
||||
| `open` | `boolean` | Whether or not the popover is open |
|
||||
|
||||
### PopoverPanel
|
||||
|
||||
This component contains the contents of your popover.
|
||||
|
||||
| Prop | Default | Type | Description |
|
||||
| --------- | ------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `as` | `div` | `string` | The element the `PopoverPanel` should render as |
|
||||
| `focus` | `false` | `boolean` | Whether the `PopoverPanel` should trap focus. If `true`, focus will move inside the `PopoverPanel` when it is opened, and if focus leaves the `PopoverPanel` it will close. |
|
||||
| `static` | `false` | `boolean` | Whether the element should ignore the internally managed open/closed state |
|
||||
| `unmount` | `true` | `boolean` | Whether the element should be unmounted, instead of just hidden, based on the open/closed state |
|
||||
|
||||
Note that `static` and `unmount` cannot be used together.
|
||||
|
||||
| Slot prop | Type | Description |
|
||||
| --------- | ---------------------------- | ----------------------------------------------------------------------------- |
|
||||
| `open` | `boolean` | Whether or not the popover is open |
|
||||
| `close` | `(el?: HTMLElement) => void` | Closes the popover and focuses `el`, if passed, or the `PopoverButton` if not |
|
||||
|
||||
### PopoverGroup
|
||||
|
||||
This component links related `Popover`s. Tabbing out of one `PopoverPanel` will focus the next popover's `PopverButton`, and tabbing outside of the `PopoverGroup` completely will close all popovers inside the group.
|
||||
|
||||
| Prop | Default | Type | Description |
|
||||
| ---- | ------- | -------- | ----------------------------------------------- |
|
||||
| `as` | `div` | `string` | The element the `PopoverGroup` should render as |
|
||||
240
src/routes/docs/radio-group.svx
Normal file
240
src/routes/docs/radio-group.svx
Normal file
@@ -0,0 +1,240 @@
|
||||
# Radio Group
|
||||
|
||||
<script>
|
||||
import Preview from "./_Preview.svelte";
|
||||
</script>
|
||||
|
||||
<Preview url="examples/radio-group" code="" class="h-[380px] bg-cyan-300"/>
|
||||
|
||||
## Basic example
|
||||
|
||||
Radio Groups are built using a combination of the `RadioGroup`, `RadioGroupOption`, `RadioGroupLabel`, and `RadioGroupDescription` components:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import {
|
||||
RadioGroup,
|
||||
RadioGroupLabel,
|
||||
RadioGroupOption,
|
||||
RadioGroupDescription,
|
||||
} from "@rgossiaux/svelte-headlessui";
|
||||
|
||||
let plan = "startup";
|
||||
</script>
|
||||
|
||||
<RadioGroup value={plan} on:change={(e) => (plan = e.detail)}>
|
||||
<RadioGroupLabel>Plan</RadioGroupLabel>
|
||||
<RadioGroupOption value="startup" let:checked>
|
||||
<span class:checked>Startup</span>
|
||||
</RadioGroupOption>
|
||||
<RadioGroupOption value="business" let:checked>
|
||||
<span class:checked>Business</span>
|
||||
</RadioGroupOption>
|
||||
<RadioGroupOption value="enterprise" let:checked>
|
||||
<span class:checked>Enterprise</span>
|
||||
</RadioGroupOption>
|
||||
</RadioGroup>
|
||||
|
||||
<style>
|
||||
.checked {
|
||||
/* Your styles here */
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
## Styling
|
||||
|
||||
[See here](general-concepts#component-styling) for some general notes on styling the components in this library.
|
||||
|
||||
### Checked option
|
||||
|
||||
To style the selected `RadioGroupOption`, you can use the `checked` slot prop that it provides, which tells you whether or not that option is the currently selected one. You can use this state to conditionally apply whatever styles you wish.
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import {
|
||||
RadioGroup,
|
||||
RadioGroupLabel,
|
||||
RadioGroupOption,
|
||||
} from "@rgossiaux/svelte-headlessui";
|
||||
|
||||
let plan = "startup";
|
||||
</script>
|
||||
|
||||
<RadioGroup value={plan} on:change={(e) => (plan = e.detail)}>
|
||||
<RadioGroupLabel>Plan</RadioGroupLabel>
|
||||
<!-- Use the `checked` slot prop to conditionally style the checked item -->
|
||||
<RadioGroupOption value="startup" let:checked>
|
||||
<span class:checked>Startup</span>
|
||||
</RadioGroupOption>
|
||||
<!-- ... -->
|
||||
</RadioGroup>
|
||||
|
||||
<style>
|
||||
.checked {
|
||||
/* Your styles here */
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
### Active options
|
||||
|
||||
`RadioGroupOption`s can also be in an `active` state if they are focused with either the mouse or the keyboard. It's a good idea to add styles specifically for this state.
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import {
|
||||
RadioGroup,
|
||||
RadioGroupLabel,
|
||||
RadioGroupOption,
|
||||
} from "@rgossiaux/svelte-headlessui";
|
||||
|
||||
let plan = "startup";
|
||||
</script>
|
||||
|
||||
<RadioGroup value={plan} on:change={(e) => (plan = e.detail)}>
|
||||
<RadioGroupLabel>Plan</RadioGroupLabel>
|
||||
<!-- Use both `active` and `checked` slot props
|
||||
to differentiate between the active and checked states -->
|
||||
<RadioGroupOption value="startup" let:active let:checked>
|
||||
<span class:active class:checked>Startup</span>
|
||||
</RadioGroupOption>
|
||||
<!-- ... -->
|
||||
</RadioGroup>
|
||||
|
||||
<style>
|
||||
.active {
|
||||
/* Your styles here */
|
||||
}
|
||||
|
||||
.checked {
|
||||
/* Your styles here */
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
## Using the Label and Description components
|
||||
|
||||
You can use the `RadioGroupLabel` and `RadioGroupDescription` components to label and describe each `RadioGroupOption`, as well as the `RadioGroup` itself. These components will automatically link with their relevant ancestor components via the `aria-labelledby` and `aria-describedby` attributes, improving the semantics and accessibility of your component.
|
||||
|
||||
By default, `RadioGroupLabel` renders a `<label>` element and `RadioGroupDescription` renders a `<p>`. These can be customized using the `as` prop, as described in the API docs below.
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import {
|
||||
RadioGroup,
|
||||
RadioGroupLabel,
|
||||
RadioGroupOption,
|
||||
RadioGroupDescription,
|
||||
} from "@rgossiaux/svelte-headlessui";
|
||||
|
||||
let plan = "startup";
|
||||
</script>
|
||||
|
||||
<RadioGroup value={plan} on:change={(e) => plan = e.detail}>
|
||||
<!-- This Label is for the `RadioGroup` -->
|
||||
<RadioGroupLabel>Plan</RadioGroupLabel>
|
||||
|
||||
<RadioGroupOption value="startup" let:checked>
|
||||
<RadioGroupLabel as="span" class:checked>Startup</span>
|
||||
<RadioGroupDescription as="span" class:checked>
|
||||
Up to 5 active job postings
|
||||
</RadioGroupDescription>
|
||||
</RadioGroupOption>
|
||||
<!-- ... -->
|
||||
</RadioGroup>
|
||||
|
||||
<style>
|
||||
/* WARNING: This is just for demonstration.
|
||||
Using :global() in this way can be risky. */
|
||||
:global(.checked) {
|
||||
/* Your styles here */
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
## Accessibility notes
|
||||
|
||||
### Mouse interaction
|
||||
|
||||
Clicking a `RadioGroupOption` will select it.
|
||||
|
||||
### Keyboard interaction
|
||||
|
||||
| Command | Description |
|
||||
| ----------------------------------------------------------- | ------------------------------------------------------------------ |
|
||||
| `<ArrowDown>` / `<ArrowRight>` when `RadioGroup` is focused | Focuses and checks the next `RadioGroupOption` |
|
||||
| `<ArrowUp>` / `<ArrowLeft>` when `RadioGroup` is focused | Focuses and checks the previous `RadioGroupOption` |
|
||||
| `<Space>` when `RadioGroup` is focused | Checks the current `RadioGroupOption` if it is not already checked |
|
||||
|
||||
### Other
|
||||
|
||||
All relevant ARIA attributes are automatically managed.
|
||||
|
||||
For a full reference on all accessibility features implemented in `RadioGroup`, see <a href="https://www.w3.org/TR/wai-aria-practices-1.2/#radiobutton">the ARIA spec on Radio Groups</a>.
|
||||
|
||||
## Component API
|
||||
|
||||
### RadioGroup
|
||||
|
||||
The main Radio Group component.
|
||||
|
||||
| Prop | Default | Type | Description |
|
||||
| ---------- | ------- | ------------------ | ------------------------------------------------------------------------ |
|
||||
| `as` | `div` | `string` | The element the `RadioGroup` should render as |
|
||||
| `disabled` | `false` | `boolean` | Whether the `RadioGroup` and all of its `RadioGroupOption`s are disabled |
|
||||
| `value` | -- | `T` \| `undefined` | The currently selected value in the `RadioGroup` |
|
||||
|
||||
This component also dispatches a custom event, which is listened to using the Svelte `on:` directive:
|
||||
|
||||
| Event name | Type of event `.detail` | Description |
|
||||
| ---------- | ----------------------- | ---------------------------------------------------------------------------------------------------------------- |
|
||||
| `change` | `T` | Dispatched when a `RadioGroupOption` is selected; the event `detail` contains the `value` of the selected option |
|
||||
|
||||
### RadioGroupOption
|
||||
|
||||
The wrapper component for each selectable option.
|
||||
|
||||
| Prop | Default | Type | Description |
|
||||
| ---------- | ------- | ------------------ | ----------------------------------------------------------------------------------------------------------- |
|
||||
| `as` | `div` | `string` | The element the `RadioGroupOption` should render as |
|
||||
| `disabled` | `false` | `boolean` | Whether the `RadioGroupOption` is disabled |
|
||||
| `value` | -- | `T` \| `undefined` | The value of the `RadioGroupOption`; the type should match the type of the `value` prop in the `RadioGroup` |
|
||||
|
||||
| Slot prop | Type | Description |
|
||||
| ---------- | --------- | ---------------------------------------------------------- |
|
||||
| `active` | `boolean` | Whether the option is active (using the mouse or keyboard) |
|
||||
| `checked` | `boolean` | Whether the option is the checked option |
|
||||
| `disabled` | `boolean` | Whether the option is disabled |
|
||||
|
||||
### RadioGroupLabel
|
||||
|
||||
Renders an element that is linked to its nearest `RadioGroup` or `RadioGroupOption` ancestor component via the `aria-labelledby` attribute and an autogenerated id.
|
||||
|
||||
| Prop | Default | Type | Description |
|
||||
| ---- | ------- | -------- | -------------------------------------------------- |
|
||||
| `as` | `label` | `string` | The element the `RadioGroupLabel` should render as |
|
||||
|
||||
If the `RadioGroupLabel` is labeling a `RadioGroupOption` (instead of the `RadioGroup`), it will also have these slot props available:
|
||||
|
||||
| Slot prop | Type | Description |
|
||||
| ---------- | --------- | ------------------------------------------------------------------------ |
|
||||
| `active` | `boolean` | Whether the corresponding option is active (using the mouse or keyboard) |
|
||||
| `checked` | `boolean` | Whether the corresponding option is the checked option |
|
||||
| `disabled` | `boolean` | Whether the corresponding option is disabled |
|
||||
|
||||
### RadioGroupDescription
|
||||
|
||||
Renders an element that is linked to its nearest `RadioGroup` or `RadioGroupOption` ancestor component via the `aria-describedby` attribute and an autogenerated id.
|
||||
|
||||
| Prop | Default | Type | Description |
|
||||
| ---- | ------- | -------- | -------------------------------------------------------- |
|
||||
| `as` | `a` | `string` | The element the `RadioGroupDescription` should render as |
|
||||
|
||||
If the `RadioGroupDescription` is describing a `RadioGroupOption` (instead of the `RadioGroup`), it will also have these slot props available:
|
||||
|
||||
| Slot prop | Type | Description |
|
||||
| ---------- | --------- | ------------------------------------------------------------------------ |
|
||||
| `active` | `boolean` | Whether the corresponding option is active (using the mouse or keyboard) |
|
||||
| `checked` | `boolean` | Whether the corresponding option is the checked option |
|
||||
| `disabled` | `boolean` | Whether the corresponding option is disabled |
|
||||
318
src/routes/docs/switch.svx
Normal file
318
src/routes/docs/switch.svx
Normal file
@@ -0,0 +1,318 @@
|
||||
# Switch
|
||||
|
||||
<script>
|
||||
import Preview from "./_Preview.svelte";
|
||||
</script>
|
||||
|
||||
<Preview url="examples/switch" code="" class="h-[180px] bg-teal-300"/>
|
||||
|
||||
## Basic example
|
||||
|
||||
Switches are built using the `Switch` component. You can toggle your switch by clicking directly on the component or by pressing the spacebar while it's focused.
|
||||
|
||||
Toggling the switch fires the `change` event with a negated version of the `checked` value.
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import { Switch } from "@rgossiaux/svelte-headlessui";
|
||||
|
||||
let enabled = false;
|
||||
</script>
|
||||
|
||||
<Switch
|
||||
checked={enabled}
|
||||
on:change={(e) => (enabled = e.detail)}
|
||||
class={enabled ? "switch switch-enabled" : "switch switch-disabled"}
|
||||
>
|
||||
<span class="sr-only">Enable notifications</span>
|
||||
<span class="toggle" class:toggle-on={enabled} class:toggle-off={!enabled} />
|
||||
</Switch>
|
||||
|
||||
<style>
|
||||
:global(.switch) {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
border-radius: 9999px;
|
||||
height: 1.5rem;
|
||||
width: 2.75rem;
|
||||
}
|
||||
|
||||
:global(.switch-enabled) {
|
||||
/* Blue */
|
||||
background-color: rgb(37 99 235);
|
||||
}
|
||||
|
||||
:global(.switch-disabled) {
|
||||
/* Gray */
|
||||
background-color: rgb(229 231 235);
|
||||
}
|
||||
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
.toggle {
|
||||
display: inline-block;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
background-color: rgb(255 255 255);
|
||||
border-radius: 9999px;
|
||||
}
|
||||
|
||||
.toggle-on {
|
||||
transform: translateX(1.1rem);
|
||||
}
|
||||
|
||||
.toggle-off {
|
||||
transform: translateX(-0.25rem);
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
## Styling
|
||||
|
||||
[See here](general-concepts#component-styling) for some general notes on styling the components in this library.
|
||||
|
||||
## Using a custom label
|
||||
|
||||
By default, the `Switch` renders a `<button>` as well as whatever children you pass into it. This can make it harder to implement certain UIs, since the children will be nested within the button.
|
||||
|
||||
In these situations, you can use the `SwitchGroup` component for more flexibility.
|
||||
|
||||
This example demonstrates how to use the `SwitchGroup`, `Switch`, and `SwitchLabel` components to render a label as a sibling to a button. Note that `SwitchLabel` is used alongside a `Switch`, and both must be rendered within a parent `SwitchGroup`.
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import {
|
||||
Switch,
|
||||
SwitchLabel,
|
||||
SwitchGroup,
|
||||
} from "@rgossiaux/svelte-headlessui";
|
||||
|
||||
let enabled = false;
|
||||
</script>
|
||||
|
||||
<SwitchGroup>
|
||||
<div class="switch-container">
|
||||
<SwitchLabel class="switch-label">Enable notifications</SwitchLabel>
|
||||
<Switch
|
||||
checked={enabled}
|
||||
on:change={(e) => (enabled = e.detail)}
|
||||
class={enabled ? "switch switch-enabled" : "switch switch-disabled"}
|
||||
>
|
||||
<span class="sr-only">Enable notifications</span>
|
||||
<span
|
||||
class="toggle"
|
||||
class:toggle-on={enabled}
|
||||
class:toggle-off={!enabled}
|
||||
/>
|
||||
</Switch>
|
||||
</div>
|
||||
</SwitchGroup>
|
||||
|
||||
<style>
|
||||
.switch-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
:global(.switch-label) {
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
:global(.switch) {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
border-radius: 9999px;
|
||||
height: 1.5rem;
|
||||
width: 2.75rem;
|
||||
transition-property: color, background-color, border-color,
|
||||
text-decoration-color, fill, stroke;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-duration: 150ms;
|
||||
}
|
||||
|
||||
:global(.switch:focus) {
|
||||
outline: 2px solid transparent;
|
||||
outline-offset: 2px;
|
||||
box-shadow: 0 0 0 2px rgb(99 102 241);
|
||||
}
|
||||
|
||||
:global(.switch-enabled) {
|
||||
/* Blue */
|
||||
background-color: rgb(37 99 235);
|
||||
}
|
||||
|
||||
:global(.switch-disabled) {
|
||||
/* Gray */
|
||||
background-color: rgb(229 231 235);
|
||||
}
|
||||
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
.toggle {
|
||||
display: inline-block;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
background-color: rgb(255 255 255);
|
||||
border-radius: 9999px;
|
||||
transition-property: transform;
|
||||
}
|
||||
|
||||
.toggle-on {
|
||||
transform: translateX(1.1rem);
|
||||
}
|
||||
|
||||
.toggle-off {
|
||||
transform: translateX(-0.25rem);
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
By default, clicking a `SwitchLabel` will toggle the switch, just like labels in native HTML checkboxes do. If you'd like to make the label non-clickable (which you might if it doesn't make sense for your design), you can add a `passive` prop to the `SwitchLabel` component:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import {
|
||||
Switch,
|
||||
SwitchLabel,
|
||||
SwitchGroup,
|
||||
} from "@rgossiaux/svelte-headlessui";
|
||||
|
||||
let enabled = false;
|
||||
</script>
|
||||
|
||||
<SwitchGroup>
|
||||
<SwitchLabel passive>Enable notifications</SwitchLabel>
|
||||
<Switch checked={enabled} on:change={(e) => (enabled = e.detail)}>
|
||||
<!-- ... -->
|
||||
</Switch>
|
||||
</SwitchGroup>
|
||||
```
|
||||
|
||||
## Transitions
|
||||
|
||||
Because switches are always rendered to the DOM (rather than being mounted/unmounted like other components in the library), there's generally no need to use the provided `Transition` component. You can just use CSS transitions or Svelte's transition directives:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import {
|
||||
Switch,
|
||||
SwitchLabel,
|
||||
SwitchGroup,
|
||||
} from "@rgossiaux/svelte-headlessui";
|
||||
|
||||
let enabled = false;
|
||||
</script>
|
||||
|
||||
<Switch checked={enabled} on:change={(e) => (enabled = e.detail)}>
|
||||
<span class="toggle" class:enabled>
|
||||
<!-- ... -->
|
||||
</span></Switch
|
||||
>
|
||||
|
||||
<style>
|
||||
.toggle {
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-duration: 150ms;
|
||||
transition-property: transform;
|
||||
}
|
||||
|
||||
.enabled {
|
||||
transform: translateX(1rem);
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
## Accessibility notes
|
||||
|
||||
### Labels
|
||||
|
||||
By default, the children of a `Switch` will be used as the label for screen readers. If you're using `SwitchLabel`, the content of your `Switch` component will be ignored by assistive technologies.
|
||||
|
||||
### Mouse interaction
|
||||
|
||||
Clicking a `Switch` or a `SwitchLabel` toggles the switch on and off.
|
||||
|
||||
### Keyboard interaction
|
||||
|
||||
When the `horizontal` prop is set, the `<ArrowUp>` and `<ArrowDown>` below become `<ArrowLeft>` and `<ArrowRight>`:
|
||||
|
||||
| Command | Description |
|
||||
| ------------------------------------ | ------------------ |
|
||||
| `<Space>` when a `Switch` is focused | Toggles the switch |
|
||||
|
||||
### Other
|
||||
|
||||
All relevant ARIA attributes are automatically managed.
|
||||
|
||||
For a full reference on all accessibility features implemented in `Switch`, see <a href="https://www.w3.org/TR/wai-aria-practices-1.2/#switch">the ARIA spec on Switch</a>.
|
||||
|
||||
## Component API
|
||||
|
||||
### Switch
|
||||
|
||||
The main switch component.
|
||||
|
||||
| Prop | Default | Type | Description |
|
||||
| --------- | -------- | --------- | ----------------------------------------- |
|
||||
| `as` | `button` | `string` | The element the `Switch` should render as |
|
||||
| `checked` | `false` | `boolean` | Whether the switch is checked |
|
||||
|
||||
| Slot prop | Type | Description |
|
||||
| --------- | --------- | ----------------------------- |
|
||||
| `checked` | `boolean` | Whether the switch is checked |
|
||||
|
||||
This component also dispatches a custom event, which is listened to using the Svelte `on:` directive:
|
||||
|
||||
| Event name | Type of event `.detail` | Description |
|
||||
| ---------- | ----------------------- | ---------------------------------------------------------------------------------------------- |
|
||||
| `change` | `T` | Dispatched when a `Switch` is toggled; the event `detail` contains the new value of the switch |
|
||||
|
||||
### SwitchLabel
|
||||
|
||||
A label that can be used for more control over the text your switch will announce to screenreaders. Renders an element that is linked to the `Switch` via the `aria-labelledby` attribute and an autogenerated id.
|
||||
|
||||
| Prop | Default | Type | Description |
|
||||
| ---- | ------- | -------- | ---------------------------------------------- |
|
||||
| `as` | `label` | `string` | The element the `SwitchLabel` should render as |
|
||||
|
||||
### SwitchDescription
|
||||
|
||||
This is the description for your switch. When this is used, it will set the `aria-describedby` on the switch.
|
||||
|
||||
| Prop | Default | Type | Description |
|
||||
| ---- | ------- | -------- | ---------------------------------------------------- |
|
||||
| `as` | `p` | `string` | The element the `SwitchDescription` should render as |
|
||||
|
||||
| Slot prop | Type | Description |
|
||||
| --------- | --------- | --------------------------------- |
|
||||
| `open` | `boolean` | Whether or not the switch is open |
|
||||
|
||||
### SwitchGroup
|
||||
|
||||
Used to wrap a `Switch` together with a `SwitchLabel` and/or `SwitchDescription`
|
||||
|
||||
| Prop | Default | Type | Description |
|
||||
| ---- | ------- | -------- | ---------------------------------------------- |
|
||||
| `as` | `div` | `string` | The element the `SwitchGroup` should render as |
|
||||
337
src/routes/docs/tabs.svx
Normal file
337
src/routes/docs/tabs.svx
Normal file
@@ -0,0 +1,337 @@
|
||||
# Tabs
|
||||
|
||||
<script>
|
||||
import Preview from "./_Preview.svelte";
|
||||
</script>
|
||||
|
||||
<Preview url="examples/tabs" code="" class="h-[340px] bg-blue-300"/>
|
||||
|
||||
## Basic example
|
||||
|
||||
Tabs are built using the `TabGroup`, `TabList`, `Tab`, `TabPanels`, and `TabPanel` components.
|
||||
|
||||
By default the first tab is selected, and clicking on any tab or selecting it with the keyboard will activate the corresponding panel.
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import {
|
||||
Tab,
|
||||
TabGroup,
|
||||
TabList,
|
||||
TabPanel,
|
||||
TabPanels,
|
||||
} from "@rgossiaux/svelte-headlessui";
|
||||
</script>
|
||||
|
||||
<TabGroup>
|
||||
<TabList>
|
||||
<Tab>Tab 1</Tab>
|
||||
<Tab>Tab 2</Tab>
|
||||
<Tab>Tab 3</Tab>
|
||||
</TabList>
|
||||
<TabPanels>
|
||||
<TabPanel>Content 1</TabPanel>
|
||||
<TabPanel>Content 2</TabPanel>
|
||||
<TabPanel>Content 3</TabPanel>
|
||||
</TabPanels>
|
||||
</TabGroup>
|
||||
```
|
||||
|
||||
## Styling
|
||||
|
||||
[See here](general-concepts#component-styling) for some general notes on styling the components in this library.
|
||||
|
||||
### The selected tab
|
||||
|
||||
To style the active `Tab`, you can use the `selected` slot prop that it provides, which tells you whether or not that tab is currently selected. You can also access `selected` as an argument to a function passed to `class` ([see here](general-concepts#styling-the-components-themselves-class-and-style)). You can use this state to conditionally apply whatever styles you wish.
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import {
|
||||
Tab,
|
||||
TabGroup,
|
||||
TabList,
|
||||
TabPanel,
|
||||
TabPanels,
|
||||
} from "@rgossiaux/svelte-headlessui";
|
||||
</script>
|
||||
|
||||
<TabGroup>
|
||||
<TabList>
|
||||
<Tab class={({selected}) => "tab-selected" : "tab-unselected"}>Tab 1</Tab>
|
||||
<!-- ... -->
|
||||
</TabList>
|
||||
<TabPanels>
|
||||
<TabPanel>Content 1</TabPanel>
|
||||
<!-- ... -->
|
||||
</TabPanels>
|
||||
</TabGroup>
|
||||
|
||||
<style>
|
||||
/* WARNING: This is just for demonstration.
|
||||
Using :global() in this way can be risky. */
|
||||
:global(.tab-selected) {
|
||||
background-color: rgb(59 130 246);
|
||||
color: rgb(255 255 255);
|
||||
}
|
||||
|
||||
:global(.tab-unselected) {
|
||||
background-color: rgb(255 255 255);
|
||||
color: rgb(0 0 0);
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
## Disabling a tab
|
||||
|
||||
To disable a tab, use the `disabled` prop on the `Tab` component. Disabled tabs cannot be selected with the mouse, and are also skipped when navigating the tab list using the keyboard.
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import {
|
||||
Tab,
|
||||
TabGroup,
|
||||
TabList,
|
||||
TabPanel,
|
||||
TabPanels,
|
||||
} from "@rgossiaux/svelte-headlessui";
|
||||
</script>
|
||||
|
||||
<TabGroup>
|
||||
<TabList>
|
||||
<Tab>Tab 1</Tab>
|
||||
<Tab disabled>Tab 2</Tab>
|
||||
<Tab>Tab 3</Tab>
|
||||
</TabList>
|
||||
<TabPanels>
|
||||
<TabPanel>Content 1</TabPanel>
|
||||
<TabPanel>Content 2</TabPanel>
|
||||
<TabPanel>Content 3</TabPanel>
|
||||
</TabPanels>
|
||||
</TabGroup>
|
||||
```
|
||||
|
||||
## Manually activating tabs
|
||||
|
||||
By default, tabs are automatically selected as the user navigates through them using the arrow keys.
|
||||
|
||||
If you'd rather not change the current tab until the user presses Enter or Space, use the `manual` prop on the `TabGroup` component. This can be helpful if selecting a tab performs an expensive operation and you don't want to run it unnecessarily.
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import {
|
||||
Tab,
|
||||
TabGroup,
|
||||
TabList,
|
||||
TabPanel,
|
||||
TabPanels,
|
||||
} from "@rgossiaux/svelte-headlessui";
|
||||
</script>
|
||||
|
||||
<TabGroup manual>
|
||||
<TabList>
|
||||
<Tab>Tab 1</Tab>
|
||||
<Tab>Tab 2</Tab>
|
||||
<Tab>Tab 3</Tab>
|
||||
</TabList>
|
||||
<TabPanels>
|
||||
<TabPanel>Content 1</TabPanel>
|
||||
<TabPanel>Content 2</TabPanel>
|
||||
<TabPanel>Content 3</TabPanel>
|
||||
</TabPanels>
|
||||
</TabGroup>
|
||||
```
|
||||
|
||||
The `manual` prop has no impact on mouse interactions; tabs will still be selected as soon as they are clicked.
|
||||
|
||||
## Vertical tabs
|
||||
|
||||
If you've styled your `TabList` to appear vertically, use the `vertical` prop to enable navigating with the up and down arrow keys instead of with the left and right arrows, and to update the `aria-orientation` attribute for assistive technologies.
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import {
|
||||
Tab,
|
||||
TabGroup,
|
||||
TabList,
|
||||
TabPanel,
|
||||
TabPanels,
|
||||
} from "@rgossiaux/svelte-headlessui";
|
||||
</script>
|
||||
|
||||
<TabGroup vertical>
|
||||
<TabList>
|
||||
<Tab>Tab 1</Tab>
|
||||
<Tab>Tab 2</Tab>
|
||||
<Tab>Tab 3</Tab>
|
||||
</TabList>
|
||||
<TabPanels>
|
||||
<TabPanel>Content 1</TabPanel>
|
||||
<TabPanel>Content 2</TabPanel>
|
||||
<TabPanel>Content 3</TabPanel>
|
||||
</TabPanels>
|
||||
</TabGroup>
|
||||
```
|
||||
|
||||
## Specifying the default tab
|
||||
|
||||
To change which tab is selected by default, use the `defaultIndex={number}` prop on the `TabGroup` component.
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import {
|
||||
Tab,
|
||||
TabGroup,
|
||||
TabList,
|
||||
TabPanel,
|
||||
TabPanels,
|
||||
} from "@rgossiaux/svelte-headlessui";
|
||||
</script>
|
||||
|
||||
<!-- Note that defaultIndex is 0-based -->
|
||||
<TabGroup defaultIndex={1}>
|
||||
<TabList>
|
||||
<Tab>Tab 1</Tab>
|
||||
|
||||
<!-- Selects this tab by default -->
|
||||
<Tab>Tab 2</Tab>
|
||||
|
||||
<Tab>Tab 3</Tab>
|
||||
</TabList>
|
||||
<TabPanels>
|
||||
<TabPanel>Content 1</TabPanel>
|
||||
|
||||
<!-- Displays this panel by default -->
|
||||
<TabPanel>Content 2</TabPanel>
|
||||
|
||||
<TabPanel>Content 3</TabPanel>
|
||||
</TabPanels>
|
||||
</TabGroup>
|
||||
```
|
||||
|
||||
If you happen to provide an index that is out of bounds, then the last non-disabled tab will be selected on initial render. For example, `<TabGroup defaultIndex={5}>` in the above example would select Tab 3.
|
||||
|
||||
## Listening for changes
|
||||
|
||||
To run a function whenever the selected tab changes, use the `change` event on the `TabGroup` component.
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import {
|
||||
Tab,
|
||||
TabGroup,
|
||||
TabList,
|
||||
TabPanel,
|
||||
TabPanels,
|
||||
} from "@rgossiaux/svelte-headlessui";
|
||||
</script>
|
||||
|
||||
<TabGroup on:change={(e) => console.log("Changed selected tab to:", e.detail)}>
|
||||
<TabList>
|
||||
<Tab>Tab 1</Tab>
|
||||
<Tab>Tab 2</Tab>
|
||||
<Tab>Tab 3</Tab>
|
||||
</TabList>
|
||||
<TabPanels>
|
||||
<TabPanel>Content 1</TabPanel>
|
||||
<TabPanel>Content 2</TabPanel>
|
||||
<TabPanel>Content 3</TabPanel>
|
||||
</TabPanels>
|
||||
</TabGroup>
|
||||
```
|
||||
|
||||
## Accessibility notes
|
||||
|
||||
### Mouse interaction
|
||||
|
||||
Clicking a `Tab` will select that tab and display the corresponding `TabPanel`.
|
||||
|
||||
### Keyboard interaction
|
||||
|
||||
All interactions apply when a `Tab` component is focused.
|
||||
|
||||
| Command | Description |
|
||||
| -------------------------------------------------- | ------------------------------------------ |
|
||||
| `<ArrowLeft>` / `<ArrowRight>` | Selects the previous/next non-disabled tab |
|
||||
| `<ArrowUp>` / `<ArrowDown>` when `vertical` is set | Selects the previous/next non-disabled tab |
|
||||
| `<Home>` / `<PageUp>` | Selects the first non-disabled tab |
|
||||
| `<End>` / `<PageEnd>` | Selects the last non-disabled tab |
|
||||
| `<Enter>` / `<Space>` when `manual` is set | Activates the selected tab |
|
||||
|
||||
### Other
|
||||
|
||||
All relevant ARIA attributes are automatically managed.
|
||||
|
||||
For a full reference on all accessibility features implemented in `Tab`, see <a href="https://www.w3.org/TR/wai-aria-practices-1.2/#tabpanel">the ARIA spec on Tabs</a>.
|
||||
|
||||
## Component API
|
||||
|
||||
### TabGroup
|
||||
|
||||
The main tab group component.
|
||||
|
||||
| Prop | Default | Type | Description |
|
||||
| -------------- | ------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `as` | `div` | `string` | The element the `TabGroup` should render as |
|
||||
| `defaultIndex` | `0` | `number` | The index of the default selected tab |
|
||||
| `vertical` | `false` | `boolean` | Whether the orientation of the `TabList` is vertical instead of horizontal |
|
||||
| `manual` | `false` | `boolean` | Whether, in keyboard navigation, the `Enter` or `Space` key is necessary to change tabs. By default, the arrow keys will change tabs automatically without hitting `Enter`/`Space`. This has no impact on mouse behavior |
|
||||
|
||||
| Slot prop | Type | Description |
|
||||
| --------------- | -------- | ---------------------------- |
|
||||
| `selectedIndex` | `number` | The currently selected index |
|
||||
|
||||
This component also dispatches a custom event, which is listened to using the Svelte `on:` directive:
|
||||
|
||||
| Event name | Type of event `.detail` | Description |
|
||||
| ---------- | ----------------------- | --------------------------------------- |
|
||||
| `change` | `number` | Emitted whenever the active tab changes |
|
||||
|
||||
### TabList
|
||||
|
||||
The container for `Tab` components. The order of the `Tab` components it contains must correspond to the order of the `TabPanels`.
|
||||
|
||||
| Prop | Default | Type | Description |
|
||||
| ---- | ------- | -------- | ------------------------------------------ |
|
||||
| `as` | `div` | `string` | The element the `TabList` should render as |
|
||||
|
||||
| Slot prop | Type | Description |
|
||||
| --------------- | -------- | ---------------------------- |
|
||||
| `selectedIndex` | `number` | The currently selected index |
|
||||
|
||||
### Tab
|
||||
|
||||
This component wraps the selector for an individual tab. All `Tab`s will be rendered at once.
|
||||
|
||||
| Prop | Default | Type | Description |
|
||||
| ---------- | -------- | --------- | --------------------------------------- |
|
||||
| `as` | `button` | `string` | The element the `Tab` should render as |
|
||||
| `disabled` | `false` | `boolean` | Whether the `Tab` is currently disabled |
|
||||
|
||||
| Slot prop | Type | Description |
|
||||
| ---------- | --------- | --------------------------------------- |
|
||||
| `selected` | `boolean` | Whether the `Tab` is currently selected |
|
||||
|
||||
### TabPanels
|
||||
|
||||
The container for `TabPanel` components. The order of the `TabPanel` components it contains must correspond to the order of the `TabList`.
|
||||
|
||||
| Prop | Default | Type | Description |
|
||||
| ---- | ------- | -------- | -------------------------------------------- |
|
||||
| `as` | `div` | `string` | The element the `TabPanels` should render as |
|
||||
|
||||
| Slot prop | Type | Description |
|
||||
| --------------- | -------- | ---------------------------- |
|
||||
| `selectedIndex` | `number` | The currently selected index |
|
||||
|
||||
### TabPanel
|
||||
|
||||
This component wraps the contents of an individual tab. Only one `TabPanel` will be visible at once.
|
||||
|
||||
| Prop | Default | Type | Description |
|
||||
| ---- | ------- | -------- | ------------------------------------------- |
|
||||
| `as` | `div` | `string` | The element the `TabPanel` should render as |
|
||||
|
||||
| Slot prop | Type | Description |
|
||||
| ---------- | --------- | --------------------------------------------------- |
|
||||
| `selected` | `boolean` | Whether or not the `TabPanel` is currently selected |
|
||||
284
src/routes/docs/tailwind-ui.svx
Normal file
284
src/routes/docs/tailwind-ui.svx
Normal file
@@ -0,0 +1,284 @@
|
||||
# Using Tailwind UI
|
||||
|
||||
The original Headless UI libraries for React & Vue were made by Tailwind Labs in order to serve as a foundation for [Tailwind UI](https://tailwindui.com/), a paid set of component templates. While this Svelte port has no affiliation with Tailwind Labs or Tailwind UI, one of the main motivations behind it is to make it easy to use Tailwind UI with Svelte. This page contains tips and instructions for converting their provided snippets to work with Svelte.
|
||||
|
||||
You can convert either the React or the Vue templates to Svelte, if you are more familiar with one or the other framework. If you are unfamiliar with both React and Vue, below are some more comprehensive instructions for converting the React snippets.
|
||||
|
||||
## General concerns
|
||||
|
||||
### Heroicons
|
||||
|
||||
Many Tailwind UI examples use the [heroicons](https://heroicons.com/) icon library. This library has official wrappers for React and Vue, but not for Svelte. For Svelte, you can use the wrapper library [@rgossiaux/svelte-heroicons](https://github.com/rgossiaux/svelte-heroicons), which provides the same API and component names as the official Tailwind wrappers, making it faster to convert Tailwind UI code to Svelte. Of course, you are free to use any other library you wish instead, if you prefer.
|
||||
|
||||
## Differences from React
|
||||
|
||||
### Component names
|
||||
|
||||
The React library uses a `.` in the names of many components: `Tab.Group`, `Listbox.Button`, etc. The Svelte library does not use this pattern and instead exports every component under its own name with no dot: `TabGroup`, `ListboxButton`, etc.
|
||||
|
||||
### `useState`
|
||||
|
||||
State declared with `useState` in React becomes just a normal variable in Svelte:
|
||||
|
||||
```jsx
|
||||
import { useState } from 'react'
|
||||
import { Switch } from '@headlessui/react'
|
||||
|
||||
function MyToggle() {
|
||||
const [enabled, setEnabled] = useState(false)
|
||||
|
||||
return (
|
||||
<Switch checked={enabled} onChange={setEnabled}>
|
||||
// ...
|
||||
</Switch>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
becomes
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import { Switch } from '@rgossiaux/svelte-headlessui'
|
||||
let enabled = false;
|
||||
</script>
|
||||
|
||||
<Switch checked={enabled} on:change={(e) => enabled = e.detail}>
|
||||
<!-- ... -->
|
||||
</Switch>
|
||||
```
|
||||
|
||||
### JSX camelCased attribute names
|
||||
|
||||
In React, some HTML attributes have different names from the standard ones that are used in Svelte. These are covered in the [React documentation](https://reactjs.org/docs/dom-elements.html#differences-in-attributes), but we repeat the most important differences here:
|
||||
|
||||
* `className` in React becomes `class` in Svelte
|
||||
* `htmlFor` in React becomes `for` in Svelte
|
||||
* SVG attributes in React like `strokeWidth`, `strokeLinecap`, etc. become `stroke-width`, `stroke-linecap`, etc. in Svelte
|
||||
|
||||
### Event handlers
|
||||
|
||||
In React, you pass event handlers using camelCased names:
|
||||
|
||||
```jsx
|
||||
import { useState } from 'react'
|
||||
import { Dialog } from '@headlessui/react'
|
||||
|
||||
function MyDialog() {
|
||||
let [isOpen, setIsOpen] = useState(true)
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onClose={() => setIsOpen(false)}>
|
||||
<Dialog.Overlay />
|
||||
|
||||
<Dialog.Title>Deactivate account</Dialog.Title>
|
||||
<Dialog.Description>
|
||||
This will permanently deactivate your account
|
||||
</Dialog.Description>
|
||||
|
||||
<p>
|
||||
Are you sure you want to deactivate your account? All of your data will
|
||||
be permanently removed. This action cannot be undone.
|
||||
</p>
|
||||
|
||||
<button onClick={() => setIsOpen(false)}>Deactivate</button>
|
||||
<button onClick={() => setIsOpen(false)}>Cancel</button>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
In Svelte, you instead use the `on:` directive:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import {
|
||||
Dialog,
|
||||
DialogOverlay,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
} from "@rgossiaux/svelte-headlessui";
|
||||
let isOpen = true;
|
||||
</script>
|
||||
|
||||
<Dialog open={isOpen} on:close={() => (isOpen = false)}>
|
||||
<DialogOverlay />
|
||||
|
||||
<DialogTitle>Deactivate account</DialogTitle>
|
||||
<DialogDescription>
|
||||
This will permanently deactivate your account
|
||||
</DialogDescription>
|
||||
|
||||
<p>
|
||||
Are you sure you want to deactivate your account? All of your data will be
|
||||
permanently removed. This action cannot be undone.
|
||||
</p>
|
||||
|
||||
<button on:click={() => (isOpen = false)}>Deactivate</button>
|
||||
<button on:click={() => (isOpen = false)}>Cancel</button>
|
||||
</Dialog>
|
||||
```
|
||||
|
||||
Furthermore, in the React library, event handlers will be called with the their data directly:
|
||||
|
||||
```jsx
|
||||
<Listbox value={selectedPerson} onChange={setSelectedPerson}>
|
||||
// ...
|
||||
</Listbox>
|
||||
```
|
||||
|
||||
In the Svelte version, your handler will be passed a `CustomEvent` object, and you need to look at its `.detail` property:
|
||||
```svelte
|
||||
<Listbox value={selectedPerson} on:change={(e) => selectedPerson = e.detail}>
|
||||
<!-- ... -->
|
||||
</Listbox>
|
||||
```
|
||||
|
||||
### Render props
|
||||
|
||||
The React components make use of render props:
|
||||
|
||||
```jsx
|
||||
<Popover.Panel>
|
||||
{({ close }) => (
|
||||
<button
|
||||
onClick={async () => {
|
||||
await fetch('/accept-terms', { method: 'POST' });
|
||||
close();
|
||||
}}
|
||||
>
|
||||
Read and accept
|
||||
</button>
|
||||
)}
|
||||
</Popover.Panel>
|
||||
```
|
||||
|
||||
The Svelte equivalent of this pattern is to use [slot props](https://svelte.dev/tutorial/slot-props):
|
||||
|
||||
```svelte
|
||||
<PopoverPanel let:close>
|
||||
<button
|
||||
on:click={async () => {
|
||||
await fetch('/accept-terms', { method: 'POST' })
|
||||
close()
|
||||
}}
|
||||
>
|
||||
Read and accept
|
||||
</button>
|
||||
</PopoverPanel>
|
||||
```
|
||||
|
||||
### Conditional rendering
|
||||
|
||||
The standard way to do conditional rendering in React is with the `&&` operator:
|
||||
|
||||
```jsx
|
||||
<Disclosure>
|
||||
{({ open }) => (
|
||||
<>
|
||||
<Disclosure.Button>Is team pricing available?</Disclosure.Button>
|
||||
|
||||
{open && (
|
||||
<div>
|
||||
{/*
|
||||
Using `static`, `Disclosure.Panel` is always rendered and
|
||||
ignores the `open` state.
|
||||
*/}
|
||||
<Disclosure.Panel static>{/* ... */}</Disclosure.Panel>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Disclosure>
|
||||
```
|
||||
|
||||
In Svelte, you use the `{#if}` templating syntax instead:
|
||||
|
||||
```svelte
|
||||
<Disclosure let:open>
|
||||
<DisclosureButton>Is team pricing available?</DisclosureButton>
|
||||
|
||||
{#if open}
|
||||
<div>
|
||||
<!-- Using `static`, `DisclosurePanel` is always rendered and -->
|
||||
<!-- ignores the `open` state. -->
|
||||
<DisclosurePanel static> <!-- ... --></DisclosurePanel>
|
||||
</div>
|
||||
{/if}
|
||||
</Disclosure>
|
||||
```
|
||||
|
||||
### Iteration / mapping
|
||||
|
||||
Tailwind UI frequenty does iteration using `Array.prototype.map()`:
|
||||
|
||||
```jsx
|
||||
<Listbox value={selectedPerson} onChange={setSelectedPerson}>
|
||||
<Listbox.Button>{selectedPerson.name}</Listbox.Button>
|
||||
<Listbox.Options>
|
||||
{people.map((person) => (
|
||||
<Listbox.Option
|
||||
key={person.id}
|
||||
value={person}
|
||||
disabled={person.unavailable}
|
||||
>
|
||||
{person.name}
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</Listbox.Options>
|
||||
</Listbox>
|
||||
```
|
||||
|
||||
In Svelte, you accomplish this by using the `{#each}` template syntax:
|
||||
|
||||
```svelte
|
||||
<Listbox value={selectedPerson} on:change={(e) => selectedPerson = e.detail}>
|
||||
<ListboxButton>{selectedPerson.name}</ListboxButton>
|
||||
<ListboxOptions>
|
||||
{#each people as person (person.id)}
|
||||
<ListboxOption
|
||||
value={person}
|
||||
disabled={person.unavailable}
|
||||
>
|
||||
{person.name}
|
||||
</ListboxOption>
|
||||
{/each}
|
||||
</ListboxOptions>
|
||||
</Listbox>
|
||||
```
|
||||
|
||||
Note that the key moves from the `key=` prop in React into the `{#each}` statement in Svelte. Don't forget to add this!
|
||||
|
||||
### Using a dynamic component
|
||||
|
||||
In React, you can use a variable as a tag name:
|
||||
|
||||
```jsx
|
||||
{solutions.map((item) => (
|
||||
// ...
|
||||
<item.icon aria-hidden="true" />
|
||||
// ...
|
||||
))}
|
||||
```
|
||||
|
||||
In Svelte, you use `<svelte:component>` instead:
|
||||
|
||||
```jsx
|
||||
{#each solutions as item}
|
||||
<!-- ... -->
|
||||
<svelte:component this={item.icon} aria-hidden="true" />
|
||||
<!-- ... -->
|
||||
{/each}
|
||||
```
|
||||
|
||||
### Fragments
|
||||
|
||||
You may see the empty `<>` fragment tag in Tailwind UI snippets. You can generally simply delete this in Svelte.
|
||||
|
||||
You may also see the `as={Fragment}` prop on some components. In the React library, you can choose render a component as a fragment, meaning that it will not render anything at all and will instead set props on its *child* component or element. This is currently impossible in Svelte, so every component in this library must always render an element. When you see `as={Fragment}`, you should just delete it.
|
||||
|
||||
Unfortunately, it some cases this can cause some visual differences. For example, templates that use z-index might require copying some classes into the component (usually a transition) that used to have `as={Fragment}`, due to changes in the [stacking context](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Positioning/Understanding_z_index/The_stacking_context).
|
||||
|
||||
Modal components that have `{/* This element is to trick the browser into centering the modal contents. */}` above a `Transition` might require moving that element inside the `Transition`.
|
||||
|
||||
If you run into problems related to this that aren't mentioned here, please [report them on GitHub](https://github.com/rgossiaux/svelte-headlessui/issues) so that we can improve the documentation.
|
||||
242
src/routes/docs/transition.svx
Normal file
242
src/routes/docs/transition.svx
Normal file
@@ -0,0 +1,242 @@
|
||||
# Transition
|
||||
|
||||
<script>
|
||||
import Preview from "./_Preview.svelte";
|
||||
</script>
|
||||
|
||||
<Preview url="examples/transition" code="" class="h-[340px] bg-rose-300"/>
|
||||
|
||||
## About this component
|
||||
|
||||
One of Svelte's strengths is its great support for transitions and animations. You are welcome and even encouraged to use them with this library when possible. So why bother with the `Transition` component?
|
||||
|
||||
There are a few reasons you may choose to use `Transition`:
|
||||
|
||||
- You are converting code from Tailwind UI which uses a `Transition` component. It may be simpler to just be able to port that code directly instead of replacing it with a Svelte transition.
|
||||
|
||||
- You want to use CSS class-based transitions (perhaps via Tailwind CSS classes, perhaps not) instead of using Svelte's transition engine. In that case this component can help, especially by helping coordinate multiple transitions (one of the main features of the component).
|
||||
|
||||
Since this component works is based on CSS class, we'll be using Tailwind CSS transition classes for the examples. However, you are free to use your own transition classes, as long as they are globally available.
|
||||
|
||||
## Basic example
|
||||
|
||||
The `Transition` accepts a `show` prop that controls whether the children should be shown or hidden, and a set of lifecycle props (like `enterFrom` and `leaveTo`) that let you add CSS classes at specific phases of a transition.
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import { Transition } from "@rgossiaux/svelte-headlessui";
|
||||
let show = false;
|
||||
</script>
|
||||
|
||||
<button on:click={() => (show = !show)}> Toggle </button>
|
||||
<Transition
|
||||
{show}
|
||||
enter="transition-opacity duration-75"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="transition-opacity duration-150"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
I will fade in and out
|
||||
</Transition>
|
||||
```
|
||||
|
||||
## Showing and hiding content
|
||||
|
||||
Wrap the content that should be conditionally rendered in a `Transition` component, and use the `show` prop to control whether the content should be visible or hidden.
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import { Transition } from "@rgossiaux/svelte-headlessui";
|
||||
let show = false;
|
||||
</script>
|
||||
|
||||
<button on:click={() => (show = !show)}> Toggle </button>
|
||||
<Transition {show}>I will appear and disappear</Transition>
|
||||
```
|
||||
|
||||
The `Transition` component will render a `div` by default, but you can use the `as` prop to render a different element instead if needed. Any other HTML attributes (like `class` or `style`) can be added directly to the `Transition` the same way they would be for regular HTML elements.
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import { Transition } from "@rgossiaux/svelte-headlessui";
|
||||
let show = false;
|
||||
</script>
|
||||
|
||||
<button on:click={() => (show = !show)}> Toggle </button>
|
||||
<Transition {show} as="a" href="/my-url" style="font-weight: 700;">
|
||||
I will appear and disappear
|
||||
</Transition>
|
||||
```
|
||||
|
||||
## Animating transitions
|
||||
|
||||
By default, a `Transition` will enter and leave instantly, which is probably not what you're looking for if you're using this component.
|
||||
|
||||
To animate your enter/leave transitions, add classes that provide the styling for each phase of the transitions using these props:
|
||||
|
||||
- `enter`: Applied the entire time an element is entering. Usually you define your duration and what properties you want to transition here: `transition-opacity duration-75`, for example.
|
||||
|
||||
- `enterFrom`: The starting point to enter from, like `opacity-0` if something should fade in.
|
||||
|
||||
- `enterTo`: The ending point to enter to, like `opacity-100` after fading in.
|
||||
|
||||
- `leave`: Applied the entire time an element is leaving. Usually you define your duration and what properties you want to transition here: `transition-opacity duration-75`, for example.
|
||||
|
||||
- `leaveFrom`: The starting point to leave from, like `opacity-100` if something should fade out.
|
||||
|
||||
- `leaveTo`: The ending point to leave to, like `opacity-0` after fading out.
|
||||
|
||||
Here's an example:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import { Transition } from "@rgossiaux/svelte-headlessui";
|
||||
let show = false;
|
||||
</script>
|
||||
|
||||
<button on:click={() => (show = !show)}> Toggle </button>
|
||||
<Transition
|
||||
{show}
|
||||
enter="transition-opacity duration-75"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="transition-opacity duration-150"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
I will fade in and out
|
||||
</Transition>
|
||||
```
|
||||
|
||||
In this example, the transitioning element will take 75ms to enter (that's the `duration-75` class), and will transition the opacity property during that time (that's `transition-opacity`).
|
||||
|
||||
It will start completely transparent before entering (that's `opacity-0` in the `enterFrom` phase), and fade in to completely opaque (`opacity-100`) when finished (that's the `enterTo` phase).
|
||||
|
||||
When the element is being removed (the `leave` phase), it will transition the opacity property, and spend 150ms doing it (`transition-opacity duration-150`).
|
||||
|
||||
It will start as completely opaque (the `opacity-100` in the `leaveFrom` phase), and finish as completely transparent (the `opacity-0` in the `leaveTo` phase).
|
||||
|
||||
All of these props are optional, and will default to just an empty string.
|
||||
|
||||
## Coordinating multiple transitions
|
||||
|
||||
Sometimes you need to transition multiple elements with different animations but all based on the same state. For example, say the user clicks a button to open a sidebar that slides over the screen, and you also need to fade-in a background overlay at the same time.
|
||||
|
||||
You can do this by wrapping the related elements with a parent `Transition` component, and wrapping each child that needs its own transition styles with a `TransitionChild` component, which will automatically communicate with the parent `Transition` and inherit the parent's `show` state.
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import { Transition, TransitionChild } from "@rgossiaux/svelte-headlessui";
|
||||
export let show = false;
|
||||
</script>
|
||||
|
||||
<!-- This `show` prop controls all nested `TransitionChild` components -->
|
||||
<Transition {show}>
|
||||
<!-- Background overlay -->
|
||||
<TransitionChild
|
||||
enter="transition-opacity ease-linear duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="transition-opacity ease-linear duration-300"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<!-- ... -->
|
||||
</TransitionChild>
|
||||
|
||||
<!-- Sliding sidebar -->
|
||||
<TransitionChild
|
||||
enter="transition ease-in-out duration-300 transform"
|
||||
enterFrom="-translate-x-full"
|
||||
enterTo="translate-x-0"
|
||||
leave="transition ease-in-out duration-300 transform"
|
||||
leaveFrom="translate-x-0"
|
||||
leaveTo="-translate-x-full"
|
||||
>
|
||||
<!-- ... -->
|
||||
</TransitionChild>
|
||||
</Transition>
|
||||
```
|
||||
|
||||
The `TransitionChild` component has the exact same API as the `Transition` component, but with no `show` prop, since the `show` value is controlled by the parent.
|
||||
|
||||
Parent `Transition` components will always automatically wait for all children to finish transitioning before unmounting, so you don't need to manage any of that timing yourself.
|
||||
|
||||
## Transitioning on initial mount
|
||||
|
||||
If you want an element to transition the very first time it's rendered, set the `appear` prop to `true`.
|
||||
|
||||
This is useful if you want something to transition in on initial page load, or when its parent is conditionally rendered.
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import { Transition, TransitionChild } from "@rgossiaux/svelte-headlessui";
|
||||
export let show = false;
|
||||
</script>
|
||||
|
||||
<Transition
|
||||
{show}
|
||||
appear={true}
|
||||
enter="transition-opacity duration-75"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="transition-opacity duration-150"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<!-- Your content goes here -->
|
||||
</Transition>
|
||||
```
|
||||
|
||||
## Component API
|
||||
|
||||
### Transition
|
||||
|
||||
| Prop | Default | Type | Description |
|
||||
| ----------- | ------- | --------- | -------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `show` | -- | `boolean` | Whether the children should be shown or hidden |
|
||||
| `as` | `div` | `string` | The element the `Transition` should render as |
|
||||
| `static` | `false` | `boolean` | Whether the element should ignore the internally managed open/closed state |
|
||||
| `unmount` | `true` | `boolean` | Whether the element should be unmounted, instead of just hidden, based on the open/closed state |
|
||||
| `enter` | `""` | `string` | Classes to add to the transitioning element during the entire enter phase |
|
||||
| `enterFrom` | `""` | `string` | Classes to add to the transitioning element before the enter phase starts |
|
||||
| `enterTo` | `""` | `string` | Classes to add to the transitioning element immediately after the enter phase starts |
|
||||
| `entered` | `""` | `string` | Classes to add to the transitioning element once the transition is done. These classes will persist after that until the `leave` phase |
|
||||
| `leave` | `""` | `string` | Classes to add to the transitioning element during the entire leave phase |
|
||||
| `leaveFrom` | `""` | `string` | Classes to add to the transitioning element before the leave phase starts |
|
||||
| `leaveTo` | `""` | `string` | Classes to add to the transitioning element immediately after the leave phase starts |
|
||||
|
||||
This component also dispatches the following custom events, which are listened to using the Svelte `on:` directive:
|
||||
|
||||
| Event name | Type of event `.detail` | Description |
|
||||
| ------------- | ----------------------- | ----------------------------------------------- |
|
||||
| `beforeEnter` | `null` | Dispatched before we start the enter transition |
|
||||
| `afterEnter` | `null` | Dispatched after we finish the enter transition |
|
||||
| `beforeLeave` | `null` | Dispatched before we start the leave transition |
|
||||
| `afterLeave` | `null` | Dispatched after we finish the leave transition |
|
||||
|
||||
### TransitionChild
|
||||
|
||||
| Prop | Default | Type | Description |
|
||||
| ----------- | ------- | --------- | -------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `as` | `div` | `string` | The element the `Transition` should render as |
|
||||
| `static` | `false` | `boolean` | Whether the element should ignore the internally managed open/closed state |
|
||||
| `unmount` | `true` | `boolean` | Whether the element should be unmounted, instead of just hidden, based on the open/closed state |
|
||||
| `enter` | `""` | `string` | Classes to add to the transitioning element during the entire enter phase |
|
||||
| `enterFrom` | `""` | `string` | Classes to add to the transitioning element before the enter phase starts |
|
||||
| `enterTo` | `""` | `string` | Classes to add to the transitioning element immediately after the enter phase starts |
|
||||
| `entered` | `""` | `string` | Classes to add to the transitioning element once the transition is done. These classes will persist after that until the `leave` phase |
|
||||
| `leave` | `""` | `string` | Classes to add to the transitioning element during the entire leave phase |
|
||||
| `leaveFrom` | `""` | `string` | Classes to add to the transitioning element before the leave phase starts |
|
||||
| `leaveTo` | `""` | `string` | Classes to add to the transitioning element immediately after the leave phase starts |
|
||||
|
||||
This component also dispatches the following custom events, which are listened to using the Svelte `on:` directive:
|
||||
|
||||
| Event name | Type of event `.detail` | Description |
|
||||
| ------------- | ----------------------- | ----------------------------------------------- |
|
||||
| `beforeEnter` | `null` | Dispatched before we start the enter transition |
|
||||
| `afterEnter` | `null` | Dispatched after we finish the enter transition |
|
||||
| `beforeLeave` | `null` | Dispatched before we start the leave transition |
|
||||
| `afterLeave` | `null` | Dispatched after we finish the leave transition |
|
||||
15
src/routes/docs/version-history.svx
Normal file
15
src/routes/docs/version-history.svx
Normal file
@@ -0,0 +1,15 @@
|
||||
|
||||
This is the release history of Svelte Headless UI and the correspondence with versions of Headless UI.
|
||||
|
||||
| Svelte Headless UI version | Date released | Headless UI version | Notes |
|
||||
| -------------------------- | ------------- | ------------------- | ----- |
|
||||
| 1.0.0-beta.10 | 2022-02-27 | 1.4.2 | |
|
||||
| 1.0.0-beta.9 | 2022-02-06 | 1.4.2 | |
|
||||
| 1.0.0-beta.8 | 2022-01-21 | 1.4.2 | |
|
||||
| 1.0.0-beta.7 | 2021-12-31 | 1.4.2 | |
|
||||
| 1.0.0-beta.6 | 2021-12-29 | 1.4.2 | |
|
||||
| 1.0.0-beta.5 | 2021-12-28 | 1.4.2 | |
|
||||
| 1.0.0-beta.4 | 2021-12-26 | 1.4.2 | |
|
||||
| 1.0.0-beta.3 | 2021-12-24 | 1.4.2 | Initial public release |
|
||||
|
||||
Full release notes and changelogs can be [found on GitHub](https://github.com/tailwindlabs/headlessui/releases).
|
||||
@@ -1,4 +1,8 @@
|
||||
<h1>Welcome to SvelteKit</h1>
|
||||
<p>
|
||||
Visit <a href="https://kit.svelte.dev">kit.svelte.dev</a> to read the documentation
|
||||
</p>
|
||||
<script context="module" lang="ts">
|
||||
export function load() {
|
||||
return {
|
||||
status: 302,
|
||||
redirect: "docs",
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user