Skip to content

Commit 434f1c2

Browse files
authored
Swappable edge (#1729)
* wip * shadow dialog * only one dialog * pre-release * with key directive * drawer with offet (edge swappable) * drawer with offset (edge swappable) and docs * coderabbit corrections * coderabbit performance suggestions * coderabbit: final tweaks * touch classes for handler * docs: css value for drawer handle * trigger playwright check
1 parent d8a32f5 commit 434f1c2

File tree

8 files changed

+200
-153
lines changed

8 files changed

+200
-153
lines changed

src/lib/dialog/Dialog.svelte

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,15 @@
3636
}
3737
3838
function _onclick(ev: MouseEvent & { currentTarget: HTMLDialogElement }) {
39-
const dlg: HTMLDialogElement = ev.currentTarget,
40-
rect = dlg.getBoundingClientRect(),
41-
clickedInContent = ev.clientX >= rect.left && ev.clientX <= rect.right && ev.clientY >= rect.top && ev.clientY <= rect.bottom;
39+
const dlg: HTMLDialogElement = ev.currentTarget;
40+
if (ev.target === dlg) {
41+
// click outside - backdrop is dialog
42+
const rect = dlg.getBoundingClientRect(),
43+
clickedInContent = ev.clientX >= rect.left && ev.clientX <= rect.right && ev.clientY >= rect.top && ev.clientY <= rect.bottom;
4244
43-
if (outsideclose && !clickedInContent) {
44-
return cancel(dlg);
45+
if (outsideclose && !clickedInContent) {
46+
return cancel(dlg);
47+
}
4548
}
4649
4750
if (autoclose && ev.target instanceof HTMLButtonElement && !permanent) {

src/lib/drawer/Drawer.svelte

Lines changed: 47 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,15 @@
66
import { sineIn } from "svelte/easing";
77
import { fly } from "svelte/transition";
88
import { drawer } from ".";
9+
import { setContext } from "svelte";
910
10-
let { children, open = $bindable(false), hidden = $bindable(), width, dismissable = false, placement = "left", class: className, transitionParams, transition = fly, outsideclose, activateClickOutside, ...restProps }: DrawerProps = $props();
11+
let { children, open = $bindable(false), hidden = $bindable(), modal, offset, width, dismissable = offset ? false : undefined, placement = "left", class: className, transitionParams, transition = fly, outsideclose, activateClickOutside, ...restProps }: DrawerProps = $props();
12+
13+
setContext("drawer", {
14+
get placement() {
15+
return placement;
16+
}
17+
});
1118
1219
// back compatibility
1320
if (hidden !== undefined) console.warn("'hidden' property is deprecated. Please use the 'open' property to manage 'Drawer'.");
@@ -35,23 +42,51 @@
3542
3643
const theme = getTheme("drawer");
3744
38-
const { base } = $derived(drawer({ placement, width, modal: restProps.modal }));
45+
let shifted = $state(true);
3946
40-
let innerWidth: number = $state(-1);
41-
let innerHeight: number = $state(-1);
42-
// let startX = $derived(position === 'fixed'? 0: )
43-
let x = $derived(placement === "left" ? -320 : placement === "right" ? innerWidth + 320 : undefined);
44-
let y = $derived(placement === "top" ? -100 : placement === "bottom" ? innerHeight + 100 : undefined);
47+
const { base } = $derived(drawer({ placement, width, modal: offset && !open ? false : modal, shifted }));
4548
46-
let transition_params = $derived(transitionParams ?? Object.assign({}, { x, y, duration: 200, easing: sineIn }));
47-
</script>
49+
let x = $state(),
50+
y = $state();
51+
52+
let transition_params = $derived({ x, y, duration: 300, easing: sineIn, opacity: 1, ...transitionParams });
53+
54+
function init(node: HTMLDialogElement) {
55+
// set initial offset, later it will be switched on/off by onintrostart
56+
if (offset) node.style[placement] = offset;
57+
}
4858
49-
<svelte:window bind:innerWidth bind:innerHeight />
59+
function onintrostart(ev: CustomEvent & { currentTarget: HTMLDialogElement }) {
60+
// set the values for transition start position
61+
const dlg = ev.currentTarget;
62+
const { innerWidth = 0, innerHeight = 0 } = dlg.ownerDocument.defaultView ?? {};
5063
51-
<Dialog bind:open {transition} {dismissable} {outsideclose} transitionParams={transition_params} {...restProps} class={base({ class: clsx(theme?.base, className) })}>
64+
const rect = dlg.getBoundingClientRect();
65+
66+
x = placement === "left" ? rect.left : placement === "right" ? rect.right - innerWidth : undefined;
67+
y = placement === "top" ? rect.top : placement === "bottom" ? rect.bottom - innerHeight : undefined;
68+
// remove shift for transition end position
69+
shifted = false;
70+
71+
// add offset if closed, remove it when open
72+
if (offset) dlg.style[placement] = open ? "" : offset;
73+
}
74+
75+
function onoutrostart(ev: CustomEvent & { currentTarget: HTMLDialogElement }) {
76+
shifted = true;
77+
}
78+
</script>
79+
80+
<Dialog {@attach init} bind:open {modal} {dismissable} {transition} {onintrostart} {onoutrostart} {outsideclose} transitionParams={transition_params} {...restProps} class={base({ class: clsx(theme?.base, className) })}>
5281
{@render children?.()}
5382
</Dialog>
5483

84+
{#if offset && !open}
85+
<Dialog {@attach init} open modal={false} {dismissable} {outsideclose} {...restProps} class={base({ class: clsx(theme?.base, className) })}>
86+
{@render children?.()}
87+
</Dialog>
88+
{/if}
89+
5590
<!--
5691
@component
5792
[Go to docs](https://flowbite-svelte.com/)
@@ -62,7 +97,7 @@
6297
@prop open = $bindable(false)
6398
@prop hidden = $bindable()
6499
@prop width
65-
@prop dismissable = false
100+
@prop dismissable
66101
@prop placement = "left"
67102
@prop class: className
68103
@prop transitionParams

src/lib/drawer/DrawerHandle.svelte

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<script lang="ts">
2+
import { getTheme } from "$lib/theme/themeUtils";
3+
import type { DrawerHandleProps } from "$lib/types";
4+
import clsx from "clsx";
5+
import { drawerhandle } from "./theme";
6+
import { getContext } from "svelte";
7+
8+
let { children, placement, "aria-label": ariaLabel, class: className, classes, ...restProps }: DrawerHandleProps = $props();
9+
10+
const ctx = getContext<{ placement: DrawerHandleProps["placement"] } | undefined>("drawer");
11+
12+
const theme = getTheme("drawerhandle");
13+
let { base, handle } = $derived(drawerhandle({ placement: placement ?? ctx?.placement ?? "left" }));
14+
</script>
15+
16+
<button type="button" aria-label={ariaLabel} {...restProps} class={base({ class: clsx(theme?.base, className) })}>
17+
{@render children?.()}
18+
<span class={handle({ class: clsx(theme?.handle, classes?.handle) })}></span>
19+
</button>

src/lib/drawer/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export { default as Drawer } from "./Drawer.svelte";
22
export { default as Drawerhead } from "./Drawerhead.svelte";
3-
export { drawer, drawerhead } from "./theme";
3+
export { default as DrawerHandle } from "./DrawerHandle.svelte";
4+
export { drawer, drawerhead, drawerhandle } from "./theme";

src/lib/drawer/theme.ts

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export type DrawerVariants = VariantProps<typeof drawer> & Classes<typeof drawer
77
export const drawer = tv({
88
extend: dialog,
99
slots: {
10-
base: "p-4 max-h-none max-w-none"
10+
base: "p-4 max-h-none max-w-none border border-gray-200 dark:border-gray-700 transform-gpu will-change-transform"
1111
},
1212
variants: {
1313
placement: {
@@ -24,11 +24,43 @@ export const drawer = tv({
2424
modal: {
2525
false: { base: "fixed inset-0" },
2626
true: { base: "" }
27+
},
28+
shifted: {
29+
true: {},
30+
false: {}
2731
}
2832
},
33+
compoundVariants: [
34+
{
35+
shifted: false,
36+
modal: false,
37+
class: { base: "z-50" }
38+
},
39+
{
40+
shifted: true,
41+
placement: 'left',
42+
class: { base: "-translate-x-full" }
43+
},
44+
{
45+
shifted: true,
46+
placement: 'right',
47+
class: { base: "translate-x-full" }
48+
},
49+
{
50+
shifted: true,
51+
placement: 'top',
52+
class: { base: "-translate-y-full" }
53+
},
54+
{
55+
shifted: true,
56+
placement: 'bottom',
57+
class: { base: "translate-y-full" }
58+
},
59+
],
2960
defaultVariants: {
3061
placement: "left",
31-
width: "default"
62+
width: "default",
63+
modal: true
3264
}
3365
});
3466

@@ -41,3 +73,20 @@ export const drawerhead = tv({
4173
svg: "h-4 w-4"
4274
}
4375
});
76+
77+
export type DrawerHandleVariants = VariantProps<typeof drawerhandle> & Classes<typeof drawerhandle>;
78+
79+
export const drawerhandle = tv({
80+
slots: {
81+
base: "p-4 absolute flex select-none cursor-grab active:cursor-grabbing focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-blue-500 dark:focus-visible:ring-offset-gray-800",
82+
handle: "absolute rounded-lg bg-gray-300 dark:bg-gray-600"
83+
},
84+
variants: {
85+
placement: {
86+
left: { base: "inset-y-0 right-0 touch-pan-y", handle: "w-1 h-8 top-1/2 -translate-y-1/2" },
87+
right: { base: "inset-y-0 left-0 touch-pan-y", handle: "w-1 h-8 top-1/2 -translate-y-1/2" },
88+
top: { base: "inset-x-0 bottom-0 touch-pan-x", handle: "w-8 h-1 left-1/2 -translate-x-1/2" },
89+
bottom: { base: "inset-x-0 top-0 touch-pan-x", handle: "w-8 h-1 left-1/2 -translate-x-1/2" }
90+
},
91+
},
92+
});

src/lib/theme/themes.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export { clipboard } from "../clipboard";
1313
export { darkmode } from "../darkmode";
1414
export { datepicker } from "../datepicker";
1515
export { android, desktop, ios, tablet } from "../device-mockups";
16-
export { drawer } from "../drawer";
16+
export { drawer, drawerhandle } from "../drawer";
1717
export { dropdown, dropdownDivider, dropdownGroup, dropdownHeader, dropdownItem } from "../dropdown";
1818
export { footer, footerBrand, footerCopyright, footerIcon, footerLink, footerLinkGroup } from "../footer";
1919
export { gallery } from "../gallery";

src/lib/types.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import type { ButtonVariants, GradientButtonVariantes, gradientButton } from "$l
1515
import type { CardVariants } from "$lib/card/theme";
1616
import type Slide from "$lib/carousel/Slide.svelte";
1717
import type { CarouselVariants, SlideVariants } from "$lib/carousel/theme";
18-
import type { DrawerVariants, DrawerheadVariants } from "$lib/drawer/theme";
18+
import type { DrawerHandleVariants, DrawerVariants, DrawerheadVariants } from "$lib/drawer/theme";
1919
import type { DropdownItemVariants } from "$lib/dropdown/theme";
2020
import type { DatepickerVariants } from "$lib/datepicker/theme";
2121
import type { FooterCopyrightVariants, FooterLinkVariants } from "$lib/footer/theme";
@@ -569,13 +569,16 @@ export interface DialogProps extends DialogVariants, HTMLDialogAttributes {
569569
}
570570

571571
// drawer
572-
export interface DrawerProps extends DrawerVariants, Omit<DialogProps, "hidden"> {
572+
export interface DrawerProps extends DrawerVariants, Omit<DialogProps, "hidden" | "classes" | "shifted"> {
573573
/** @deprecated Use `outsideclose` instead. Will be removed in next minor version. */
574574
activateClickOutside?: boolean;
575575
/** @deprecated Use `open` instead. Will be removed in next minor version. */
576576
hidden?: boolean | null;
577+
offset?: string;
577578
}
578579

580+
export interface DrawerHandleProps extends DrawerHandleVariants, HTMLButtonAttributes { }
581+
579582
export interface DrawerheadProps extends DrawerheadVariants, HTMLButtonAttributes {
580583
closeIcon?: Snippet;
581584
buttonClass?: ClassValue;
@@ -737,7 +740,7 @@ export interface FloatingLabelInputProps extends FloatingLabelInputVaratiants, O
737740
}
738741

739742
// helper
740-
export interface HelperProps extends HelperVariants, Omit<HTMLAttributes<HTMLParagraphElement>, "color"> {}
743+
export interface HelperProps extends HelperVariants, Omit<HTMLAttributes<HTMLParagraphElement>, "color"> { }
741744

742745
// input
743746
export type InputValue = string | number | string[] | undefined;
@@ -931,7 +934,7 @@ export interface TimepickerProps {
931934
timeIntervals?: string[];
932935
columns?: ColumnCount;
933936
// Callback props instead of events
934-
onselect?: (data: { time: string; endTime: string; [key: string]: string }) => void;
937+
onselect?: (data: { time: string; endTime: string;[key: string]: string }) => void;
935938
}
936939

937940
// textarea
@@ -1120,7 +1123,7 @@ export interface ToolbarProps extends ToolbarVariants, Omit<HTMLAttributes<HTMLD
11201123
end?: Snippet;
11211124
}
11221125

1123-
export interface ToolbarGroupProps extends ToolbarGroupVariants, HTMLAttributes<HTMLDivElement> {}
1126+
export interface ToolbarGroupProps extends ToolbarGroupVariants, HTMLAttributes<HTMLDivElement> { }
11241127

11251128
export type ToolbarButtonProps = ToolbarButtonVariants &
11261129
AnchorButtonAttributes & {
@@ -1430,7 +1433,7 @@ export interface SkeletonProps extends SkeletonVariants, HTMLAttributes<HTMLDivE
14301433
size?: SkeletonVariants["size"];
14311434
}
14321435

1433-
export interface TestimonialPlaceholderProps extends TestimonialPlaceholderVariants, HTMLAttributes<HTMLDivElement> {}
1436+
export interface TestimonialPlaceholderProps extends TestimonialPlaceholderVariants, HTMLAttributes<HTMLDivElement> { }
14341437

14351438
export interface TextPlaceholderProps extends TextPlaceholderVariants, HTMLAttributes<HTMLDivElement> {
14361439
size?: TextPlaceholderVariants["size"];
@@ -1440,7 +1443,7 @@ export interface VideoPlaceholderProps extends VideoPlaceholderVariants, HTMLAtt
14401443
size?: VideoPlaceholderVariants["size"];
14411444
}
14421445

1443-
export interface WidgetPlaceholderProps extends WidgetPlaceholderVariants, HTMLAttributes<HTMLDivElement> {}
1446+
export interface WidgetPlaceholderProps extends WidgetPlaceholderVariants, HTMLAttributes<HTMLDivElement> { }
14441447

14451448
// speeddial
14461449
export interface SpeedCtxType {

0 commit comments

Comments
 (0)