@noxlovette/material 0.4.7 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/dist/animation/TransitionPatterns.stories.svelte +357 -0
  2. package/dist/animation/TransitionPatterns.stories.svelte.d.ts +3 -0
  3. package/dist/animation/containerTransform.d.ts +26 -12
  4. package/dist/animation/containerTransform.js +22 -138
  5. package/dist/animation/enterExit.d.ts +21 -3
  6. package/dist/animation/enterExit.js +69 -22
  7. package/dist/animation/index.d.ts +1 -1
  8. package/dist/animation/index.js +1 -1
  9. package/dist/animation/presence.svelte.d.ts +47 -0
  10. package/dist/animation/presence.svelte.js +84 -0
  11. package/dist/animation/sharedAxisTransition.d.ts +35 -37
  12. package/dist/animation/sharedAxisTransition.js +50 -22
  13. package/dist/animation/skeleton.d.ts +11 -0
  14. package/dist/animation/skeleton.js +23 -0
  15. package/dist/animation/spring.d.ts +61 -0
  16. package/dist/animation/spring.js +53 -0
  17. package/dist/animation/springValue.svelte.d.ts +22 -0
  18. package/dist/animation/springValue.svelte.js +42 -0
  19. package/dist/animation/transition.d.ts +3 -1
  20. package/dist/animation/transition.js +3 -1
  21. package/dist/attachments/clickOutside.d.ts +13 -0
  22. package/dist/attachments/clickOutside.js +18 -0
  23. package/dist/attachments/index.d.ts +1 -0
  24. package/dist/attachments/index.js +1 -0
  25. package/dist/components/buttons/FAB.svelte +23 -16
  26. package/dist/components/buttons/FABMenu.svelte +24 -29
  27. package/dist/components/buttons/FABMenu.svelte.d.ts +2 -1
  28. package/dist/components/buttons/FABMenuItem.svelte +23 -20
  29. package/dist/components/buttons/FABMenuItem.svelte.d.ts +5 -1
  30. package/dist/components/buttons/split-button/SplitButton.svelte +10 -17
  31. package/dist/components/buttons/split-button/theme.js +1 -1
  32. package/dist/components/buttons/types.d.ts +3 -3
  33. package/dist/components/cards/theme.js +1 -1
  34. package/dist/components/containers/bottom-sheet/BottomSheet.stories.svelte +8 -12
  35. package/dist/components/containers/bottom-sheet/BottomSheet.svelte +67 -55
  36. package/dist/components/containers/bottom-sheet/BottomSheet.svelte.d.ts +1 -1
  37. package/dist/components/containers/bottom-sheet/types.d.ts +8 -2
  38. package/dist/components/containers/context-menu/ContextMenu.svelte +23 -36
  39. package/dist/components/containers/context-menu/theme.js +2 -2
  40. package/dist/components/containers/dialogue/Dialogue.svelte +49 -65
  41. package/dist/components/containers/link-preview/LinkPreview.svelte +12 -23
  42. package/dist/components/containers/link-preview/theme.d.ts +3 -3
  43. package/dist/components/containers/menu/Menu.svelte +14 -21
  44. package/dist/components/containers/menu/MenuSub.svelte +10 -17
  45. package/dist/components/containers/pane/DraggablePane.svelte +90 -5
  46. package/dist/components/containers/pane/DraggablePane.svelte.d.ts +1 -1
  47. package/dist/components/containers/pane/theme.js +2 -2
  48. package/dist/components/containers/popover/Popover.svelte +31 -42
  49. package/dist/components/containers/popover/theme.d.ts +3 -3
  50. package/dist/components/containers/scroll-area/theme.js +2 -2
  51. package/dist/components/containers/side-sheet/SideSheet.stories.svelte +10 -15
  52. package/dist/components/containers/side-sheet/SideSheet.svelte +44 -44
  53. package/dist/components/containers/side-sheet/SideSheet.svelte.d.ts +4 -3
  54. package/dist/components/containers/side-sheet/types.d.ts +8 -2
  55. package/dist/components/date/DateField.svelte +85 -92
  56. package/dist/components/date/DateField.svelte.d.ts +2 -5
  57. package/dist/components/date/DateRangeField.svelte +113 -155
  58. package/dist/components/date/DateRangeField.svelte.d.ts +2 -5
  59. package/dist/components/date/theme.d.ts +95 -0
  60. package/dist/components/date/theme.js +99 -14
  61. package/dist/components/date/types.d.ts +13 -0
  62. package/dist/components/forms/checkbox/theme.js +2 -2
  63. package/dist/components/forms/command/theme.d.ts +3 -3
  64. package/dist/components/forms/command/theme.js +1 -1
  65. package/dist/components/forms/pin/theme.js +2 -2
  66. package/dist/components/forms/radio-group/theme.js +2 -2
  67. package/dist/components/forms/select/Select.svelte +42 -54
  68. package/dist/components/forms/select/theme.js +4 -4
  69. package/dist/components/forms/slider/Slider.svelte +144 -126
  70. package/dist/components/forms/slider/Slider.svelte.d.ts +3 -0
  71. package/dist/components/forms/slider/theme.d.ts +154 -48
  72. package/dist/components/forms/slider/theme.js +60 -20
  73. package/dist/components/forms/slider/types.d.ts +1 -1
  74. package/dist/components/forms/switch/theme.js +4 -4
  75. package/dist/components/forms/textfield/Textfield.svelte +1 -1
  76. package/dist/components/forms/textfield/theme.d.ts +9 -0
  77. package/dist/components/forms/textfield/theme.js +26 -9
  78. package/dist/components/forms/toggle-group/theme.js +1 -1
  79. package/dist/components/forms/tooltip/Tooltip.svelte +24 -35
  80. package/dist/components/nav/breadcrumb/theme.js +1 -1
  81. package/dist/components/nav/navbar/theme.js +2 -2
  82. package/dist/components/nav/rail/Rail.svelte +4 -5
  83. package/dist/components/nav/rail/theme.js +7 -7
  84. package/dist/components/nav/tabs/theme.js +1 -1
  85. package/dist/components/pill/theme.d.ts +1 -1
  86. package/dist/components/pill/theme.js +1 -1
  87. package/dist/components/progress/CircularProgress.svelte +78 -4
  88. package/dist/components/progress/CircularProgress.svelte.d.ts +5 -0
  89. package/dist/components/progress/LinearProgress.svelte +70 -7
  90. package/dist/components/progress/LinearProgress.svelte.d.ts +5 -0
  91. package/dist/components/progress/WavyLinearProgress.svelte +27 -3
  92. package/dist/components/progress/WavyLinearProgress.svelte.d.ts +3 -0
  93. package/dist/components/progress/_wavy.d.ts +2 -0
  94. package/dist/components/progress/_wavy.js +3 -1
  95. package/dist/components/snackbar/Snackbar.svelte +13 -17
  96. package/dist/components/table/Table.stories.svelte +24 -0
  97. package/dist/components/table/TableBody.svelte +3 -16
  98. package/dist/components/table/TableBody.svelte.d.ts +1 -5
  99. package/dist/components/table/TableHeader.svelte +1 -1
  100. package/dist/components/table/theme.js +2 -2
  101. package/dist/components/table/types.d.ts +0 -7
  102. package/dist/components/time/TimeField.stories.svelte +41 -0
  103. package/dist/components/time/TimeField.stories.svelte.d.ts +19 -0
  104. package/dist/components/time/TimeField.svelte +62 -61
  105. package/dist/components/toolbar/theme.js +2 -2
  106. package/dist/index.d.ts +1 -1
  107. package/dist/index.js +1 -1
  108. package/dist/styles/component.css +21 -22
  109. package/dist/styles/motion.css +317 -38
  110. package/dist/utils/Layer.svelte +16 -29
  111. package/dist/utils/icon/LoadingIndicator.svelte +61 -22
  112. package/dist/utils/icon/loadingShapes.d.ts +1 -0
  113. package/dist/utils/icon/loadingShapes.js +15 -0
  114. package/package.json +5 -2
  115. package/dist/actions/clickOutside.d.ts +0 -3
  116. package/dist/actions/clickOutside.js +0 -13
  117. package/dist/actions/floating.d.ts +0 -20
  118. package/dist/actions/floating.js +0 -87
  119. package/dist/actions/index.d.ts +0 -6
  120. package/dist/actions/index.js +0 -6
  121. package/dist/actions/keyboard.d.ts +0 -3
  122. package/dist/actions/keyboard.js +0 -14
  123. package/dist/actions/portal.d.ts +0 -3
  124. package/dist/actions/portal.js +0 -8
  125. package/dist/actions/positionFloating.d.ts +0 -10
  126. package/dist/actions/positionFloating.js +0 -59
  127. package/dist/actions/rovingFocusGroup.d.ts +0 -20
  128. package/dist/actions/rovingFocusGroup.js +0 -82
  129. package/dist/animation/easing.d.ts +0 -6
  130. package/dist/animation/easing.js +0 -78
  131. package/dist/animation/outroClass.d.ts +0 -3
  132. package/dist/animation/outroClass.js +0 -20
  133. package/dist/animation/transitionTypes.d.ts +0 -8
  134. package/dist/animation/transitionTypes.js +0 -1
@@ -0,0 +1,18 @@
1
+ import { on } from 'svelte/events';
2
+ /**
3
+ * Calls `handler` when a click lands outside the element. Listens in the capture phase so
4
+ * it still fires when something inside the page stops propagation.
5
+ *
6
+ * Attach it conditionally so the document listener only exists while there is something to
7
+ * dismiss:
8
+ *
9
+ * ```svelte
10
+ * <div {@attach open && clickOutside(() => (open = false))}>...</div>
11
+ * ```
12
+ */
13
+ export function clickOutside(handler) {
14
+ return (node) => on(document, 'click', (event) => {
15
+ if (!event.composedPath().includes(node))
16
+ handler(event);
17
+ }, { capture: true });
18
+ }
@@ -0,0 +1 @@
1
+ export * from './clickOutside.js';
@@ -0,0 +1 @@
1
+ export * from './clickOutside.js';
@@ -15,8 +15,7 @@ Floating action buttons (FABs) help people take primary actions.
15
15
  import type { FABProps } from './types.js';
16
16
  import FABMenu from './FABMenu.svelte';
17
17
  import { Layer, Icon, LoadingIndicator } from '../../utils/index.js';
18
- import { clickOutside } from '../../actions/index.js';
19
- import { Button, type ButtonRootProps } from 'bits-ui';
18
+ import { Button, DropdownMenu, type ButtonRootProps } from 'bits-ui';
20
19
 
21
20
  let {
22
21
  children,
@@ -30,44 +29,43 @@ Floating action buttons (FABs) help people take primary actions.
30
29
  expanded = false,
31
30
  class: className,
32
31
  withMenu,
32
+ onclick,
33
33
  ...restProps
34
34
  }: FABProps = $props();
35
35
 
36
36
  const { base, icon, label: labelClass } = $derived(fab({ size, config, expanded }));
37
37
 
38
38
  let showMenu = $state(false);
39
- let wrapperEl = $state<HTMLElement | null>(null);
40
39
 
41
40
  const btnCls = $derived(
42
41
  base({
43
42
  class: clsx(
44
43
  className,
45
- 'transition-[border-radius] duration-300',
44
+ 'transition-[border-radius] md-sys-motion-fast-spatial',
46
45
  withMenu && showMenu && '!rounded-full'
47
46
  )
48
47
  })
49
48
  );
50
49
 
51
- function handleClick(e: MouseEvent) {
50
+ function handleClick(e: MouseEvent, triggerClick?: unknown) {
52
51
  if (disabled || loading) {
53
52
  e.preventDefault();
54
53
  return;
55
54
  }
56
- if (withMenu) {
57
- e.preventDefault();
58
- showMenu = !showMenu;
59
- }
55
+ (onclick as ((e: MouseEvent) => void) | null | undefined)?.(e);
56
+ if (typeof triggerClick === 'function') triggerClick(e);
60
57
  }
61
58
  </script>
62
59
 
63
- <div class="contents" bind:this={wrapperEl} use:clickOutside={() => (showMenu = false)}>
60
+ {#snippet fabButton(triggerProps: Record<string, unknown>)}
64
61
  <Button.Root
65
62
  {disabled}
66
63
  {formaction}
67
- class={btnCls}
68
- onclick={handleClick}
69
64
  data-cy="m3-fab"
70
65
  {...restProps as ButtonRootProps}
66
+ {...triggerProps}
67
+ class={btnCls}
68
+ onclick={(e: MouseEvent) => handleClick(e, triggerProps.onclick)}
71
69
  >
72
70
  {#if loading}
73
71
  <LoadingIndicator />
@@ -81,10 +79,19 @@ Floating action buttons (FABs) help people take primary actions.
81
79
  </p>
82
80
  {/if}
83
81
  </Button.Root>
82
+ {/snippet}
84
83
 
85
- {#if withMenu && showMenu && wrapperEl?.firstElementChild}
86
- <FABMenu anchorEl={wrapperEl.firstElementChild as HTMLElement}>
84
+ {#if withMenu}
85
+ <DropdownMenu.Root bind:open={showMenu}>
86
+ <DropdownMenu.Trigger disabled={disabled || loading}>
87
+ {#snippet child({ props })}
88
+ {@render fabButton(props)}
89
+ {/snippet}
90
+ </DropdownMenu.Trigger>
91
+ <FABMenu>
87
92
  {@render children?.()}
88
93
  </FABMenu>
89
- {/if}
90
- </div>
94
+ </DropdownMenu.Root>
95
+ {:else}
96
+ {@render fabButton({})}
97
+ {/if}
@@ -1,42 +1,37 @@
1
1
  <!--
2
2
  @component
3
- A menu container for multiple FAB actions.
3
+ The floating panel of a FAB menu. Rendered by `FAB` when `withMenu` is set — must live inside
4
+ its `DropdownMenu.Root`, which owns open state, positioning, and outside-click/Escape dismissal.
4
5
  Should be used with `FABMenuItem` as children.
5
6
  -->
6
7
  <script lang="ts">
7
8
  import clsx from 'clsx';
8
- import { flip } from '@floating-ui/dom';
9
+ import { DropdownMenu } from 'bits-ui';
9
10
  import { fabMenu } from './theme.js';
10
11
  import type { FABMenuProps } from './types.js';
11
- import { enterExit } from '../../animation/enterExit.js';
12
- import { easeEmphasizedAccel, easeEmphasizedDecel } from '../../animation/easing.js';
13
- import { floating, portal } from '../../actions/index.js';
12
+ import { enterExit, presence } from '../../animation/index.js';
14
13
 
15
- let { children, class: className, anchorEl, ...restProps }: FABMenuProps = $props();
14
+ let { children, class: className, ...restProps }: FABMenuProps = $props();
16
15
 
17
16
  const { base } = fabMenu();
18
17
  </script>
19
18
 
20
- <ul
21
- data-cy="m3-fab-menu"
22
- {...restProps}
23
- class={base({ class: clsx(className, 'fab-menu overflow-y-auto') })}
24
- in:enterExit={{ duration: 300, easing: easeEmphasizedDecel, mode: 'scale' }}
25
- out:enterExit={{ duration: 200, easing: easeEmphasizedAccel, mode: 'scale' }}
26
- use:portal
27
- use:floating={{
28
- reference: anchorEl,
29
- strategy: 'fixed',
30
- placement: 'top-end',
31
- offsetPx: 8,
32
- middleware: [flip()]
33
- }}
34
- >
35
- {@render children()}
36
- </ul>
37
-
38
- <style>
39
- .fab-menu {
40
- transform-origin: bottom right;
41
- }
42
- </style>
19
+ <DropdownMenu.Portal>
20
+ <DropdownMenu.Content side="top" align="end" sideOffset={8} collisionPadding={8}>
21
+ {#snippet child({ wrapperProps, props, open })}
22
+ <div {...wrapperProps}>
23
+ <div
24
+ data-cy="m3-fab-menu"
25
+ {...restProps}
26
+ {...props}
27
+ class={base({
28
+ class: clsx(className, 'overflow-y-auto')
29
+ })}
30
+ {@attach presence(() => open, enterExit.scale)}
31
+ >
32
+ {@render children()}
33
+ </div>
34
+ </div>
35
+ {/snippet}
36
+ </DropdownMenu.Content>
37
+ </DropdownMenu.Portal>
@@ -1,6 +1,7 @@
1
1
  import type { FABMenuProps } from './types.js';
2
2
  /**
3
- * A menu container for multiple FAB actions.
3
+ * The floating panel of a FAB menu. Rendered by `FAB` when `withMenu` is set — must live inside
4
+ * its `DropdownMenu.Root`, which owns open state, positioning, and outside-click/Escape dismissal.
4
5
  * Should be used with `FABMenuItem` as children.
5
6
  */
6
7
  declare const FABMenu: import("svelte").Component<FABMenuProps, {}, "">;
@@ -1,13 +1,15 @@
1
1
  <!--
2
2
  @component
3
- An individual action within a FAB menu.
3
+ An individual action within a FAB menu. Wraps bits-ui's `DropdownMenu.Item`, so it gets the
4
+ `menuitem` role, arrow-key navigation, and closes the menu when selected. Must be rendered
5
+ inside a `FAB` with `withMenu`.
4
6
  -->
5
7
  <script lang="ts">
6
8
  import clsx from 'clsx';
7
9
  import { fabMenuItem } from './theme.js';
8
10
  import type { FABMenuItemProps } from './types.js';
9
11
  import { Icon, LoadingIndicator, Layer } from '../../utils/index.js';
10
- import { Button, type ButtonRootProps } from 'bits-ui';
12
+ import { Button, DropdownMenu, type DropdownMenuItemProps } from 'bits-ui';
11
13
 
12
14
  let {
13
15
  class: className,
@@ -16,6 +18,7 @@ An individual action within a FAB menu.
16
18
  children,
17
19
  iconProps,
18
20
  formaction,
21
+ disabled,
19
22
  ...restProps
20
23
  }: FABMenuItemProps = $props();
21
24
 
@@ -23,21 +26,21 @@ An individual action within a FAB menu.
23
26
  const btnCls = $derived(base({ class: clsx(className) }));
24
27
  </script>
25
28
 
26
- <Button.Root
27
- {formaction}
28
- class={btnCls}
29
- data-cy="m3-fab-menu-item"
30
- {...restProps as ButtonRootProps}
31
- >
32
- {#if iconProps}
33
- {#if loading}
34
- <LoadingIndicator />
35
- {:else}
36
- <Icon class={icon()} {...iconProps} />
37
- {/if}
38
- {:else if loading}
39
- <LoadingIndicator />
40
- {/if}
41
- {@render children?.()}
42
- <Layer />
43
- </Button.Root>
29
+ <!-- Spread onto the Item, not the button: bits-ui merges (and chains handlers of) these into `props`. -->
30
+ <DropdownMenu.Item {disabled} {...restProps as DropdownMenuItemProps}>
31
+ {#snippet child({ props })}
32
+ <Button.Root {formaction} {disabled} data-cy="m3-fab-menu-item" {...props} class={btnCls}>
33
+ {#if iconProps}
34
+ {#if loading}
35
+ <LoadingIndicator />
36
+ {:else}
37
+ <Icon class={icon()} {...iconProps} />
38
+ {/if}
39
+ {:else if loading}
40
+ <LoadingIndicator />
41
+ {/if}
42
+ {@render children?.()}
43
+ <Layer />
44
+ </Button.Root>
45
+ {/snippet}
46
+ </DropdownMenu.Item>
@@ -1,5 +1,9 @@
1
1
  import type { FABMenuItemProps } from './types.js';
2
- /** An individual action within a FAB menu. */
2
+ /**
3
+ * An individual action within a FAB menu. Wraps bits-ui's `DropdownMenu.Item`, so it gets the
4
+ * `menuitem` role, arrow-key navigation, and closes the menu when selected. Must be rendered
5
+ * inside a `FAB` with `withMenu`.
6
+ */
3
7
  declare const FABMenuItem: import("svelte").Component<FABMenuItemProps, {}, "">;
4
8
  type FABMenuItem = ReturnType<typeof FABMenuItem>;
5
9
  export default FABMenuItem;
@@ -15,10 +15,9 @@ selectable items, no menu).
15
15
  @see https://m3.material.io/components/buttons/guidelines#834f5f31-e363-4d0f-bc4d-abac128f5f0e
16
16
  -->
17
17
  <script lang="ts">
18
+ import { enterExit, presence } from '../../../animation/index.js';
18
19
  import { DropdownMenu } from 'bits-ui';
19
20
  import clsx from 'clsx';
20
- import { easeEmphasizedDecel } from '../../../animation/easing.js';
21
- import { enterExit } from '../../../animation/enterExit.js';
22
21
  import Button from '../Button.svelte';
23
22
  import ButtonIcon from '../ButtonIcon.svelte';
24
23
  import { buttonIcon } from '../theme.js';
@@ -80,23 +79,17 @@ selectable items, no menu).
80
79
  </DropdownMenu.Trigger>
81
80
 
82
81
  <DropdownMenu.Portal>
83
- <DropdownMenu.Content forceMount {align} sideOffset={4} class="z-100">
82
+ <DropdownMenu.Content {align} sideOffset={4} class="z-100">
84
83
  {#snippet child({ wrapperProps, props, open: isOpen })}
85
- {#if isOpen}
86
- <div {...wrapperProps} class={wrapperProps.class as any}>
87
- <div
88
- {...props}
89
- class="bg-md-sys-color-surface-container-high shadow-elevation-3 ring-md-sys-color-outline/10 max-w-sm min-w-48 gap-1 overflow-y-auto rounded-lg px-2 py-1"
90
- transition:enterExit={{
91
- duration: 200,
92
- easing: easeEmphasizedDecel,
93
- mode: 'scale'
94
- }}
95
- >
96
- {@render menuChildren()}
97
- </div>
84
+ <div {...wrapperProps} class={wrapperProps.class as any}>
85
+ <div
86
+ {...props}
87
+ class="bg-md-sys-color-surface-container-high shadow-elevation-3 ring-md-sys-color-outline/10 max-w-sm min-w-48 gap-1 overflow-y-auto rounded-lg px-2 py-1"
88
+ {@attach presence(() => isOpen, enterExit.scale)}
89
+ >
90
+ {@render menuChildren()}
98
91
  </div>
99
- {/if}
92
+ </div>
100
93
  {/snippet}
101
94
  </DropdownMenu.Content>
102
95
  </DropdownMenu.Portal>
@@ -12,7 +12,7 @@ export const SPLIT_BUTTON_INNER_RADIUS = {
12
12
  export const splitButton = tv({
13
13
  slots: {
14
14
  root: 'inline-flex items-center gap-0.5',
15
- icon: 'transition-transform duration-200'
15
+ icon: 'transition-transform md-sys-motion-fast-spatial'
16
16
  },
17
17
  variants: {
18
18
  open: {
@@ -146,9 +146,7 @@ export type FABProps = FABVariants & AnchorButtonAttributes & {
146
146
  */
147
147
  children?: Snippet;
148
148
  };
149
- export type FABMenuProps = HTMLAttributes<HTMLUListElement> & {
150
- /** The FAB button element to position the menu relative to. */
151
- anchorEl: HTMLElement;
149
+ export type FABMenuProps = HTMLAttributes<HTMLDivElement> & {
152
150
  /**
153
151
  * The menu items. Use `FABMenuItem` components.
154
152
  */
@@ -159,6 +157,8 @@ export type FABMenuItemProps = FABMenuItemVariants & AnchorButtonAttributes & {
159
157
  * The visual variant of the menu item.
160
158
  */
161
159
  variant?: FABMenuItemVariants['variant'];
160
+ /** Whether the menu item is disabled (skipped by keyboard navigation). */
161
+ disabled?: boolean;
162
162
  /** Whether the menu item is in a loading state. */
163
163
  loading?: boolean;
164
164
  /**
@@ -1,7 +1,7 @@
1
1
  import { tv } from 'tailwind-variants';
2
2
  export const card = tv({
3
3
  slots: {
4
- base: 'relative flex flex-col rounded-lg disabled:opacity-[0.38] disabled:bg-md-sys-color-surface-variant state-layer before:rounded-lg [transition:box-shadow_var(--md-sys-motion-duration,200ms)_var(--md-sys-motion-timing-function,cubic-bezier(0.34,0.8,0.34,1))]',
4
+ base: 'relative flex flex-col rounded-lg disabled:opacity-[0.38] disabled:bg-md-sys-color-surface-variant state-layer before:rounded-lg transition-shadow md-sys-motion-effects',
5
5
  icon: 'size-6 text-[24px] text-md-sys-color-primary'
6
6
  },
7
7
  variants: {
@@ -1,9 +1,7 @@
1
1
  <!--
2
- BottomSheet has no `open` prop of its own — mounting it calls the native <dialog>'s
3
- showModal() immediately, so visibility is controlled by conditionally rendering the component
4
- ({#if open}<BottomSheet ...>{/if}). `close` is a required callback the sheet invokes on Esc,
5
- backdrop click, or dragging the handle down below a minimum height ('esc' | 'click' | 'low') —
6
- the consumer is expected to flip its own `open` flag to false in response.
2
+ BottomSheet is controlled with `bind:open`: it dismisses itself on Esc, backdrop click, or
3
+ dragging the handle below a minimum height, and stays mounted until its exit animation
4
+ finishes. The optional `close` callback reports why ('esc' | 'click' | 'low').
7
5
  -->
8
6
  <script module lang="ts">
9
7
  import { defineMeta } from '@storybook/addon-svelte-csf';
@@ -24,12 +22,10 @@
24
22
  <Story name="Playground" asChild>
25
23
  <div class="p-6">
26
24
  <Button onclick={() => (playgroundOpen = true)}>Open bottom sheet</Button>
27
- {#if playgroundOpen}
28
- <BottomSheet close={() => (playgroundOpen = false)}>
29
- <MenuItem iconProps={{ name: 'share' }}>Share</MenuItem>
30
- <MenuItem iconProps={{ name: 'link' }}>Copy link</MenuItem>
31
- <MenuItem iconProps={{ name: 'download' }}>Download</MenuItem>
32
- </BottomSheet>
33
- {/if}
25
+ <BottomSheet bind:open={playgroundOpen}>
26
+ <MenuItem iconProps={{ name: 'share' }}>Share</MenuItem>
27
+ <MenuItem iconProps={{ name: 'link' }}>Copy link</MenuItem>
28
+ <MenuItem iconProps={{ name: 'download' }}>Download</MenuItem>
29
+ </BottomSheet>
34
30
  </div>
35
31
  </Story>
@@ -9,31 +9,37 @@ while keeping the main screen content visible.
9
9
  -->
10
10
  <script lang="ts">
11
11
  import type { BottomSheetProps } from './types.js';
12
- import type { TransitionConfig } from 'svelte/transition';
13
- import { easeEmphasizedAccel, easeEmphasizedDecel } from '../../../animation/easing.js';
14
- import { outroClass } from '../../../animation/outroClass.js';
12
+ import { untrack } from 'svelte';
13
+ import { enterExit, Presence } from '../../../animation/index.js';
15
14
 
16
- let { children, close }: BottomSheetProps = $props();
15
+ const DEFAULT_HEIGHT = 480;
16
+ const DISMISS_HEIGHT = 48;
17
17
 
18
- let height = $state(480);
18
+ let { children, open = $bindable(true), close }: BottomSheetProps = $props();
19
+
20
+ let height = $state(DEFAULT_HEIGHT);
19
21
  let container: HTMLDivElement | undefined = $state();
20
22
  let isDragging = $state(false);
21
23
  let startY = $state(0);
22
24
 
23
- const open = (node: HTMLDialogElement) => node.showModal();
25
+ const sheet = new Presence(() => open);
26
+
27
+ // untrack: re-running this attachment on every drag would call showModal() on an open dialog.
28
+ const showModal = (node: HTMLDialogElement) => {
29
+ untrack(() => node.clientHeight < height && (height = node.clientHeight));
30
+ node.showModal();
31
+ };
24
32
 
25
- const heightAnim = (
26
- node: HTMLDialogElement,
27
- options: { duration: number; easing: typeof easeEmphasizedDecel }
28
- ): TransitionConfig => {
29
- if (node.clientHeight < height) height = node.clientHeight;
30
- return {
31
- duration: options.duration,
32
- easing: options.easing,
33
- css: (t) => `max-height: ${t * height}px`
34
- };
33
+ const dismiss = (reason: 'esc' | 'click' | 'low') => {
34
+ open = false;
35
+ close?.(reason);
35
36
  };
36
37
 
38
+ // A sheet dragged shut keeps its collapsed height through the exit; restore it on reopen.
39
+ $effect.pre(() => {
40
+ if (open) untrack(() => height < DISMISS_HEIGHT && (height = DEFAULT_HEIGHT));
41
+ });
42
+
37
43
  const moveWheel = (e: WheelEvent) => {
38
44
  e.preventDefault();
39
45
  height += e.deltaY;
@@ -49,7 +55,7 @@ while keeping the main screen content visible.
49
55
  };
50
56
 
51
57
  $effect(() => {
52
- if (height < 48) close('low');
58
+ if (height < DISMISS_HEIGHT && untrack(() => open)) dismiss('low');
53
59
  });
54
60
  </script>
55
61
 
@@ -60,51 +66,57 @@ while keeping the main screen content visible.
60
66
  ontouchend={() => (isDragging = false)}
61
67
  />
62
68
 
63
- <dialog
64
- class="bg-md-sys-color-surface-container-low text-md-sys-color-on-surface mx-auto mt-auto w-full max-w-2xl overflow-hidden rounded-t-md"
65
- style:max-height="{height}px"
66
- use:open
67
- use:outroClass
68
- oncancel={(e) => {
69
- e.preventDefault();
70
- close('esc');
71
- }}
72
- onmousedown={(e) => {
73
- if (e.target != e.currentTarget) return;
74
- close('click');
75
- }}
76
- onwheel={moveWheel}
77
- in:heightAnim={{ easing: easeEmphasizedDecel, duration: 400 }}
78
- out:heightAnim={{ easing: easeEmphasizedAccel, duration: 300 }}
79
- >
80
- <div class="px-4" bind:this={container}>
81
- <!-- svelte-ignore a11y_no_static_element_interactions -->
82
- <div
83
- class="flex h-12 w-full cursor-grab items-center justify-center"
84
- onmousedown={(e) => {
85
- e.preventDefault();
86
- isDragging = true;
87
- startY = e.clientY;
88
- }}
89
- ontouchstart={(e) => {
90
- isDragging = true;
91
- startY = e.touches[0].clientY;
92
- }}
93
- >
94
- <div class="bg-md-sys-color-on-surface-variant/40 h-1 w-8 rounded-full"></div>
69
+ {#if sheet.mounted}
70
+ <dialog
71
+ class="bg-md-sys-color-surface-container-low text-md-sys-color-on-surface mx-auto mt-auto w-full max-w-2xl overflow-hidden rounded-t-md"
72
+ style:max-height="{height}px"
73
+ data-state={open ? 'open' : 'closed'}
74
+ {@attach showModal}
75
+ {@attach sheet.attach(enterExit.bottomSheet)}
76
+ oncancel={(e) => {
77
+ e.preventDefault();
78
+ dismiss('esc');
79
+ }}
80
+ onmousedown={(e) => {
81
+ if (e.target != e.currentTarget) return;
82
+ dismiss('click');
83
+ }}
84
+ onwheel={moveWheel}
85
+ >
86
+ <div class="px-4" bind:this={container}>
87
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
88
+ <div
89
+ class="flex h-12 w-full cursor-grab items-center justify-center"
90
+ onmousedown={(e) => {
91
+ e.preventDefault();
92
+ isDragging = true;
93
+ startY = e.clientY;
94
+ }}
95
+ ontouchstart={(e) => {
96
+ isDragging = true;
97
+ startY = e.touches[0].clientY;
98
+ }}
99
+ >
100
+ <div class="bg-md-sys-color-on-surface-variant/40 h-1 w-8 rounded-full"></div>
101
+ </div>
102
+ {@render children()}
95
103
  </div>
96
- {@render children()}
97
- </div>
98
- </dialog>
104
+ </dialog>
105
+ {/if}
99
106
 
100
107
  <style>
101
108
  dialog::backdrop {
102
109
  background-color: rgba(0, 0, 0, 0.5);
103
- animation: backdrop 400ms;
110
+ animation: backdrop var(--md-sys-motion-duration-effects-spring)
111
+ var(--md-sys-motion-timing-function-effects-spring);
112
+ }
113
+ dialog[data-state='closed'] {
114
+ pointer-events: none;
104
115
  }
105
- dialog:global(.leaving)::backdrop {
116
+ dialog[data-state='closed']::backdrop {
106
117
  background-color: transparent;
107
- animation: backdropReverse 400ms;
118
+ animation: backdropReverse var(--md-sys-motion-duration-fast-effects-spring)
119
+ var(--md-sys-motion-timing-function-fast-effects-spring);
108
120
  }
109
121
  @keyframes backdrop {
110
122
  from {
@@ -7,6 +7,6 @@ import type { BottomSheetProps } from './types.js';
7
7
  *
8
8
  * @see https://m3.material.io/components/bottom-sheets/overview
9
9
  */
10
- declare const BottomSheet: import("svelte").Component<BottomSheetProps, {}, "">;
10
+ declare const BottomSheet: import("svelte").Component<BottomSheetProps, {}, "open">;
11
11
  type BottomSheet = ReturnType<typeof BottomSheet>;
12
12
  export default BottomSheet;
@@ -9,8 +9,14 @@ export interface BottomSheetProps {
9
9
  */
10
10
  children: Snippet;
11
11
  /**
12
- * Callback function to close the bottom sheet.
12
+ * Whether the sheet is shown. Bind it (`bind:open`) so the sheet can play its exit animation
13
+ * before unmounting.
14
+ * @default true
15
+ */
16
+ open?: boolean;
17
+ /**
18
+ * Called when the user dismisses the sheet.
13
19
  * @param reason The reason for closing ('esc' key, 'click' outside, or 'low' drag height).
14
20
  */
15
- close: (reason: 'esc' | 'click' | 'low') => void;
21
+ close?: (reason: 'esc' | 'click' | 'low') => void;
16
22
  }