@limonify/zest-ui 0.1.1 → 0.1.3

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 (62) hide show
  1. package/lib/accordion/index.d.ts +1 -0
  2. package/lib/accordion/index.d.ts.map +1 -1
  3. package/lib/accordion/index.js +3 -1
  4. package/lib/accordion/index.js.map +1 -1
  5. package/lib/accordion/panel/AccordionPanel.d.ts +19 -1
  6. package/lib/accordion/panel/AccordionPanel.d.ts.map +1 -1
  7. package/lib/accordion/panel/AccordionPanel.js +18 -2
  8. package/lib/accordion/panel/AccordionPanel.js.map +1 -1
  9. package/lib/collapsible/index.d.ts +1 -0
  10. package/lib/collapsible/index.d.ts.map +1 -1
  11. package/lib/collapsible/index.js +3 -1
  12. package/lib/collapsible/index.js.map +1 -1
  13. package/lib/collapsible/panel/CollapsiblePanel.d.ts +19 -1
  14. package/lib/collapsible/panel/CollapsiblePanel.d.ts.map +1 -1
  15. package/lib/collapsible/panel/CollapsiblePanel.js +18 -2
  16. package/lib/collapsible/panel/CollapsiblePanel.js.map +1 -1
  17. package/lib/collapsible/panel/CollapsiblePanelContext.d.ts +21 -0
  18. package/lib/collapsible/panel/CollapsiblePanelContext.d.ts.map +1 -0
  19. package/lib/collapsible/panel/CollapsiblePanelContext.js +48 -0
  20. package/lib/collapsible/panel/CollapsiblePanelContext.js.map +1 -0
  21. package/lib/collapsible/panel/useCollapsiblePanel.d.ts +17 -0
  22. package/lib/collapsible/panel/useCollapsiblePanel.d.ts.map +1 -1
  23. package/lib/collapsible/panel/useCollapsiblePanel.js +4 -2
  24. package/lib/collapsible/panel/useCollapsiblePanel.js.map +1 -1
  25. package/lib/combobox/portal/ComboboxPortal.js +1 -1
  26. package/lib/dialog/portal/DialogPortal.d.ts +4 -2
  27. package/lib/dialog/portal/DialogPortal.d.ts.map +1 -1
  28. package/lib/dialog/portal/DialogPortal.js +5 -3
  29. package/lib/dialog/portal/DialogPortal.js.map +1 -1
  30. package/lib/menu/portal/MenuPortal.js +1 -1
  31. package/lib/popover/portal/PopoverPortal.js +1 -1
  32. package/lib/select/portal/SelectPortal.js +1 -1
  33. package/lib/switch/thumb/SwitchThumb.d.ts +19 -2
  34. package/lib/switch/thumb/SwitchThumb.d.ts.map +1 -1
  35. package/lib/switch/thumb/SwitchThumb.js +21 -3
  36. package/lib/switch/thumb/SwitchThumb.js.map +1 -1
  37. package/lib/toast/root/ToastRoot.d.ts +20 -8
  38. package/lib/toast/root/ToastRoot.d.ts.map +1 -1
  39. package/lib/toast/root/ToastRoot.js +28 -8
  40. package/lib/toast/root/ToastRoot.js.map +1 -1
  41. package/lib/toast/store.js +1 -1
  42. package/lib/toast/store.js.map +1 -1
  43. package/lib/toast/useToastManager.d.ts +7 -0
  44. package/lib/toast/useToastManager.d.ts.map +1 -1
  45. package/lib/tooltip/portal/TooltipPortal.js +1 -1
  46. package/package.json +1 -1
  47. package/src/accordion/index.ts +1 -0
  48. package/src/accordion/panel/AccordionPanel.tsx +40 -2
  49. package/src/collapsible/index.ts +1 -0
  50. package/src/collapsible/panel/CollapsiblePanel.tsx +40 -2
  51. package/src/collapsible/panel/CollapsiblePanelContext.ts +34 -0
  52. package/src/collapsible/panel/useCollapsiblePanel.tsx +27 -3
  53. package/src/combobox/portal/ComboboxPortal.tsx +1 -1
  54. package/src/dialog/portal/DialogPortal.tsx +5 -3
  55. package/src/menu/portal/MenuPortal.tsx +1 -1
  56. package/src/popover/portal/PopoverPortal.tsx +1 -1
  57. package/src/select/portal/SelectPortal.tsx +1 -1
  58. package/src/switch/thumb/SwitchThumb.tsx +40 -5
  59. package/src/toast/root/ToastRoot.tsx +37 -9
  60. package/src/toast/store.ts +1 -1
  61. package/src/toast/useToastManager.ts +7 -0
  62. package/src/tooltip/portal/TooltipPortal.tsx +1 -1
@@ -31,7 +31,7 @@ export function MenuPortal(props: MenuPortal.Props) {
31
31
  <Modal
32
32
  transparent
33
33
  visible={open}
34
- animationType="none"
34
+ animationType="fade"
35
35
  statusBarTranslucent
36
36
  navigationBarTranslucent
37
37
  onRequestClose={(event: NativeSyntheticEvent<unknown>) => {
@@ -33,7 +33,7 @@ export function PopoverPortal(props: PopoverPortal.Props) {
33
33
  <Modal
34
34
  transparent
35
35
  visible={open}
36
- animationType="none"
36
+ animationType="fade"
37
37
  statusBarTranslucent
38
38
  navigationBarTranslucent
39
39
  onRequestClose={(event: NativeSyntheticEvent<unknown>) => {
@@ -27,7 +27,7 @@ export function SelectPortal(props: SelectPortal.Props) {
27
27
  <Modal
28
28
  transparent
29
29
  visible={open}
30
- animationType="none"
30
+ animationType="fade"
31
31
  statusBarTranslucent
32
32
  navigationBarTranslucent
33
33
  onRequestClose={(event: NativeSyntheticEvent<unknown>) => {
@@ -2,29 +2,64 @@
2
2
  import { View } from 'react-native';
3
3
  import { useSwitchRootContext } from '../root/SwitchRootContext';
4
4
  import { useRenderElement } from '../../use-render/useRenderElement';
5
+ import { useIsoLayoutEffect } from '../../hooks/useIsoLayoutEffect';
6
+ import { useTransitionStatus, type TransitionStatus } from '../../internals/useTransitionStatus';
5
7
  import type { SwitchRootState } from '../root/SwitchRoot';
6
8
  import type { ZestUIComponentProps } from '../../types';
7
9
 
8
10
  /**
9
11
  * The movable part of the switch that indicates whether the switch is on or off.
10
- * Renders a `<View>`.
12
+ * Renders a `<View>`, or nothing when the switch is unchecked.
13
+ *
14
+ * Follows the animation contract: with `keepMounted`, the thumb stays mounted
15
+ * while unchecked and publishes `transitionStatus` so the consumer can animate
16
+ * it out. Without it, the thumb unmounts the moment it unchecks — React Native
17
+ * cannot report when an exit animation finished.
11
18
  */
12
19
  export function SwitchThumb(componentProps: SwitchThumb.Props) {
13
- const { render, className, style, ref, ...elementProps } = componentProps;
20
+ const { render, className, style, keepMounted = false, ref, ...elementProps } = componentProps;
14
21
 
15
- const state = useSwitchRootContext();
22
+ const rootState = useSwitchRootContext();
23
+
24
+ const { mounted, transitionStatus, setMounted } = useTransitionStatus(rootState.checked);
25
+
26
+ // Nothing reports when an exit animation finished, so a thumb that isn't
27
+ // kept mounted is unmounted the moment it unchecks.
28
+ useIsoLayoutEffect(() => {
29
+ if (!rootState.checked && mounted && !keepMounted) {
30
+ setMounted(false);
31
+ }
32
+ }, [rootState.checked, mounted, keepMounted, setMounted]);
33
+
34
+ const state: SwitchThumbState = { ...rootState, transitionStatus };
35
+
36
+ const shouldRender = keepMounted || mounted;
16
37
 
17
38
  return useRenderElement(View, componentProps, {
18
39
  state,
19
40
  ref,
41
+ enabled: shouldRender,
20
42
  props: elementProps,
21
43
  });
22
44
  }
23
45
 
24
- export interface SwitchThumbState extends SwitchRootState {}
46
+ export interface SwitchThumbState extends SwitchRootState {
47
+ /**
48
+ * The transition status of the thumb, for driving enter/exit animations.
49
+ */
50
+ transitionStatus: TransitionStatus;
51
+ }
25
52
 
26
53
  export interface SwitchThumbProps
27
- extends ZestUIComponentProps<typeof View, SwitchThumbState> {}
54
+ extends ZestUIComponentProps<typeof View, SwitchThumbState> {
55
+ /**
56
+ * Whether to keep the element mounted when the switch is unchecked.
57
+ *
58
+ * Required to animate the thumb out; without it, it unmounts on uncheck.
59
+ * @default false
60
+ */
61
+ keepMounted?: boolean | undefined;
62
+ }
28
63
 
29
64
  export namespace SwitchThumb {
30
65
  export type State = SwitchThumbState;
@@ -6,6 +6,7 @@ import { useToastProviderContext } from '../provider/ToastProviderContext';
6
6
  import { useRenderElement } from '../../use-render/useRenderElement';
7
7
  import { useIsoLayoutEffect } from '../../hooks/useIsoLayoutEffect';
8
8
  import { useStableCallback } from '../../hooks/useStableCallback';
9
+ import { AnimationFrame } from '../../hooks/useAnimationFrame';
9
10
  import type { ZestUIComponentProps } from '../../types';
10
11
  import type { ToastObject } from '../useToastManager';
11
12
  import { ToastRootContext } from './ToastRootContext';
@@ -23,12 +24,17 @@ export type ToastSwipeDirection = 'up' | 'down' | 'left' | 'right';
23
24
  * `transitionStatus`, its stacking `offsetY`/`index`, and how far a swipe has
24
25
  * travelled, and the consumer turns those into a transform.
25
26
  *
26
- * Nothing in React Native reports that a closing animation finished, so a closed
27
- * toast would otherwise sit in the list forever. `removeOnClose` (on by default)
28
- * drops it the moment it closes, which is what a consumer who does not animate
29
- * wants. To animate the exit, set `removeOnClose={false}`, drive the exit from
30
- * `state.transitionStatus === 'ending'`, and call `useToastManager().remove(id)`
31
- * when it finishes. This is the same lever as `Collapsible.Panel`'s `keepMounted`.
27
+ * **Enter:** `transitionStatus` starts as `'starting'` (auto-clears to
28
+ * `undefined` after one frame). Key off `state.transitionStatus === 'starting'`
29
+ * to trigger a slide-in or fade-in animation.
30
+ *
31
+ * **Exit:** `removeOnClose` (on by default) drops the toast the moment it
32
+ * closes the right default for a consumer who does not animate. To animate
33
+ * the exit, set `removeOnClose={false}`, drive the exit from
34
+ * `state.transitionStatus === 'ending'` using `state.measuredHeight` as the
35
+ * starting value (the animation target is `0`), and call
36
+ * `useToastManager().remove(id)` when the animation finishes.
37
+ * This is the same lever as `Collapsible.Panel`'s `keepMounted`.
32
38
  */
33
39
  export function ToastRoot(componentProps: ToastRoot.Props) {
34
40
  const {
@@ -64,13 +70,26 @@ export function ToastRoot(componentProps: ToastRoot.Props) {
64
70
  }
65
71
  }, [removeOnClose, toast.transitionStatus, toast.id, store]);
66
72
 
73
+ // `transitionStatus` starts as `'starting'` and stays that way forever.
74
+ // Auto-clear it after one frame so the consumer can react to `'starting'`
75
+ // for an enter animation without repeatedly re-triggering.
76
+ useIsoLayoutEffect(() => {
77
+ if (toast.transitionStatus === 'starting') {
78
+ const frame = AnimationFrame.request(() => {
79
+ store.updateToastInternal(toast.id, { transitionStatus: undefined });
80
+ });
81
+ return () => AnimationFrame.cancel(frame);
82
+ }
83
+ return undefined;
84
+ }, [toast.transitionStatus, toast.id, store]);
85
+
67
86
  const handleLayout = useStableCallback((event: LayoutChangeEvent) => {
68
87
  const { height } = event.nativeEvent.layout;
69
88
 
70
89
  // A closing toast reports a height of 0 through the store; measuring it again
71
90
  // here would undo that and leave a gap in the stack.
72
91
  if (toast.transitionStatus !== 'ending' && height !== toast.height) {
73
- store.updateToastInternal(toast.id, { height });
92
+ store.updateToastInternal(toast.id, { height, measuredHeight: height });
74
93
  }
75
94
  });
76
95
 
@@ -124,6 +143,7 @@ export function ToastRoot(componentProps: ToastRoot.Props) {
124
143
  visibleIndex,
125
144
  offsetY,
126
145
  height: toast.height ?? 0,
146
+ measuredHeight: toast.measuredHeight ?? toast.height ?? 0,
127
147
  swiping,
128
148
  swipeMovement,
129
149
  swipeDirection,
@@ -137,6 +157,7 @@ export function ToastRoot(componentProps: ToastRoot.Props) {
137
157
  visibleIndex,
138
158
  offsetY,
139
159
  toast.height,
160
+ toast.measuredHeight,
140
161
  swiping,
141
162
  swipeMovement,
142
163
  swipeDirection,
@@ -192,8 +213,9 @@ function getDisplacement(
192
213
 
193
214
  export interface ToastRootState {
194
215
  /**
195
- * The toast's transition status: `'starting'` as it arrives, `'ending'` once it
196
- * is closing.
216
+ * The toast's transition status: `'starting'` as it arrives (auto-clears to
217
+ * `undefined` after one frame for enter animation), `'ending'` once it is
218
+ * closing.
197
219
  */
198
220
  transitionStatus: 'starting' | 'ending' | undefined;
199
221
  /**
@@ -227,6 +249,12 @@ export interface ToastRootState {
227
249
  * The toast's measured height.
228
250
  */
229
251
  height: number;
252
+ /**
253
+ * The last measured height before the toast closed, preserved for the
254
+ * consumer to use as the starting value for an exit animation.
255
+ * Falls back to `height` when no close has occurred yet.
256
+ */
257
+ measuredHeight: number;
230
258
  /**
231
259
  * Whether a swipe is currently in progress.
232
260
  */
@@ -301,7 +301,7 @@ export class ToastStore extends ReactStore<State, {}, typeof selectors> {
301
301
 
302
302
  const endingToasts = toasts.map((item) =>
303
303
  closeAll || item.id === toastId
304
- ? { ...item, transitionStatus: 'ending' as const, height: 0 }
304
+ ? { ...item, transitionStatus: 'ending' as const, height: 0, measuredHeight: item.height }
305
305
  : item,
306
306
  );
307
307
  const newToasts = applyLimited(endingToasts, limit);
@@ -70,6 +70,13 @@ export interface ToastObject<Data extends object> {
70
70
  * The height of the toast.
71
71
  */
72
72
  height?: number | undefined;
73
+ /**
74
+ * The last measured height of the toast, captured at the moment it closes.
75
+ * Unlike `height` (which is set to `0` on close for stack collapse), this
76
+ * preserves the value so the consumer can use it as the starting point for
77
+ * an exit animation.
78
+ */
79
+ measuredHeight?: number | undefined;
73
80
  /**
74
81
  * Callback function to be called when the toast is closed.
75
82
  */
@@ -38,7 +38,7 @@ export function TooltipPortal(props: TooltipPortal.Props) {
38
38
  <Modal
39
39
  transparent
40
40
  visible={open}
41
- animationType="none"
41
+ animationType="fade"
42
42
  statusBarTranslucent
43
43
  navigationBarTranslucent
44
44
  onRequestClose={(event: NativeSyntheticEvent<unknown>) => {