@astryxdesign/core 0.2.0-canary.ea693ac → 0.2.0-canary.fc53ff7

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 (48) hide show
  1. package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts.map +1 -1
  2. package/dist/DropdownMenu/DropdownMenuSubMenu.js +9 -1
  3. package/dist/DropdownMenu/menuItemHover.d.ts +1 -0
  4. package/dist/DropdownMenu/menuItemHover.d.ts.map +1 -1
  5. package/dist/DropdownMenu/menuItemHover.js +1 -0
  6. package/dist/FieldStatus/FieldStatus.d.ts.map +1 -1
  7. package/dist/FieldStatus/FieldStatus.js +12 -2
  8. package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
  9. package/dist/MultiSelector/MultiSelector.js +1 -1
  10. package/dist/Thumbnail/Thumbnail.d.ts.map +1 -1
  11. package/dist/Thumbnail/Thumbnail.js +42 -43
  12. package/dist/TopNav/TopNavMegaMenu.d.ts.map +1 -1
  13. package/dist/TopNav/TopNavMegaMenu.js +9 -3
  14. package/dist/astryx.css +35 -5
  15. package/dist/astryx.umd.js +46 -46
  16. package/dist/astryx.umd.js.map +4 -4
  17. package/dist/hooks/containerReveal.pool.stylex.d.ts +281 -0
  18. package/dist/hooks/containerReveal.pool.stylex.d.ts.map +1 -0
  19. package/dist/hooks/containerReveal.pool.stylex.js +281 -0
  20. package/dist/hooks/index.d.ts +2 -0
  21. package/dist/hooks/index.d.ts.map +1 -1
  22. package/dist/hooks/index.js +2 -1
  23. package/dist/hooks/useContainerReveal.d.ts +83 -0
  24. package/dist/hooks/useContainerReveal.d.ts.map +1 -0
  25. package/dist/hooks/useContainerReveal.js +123 -0
  26. package/dist/hooks/useInputStatusIcon.d.ts.map +1 -1
  27. package/dist/hooks/useInputStatusIcon.js +11 -0
  28. package/package.json +1 -1
  29. package/src/DropdownMenu/DropdownMenuSubMenu.test.tsx +29 -1
  30. package/src/DropdownMenu/DropdownMenuSubMenu.tsx +13 -5
  31. package/src/DropdownMenu/menuItemHover.ts +1 -0
  32. package/src/Field/Field.doc.mjs +4 -0
  33. package/src/FieldStatus/FieldStatus.doc.mjs +1 -0
  34. package/src/FieldStatus/FieldStatus.test.tsx +61 -0
  35. package/src/FieldStatus/FieldStatus.tsx +14 -3
  36. package/src/MultiSelector/MultiSelector.tsx +6 -5
  37. package/src/Thumbnail/Thumbnail.tsx +18 -32
  38. package/src/TopNav/TopNavMegaMenu.tsx +26 -2
  39. package/src/hooks/containerReveal.pool.stylex.ts +464 -0
  40. package/src/hooks/index.ts +7 -0
  41. package/src/hooks/useContainerReveal.test.tsx +116 -0
  42. package/src/hooks/useContainerReveal.ts +180 -0
  43. package/src/hooks/useInputStatusIcon.test.tsx +59 -0
  44. package/src/hooks/useInputStatusIcon.tsx +12 -1
  45. package/dist/Thumbnail/thumbnail.markers.stylex.d.ts +0 -9
  46. package/dist/Thumbnail/thumbnail.markers.stylex.d.ts.map +0 -1
  47. package/dist/Thumbnail/thumbnail.markers.stylex.js +0 -14
  48. package/src/Thumbnail/thumbnail.markers.stylex.ts +0 -12
@@ -0,0 +1,464 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file containerReveal.pool.stylex.ts
5
+ * @input Uses StyleX, theme tokens
6
+ * @output A fixed pool of scoped markers, each paired with its reveal style
7
+ * blocks (reveal / conceal, each with a layout-preserved variant) compiled
8
+ * against that marker.
9
+ * @position Internal to useContainerReveal.
10
+ *
11
+ * WHY A POOL OF LITERAL BLOCKS: `stylex.when.ancestor(':hover', marker)` is
12
+ * resolved by the StyleX compiler, which welds the marker's class into the
13
+ * emitted selector (`.marker:hover *`). A marker and the styles referencing it
14
+ * are therefore inseparable at build time — the marker cannot be passed as a
15
+ * runtime argument, so the blocks cannot be produced by a loop or a helper and
16
+ * must be written out literally per marker. A single shared marker would leak
17
+ * hover/focus across NESTED reveal containers (an ancestor container's :hover
18
+ * matches a descendant container's revealed content), so useContainerReveal
19
+ * hands each concurrently mounted container a distinct marker from this pool
20
+ * (see the free-list there).
21
+ *
22
+ * The reveal recipe hides content with `position: absolute` (removed from
23
+ * layout flow, so it reserves no space) plus `opacity: 0`. Both keep the
24
+ * content in the accessibility tree and tab order — unlike display/visibility
25
+ * — so it stays reachable by keyboard and assistive tech while hidden.
26
+ */
27
+
28
+ import * as stylex from '@stylexjs/stylex';
29
+ import {durationVars, easeVars} from '../theme/tokens.stylex';
30
+
31
+ export const POOL_SIZE = 6;
32
+
33
+ export const m0 = stylex.defineMarker();
34
+ const s0 = stylex.create({
35
+ // Hidden at rest via position + opacity (kept in the a11y tree and tab
36
+ // order); on container :hover (fine pointer) or :focus-within it returns to
37
+ // flow and fades in. Always shown on touch.
38
+ reveal: {
39
+ transitionProperty: 'opacity',
40
+ transitionDuration: {
41
+ default: durationVars['--duration-fast'],
42
+ '@media (prefers-reduced-motion: reduce)': '0s',
43
+ },
44
+ transitionTimingFunction: easeVars['--ease-standard'],
45
+ opacity: {
46
+ default: 0,
47
+ [stylex.when.ancestor(':hover', m0)]: {'@media (hover: hover)': 1},
48
+ [stylex.when.ancestor(':focus-within', m0)]: 1,
49
+ '@media (any-pointer: coarse)': 1,
50
+ },
51
+ position: {
52
+ default: 'absolute',
53
+ [stylex.when.ancestor(':hover', m0)]: {'@media (hover: hover)': 'static'},
54
+ [stylex.when.ancestor(':focus-within', m0)]: 'static',
55
+ '@media (any-pointer: coarse)': 'static',
56
+ },
57
+ },
58
+ // Opacity-only reveal that keeps the box reserved (no layout shift). Use for
59
+ // content that is already positioned (e.g. an absolute overlay).
60
+ revealLayoutPreserved: {
61
+ transitionProperty: 'opacity',
62
+ transitionDuration: {
63
+ default: durationVars['--duration-fast'],
64
+ '@media (prefers-reduced-motion: reduce)': '0s',
65
+ },
66
+ transitionTimingFunction: easeVars['--ease-standard'],
67
+ opacity: {
68
+ default: 0,
69
+ [stylex.when.ancestor(':hover', m0)]: {'@media (hover: hover)': 1},
70
+ [stylex.when.ancestor(':focus-within', m0)]: 1,
71
+ '@media (any-pointer: coarse)': 1,
72
+ },
73
+ },
74
+ // Inverted: visible at rest, fades OUT on :hover only. Deliberately ignores
75
+ // :focus-within (a keyboard user must never watch content vanish) and stays
76
+ // visible on touch. Never leaves the a11y tree.
77
+ conceal: {
78
+ transitionProperty: 'opacity',
79
+ transitionDuration: {
80
+ default: durationVars['--duration-fast'],
81
+ '@media (prefers-reduced-motion: reduce)': '0s',
82
+ },
83
+ transitionTimingFunction: easeVars['--ease-standard'],
84
+ opacity: {
85
+ default: 1,
86
+ [stylex.when.ancestor(':hover', m0)]: {'@media (hover: hover)': 0},
87
+ },
88
+ },
89
+ concealLayoutPreserved: {
90
+ transitionProperty: 'opacity',
91
+ transitionDuration: {
92
+ default: durationVars['--duration-fast'],
93
+ '@media (prefers-reduced-motion: reduce)': '0s',
94
+ },
95
+ transitionTimingFunction: easeVars['--ease-standard'],
96
+ opacity: {
97
+ default: 1,
98
+ [stylex.when.ancestor(':hover', m0)]: {'@media (hover: hover)': 0},
99
+ },
100
+ },
101
+ });
102
+
103
+ export const m1 = stylex.defineMarker();
104
+ const s1 = stylex.create({
105
+ // Hidden at rest via position + opacity (kept in the a11y tree and tab
106
+ // order); on container :hover (fine pointer) or :focus-within it returns to
107
+ // flow and fades in. Always shown on touch.
108
+ reveal: {
109
+ transitionProperty: 'opacity',
110
+ transitionDuration: {
111
+ default: durationVars['--duration-fast'],
112
+ '@media (prefers-reduced-motion: reduce)': '0s',
113
+ },
114
+ transitionTimingFunction: easeVars['--ease-standard'],
115
+ opacity: {
116
+ default: 0,
117
+ [stylex.when.ancestor(':hover', m1)]: {'@media (hover: hover)': 1},
118
+ [stylex.when.ancestor(':focus-within', m1)]: 1,
119
+ '@media (any-pointer: coarse)': 1,
120
+ },
121
+ position: {
122
+ default: 'absolute',
123
+ [stylex.when.ancestor(':hover', m1)]: {'@media (hover: hover)': 'static'},
124
+ [stylex.when.ancestor(':focus-within', m1)]: 'static',
125
+ '@media (any-pointer: coarse)': 'static',
126
+ },
127
+ },
128
+ // Opacity-only reveal that keeps the box reserved (no layout shift). Use for
129
+ // content that is already positioned (e.g. an absolute overlay).
130
+ revealLayoutPreserved: {
131
+ transitionProperty: 'opacity',
132
+ transitionDuration: {
133
+ default: durationVars['--duration-fast'],
134
+ '@media (prefers-reduced-motion: reduce)': '0s',
135
+ },
136
+ transitionTimingFunction: easeVars['--ease-standard'],
137
+ opacity: {
138
+ default: 0,
139
+ [stylex.when.ancestor(':hover', m1)]: {'@media (hover: hover)': 1},
140
+ [stylex.when.ancestor(':focus-within', m1)]: 1,
141
+ '@media (any-pointer: coarse)': 1,
142
+ },
143
+ },
144
+ // Inverted: visible at rest, fades OUT on :hover only. Deliberately ignores
145
+ // :focus-within (a keyboard user must never watch content vanish) and stays
146
+ // visible on touch. Never leaves the a11y tree.
147
+ conceal: {
148
+ transitionProperty: 'opacity',
149
+ transitionDuration: {
150
+ default: durationVars['--duration-fast'],
151
+ '@media (prefers-reduced-motion: reduce)': '0s',
152
+ },
153
+ transitionTimingFunction: easeVars['--ease-standard'],
154
+ opacity: {
155
+ default: 1,
156
+ [stylex.when.ancestor(':hover', m1)]: {'@media (hover: hover)': 0},
157
+ },
158
+ },
159
+ concealLayoutPreserved: {
160
+ transitionProperty: 'opacity',
161
+ transitionDuration: {
162
+ default: durationVars['--duration-fast'],
163
+ '@media (prefers-reduced-motion: reduce)': '0s',
164
+ },
165
+ transitionTimingFunction: easeVars['--ease-standard'],
166
+ opacity: {
167
+ default: 1,
168
+ [stylex.when.ancestor(':hover', m1)]: {'@media (hover: hover)': 0},
169
+ },
170
+ },
171
+ });
172
+
173
+ export const m2 = stylex.defineMarker();
174
+ const s2 = stylex.create({
175
+ // Hidden at rest via position + opacity (kept in the a11y tree and tab
176
+ // order); on container :hover (fine pointer) or :focus-within it returns to
177
+ // flow and fades in. Always shown on touch.
178
+ reveal: {
179
+ transitionProperty: 'opacity',
180
+ transitionDuration: {
181
+ default: durationVars['--duration-fast'],
182
+ '@media (prefers-reduced-motion: reduce)': '0s',
183
+ },
184
+ transitionTimingFunction: easeVars['--ease-standard'],
185
+ opacity: {
186
+ default: 0,
187
+ [stylex.when.ancestor(':hover', m2)]: {'@media (hover: hover)': 1},
188
+ [stylex.when.ancestor(':focus-within', m2)]: 1,
189
+ '@media (any-pointer: coarse)': 1,
190
+ },
191
+ position: {
192
+ default: 'absolute',
193
+ [stylex.when.ancestor(':hover', m2)]: {'@media (hover: hover)': 'static'},
194
+ [stylex.when.ancestor(':focus-within', m2)]: 'static',
195
+ '@media (any-pointer: coarse)': 'static',
196
+ },
197
+ },
198
+ // Opacity-only reveal that keeps the box reserved (no layout shift). Use for
199
+ // content that is already positioned (e.g. an absolute overlay).
200
+ revealLayoutPreserved: {
201
+ transitionProperty: 'opacity',
202
+ transitionDuration: {
203
+ default: durationVars['--duration-fast'],
204
+ '@media (prefers-reduced-motion: reduce)': '0s',
205
+ },
206
+ transitionTimingFunction: easeVars['--ease-standard'],
207
+ opacity: {
208
+ default: 0,
209
+ [stylex.when.ancestor(':hover', m2)]: {'@media (hover: hover)': 1},
210
+ [stylex.when.ancestor(':focus-within', m2)]: 1,
211
+ '@media (any-pointer: coarse)': 1,
212
+ },
213
+ },
214
+ // Inverted: visible at rest, fades OUT on :hover only. Deliberately ignores
215
+ // :focus-within (a keyboard user must never watch content vanish) and stays
216
+ // visible on touch. Never leaves the a11y tree.
217
+ conceal: {
218
+ transitionProperty: 'opacity',
219
+ transitionDuration: {
220
+ default: durationVars['--duration-fast'],
221
+ '@media (prefers-reduced-motion: reduce)': '0s',
222
+ },
223
+ transitionTimingFunction: easeVars['--ease-standard'],
224
+ opacity: {
225
+ default: 1,
226
+ [stylex.when.ancestor(':hover', m2)]: {'@media (hover: hover)': 0},
227
+ },
228
+ },
229
+ concealLayoutPreserved: {
230
+ transitionProperty: 'opacity',
231
+ transitionDuration: {
232
+ default: durationVars['--duration-fast'],
233
+ '@media (prefers-reduced-motion: reduce)': '0s',
234
+ },
235
+ transitionTimingFunction: easeVars['--ease-standard'],
236
+ opacity: {
237
+ default: 1,
238
+ [stylex.when.ancestor(':hover', m2)]: {'@media (hover: hover)': 0},
239
+ },
240
+ },
241
+ });
242
+
243
+ export const m3 = stylex.defineMarker();
244
+ const s3 = stylex.create({
245
+ // Hidden at rest via position + opacity (kept in the a11y tree and tab
246
+ // order); on container :hover (fine pointer) or :focus-within it returns to
247
+ // flow and fades in. Always shown on touch.
248
+ reveal: {
249
+ transitionProperty: 'opacity',
250
+ transitionDuration: {
251
+ default: durationVars['--duration-fast'],
252
+ '@media (prefers-reduced-motion: reduce)': '0s',
253
+ },
254
+ transitionTimingFunction: easeVars['--ease-standard'],
255
+ opacity: {
256
+ default: 0,
257
+ [stylex.when.ancestor(':hover', m3)]: {'@media (hover: hover)': 1},
258
+ [stylex.when.ancestor(':focus-within', m3)]: 1,
259
+ '@media (any-pointer: coarse)': 1,
260
+ },
261
+ position: {
262
+ default: 'absolute',
263
+ [stylex.when.ancestor(':hover', m3)]: {'@media (hover: hover)': 'static'},
264
+ [stylex.when.ancestor(':focus-within', m3)]: 'static',
265
+ '@media (any-pointer: coarse)': 'static',
266
+ },
267
+ },
268
+ // Opacity-only reveal that keeps the box reserved (no layout shift). Use for
269
+ // content that is already positioned (e.g. an absolute overlay).
270
+ revealLayoutPreserved: {
271
+ transitionProperty: 'opacity',
272
+ transitionDuration: {
273
+ default: durationVars['--duration-fast'],
274
+ '@media (prefers-reduced-motion: reduce)': '0s',
275
+ },
276
+ transitionTimingFunction: easeVars['--ease-standard'],
277
+ opacity: {
278
+ default: 0,
279
+ [stylex.when.ancestor(':hover', m3)]: {'@media (hover: hover)': 1},
280
+ [stylex.when.ancestor(':focus-within', m3)]: 1,
281
+ '@media (any-pointer: coarse)': 1,
282
+ },
283
+ },
284
+ // Inverted: visible at rest, fades OUT on :hover only. Deliberately ignores
285
+ // :focus-within (a keyboard user must never watch content vanish) and stays
286
+ // visible on touch. Never leaves the a11y tree.
287
+ conceal: {
288
+ transitionProperty: 'opacity',
289
+ transitionDuration: {
290
+ default: durationVars['--duration-fast'],
291
+ '@media (prefers-reduced-motion: reduce)': '0s',
292
+ },
293
+ transitionTimingFunction: easeVars['--ease-standard'],
294
+ opacity: {
295
+ default: 1,
296
+ [stylex.when.ancestor(':hover', m3)]: {'@media (hover: hover)': 0},
297
+ },
298
+ },
299
+ concealLayoutPreserved: {
300
+ transitionProperty: 'opacity',
301
+ transitionDuration: {
302
+ default: durationVars['--duration-fast'],
303
+ '@media (prefers-reduced-motion: reduce)': '0s',
304
+ },
305
+ transitionTimingFunction: easeVars['--ease-standard'],
306
+ opacity: {
307
+ default: 1,
308
+ [stylex.when.ancestor(':hover', m3)]: {'@media (hover: hover)': 0},
309
+ },
310
+ },
311
+ });
312
+
313
+ export const m4 = stylex.defineMarker();
314
+ const s4 = stylex.create({
315
+ // Hidden at rest via position + opacity (kept in the a11y tree and tab
316
+ // order); on container :hover (fine pointer) or :focus-within it returns to
317
+ // flow and fades in. Always shown on touch.
318
+ reveal: {
319
+ transitionProperty: 'opacity',
320
+ transitionDuration: {
321
+ default: durationVars['--duration-fast'],
322
+ '@media (prefers-reduced-motion: reduce)': '0s',
323
+ },
324
+ transitionTimingFunction: easeVars['--ease-standard'],
325
+ opacity: {
326
+ default: 0,
327
+ [stylex.when.ancestor(':hover', m4)]: {'@media (hover: hover)': 1},
328
+ [stylex.when.ancestor(':focus-within', m4)]: 1,
329
+ '@media (any-pointer: coarse)': 1,
330
+ },
331
+ position: {
332
+ default: 'absolute',
333
+ [stylex.when.ancestor(':hover', m4)]: {'@media (hover: hover)': 'static'},
334
+ [stylex.when.ancestor(':focus-within', m4)]: 'static',
335
+ '@media (any-pointer: coarse)': 'static',
336
+ },
337
+ },
338
+ // Opacity-only reveal that keeps the box reserved (no layout shift). Use for
339
+ // content that is already positioned (e.g. an absolute overlay).
340
+ revealLayoutPreserved: {
341
+ transitionProperty: 'opacity',
342
+ transitionDuration: {
343
+ default: durationVars['--duration-fast'],
344
+ '@media (prefers-reduced-motion: reduce)': '0s',
345
+ },
346
+ transitionTimingFunction: easeVars['--ease-standard'],
347
+ opacity: {
348
+ default: 0,
349
+ [stylex.when.ancestor(':hover', m4)]: {'@media (hover: hover)': 1},
350
+ [stylex.when.ancestor(':focus-within', m4)]: 1,
351
+ '@media (any-pointer: coarse)': 1,
352
+ },
353
+ },
354
+ // Inverted: visible at rest, fades OUT on :hover only. Deliberately ignores
355
+ // :focus-within (a keyboard user must never watch content vanish) and stays
356
+ // visible on touch. Never leaves the a11y tree.
357
+ conceal: {
358
+ transitionProperty: 'opacity',
359
+ transitionDuration: {
360
+ default: durationVars['--duration-fast'],
361
+ '@media (prefers-reduced-motion: reduce)': '0s',
362
+ },
363
+ transitionTimingFunction: easeVars['--ease-standard'],
364
+ opacity: {
365
+ default: 1,
366
+ [stylex.when.ancestor(':hover', m4)]: {'@media (hover: hover)': 0},
367
+ },
368
+ },
369
+ concealLayoutPreserved: {
370
+ transitionProperty: 'opacity',
371
+ transitionDuration: {
372
+ default: durationVars['--duration-fast'],
373
+ '@media (prefers-reduced-motion: reduce)': '0s',
374
+ },
375
+ transitionTimingFunction: easeVars['--ease-standard'],
376
+ opacity: {
377
+ default: 1,
378
+ [stylex.when.ancestor(':hover', m4)]: {'@media (hover: hover)': 0},
379
+ },
380
+ },
381
+ });
382
+
383
+ export const m5 = stylex.defineMarker();
384
+ const s5 = stylex.create({
385
+ // Hidden at rest via position + opacity (kept in the a11y tree and tab
386
+ // order); on container :hover (fine pointer) or :focus-within it returns to
387
+ // flow and fades in. Always shown on touch.
388
+ reveal: {
389
+ transitionProperty: 'opacity',
390
+ transitionDuration: {
391
+ default: durationVars['--duration-fast'],
392
+ '@media (prefers-reduced-motion: reduce)': '0s',
393
+ },
394
+ transitionTimingFunction: easeVars['--ease-standard'],
395
+ opacity: {
396
+ default: 0,
397
+ [stylex.when.ancestor(':hover', m5)]: {'@media (hover: hover)': 1},
398
+ [stylex.when.ancestor(':focus-within', m5)]: 1,
399
+ '@media (any-pointer: coarse)': 1,
400
+ },
401
+ position: {
402
+ default: 'absolute',
403
+ [stylex.when.ancestor(':hover', m5)]: {'@media (hover: hover)': 'static'},
404
+ [stylex.when.ancestor(':focus-within', m5)]: 'static',
405
+ '@media (any-pointer: coarse)': 'static',
406
+ },
407
+ },
408
+ // Opacity-only reveal that keeps the box reserved (no layout shift). Use for
409
+ // content that is already positioned (e.g. an absolute overlay).
410
+ revealLayoutPreserved: {
411
+ transitionProperty: 'opacity',
412
+ transitionDuration: {
413
+ default: durationVars['--duration-fast'],
414
+ '@media (prefers-reduced-motion: reduce)': '0s',
415
+ },
416
+ transitionTimingFunction: easeVars['--ease-standard'],
417
+ opacity: {
418
+ default: 0,
419
+ [stylex.when.ancestor(':hover', m5)]: {'@media (hover: hover)': 1},
420
+ [stylex.when.ancestor(':focus-within', m5)]: 1,
421
+ '@media (any-pointer: coarse)': 1,
422
+ },
423
+ },
424
+ // Inverted: visible at rest, fades OUT on :hover only. Deliberately ignores
425
+ // :focus-within (a keyboard user must never watch content vanish) and stays
426
+ // visible on touch. Never leaves the a11y tree.
427
+ conceal: {
428
+ transitionProperty: 'opacity',
429
+ transitionDuration: {
430
+ default: durationVars['--duration-fast'],
431
+ '@media (prefers-reduced-motion: reduce)': '0s',
432
+ },
433
+ transitionTimingFunction: easeVars['--ease-standard'],
434
+ opacity: {
435
+ default: 1,
436
+ [stylex.when.ancestor(':hover', m5)]: {'@media (hover: hover)': 0},
437
+ },
438
+ },
439
+ concealLayoutPreserved: {
440
+ transitionProperty: 'opacity',
441
+ transitionDuration: {
442
+ default: durationVars['--duration-fast'],
443
+ '@media (prefers-reduced-motion: reduce)': '0s',
444
+ },
445
+ transitionTimingFunction: easeVars['--ease-standard'],
446
+ opacity: {
447
+ default: 1,
448
+ [stylex.when.ancestor(':hover', m5)]: {'@media (hover: hover)': 0},
449
+ },
450
+ },
451
+ });
452
+
453
+ // The compiled StyleX objects carry `when.ancestor` conditionals the public
454
+ // `StyleXStyles` type does not model, so the slot shape is inferred.
455
+ export const POOL = [
456
+ {marker: m0, reveal: s0.reveal, revealLayoutPreserved: s0.revealLayoutPreserved, conceal: s0.conceal, concealLayoutPreserved: s0.concealLayoutPreserved},
457
+ {marker: m1, reveal: s1.reveal, revealLayoutPreserved: s1.revealLayoutPreserved, conceal: s1.conceal, concealLayoutPreserved: s1.concealLayoutPreserved},
458
+ {marker: m2, reveal: s2.reveal, revealLayoutPreserved: s2.revealLayoutPreserved, conceal: s2.conceal, concealLayoutPreserved: s2.concealLayoutPreserved},
459
+ {marker: m3, reveal: s3.reveal, revealLayoutPreserved: s3.revealLayoutPreserved, conceal: s3.conceal, concealLayoutPreserved: s3.concealLayoutPreserved},
460
+ {marker: m4, reveal: s4.reveal, revealLayoutPreserved: s4.revealLayoutPreserved, conceal: s4.conceal, concealLayoutPreserved: s4.concealLayoutPreserved},
461
+ {marker: m5, reveal: s5.reveal, revealLayoutPreserved: s5.revealLayoutPreserved, conceal: s5.conceal, concealLayoutPreserved: s5.concealLayoutPreserved},
462
+ ] as const;
463
+
464
+ export type RevealSlot = (typeof POOL)[number];
@@ -92,3 +92,10 @@ export {useLongPress} from './useLongPress';
92
92
  export type {UseLongPressOptions, UseLongPressHandlers} from './useLongPress';
93
93
 
94
94
  export {useDevWarning} from './useDevWarning';
95
+
96
+ export {useContainerReveal} from './useContainerReveal';
97
+ export type {
98
+ UseContainerRevealOptions,
99
+ UseContainerRevealReturn,
100
+ ContentRevealOptions,
101
+ } from './useContainerReveal';
@@ -0,0 +1,116 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file useContainerReveal.test.tsx
5
+ * @input Uses vitest, @testing-library/react, useContainerReveal
6
+ * @output Unit tests for the marker-pool free-list, per-instance scoping, the
7
+ * inert (disabled) path, and the content-option → style-block mapping.
8
+ * @position Testing; validates useContainerReveal.ts and its pool.
9
+ *
10
+ * SYNC: When useContainerReveal.ts changes, update these tests.
11
+ */
12
+
13
+ import {describe, it, expect} from 'vitest';
14
+ import {renderHook, render, act} from '@testing-library/react';
15
+ import {useContainerReveal} from './useContainerReveal';
16
+ import {POOL_SIZE} from './containerReveal.pool.stylex';
17
+
18
+ describe('useContainerReveal', () => {
19
+ it('returns spreadable getter props for container and content', () => {
20
+ const {result} = renderHook(() => useContainerReveal());
21
+ const container = result.current.getContainerProps();
22
+ const content = result.current.getContentRevealProps();
23
+ expect(container).toHaveProperty('className');
24
+ expect(content).toHaveProperty('className');
25
+ expect(typeof container.className).toBe('string');
26
+ expect(typeof content.className).toBe('string');
27
+ });
28
+
29
+ it('is inert when disabled: no marker class, empty content props', () => {
30
+ const {result} = renderHook(() =>
31
+ useContainerReveal({isEnabled: false}),
32
+ );
33
+ expect(result.current.getContainerProps()).toEqual({});
34
+ expect(result.current.getContentRevealProps()).toEqual({});
35
+ });
36
+
37
+ it('gives two concurrently mounted containers DISTINCT marker classes', () => {
38
+ // The core leak-safety property: nested/concurrent containers must never
39
+ // share a marker, or an ancestor container's :hover would reveal a
40
+ // descendant container's content.
41
+ const a = renderHook(() => useContainerReveal());
42
+ const b = renderHook(() => useContainerReveal());
43
+ const classA = a.result.current.getContainerProps().className;
44
+ const classB = b.result.current.getContainerProps().className;
45
+ expect(classA).toBeTruthy();
46
+ expect(classB).toBeTruthy();
47
+ expect(classA).not.toBe(classB);
48
+ a.unmount();
49
+ b.unmount();
50
+ });
51
+
52
+ it('recycles a slot after unmount (free-list release)', () => {
53
+ const first = renderHook(() => useContainerReveal());
54
+ const firstClass = first.result.current.getContainerProps().className;
55
+ first.unmount();
56
+ // A fresh mount should be able to reclaim the just-released slot.
57
+ const second = renderHook(() => useContainerReveal());
58
+ const secondClass = second.result.current.getContainerProps().className;
59
+ expect(secondClass).toBe(firstClass);
60
+ second.unmount();
61
+ });
62
+
63
+ it('reveal and conceal map to different style blocks', () => {
64
+ const {result} = renderHook(() => useContainerReveal());
65
+ const reveal = result.current.getContentRevealProps().className;
66
+ const conceal = result.current.getContentRevealProps({
67
+ isRevealInverted: true,
68
+ }).className;
69
+ expect(reveal).toBeTruthy();
70
+ expect(conceal).toBeTruthy();
71
+ expect(reveal).not.toBe(conceal);
72
+ });
73
+
74
+ it('layout-preserved reveal differs from clipped reveal', () => {
75
+ const {result} = renderHook(() => useContainerReveal());
76
+ const clipped = result.current.getContentRevealProps().className;
77
+ const preserved = result.current.getContentRevealProps({
78
+ isLayoutPreserved: true,
79
+ }).className;
80
+ expect(clipped).not.toBe(preserved);
81
+ });
82
+
83
+ it('assigns distinct markers across a full pool of concurrent containers', () => {
84
+ function Probe({onClass}: {onClass: (c: string) => void}) {
85
+ const {getContainerProps} = useContainerReveal();
86
+ onClass(getContainerProps().className ?? '');
87
+ return null;
88
+ }
89
+ const classes: string[] = [];
90
+ const {unmount} = render(
91
+ <>
92
+ {Array.from({length: POOL_SIZE}, (_, i) => (
93
+ <Probe key={i} onClass={(c) => classes.push(c)} />
94
+ ))}
95
+ </>,
96
+ );
97
+ const unique = new Set(classes.filter(Boolean));
98
+ expect(unique.size).toBe(POOL_SIZE);
99
+ unmount();
100
+ });
101
+
102
+ it('survives repeated mount/unmount without leaking the pool', () => {
103
+ // Mount and unmount a full pool several times; each cycle must still hand
104
+ // out POOL_SIZE distinct markers, proving slots are released.
105
+ for (let cycle = 0; cycle < 3; cycle++) {
106
+ const hooks = Array.from({length: POOL_SIZE}, () =>
107
+ renderHook(() => useContainerReveal()),
108
+ );
109
+ const classes = hooks.map(
110
+ (h) => h.result.current.getContainerProps().className,
111
+ );
112
+ expect(new Set(classes).size).toBe(POOL_SIZE);
113
+ act(() => hooks.forEach((h) => h.unmount()));
114
+ }
115
+ });
116
+ });