@proyecto-viviana/solidaria-components 0.2.5 → 0.3.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 (225) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +39 -272
  3. package/dist/ActionBar.d.ts +79 -0
  4. package/dist/ActionBar.d.ts.map +1 -0
  5. package/dist/ActionGroup.d.ts +74 -0
  6. package/dist/ActionGroup.d.ts.map +1 -0
  7. package/dist/Alert.d.ts +70 -0
  8. package/dist/Alert.d.ts.map +1 -0
  9. package/dist/Autocomplete.d.ts +5 -5
  10. package/dist/Autocomplete.d.ts.map +1 -1
  11. package/dist/Breadcrumbs.d.ts +27 -8
  12. package/dist/Breadcrumbs.d.ts.map +1 -1
  13. package/dist/Button.d.ts +28 -5
  14. package/dist/Button.d.ts.map +1 -1
  15. package/dist/Calendar.d.ts +51 -7
  16. package/dist/Calendar.d.ts.map +1 -1
  17. package/dist/Checkbox.d.ts +33 -8
  18. package/dist/Checkbox.d.ts.map +1 -1
  19. package/dist/Collection.d.ts +130 -0
  20. package/dist/Collection.d.ts.map +1 -0
  21. package/dist/Color.d.ts +210 -9
  22. package/dist/Color.d.ts.map +1 -1
  23. package/dist/ColorEditor.d.ts +42 -0
  24. package/dist/ColorEditor.d.ts.map +1 -0
  25. package/dist/ComboBox.d.ts +146 -16
  26. package/dist/ComboBox.d.ts.map +1 -1
  27. package/dist/ContextualHelpTrigger.d.ts +40 -0
  28. package/dist/ContextualHelpTrigger.d.ts.map +1 -0
  29. package/dist/DateField.d.ts +35 -8
  30. package/dist/DateField.d.ts.map +1 -1
  31. package/dist/DatePicker.d.ts +101 -5
  32. package/dist/DatePicker.d.ts.map +1 -1
  33. package/dist/DateRangePickerContext.d.ts +30 -0
  34. package/dist/DateRangePickerContext.d.ts.map +1 -0
  35. package/dist/Dialog.d.ts +5 -5
  36. package/dist/Dialog.d.ts.map +1 -1
  37. package/dist/Disclosure.d.ts +25 -5
  38. package/dist/Disclosure.d.ts.map +1 -1
  39. package/dist/DragAndDrop.d.ts +80 -0
  40. package/dist/DragAndDrop.d.ts.map +1 -0
  41. package/dist/DragPreview.d.ts +14 -0
  42. package/dist/DragPreview.d.ts.map +1 -0
  43. package/dist/DropZone.d.ts +27 -0
  44. package/dist/DropZone.d.ts.map +1 -0
  45. package/dist/FieldError.d.ts +27 -0
  46. package/dist/FieldError.d.ts.map +1 -0
  47. package/dist/FileTrigger.d.ts +26 -0
  48. package/dist/FileTrigger.d.ts.map +1 -0
  49. package/dist/Focusable.d.ts +27 -0
  50. package/dist/Focusable.d.ts.map +1 -0
  51. package/dist/Form.d.ts +41 -0
  52. package/dist/Form.d.ts.map +1 -0
  53. package/dist/GridList.d.ts +69 -10
  54. package/dist/GridList.d.ts.map +1 -1
  55. package/dist/HiddenDateInput.d.ts +26 -0
  56. package/dist/HiddenDateInput.d.ts.map +1 -0
  57. package/dist/HiddenTimeInput.d.ts +25 -0
  58. package/dist/HiddenTimeInput.d.ts.map +1 -0
  59. package/dist/Icon.d.ts +57 -0
  60. package/dist/Icon.d.ts.map +1 -0
  61. package/dist/Keyboard.d.ts +13 -0
  62. package/dist/Keyboard.d.ts.map +1 -0
  63. package/dist/Landmark.d.ts +3 -3
  64. package/dist/Landmark.d.ts.map +1 -1
  65. package/dist/Link.d.ts +10 -4
  66. package/dist/Link.d.ts.map +1 -1
  67. package/dist/ListBox.d.ts +73 -11
  68. package/dist/ListBox.d.ts.map +1 -1
  69. package/dist/ListDropTargetDelegate.d.ts +38 -0
  70. package/dist/ListDropTargetDelegate.d.ts.map +1 -0
  71. package/dist/Menu.d.ts +79 -10
  72. package/dist/Menu.d.ts.map +1 -1
  73. package/dist/Meter.d.ts +4 -4
  74. package/dist/Meter.d.ts.map +1 -1
  75. package/dist/Modal.d.ts +6 -4
  76. package/dist/Modal.d.ts.map +1 -1
  77. package/dist/NumberField.d.ts +10 -12
  78. package/dist/NumberField.d.ts.map +1 -1
  79. package/dist/Popover.d.ts +32 -7
  80. package/dist/Popover.d.ts.map +1 -1
  81. package/dist/Pressable.d.ts +27 -0
  82. package/dist/Pressable.d.ts.map +1 -0
  83. package/dist/ProgressBar.d.ts +6 -4
  84. package/dist/ProgressBar.d.ts.map +1 -1
  85. package/dist/RadioGroup.d.ts +43 -9
  86. package/dist/RadioGroup.d.ts.map +1 -1
  87. package/dist/RangeCalendar.d.ts +39 -7
  88. package/dist/RangeCalendar.d.ts.map +1 -1
  89. package/dist/RouterProvider.d.ts +75 -0
  90. package/dist/RouterProvider.d.ts.map +1 -0
  91. package/dist/SearchField.d.ts +23 -21
  92. package/dist/SearchField.d.ts.map +1 -1
  93. package/dist/Select.d.ts +48 -7
  94. package/dist/Select.d.ts.map +1 -1
  95. package/dist/SelectionIndicator.d.ts +30 -0
  96. package/dist/SelectionIndicator.d.ts.map +1 -0
  97. package/dist/Separator.d.ts +9 -3
  98. package/dist/Separator.d.ts.map +1 -1
  99. package/dist/SharedElementTransition.d.ts +41 -0
  100. package/dist/SharedElementTransition.d.ts.map +1 -0
  101. package/dist/Slider.d.ts +15 -8
  102. package/dist/Slider.d.ts.map +1 -1
  103. package/dist/StepList.d.ts +90 -0
  104. package/dist/StepList.d.ts.map +1 -0
  105. package/dist/Switch.d.ts +11 -5
  106. package/dist/Switch.d.ts.map +1 -1
  107. package/dist/Table.d.ts +222 -19
  108. package/dist/Table.d.ts.map +1 -1
  109. package/dist/Tabs.d.ts +47 -10
  110. package/dist/Tabs.d.ts.map +1 -1
  111. package/dist/TagGroup.d.ts +22 -10
  112. package/dist/TagGroup.d.ts.map +1 -1
  113. package/dist/Text.d.ts +10 -0
  114. package/dist/Text.d.ts.map +1 -0
  115. package/dist/TextField.d.ts +19 -11
  116. package/dist/TextField.d.ts.map +1 -1
  117. package/dist/TimeField.d.ts +32 -7
  118. package/dist/TimeField.d.ts.map +1 -1
  119. package/dist/Toast.d.ts +29 -14
  120. package/dist/Toast.d.ts.map +1 -1
  121. package/dist/ToggleButton.d.ts +36 -0
  122. package/dist/ToggleButton.d.ts.map +1 -0
  123. package/dist/ToggleButtonGroup.d.ts +33 -0
  124. package/dist/ToggleButtonGroup.d.ts.map +1 -0
  125. package/dist/Toolbar.d.ts +7 -3
  126. package/dist/Toolbar.d.ts.map +1 -1
  127. package/dist/Tooltip.d.ts +58 -7
  128. package/dist/Tooltip.d.ts.map +1 -1
  129. package/dist/Tree.d.ts +102 -11
  130. package/dist/Tree.d.ts.map +1 -1
  131. package/dist/Virtualizer.d.ts +61 -0
  132. package/dist/Virtualizer.d.ts.map +1 -0
  133. package/dist/VirtualizerLayouts.d.ts +82 -0
  134. package/dist/VirtualizerLayouts.d.ts.map +1 -0
  135. package/dist/VisuallyHidden.d.ts +4 -2
  136. package/dist/VisuallyHidden.d.ts.map +1 -1
  137. package/dist/contexts.d.ts +6 -1
  138. package/dist/contexts.d.ts.map +1 -1
  139. package/dist/index.d.ts +73 -39
  140. package/dist/index.d.ts.map +1 -1
  141. package/dist/index.js +23342 -10644
  142. package/dist/index.js.map +1 -7
  143. package/dist/index.jsx +18110 -0
  144. package/dist/index.jsx.map +1 -0
  145. package/dist/useDragAndDrop.d.ts +93 -0
  146. package/dist/useDragAndDrop.d.ts.map +1 -0
  147. package/dist/utils.d.ts +8 -2
  148. package/dist/utils.d.ts.map +1 -1
  149. package/dist/virtualizer/Layout.d.ts +79 -0
  150. package/dist/virtualizer/Layout.d.ts.map +1 -0
  151. package/package.json +33 -32
  152. package/src/ActionBar.tsx +251 -0
  153. package/src/ActionGroup.tsx +277 -0
  154. package/src/Alert.tsx +152 -0
  155. package/src/Autocomplete.tsx +39 -44
  156. package/src/Breadcrumbs.tsx +227 -72
  157. package/src/Button.tsx +315 -74
  158. package/src/Calendar.tsx +347 -141
  159. package/src/Checkbox.tsx +414 -123
  160. package/src/Collection.tsx +350 -0
  161. package/src/Color.tsx +1325 -284
  162. package/src/ColorEditor.tsx +213 -0
  163. package/src/ComboBox.tsx +644 -245
  164. package/src/ContextualHelpTrigger.tsx +195 -0
  165. package/src/DateField.tsx +274 -106
  166. package/src/DatePicker.tsx +892 -111
  167. package/src/DateRangePickerContext.tsx +44 -0
  168. package/src/Dialog.tsx +173 -104
  169. package/src/Disclosure.tsx +158 -105
  170. package/src/DragAndDrop.tsx +340 -0
  171. package/src/DragPreview.tsx +47 -0
  172. package/src/DropZone.tsx +233 -0
  173. package/src/FieldError.tsx +89 -0
  174. package/src/FileTrigger.tsx +83 -0
  175. package/src/Focusable.tsx +103 -0
  176. package/src/Form.tsx +140 -0
  177. package/src/GridList.tsx +542 -128
  178. package/src/HiddenDateInput.tsx +153 -0
  179. package/src/HiddenTimeInput.tsx +133 -0
  180. package/src/Icon.tsx +133 -0
  181. package/src/Keyboard.tsx +26 -0
  182. package/src/Landmark.tsx +37 -63
  183. package/src/Link.tsx +132 -69
  184. package/src/ListBox.tsx +656 -106
  185. package/src/ListDropTargetDelegate.ts +283 -0
  186. package/src/Menu.tsx +1234 -132
  187. package/src/Meter.tsx +44 -58
  188. package/src/Modal.tsx +262 -166
  189. package/src/NumberField.tsx +267 -151
  190. package/src/Popover.tsx +452 -343
  191. package/src/Pressable.tsx +108 -0
  192. package/src/ProgressBar.tsx +54 -59
  193. package/src/RadioGroup.tsx +533 -121
  194. package/src/RangeCalendar.tsx +249 -150
  195. package/src/RouterProvider.tsx +223 -0
  196. package/src/SearchField.tsx +460 -133
  197. package/src/Select.tsx +804 -233
  198. package/src/SelectionIndicator.tsx +108 -0
  199. package/src/Separator.tsx +47 -49
  200. package/src/SharedElementTransition.tsx +264 -0
  201. package/src/Slider.tsx +148 -98
  202. package/src/StepList.tsx +272 -0
  203. package/src/Switch.tsx +93 -46
  204. package/src/Table.tsx +1551 -225
  205. package/src/Tabs.tsx +377 -123
  206. package/src/TagGroup.tsx +233 -135
  207. package/src/Text.tsx +18 -0
  208. package/src/TextField.tsx +413 -86
  209. package/src/TimeField.tsx +232 -222
  210. package/src/Toast.tsx +306 -160
  211. package/src/ToggleButton.tsx +169 -0
  212. package/src/ToggleButtonGroup.tsx +141 -0
  213. package/src/Toolbar.tsx +61 -70
  214. package/src/Tooltip.tsx +473 -116
  215. package/src/Tree.tsx +1514 -175
  216. package/src/Virtualizer.tsx +730 -0
  217. package/src/VirtualizerLayouts.ts +280 -0
  218. package/src/VisuallyHidden.tsx +32 -38
  219. package/src/contexts.ts +29 -36
  220. package/src/index.ts +972 -620
  221. package/src/useDragAndDrop.ts +367 -0
  222. package/src/utils.tsx +69 -50
  223. package/src/virtualizer/Layout.ts +192 -0
  224. package/dist/index.ssr.js +0 -9785
  225. package/dist/index.ssr.js.map +0 -7
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Shared SelectionIndicator primitive for selected collection items.
3
+ */
4
+
5
+ import { type JSX, createContext, createMemo, splitProps, useContext, Show } from "solid-js";
6
+ import {
7
+ type RenderChildren,
8
+ type ClassNameOrFunction,
9
+ type StyleOrFunction,
10
+ type SlotProps,
11
+ useRenderProps,
12
+ } from "./utils";
13
+ import { SharedElement, useHasSharedElementTransitionScope } from "./SharedElementTransition";
14
+
15
+ export interface SelectionIndicatorContextValue {
16
+ isSelected: () => boolean;
17
+ }
18
+
19
+ export const SelectionIndicatorContext = createContext<SelectionIndicatorContextValue | null>(null);
20
+
21
+ export interface SelectionIndicatorRenderProps {
22
+ /** Whether the parent item is selected. */
23
+ isSelected: boolean;
24
+ }
25
+
26
+ export interface SelectionIndicatorProps
27
+ extends SlotProps, Omit<JSX.HTMLAttributes<HTMLSpanElement>, "class" | "style" | "children"> {
28
+ /** Optional controlled selected state override. */
29
+ isSelected?: boolean;
30
+ /** Whether to keep mounted when not selected. */
31
+ shouldForceMount?: boolean;
32
+ /** The children content. */
33
+ children?: RenderChildren<SelectionIndicatorRenderProps>;
34
+ /** The CSS className for the element. */
35
+ class?: ClassNameOrFunction<SelectionIndicatorRenderProps>;
36
+ /** The inline style for the element. */
37
+ style?: StyleOrFunction<SelectionIndicatorRenderProps>;
38
+ }
39
+
40
+ /**
41
+ * SelectionIndicator renders when its parent item is selected.
42
+ */
43
+ export function SelectionIndicator(props: SelectionIndicatorProps): JSX.Element {
44
+ const [local, domProps] = splitProps(props, [
45
+ "isSelected",
46
+ "shouldForceMount",
47
+ "children",
48
+ "class",
49
+ "style",
50
+ "slot",
51
+ ]);
52
+
53
+ const context = useContext(SelectionIndicatorContext);
54
+ const hasSharedElementScope = useHasSharedElementTransitionScope();
55
+ const isSelected = () => local.isSelected ?? context?.isSelected() ?? false;
56
+ const isVisible = () => local.shouldForceMount || isSelected();
57
+
58
+ const renderValues = createMemo<SelectionIndicatorRenderProps>(() => ({
59
+ isSelected: isSelected(),
60
+ }));
61
+
62
+ const renderProps = useRenderProps(
63
+ {
64
+ children: local.children,
65
+ class: local.class,
66
+ style: local.style,
67
+ defaultClassName: "solidaria-SelectionIndicator",
68
+ },
69
+ renderValues,
70
+ );
71
+
72
+ const sharedElementProps = createMemo(() => {
73
+ const { ref: _ref, ...rest } = domProps as JSX.HTMLAttributes<HTMLSpanElement> & {
74
+ ref?: unknown;
75
+ };
76
+ return rest;
77
+ });
78
+
79
+ if (hasSharedElementScope) {
80
+ return (
81
+ <SharedElement
82
+ {...sharedElementProps()}
83
+ name="SelectionIndicator"
84
+ isVisible={isVisible()}
85
+ aria-hidden="true"
86
+ class={renderProps.class()}
87
+ style={renderProps.style()}
88
+ data-selected={isSelected() || undefined}
89
+ >
90
+ {renderProps.renderChildren()}
91
+ </SharedElement>
92
+ );
93
+ }
94
+
95
+ return (
96
+ <Show when={isVisible()}>
97
+ <span
98
+ {...domProps}
99
+ aria-hidden="true"
100
+ class={renderProps.class()}
101
+ style={renderProps.style()}
102
+ data-selected={isSelected() || undefined}
103
+ >
104
+ {renderProps.renderChildren()}
105
+ </span>
106
+ </Show>
107
+ );
108
+ }
package/src/Separator.tsx CHANGED
@@ -5,51 +5,42 @@
5
5
  * Port of react-aria-components/src/Separator.tsx
6
6
  */
7
7
 
8
- import {
9
- type JSX,
10
- createContext,
11
- createMemo,
12
- splitProps,
13
- } from 'solid-js';
14
- import { Dynamic } from 'solid-js/web';
8
+ import { type JSX, createContext, createMemo, splitProps } from "solid-js";
9
+ import { Dynamic } from "solid-js/web";
15
10
  import {
16
11
  createSeparator,
17
12
  type AriaSeparatorProps,
18
13
  type Orientation,
19
- } from '@proyecto-viviana/solidaria';
20
- import {
21
- type SlotProps,
22
- filterDOMProps,
23
- } from './utils';
14
+ } from "@proyecto-viviana/solidaria";
15
+ import { type SlotProps, filterDOMProps } from "./utils";
16
+
17
+ type RefLike<T> = ((el: T) => void) | { current?: T | null } | undefined;
24
18
 
25
- // ============================================
26
- // TYPES
27
- // ============================================
19
+ function assignRef<T>(ref: RefLike<T>, el: T): void {
20
+ if (!ref) return;
21
+ if (typeof ref === "function") {
22
+ ref(el);
23
+ } else {
24
+ ref.current = el;
25
+ }
26
+ }
28
27
 
29
28
  export interface SeparatorRenderProps {
30
29
  /** The orientation of the separator. */
31
30
  orientation: Orientation;
32
31
  }
33
32
 
34
- export interface SeparatorProps
35
- extends AriaSeparatorProps,
36
- SlotProps {
33
+ export interface SeparatorProps extends AriaSeparatorProps, SlotProps {
37
34
  /** The CSS className for the element. A function may be provided to receive render props. */
38
35
  class?: string | ((renderProps: SeparatorRenderProps) => string);
39
36
  /** The inline style for the element. A function may be provided to receive render props. */
40
37
  style?: JSX.CSSProperties | ((renderProps: SeparatorRenderProps) => JSX.CSSProperties);
38
+ /** Ref for the underlying separator element. */
39
+ ref?: RefLike<HTMLElement>;
41
40
  }
42
41
 
43
- // ============================================
44
- // CONTEXT
45
- // ============================================
46
-
47
42
  export const SeparatorContext = createContext<SeparatorProps | null>(null);
48
43
 
49
- // ============================================
50
- // SEPARATOR COMPONENT
51
- // ============================================
52
-
53
44
  /**
54
45
  * A separator is a visual divider between two groups of content,
55
46
  * e.g. groups of menu items or sections of a page.
@@ -66,55 +57,61 @@ export const SeparatorContext = createContext<SeparatorProps | null>(null);
66
57
  * ```
67
58
  */
68
59
  export function Separator(props: SeparatorProps): JSX.Element {
69
- const [local, ariaProps] = splitProps(props, [
70
- 'class',
71
- 'style',
72
- 'slot',
73
- ]);
60
+ const [local, ariaProps] = splitProps(props, ["class", "style", "ref", "slot"]);
74
61
 
75
- // Determine the element type
76
62
  const elementType = createMemo(() => {
77
- let element = ariaProps.elementType || 'hr';
63
+ let element = ariaProps.elementType || "hr";
78
64
  // If vertical and using hr, switch to div since hr is inherently horizontal
79
- if (element === 'hr' && ariaProps.orientation === 'vertical') {
80
- element = 'div';
65
+ if (element === "hr" && ariaProps.orientation === "vertical") {
66
+ element = "div";
81
67
  }
82
68
  return element;
83
69
  });
84
70
 
85
- // Create separator aria props
86
71
  const separatorAria = createSeparator({
87
- get orientation() { return ariaProps.orientation; },
88
- get elementType() { return elementType(); },
89
- get 'aria-label'() { return ariaProps['aria-label']; },
90
- get 'aria-labelledby'() { return ariaProps['aria-labelledby']; },
91
- get id() { return ariaProps.id; },
72
+ get orientation() {
73
+ return ariaProps.orientation;
74
+ },
75
+ get elementType() {
76
+ return ariaProps.elementType;
77
+ },
78
+ get "aria-label"() {
79
+ return ariaProps["aria-label"];
80
+ },
81
+ get "aria-labelledby"() {
82
+ return ariaProps["aria-labelledby"];
83
+ },
84
+ get "aria-describedby"() {
85
+ return ariaProps["aria-describedby"];
86
+ },
87
+ get "aria-details"() {
88
+ return ariaProps["aria-details"];
89
+ },
90
+ get id() {
91
+ return ariaProps.id;
92
+ },
92
93
  });
93
94
 
94
- // Render props values
95
95
  const renderValues = createMemo<SeparatorRenderProps>(() => ({
96
- orientation: ariaProps.orientation ?? 'horizontal',
96
+ orientation: ariaProps.orientation ?? "horizontal",
97
97
  }));
98
98
 
99
- // Resolve class
100
99
  const resolvedClass = createMemo(() => {
101
100
  const cls = local.class;
102
- if (typeof cls === 'function') {
101
+ if (typeof cls === "function") {
103
102
  return cls(renderValues());
104
103
  }
105
- return cls ?? 'solidaria-Separator';
104
+ return cls ?? "solidaria-Separator";
106
105
  });
107
106
 
108
- // Resolve style
109
107
  const resolvedStyle = createMemo(() => {
110
108
  const style = local.style;
111
- if (typeof style === 'function') {
109
+ if (typeof style === "function") {
112
110
  return style(renderValues());
113
111
  }
114
112
  return style;
115
113
  });
116
114
 
117
- // Filter DOM props
118
115
  const domProps = createMemo(() => filterDOMProps(ariaProps, { global: true }));
119
116
 
120
117
  return (
@@ -122,6 +119,7 @@ export function Separator(props: SeparatorProps): JSX.Element {
122
119
  component={elementType()}
123
120
  {...domProps()}
124
121
  {...separatorAria.separatorProps}
122
+ ref={(el: HTMLElement) => assignRef(local.ref, el)}
125
123
  class={resolvedClass()}
126
124
  style={resolvedStyle()}
127
125
  slot={local.slot}
@@ -0,0 +1,264 @@
1
+ /**
2
+ * SharedElementTransition primitives for solidaria-components.
3
+ *
4
+ * Provides FLIP-based shared element animations when elements move between
5
+ * parents within a scope. Captures geometry snapshots on unmount and applies
6
+ * transition animations on mount.
7
+ *
8
+ * Parity target: react-aria-components/src/SharedElementTransition.tsx
9
+ */
10
+
11
+ import {
12
+ type JSX,
13
+ createContext,
14
+ createEffect,
15
+ createMemo,
16
+ createSignal,
17
+ onCleanup,
18
+ splitProps,
19
+ useContext,
20
+ Show,
21
+ on,
22
+ } from "solid-js";
23
+ import {
24
+ type ClassNameOrFunction,
25
+ type StyleOrFunction,
26
+ type RenderChildren,
27
+ useRenderProps,
28
+ filterDOMProps,
29
+ } from "./utils";
30
+
31
+ type SharedElementLifecycle = "hidden" | "entering" | "visible" | "exiting";
32
+
33
+ /** Safe wrapper — jsdom doesn't implement the Web Animations API. */
34
+ function getAnimations(el: HTMLElement): Animation[] {
35
+ return typeof el.getAnimations === "function" ? el.getAnimations() : [];
36
+ }
37
+
38
+ interface Snapshot {
39
+ rect: DOMRect;
40
+ style: [string, string][];
41
+ }
42
+
43
+ interface SharedElementScope {
44
+ snapshots: { [name: string]: Snapshot };
45
+ }
46
+
47
+ const SharedElementContext = createContext<SharedElementScope | null>(null);
48
+
49
+ export function useHasSharedElementTransitionScope(): boolean {
50
+ return useContext(SharedElementContext) != null;
51
+ }
52
+
53
+ export interface SharedElementTransitionProps {
54
+ children?: JSX.Element;
55
+ }
56
+
57
+ /**
58
+ * A scope for SharedElements, which animate between parents.
59
+ */
60
+ export function SharedElementTransition(props: SharedElementTransitionProps): JSX.Element {
61
+ const scope: SharedElementScope = {
62
+ snapshots: {},
63
+ };
64
+
65
+ return (
66
+ <SharedElementContext.Provider value={scope}>{props.children}</SharedElementContext.Provider>
67
+ );
68
+ }
69
+
70
+ export interface SharedElementRenderProps {
71
+ isEntering: boolean;
72
+ isExiting: boolean;
73
+ }
74
+
75
+ export interface SharedElementPropsBase extends Omit<
76
+ JSX.HTMLAttributes<HTMLDivElement>,
77
+ "children" | "class" | "style" | "ref"
78
+ > {
79
+ children?: RenderChildren<SharedElementRenderProps>;
80
+ class?: ClassNameOrFunction<SharedElementRenderProps>;
81
+ style?: StyleOrFunction<SharedElementRenderProps>;
82
+ }
83
+
84
+ export interface SharedElementProps extends SharedElementPropsBase {
85
+ name: string;
86
+ isVisible?: boolean;
87
+ ref?: ((el: HTMLDivElement) => void) | { current?: HTMLDivElement };
88
+ }
89
+
90
+ /**
91
+ * An element that animates between its old and new position when moving
92
+ * between parents within a SharedElementTransition scope.
93
+ */
94
+ export function SharedElement(props: SharedElementProps): JSX.Element | null {
95
+ const scope = useContext(SharedElementContext);
96
+ if (!scope) {
97
+ throw new Error("<SharedElement> must be rendered inside a <SharedElementTransition>");
98
+ }
99
+
100
+ const [local, domProps] = splitProps(props, [
101
+ "name",
102
+ "isVisible",
103
+ "children",
104
+ "class",
105
+ "style",
106
+ "ref",
107
+ ]);
108
+
109
+ const [lifecycle, setLifecycle] = createSignal<SharedElementLifecycle>(
110
+ local.isVisible === false ? "hidden" : "visible",
111
+ );
112
+
113
+ let elementRef: HTMLDivElement | undefined;
114
+ let frame: number | undefined;
115
+
116
+ const setRef = (el: HTMLDivElement) => {
117
+ elementRef = el;
118
+ // Forward ref to consumer
119
+ const userRef = local.ref;
120
+ if (typeof userRef === "function") {
121
+ userRef(el);
122
+ } else if (userRef !== undefined) {
123
+ userRef.current = el;
124
+ }
125
+ };
126
+
127
+ // Handle visibility transitions with FLIP animation
128
+ createEffect(
129
+ on(
130
+ () => local.isVisible !== false,
131
+ (isVisible) => {
132
+ const name = local.name;
133
+ const element = elementRef;
134
+
135
+ if (frame != null) {
136
+ cancelAnimationFrame(frame);
137
+ frame = undefined;
138
+ }
139
+
140
+ if (isVisible && element) {
141
+ const prevSnapshot = scope.snapshots[name];
142
+
143
+ if (prevSnapshot) {
144
+ // FLIP: Element is transitioning from a previous instance.
145
+ setLifecycle("visible");
146
+ const animations = getAnimations(element);
147
+
148
+ // Set properties to animate from.
149
+ const values = prevSnapshot.style.map(([property, prevValue]) => {
150
+ const value = element.style.getPropertyValue(property);
151
+ if (property === "translate") {
152
+ const prevRect = prevSnapshot.rect;
153
+ const currentRect = element.getBoundingClientRect();
154
+ const deltaX = prevRect.left - currentRect.left;
155
+ const deltaY = prevRect.top - currentRect.top;
156
+ element.style.setProperty("translate", `${deltaX}px ${deltaY}px`);
157
+ } else {
158
+ element.style.setProperty(property, prevValue);
159
+ }
160
+ return [property, value] as [string, string];
161
+ });
162
+
163
+ // Cancel any new animations triggered by these properties.
164
+ for (const a of getAnimations(element)) {
165
+ if (!animations.includes(a)) {
166
+ a.cancel();
167
+ }
168
+ }
169
+
170
+ // Remove overrides after one frame to animate to the current values.
171
+ frame = requestAnimationFrame(() => {
172
+ frame = undefined;
173
+ for (const [property, value] of values) {
174
+ element.style.setProperty(property, value);
175
+ }
176
+ });
177
+
178
+ delete scope.snapshots[name];
179
+ } else {
180
+ // No previous instance exists, apply the entering state.
181
+ queueMicrotask(() => setLifecycle("entering"));
182
+ frame = requestAnimationFrame(() => {
183
+ frame = undefined;
184
+ setLifecycle("visible");
185
+ });
186
+ }
187
+ } else if (!isVisible && element) {
188
+ // Wait a microtask to check if a snapshot still exists (meaning no new
189
+ // SharedElement consumed it), then enter exiting state.
190
+ queueMicrotask(() => {
191
+ if (scope.snapshots[name]) {
192
+ delete scope.snapshots[name];
193
+ setLifecycle("exiting");
194
+ // Wait for animations to finish before hiding.
195
+ Promise.all(getAnimations(element).map((a) => a.finished))
196
+ .then(() => setLifecycle("hidden"))
197
+ .catch(() => {});
198
+ } else {
199
+ // Snapshot was consumed by another instance, unmount immediately.
200
+ setLifecycle("hidden");
201
+ }
202
+ });
203
+ } else if (isVisible) {
204
+ // Element not yet in DOM, entering fresh
205
+ setLifecycle("entering");
206
+ frame = requestAnimationFrame(() => {
207
+ frame = undefined;
208
+ setLifecycle("visible");
209
+ });
210
+ }
211
+ },
212
+ ),
213
+ );
214
+
215
+ // Capture snapshot on cleanup (unmount)
216
+ onCleanup(() => {
217
+ if (frame != null) {
218
+ cancelAnimationFrame(frame);
219
+ }
220
+
221
+ const element = elementRef;
222
+ if (element && element.isConnected && !element.hasAttribute("data-exiting")) {
223
+ // Store a snapshot of the rectangle and computed style for transitioning properties.
224
+ const style = window.getComputedStyle(element);
225
+ if (style.transitionProperty !== "none") {
226
+ const transitionProperty = style.transitionProperty.split(/\s*,\s*/);
227
+ scope.snapshots[local.name] = {
228
+ rect: element.getBoundingClientRect(),
229
+ style: transitionProperty.map((property) => [property, style.getPropertyValue(property)]),
230
+ };
231
+ }
232
+ }
233
+ });
234
+
235
+ const renderProps = useRenderProps(
236
+ {
237
+ children: local.children,
238
+ class: local.class,
239
+ style: local.style,
240
+ defaultClassName: "solidaria-SharedElement",
241
+ },
242
+ () => ({
243
+ isEntering: lifecycle() === "entering",
244
+ isExiting: lifecycle() === "exiting",
245
+ }),
246
+ );
247
+
248
+ const filteredDomProps = createMemo(() => filterDOMProps(domProps, { global: true }));
249
+
250
+ return (
251
+ <Show when={lifecycle() !== "hidden"}>
252
+ <div
253
+ ref={setRef}
254
+ {...filteredDomProps()}
255
+ class={renderProps.class()}
256
+ style={renderProps.style()}
257
+ data-entering={lifecycle() === "entering" || undefined}
258
+ data-exiting={lifecycle() === "exiting" || undefined}
259
+ >
260
+ {renderProps.renderChildren()}
261
+ </div>
262
+ </Show>
263
+ );
264
+ }