Initial commit with files

Still need to fix the imports
This commit is contained in:
Ryan Gossiaux
2021-12-13 17:13:47 -08:00
parent 42aba8a158
commit db9ec57065
56 changed files with 4034 additions and 0 deletions

View File

@@ -0,0 +1,243 @@
<script lang="ts" context="module">
import {
getContext,
setContext,
createEventDispatcher,
tick,
} from "svelte";
export enum DialogStates {
Open,
Closed,
}
export interface StateDefinition {
dialogState: DialogStates;
titleId: string | null;
setTitleId(id: string | null): void;
close(): void;
}
const DIALOG_CONTEXT_NAME = "DialogContext";
export function useDialogContext(
component: string
): Writable<StateDefinition | undefined> {
let context = getContext(DIALOG_CONTEXT_NAME) as
| Writable<StateDefinition | undefined>
| undefined;
if (context === undefined) {
throw new Error(
`<${component} /> is missing a parent <Dialog /> component.`
);
}
return context;
}
</script>
<script lang="ts">
import { State } from "./open-closed";
import { writable, Writable } from "svelte/store";
import { match } from "./match";
import { useId } from "./use-id";
import { useInertOthers } from "./use-inert-others";
import { contains } from "./dom-containers";
import { Keys } from "./keyboard";
import FocusTrap from "./FocusTrap.svelte";
import StackContextProvider, {
StackMessage,
} from "./StackContextProvider.svelte";
import DescriptionProvider from "./DescriptionProvider.svelte";
import ForcePortalRootContext from "./ForcePortalRootContext.svelte";
import Portal from "./Portal.svelte";
import PortalGroup from "./PortalGroup.svelte";
export let open: Boolean | undefined = undefined;
export let initialFocus: HTMLElement | null = null;
const dispatch = createEventDispatcher();
let containers: Set<HTMLElement> = new Set();
let openClosedState: Writable<State> | undefined = getContext("OpenClosed");
$: open =
open === undefined && openClosedState !== undefined
? match($openClosedState, {
[State.Open]: true,
[State.Closed]: false,
})
: open;
// Validations
let hasOpen = open !== undefined || openClosedState !== null;
if (!hasOpen) {
throw new Error(
`You forgot to provide an \`open\` prop to the \`Dialog\`.`
);
}
if (typeof open !== "boolean") {
throw new Error(
`You provided an \`open\` prop to the \`Dialog\`, but the value is not a boolean. Received: ${open}`
);
}
$: dialogState = open ? DialogStates.Open : DialogStates.Closed;
$: visible =
openClosedState !== undefined
? $openClosedState === State.Open
: dialogState === DialogStates.Open;
let internalDialogRef: HTMLDivElement | null = null;
$: enabled = dialogState === DialogStates.Open;
const id = `headlessui-dialog-${useId()}`;
$: _cleanup = (() => {
if (_cleanup) {
_cleanup();
}
return useInertOthers(internalDialogRef, enabled);
})();
let titleId: StateDefinition["titleId"] = null;
let api: Writable<StateDefinition | undefined> = writable();
setContext(DIALOG_CONTEXT_NAME, api);
$: api.set({
titleId,
dialogState,
setTitleId(id: string | null) {
if (titleId === id) return;
titleId = id;
},
close() {
dispatch("close", false);
},
});
// Handle outside click
async function handleWindowMousedown(event: MouseEvent) {
let target = event.target as HTMLElement;
if (dialogState !== DialogStates.Open) return;
if (containers.size !== 1) return;
if (contains(containers, target)) return;
$api.close();
await tick();
target?.focus();
}
// Handle `Escape` to close
function handleWindowKeydown(event: KeyboardEvent) {
if (event.key !== Keys.Escape) return;
if (dialogState !== DialogStates.Open) return;
if (containers.size > 1) return; // 1 is myself, otherwise other elements in the Stack
event.preventDefault();
event.stopPropagation();
$api.close();
}
$: _cleanupScrollLock = (() => {
if (_cleanupScrollLock) {
_cleanupScrollLock();
}
if (dialogState !== DialogStates.Open) return;
let overflow = document.documentElement.style.overflow;
let paddingRight = document.documentElement.style.paddingRight;
let scrollbarWidth =
window.innerWidth - document.documentElement.clientWidth;
document.documentElement.style.overflow = "hidden";
document.documentElement.style.paddingRight = `${scrollbarWidth}px`;
return () => {
document.documentElement.style.overflow = overflow;
document.documentElement.style.paddingRight = paddingRight;
};
})();
$: _cleanupClose = () => {
if (_cleanupClose) {
_cleanupClose();
}
if (dialogState !== DialogStates.Open) return;
let container = internalDialogRef;
if (!container) return;
let observer = new IntersectionObserver((entries) => {
for (let entry of entries) {
if (
entry.boundingClientRect.x === 0 &&
entry.boundingClientRect.y === 0 &&
entry.boundingClientRect.width === 0 &&
entry.boundingClientRect.height === 0
) {
$api.close();
}
}
});
observer.observe(container);
return () => observer.disconnect();
};
function handleClick(event: MouseEvent) {
event.stopPropagation();
}
$: propsWeControl = {
id,
role: "dialog",
"aria-modal": dialogState === DialogStates.Open ? true : undefined,
"aria-labelledby": titleId,
};
</script>
<svelte:window
on:mousedown={handleWindowMousedown}
on:keydown={handleWindowKeydown}
/>
{#if open}
<FocusTrap {containers} {enabled} options={{ initialFocus }} />
<StackContextProvider
element={internalDialogRef}
onUpdate={(message, element) => {
return match(message, {
[StackMessage.Add]() {
containers.add(element);
},
[StackMessage.Remove]() {
containers.delete(element);
},
});
}}
>
<ForcePortalRootContext force={true}>
<Portal>
<PortalGroup target={internalDialogRef}>
<ForcePortalRootContext force={false}>
<DescriptionProvider
name={"Dialog.Description"}
let:describedby
>
<div
{...{ ...$$restProps, ...propsWeControl }}
aria-describedby={describedby}
on:click={handleClick}
>
<slot {open} />
</div>
</DescriptionProvider>
</ForcePortalRootContext>
</PortalGroup>
</Portal>
</ForcePortalRootContext>
</StackContextProvider>
{/if}