@oxyhq/bloom 0.54.1 → 0.56.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 (85) hide show
  1. package/README.md +32 -1
  2. package/lib/commonjs/provider/index.js +67 -0
  3. package/lib/commonjs/provider/index.js.map +1 -0
  4. package/lib/commonjs/provider/scroll-provider.js +13 -0
  5. package/lib/commonjs/provider/scroll-provider.js.map +1 -0
  6. package/lib/commonjs/provider/scroll-provider.web.js +13 -0
  7. package/lib/commonjs/provider/scroll-provider.web.js.map +1 -0
  8. package/lib/commonjs/toast/ToastRow.js +35 -10
  9. package/lib/commonjs/toast/ToastRow.js.map +1 -1
  10. package/lib/commonjs/toast/ToastSwipeHandler.js +15 -5
  11. package/lib/commonjs/toast/ToastSwipeHandler.js.map +1 -1
  12. package/lib/commonjs/toast/constants.js +18 -1
  13. package/lib/commonjs/toast/constants.js.map +1 -1
  14. package/lib/commonjs/toast/toast-store.js +39 -1
  15. package/lib/commonjs/toast/toast-store.js.map +1 -1
  16. package/lib/commonjs/toast/use-stack-hover.js +175 -0
  17. package/lib/commonjs/toast/use-stack-hover.js.map +1 -0
  18. package/lib/commonjs/toast/use-stack-hover.native.js +42 -0
  19. package/lib/commonjs/toast/use-stack-hover.native.js.map +1 -0
  20. package/lib/module/provider/index.js +63 -0
  21. package/lib/module/provider/index.js.map +1 -0
  22. package/lib/module/provider/scroll-provider.js +16 -0
  23. package/lib/module/provider/scroll-provider.js.map +1 -0
  24. package/lib/module/provider/scroll-provider.web.js +5 -0
  25. package/lib/module/provider/scroll-provider.web.js.map +1 -0
  26. package/lib/module/toast/ToastRow.js +35 -10
  27. package/lib/module/toast/ToastRow.js.map +1 -1
  28. package/lib/module/toast/ToastSwipeHandler.js +16 -4
  29. package/lib/module/toast/ToastSwipeHandler.js.map +1 -1
  30. package/lib/module/toast/constants.js +18 -1
  31. package/lib/module/toast/constants.js.map +1 -1
  32. package/lib/module/toast/toast-store.js +39 -1
  33. package/lib/module/toast/toast-store.js.map +1 -1
  34. package/lib/module/toast/use-stack-hover.js +168 -0
  35. package/lib/module/toast/use-stack-hover.js.map +1 -0
  36. package/lib/module/toast/use-stack-hover.native.js +35 -0
  37. package/lib/module/toast/use-stack-hover.native.js.map +1 -0
  38. package/lib/typescript/commonjs/provider/index.d.ts +45 -0
  39. package/lib/typescript/commonjs/provider/index.d.ts.map +1 -0
  40. package/lib/typescript/commonjs/provider/scroll-provider.d.ts +14 -0
  41. package/lib/typescript/commonjs/provider/scroll-provider.d.ts.map +1 -0
  42. package/lib/typescript/commonjs/provider/scroll-provider.web.d.ts +3 -0
  43. package/lib/typescript/commonjs/provider/scroll-provider.web.d.ts.map +1 -0
  44. package/lib/typescript/commonjs/toast/ToastRow.d.ts.map +1 -1
  45. package/lib/typescript/commonjs/toast/ToastSwipeHandler.d.ts +4 -0
  46. package/lib/typescript/commonjs/toast/ToastSwipeHandler.d.ts.map +1 -1
  47. package/lib/typescript/commonjs/toast/constants.d.ts.map +1 -1
  48. package/lib/typescript/commonjs/toast/toast-store.d.ts +15 -0
  49. package/lib/typescript/commonjs/toast/toast-store.d.ts.map +1 -1
  50. package/lib/typescript/commonjs/toast/use-stack-hover.d.ts +55 -0
  51. package/lib/typescript/commonjs/toast/use-stack-hover.d.ts.map +1 -0
  52. package/lib/typescript/commonjs/toast/use-stack-hover.native.d.ts +26 -0
  53. package/lib/typescript/commonjs/toast/use-stack-hover.native.d.ts.map +1 -0
  54. package/lib/typescript/module/provider/index.d.ts +45 -0
  55. package/lib/typescript/module/provider/index.d.ts.map +1 -0
  56. package/lib/typescript/module/provider/scroll-provider.d.ts +14 -0
  57. package/lib/typescript/module/provider/scroll-provider.d.ts.map +1 -0
  58. package/lib/typescript/module/provider/scroll-provider.web.d.ts +3 -0
  59. package/lib/typescript/module/provider/scroll-provider.web.d.ts.map +1 -0
  60. package/lib/typescript/module/toast/ToastRow.d.ts.map +1 -1
  61. package/lib/typescript/module/toast/ToastSwipeHandler.d.ts +4 -0
  62. package/lib/typescript/module/toast/ToastSwipeHandler.d.ts.map +1 -1
  63. package/lib/typescript/module/toast/constants.d.ts.map +1 -1
  64. package/lib/typescript/module/toast/toast-store.d.ts +15 -0
  65. package/lib/typescript/module/toast/toast-store.d.ts.map +1 -1
  66. package/lib/typescript/module/toast/use-stack-hover.d.ts +55 -0
  67. package/lib/typescript/module/toast/use-stack-hover.d.ts.map +1 -0
  68. package/lib/typescript/module/toast/use-stack-hover.native.d.ts +26 -0
  69. package/lib/typescript/module/toast/use-stack-hover.native.d.ts.map +1 -0
  70. package/package.json +12 -1
  71. package/src/__tests__/BloomProvider.web.test.tsx +114 -0
  72. package/src/__tests__/Toaster.test.tsx +554 -0
  73. package/src/__tests__/toast-position-utils.test.ts +28 -0
  74. package/src/__tests__/toast-stack-hover.test.ts +92 -0
  75. package/src/__tests__/toast-store.test.ts +72 -0
  76. package/src/provider/index.tsx +68 -0
  77. package/src/provider/scroll-provider.ts +13 -0
  78. package/src/provider/scroll-provider.web.ts +2 -0
  79. package/src/toast/Toast.stories.tsx +10 -0
  80. package/src/toast/ToastRow.tsx +33 -10
  81. package/src/toast/ToastSwipeHandler.tsx +14 -2
  82. package/src/toast/constants.ts +18 -1
  83. package/src/toast/toast-store.ts +39 -1
  84. package/src/toast/use-stack-hover.native.ts +34 -0
  85. package/src/toast/use-stack-hover.ts +182 -0
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Bloom-original — WEB/default. `use-stack-hover.native.ts` is the native no-op,
3
+ * filename-resolved by Metro exactly as `ToastHost.native.tsx` is, so `toast/`
4
+ * stays one universal engine with no `index.web` fork.
5
+ *
6
+ * sonner expands its stack on HOVER; sonner-native only has press, because a phone
7
+ * has no pointer. Bloom needs both: press is the touch and native affordance, hover
8
+ * is what a desktop user expects, and a touch-capable web device must not get a
9
+ * phantom hover from its own taps.
10
+ *
11
+ * THIS IS A TRIGGER, NOT A SECOND MECHANISM. It drives the store's existing
12
+ * `expand`/`collapse`, which already pause and resume the auto-close timers
13
+ * (`ToastStore.expand` calls `timers.pauseAll()`, `collapse` calls `resumeAll()`),
14
+ * so hovering to read a toast cannot let it expire under the cursor — no new
15
+ * pausing path, no new store state. With stacking OFF there is nothing to expand,
16
+ * so the pointer pauses the timers directly instead: the reason sonner pauses is
17
+ * that the pointer is over the toast, which has nothing to do with stacking.
18
+ *
19
+ * TWO THINGS ARE LOAD-BEARING:
20
+ *
21
+ * - `pointerType === 'mouse'`. Touch fires `pointerenter` before `pointerdown`
22
+ * and `pointerleave` after `pointerup` (the pointer ceases to exist), so
23
+ * without this guard every TAP on a touch-capable web device would expand then
24
+ * collapse the stack while the press toggle fired too — three handlers fighting
25
+ * over one tap. Hover is additive: pens and touch fall through to press.
26
+ * - THE LEAVE IS DEFERRED BY A FRAME-ISH GRACE PERIOD, and any enter cancels it.
27
+ * Rows are separate absolutely-positioned boxes with `gap` between them once
28
+ * expanded, so moving the pointer from one row to the next leaves the first
29
+ * before entering the second. Collapsing on that would snap the stack shut and
30
+ * reopen it on whatever row ends up under the pointer — a jitter loop, plus a
31
+ * resume/pause pair per crossing that re-banks each timer's remaining time.
32
+ *
33
+ * The state is module-level on purpose: there is ONE stack per document, the same
34
+ * reason `toastStore` is a module singleton. It is written and read only inside
35
+ * event handlers — never in render — so the React Compiler has nothing to freeze.
36
+ */
37
+ import { useMemo } from 'react';
38
+
39
+ import { toastStore } from './toast-store';
40
+
41
+ /**
42
+ * How long a leave waits for a neighbouring row to claim the pointer. One frame is
43
+ * enough for the same-tick leave/enter pair a row crossing produces; this is a
44
+ * little more so a slow frame cannot flash the stack shut. Exported so the suite
45
+ * asserts against the real value rather than a copy of it.
46
+ */
47
+ export const HOVER_LEAVE_GRACE = 80;
48
+
49
+ /** The portal root every toast row lives under — see `portal/index.web.tsx`. */
50
+ const PORTAL_ROOT_ID = 'bloom-portal-root';
51
+
52
+ type HoverEvent = { nativeEvent: { pointerType?: string } };
53
+
54
+ let hovered = false;
55
+ let leaveTimeout: ReturnType<typeof setTimeout> | null = null;
56
+
57
+ const isMouse = (event: HoverEvent) => event.nativeEvent.pointerType === 'mouse';
58
+
59
+ const cancelPendingLeave = () => {
60
+ if (leaveTimeout !== null) {
61
+ clearTimeout(leaveTimeout);
62
+ leaveTimeout = null;
63
+ }
64
+ };
65
+
66
+ /**
67
+ * Whether the pointer is over the stack RIGHT NOW.
68
+ *
69
+ * Two sources, and both are load-bearing:
70
+ *
71
+ * - the `hovered` latch answers "a mouse entered and has not left", which is the
72
+ * only thing available where there is no DOM (jest runs on `testEnvironment:
73
+ * 'node'`), and is a cheap early-out.
74
+ * - the live `:hover` query answers "a row is under the pointer", which the latch
75
+ * CANNOT: a row can stop being hovered with no event whatsoever. Measured — a row
76
+ * culled by `visibleToasts` under a stationary cursor fires no `pointerleave`,
77
+ * `pointerout` or `mouseleave` at all, and the restack then slides the remaining
78
+ * rows out from under the pointer. A latch alone therefore sticks TRUE, which
79
+ * would disable the press toggle for the rest of the session and suppress every
80
+ * later auto-collapse.
81
+ *
82
+ * Requiring both means a stale latch is corrected by the DOM and a stray DOM match
83
+ * (a pen, a touch) is rejected by the latch.
84
+ *
85
+ * COVERAGE, stated plainly: jest cannot see the DOM half at all
86
+ * (`testEnvironment: 'node'`), and a browser cannot isolate it either — its two
87
+ * consequences are a press toggle standing down (unreachable with a mouse, which
88
+ * always hovers what it presses) and a suppressed auto-collapse (which the deferred
89
+ * leave below independently corrects the moment any boundary event fires). Both
90
+ * halves are kept because each is measurably more accurate than the other alone, not
91
+ * because a test proves the composite.
92
+ *
93
+ * `ToastRow` asks so a press does not TOGGLE what hover already opened, and
94
+ * `toastStore` asks before auto-collapsing a stack down to its last row. The press
95
+ * still runs the toast's own `onPress`; only the expansion toggle steps aside.
96
+ */
97
+ export function isStackHovered(): boolean {
98
+ return hovered && rowUnderPointer() !== false;
99
+ }
100
+
101
+ /**
102
+ * The live DOM answer, or `undefined` where there is no DOM to ask (jest runs on
103
+ * `testEnvironment: 'node'`). Deliberately tri-state: "unknowable" and "no" must not
104
+ * collapse into the same value, because the two callers want opposite defaults —
105
+ * `isStackHovered` trusts the latch when it cannot check, while the deferred leave
106
+ * only stands down on a positive "yes, still hovered".
107
+ */
108
+ function rowUnderPointer(): boolean | undefined {
109
+ if (typeof document === 'undefined') {
110
+ return undefined;
111
+ }
112
+ return document.querySelector(`#${PORTAL_ROOT_ID} [aria-live]:hover`) !== null;
113
+ }
114
+
115
+ /**
116
+ * A pointer resting on the stack HOLDS it open, so the store must not auto-collapse
117
+ * it out from under the cursor. Registered here rather than passed in because this
118
+ * file is the one that knows what hovering means; the store only knows that
119
+ * something says "not yet". Native's fork registers nothing.
120
+ */
121
+ toastStore.setExpansionHold(isStackHovered);
122
+
123
+ export type StackHoverProps = {
124
+ onPointerEnter: (event: HoverEvent) => void;
125
+ onPointerLeave: (event: HoverEvent) => void;
126
+ };
127
+
128
+ export function useStackHover({
129
+ enableStacking,
130
+ }: {
131
+ enableStacking: boolean;
132
+ }): StackHoverProps {
133
+ // Stable per outlet configuration: these land on the row box as props, and every
134
+ // row re-renders on each stack change.
135
+ return useMemo(
136
+ () => ({
137
+ onPointerEnter: (event: HoverEvent) => {
138
+ if (!isMouse(event)) {
139
+ return;
140
+ }
141
+ cancelPendingLeave();
142
+ hovered = true;
143
+ if (enableStacking) {
144
+ // Pauses the timers as part of expanding.
145
+ toastStore.expand();
146
+ } else {
147
+ toastStore.pauseAllTimers();
148
+ }
149
+ },
150
+ onPointerLeave: (event: HoverEvent) => {
151
+ if (!isMouse(event) || !hovered) {
152
+ return;
153
+ }
154
+ cancelPendingLeave();
155
+ leaveTimeout = setTimeout(() => {
156
+ leaveTimeout = null;
157
+ // The rows may have moved rather than the pointer: an expand, a collapse
158
+ // or a restack slides them under a stationary cursor without firing a
159
+ // single pointer event, so re-ask the DOM before acting on a leave that
160
+ // fired 80ms ago. Only a positive "still hovered" stands the collapse down.
161
+ if (rowUnderPointer() === true) {
162
+ return;
163
+ }
164
+ hovered = false;
165
+ if (enableStacking) {
166
+ // Resumes the timers as part of collapsing.
167
+ toastStore.collapse();
168
+ } else {
169
+ toastStore.resumeAllTimers();
170
+ }
171
+ }, HOVER_LEAVE_GRACE);
172
+ },
173
+ }),
174
+ [enableStacking],
175
+ );
176
+ }
177
+
178
+ /** Test-only reset: the module state outlives a render tree, so a suite must clear it. */
179
+ export function resetStackHoverForTests(): void {
180
+ cancelPendingLeave();
181
+ hovered = false;
182
+ }