Skip to content

Commit

Permalink
fix(trapFocus): added to keep focus when dialogs are open
Browse files Browse the repository at this point in the history
fix(Modal): added trapFocus
fix(Drawer): added trapFocus
fix(Drawer): added example for multi-level drawers
  • Loading branch information
Craig Howell committed Feb 6, 2023
1 parent 9178e9f commit d15858f
Show file tree
Hide file tree
Showing 6 changed files with 225 additions and 8 deletions.
73 changes: 70 additions & 3 deletions src/lib/components/drawer/Drawer.svelte
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<script lang="ts">
import { setContext } from 'svelte';
import { trapFocus } from '$lib/utils';
import { onMount, setContext } from 'svelte';
import { fly } from 'svelte/transition';
import { twMerge } from 'tailwind-merge';
import Backdrop from './Backdrop.svelte';
Expand All @@ -17,6 +18,36 @@
!disableEscClose &&
handleClose
) {
onClose();
}
}
function onClose() {
const dialogs = document.querySelectorAll(
`[data-placement=${placement}]`
) as unknown as HTMLDivElement[];
let offset = 0;
for (let i = dialogs.length - 1; i >= 0; i--) {
if (i !== dialogs.length - 1) {
if (placement === 'left') {
dialogs[i].style.transform = `translateX(${offset}px)`;
offset -= 180;
} else if (placement === 'top') {
dialogs[i].style.transform = `translateY(${offset}px)`;
offset -= 180;
} else if (placement === 'bottom') {
dialogs[i].style.transform = `translateY(${offset}px)`;
offset += 180;
} else {
dialogs[i].style.transform = `translateX(${offset}px)`;
offset += 180;
}
}
}
if (handleClose) {
handleClose();
}
}
Expand All @@ -30,12 +61,42 @@
flyConfig = { y: 448 };
}
setContext('drawer-handleClose', handleClose);
setContext('drawer-handleClose', onClose);
setContext('drawer-disableOverlayClose', disableOverlayClose);
function shiftDrawers() {
const dialogs = document.querySelectorAll(
`[data-placement=${placement}]`
) as unknown as HTMLDivElement[];
let offset = 0;
for (let i = 0; i < dialogs.length; i++) {
if (i !== dialogs.length - 1) {
if (placement === 'left') {
offset += 180;
dialogs[i].style.transform = `translateX(${offset}px)`;
} else if (placement === 'top') {
offset += 180;
dialogs[i].style.transform = `translateY(${offset}px)`;
} else if (placement === 'bottom') {
offset -= 180;
dialogs[i].style.transform = `translateY(${offset}px)`;
} else {
offset -= 180;
dialogs[i].style.transform = `translateX(${offset}px)`;
}
}
}
}
const defaultClass =
'flex inner-panel flex-col bg-light-surface dark:bg-dark-surface overflow-hidden';
$: finalClass = twMerge(defaultClass, $$props.class);
onMount(() => {
shiftDrawers();
});
</script>

<svelte:window on:keydown={captureEscapeEvent} />
Expand All @@ -57,7 +118,8 @@
class:bottom={placement === 'bottom'}
>
<div
class="pointer-events-auto panel dark:shadow-black"
use:trapFocus
class="pointer-events-auto panel dark:shadow-black transition-transform duration-200"
class:left={placement === 'left'}
class:right={placement === 'right'}
class:top={placement === 'top'}
Expand All @@ -66,6 +128,8 @@
class:shadow-negative-2xl={placement === 'bottom'}
style="opactiy: 1"
transition:fly={flyConfig}
data-dialog
data-placement={placement}
>
<div
class={finalClass}
Expand All @@ -75,6 +139,9 @@
class:bottom={placement === 'bottom'}
style={$$props.style}
>
<button
class="h-0 w-0 border-none outline-none ring-0 focus:border-none focus:outline-none focus:ring-0"
/>
<slot name="header" />
<slot name="content" />
<slot />
Expand Down
5 changes: 5 additions & 0 deletions src/lib/components/modal/Modal.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import { forwardEventsBuilder, useActions, type ActionArray } from '../../actions';
export let use: ActionArray = [];
import { exclude } from '../../utils/exclude';
import { trapFocus } from '$lib/utils';
const forwardEvents = forwardEventsBuilder(get_current_component());
export let handleClose: () => void;
Expand All @@ -31,12 +32,16 @@

<div
class={finalClass}
use:trapFocus
use:useActions={use}
use:forwardEvents
{...exclude($$props, ['use', 'class'])}
in:scale={{ start: 0.9, duration: 250, delay: 150 }}
out:scale={{ start: 0.95, duration: 150 }}
>
<button
class="h-0 w-0 border-none outline-none ring-0 focus:border-none focus:outline-none focus:ring-0"
/>
<slot name="content" />
<slot />
</div>
Expand Down
4 changes: 3 additions & 1 deletion src/lib/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import computeTrendValue from './computeTrendValue';
import computeTrendPercent from './computeTrendPercent';
import computeProgress from './computeProgress';
import { copyToClipboard } from './copyToClipboard';
import { trapFocus } from './trapFocus';

export {
formatDate,
Expand All @@ -13,5 +14,6 @@ export {
computeProgress,
computeTrendPercent,
computeTrendValue,
copyToClipboard
copyToClipboard,
trapFocus
};
43 changes: 43 additions & 0 deletions src/lib/utils/trapFocus.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
let trapFocusList: HTMLElement[] = [];

if (typeof window !== 'undefined') {
const isNext = (event: KeyboardEvent) => event.keyCode === 9 && !event.shiftKey;
const isPrevious = (event: KeyboardEvent) => event.keyCode === 9 && event.shiftKey;
const trapFocusListener = (event: KeyboardEvent) => {
if (event.target === window) {
return;
}

const eventTarget = event.target as unknown as Element;

const parentNode = trapFocusList.find((node) => node.contains(eventTarget));
if (!parentNode) {
return;
}

const focusable: NodeListOf<HTMLElement> = parentNode.querySelectorAll(
'a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), iframe, object, embed, *[tabindex], *[contenteditable]'
);
const first = focusable[0];
const last = focusable[focusable.length - 1];
if (isNext(event) && event.target === last) {
event.preventDefault();
first.focus();
} else if (isPrevious(event) && event.target === first) {
event.preventDefault();
last.focus();
}
};

document.addEventListener('keydown', trapFocusListener);
}

export const trapFocus = (node: HTMLElement) => {
trapFocusList.push(node);
node.getElementsByTagName('button')[0].focus();
return {
destroy() {
trapFocusList = trapFocusList.filter((element) => element !== node);
}
};
};
57 changes: 56 additions & 1 deletion src/routes/drawer/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import {
example,
placementExample,
multiOneExample,
props,
slots,
headerSlots,
Expand All @@ -15,6 +16,24 @@
let drawerLeftOpen = false;
let drawerTopOpen = false;
let drawerBottomOpen = false;
let drawerMultiOne = false;
let drawerInsideOpen = false;
function openMultiOneDrawer() {
drawerMultiOne = true;
}
function closeMultiOneDrawer() {
drawerMultiOne = false;
}
function openInsideDrawer() {
drawerInsideOpen = true;
}
function closeInsideDrawer() {
drawerInsideOpen = false;
}
function openDrawerRight() {
drawerRightOpen = true;
Expand Down Expand Up @@ -83,11 +102,28 @@
</Card>
</Col>

<Col class="col-24 md:col-12">
<Card bordered={false}>
<Card.Header slot="header">Multiple Drawer Levels</Card.Header>
<Card.Content slot="content" class="p-4">
<Button type="primary" on:click={openMultiOneDrawer}>Open Right</Button>

<br />
<br />

<CodeBlock language="svelte" code={multiOneExample} />
</Card.Content>
</Card>
</Col>

<Portal>
{#if drawerRightOpen}
<Drawer handleClose={closeDrawerRight}>
<Drawer.Header slot="header">Drawer Header</Drawer.Header>
<Drawer.Content slot="content">Drawer Content</Drawer.Content>
<Drawer.Content slot="content"
>Drawer Content
<Button type="primary" on:click={openInsideDrawer}>Open Drawer</Button>
</Drawer.Content>
<Drawer.Footer slot="footer">Drawer Footer</Drawer.Footer>
</Drawer>
{/if}
Expand All @@ -111,6 +147,25 @@
{/if}
</Portal>

<Portal>
{#if drawerMultiOne}
<Drawer handleClose={closeMultiOneDrawer}>
<Drawer.Header slot="header">Drawer Header</Drawer.Header>
<Drawer.Content slot="content"
>Drawer Content
<Button type="primary" on:click={openInsideDrawer}>Open Drawer</Button>
</Drawer.Content>
<Drawer.Footer slot="footer">Drawer Footer</Drawer.Footer>

<Portal>
{#if drawerInsideOpen}
<Drawer handleClose={closeInsideDrawer}>Content</Drawer>
{/if}
</Portal>
</Drawer>
{/if}
</Portal>

<Col class="col-24">
<PropsTable component="Drawer" {props} />
</Col>
Expand Down
51 changes: 48 additions & 3 deletions src/routes/drawer/examples.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ export const footerSlots: Slot[] = [

export const example = `
<script lang="ts">
import { Dropdown, Button, Portal } from 'stwui';
import { Button, Portal } from 'stwui';
let open = false;
Expand All @@ -94,7 +94,7 @@ export const example = `
}
</script>
<Button type="primary" on:click={open}>Open</Button>
<Button type="primary" on:click={openDrawer}>Open</Button>
<Portal>
{#if open}
Expand All @@ -108,7 +108,7 @@ export const example = `

export const placementExample = `
<script lang="ts">
import { Dropdown, Button, Portal } from 'stwui';
import { Button, Portal } from 'stwui';
let drawerLeftOpen = false;
let drawerTopOpen = false;
Expand Down Expand Up @@ -162,3 +162,48 @@ export const placementExample = `
<Drawer handleClose={closeDrawerBottom} placement="bottom" />
{/if}
</Portal>`;

export const multiOneExample = `
<script lang="ts">
import { Button, Portal } from 'stwui';
let open = false;
let drawerInsideOpen = false;
function openDrawer() {
drawerMultiOne = true;
}
function closeDrawer() {
drawerMultiOne = false;
}
function openInsideDrawer() {
drawerInsideOpen = true;
}
function closeInsideDrawer() {
drawerInsideOpen = false;
}
</script>
<Button type="primary" on:click={openDrawer}>Open</Button>
<Portal>
{#if open}
<Drawer handleClose={closeDrawer}>
<Drawer.Header slot="header">Drawer Header</Drawer.Header>
<Drawer.Content slot="content"
>Drawer Content
<Button type="primary" on:click={openInsideDrawer}>Open Drawer</Button>
</Drawer.Content>
<Drawer.Footer slot="footer">Drawer Footer</Drawer.Footer>
<Portal>
{#if drawerInsideOpen}
<Drawer handleClose={closeInsideDrawer}>Content</Drawer>
{/if}
</Portal>
</Drawer>
{/if}
</Portal>`;

1 comment on commit d15858f

@vercel
Copy link

@vercel vercel bot commented on d15858f Feb 6, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

stwui – ./

stwui.vercel.app
stwui-git-main-n00nday.vercel.app
stwui-n00nday.vercel.app

Please sign in to comment.