@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.
- package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuSubMenu.js +9 -1
- package/dist/DropdownMenu/menuItemHover.d.ts +1 -0
- package/dist/DropdownMenu/menuItemHover.d.ts.map +1 -1
- package/dist/DropdownMenu/menuItemHover.js +1 -0
- package/dist/FieldStatus/FieldStatus.d.ts.map +1 -1
- package/dist/FieldStatus/FieldStatus.js +12 -2
- package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
- package/dist/MultiSelector/MultiSelector.js +1 -1
- package/dist/Thumbnail/Thumbnail.d.ts.map +1 -1
- package/dist/Thumbnail/Thumbnail.js +42 -43
- package/dist/TopNav/TopNavMegaMenu.d.ts.map +1 -1
- package/dist/TopNav/TopNavMegaMenu.js +9 -3
- package/dist/astryx.css +35 -5
- package/dist/astryx.umd.js +46 -46
- package/dist/astryx.umd.js.map +4 -4
- package/dist/hooks/containerReveal.pool.stylex.d.ts +281 -0
- package/dist/hooks/containerReveal.pool.stylex.d.ts.map +1 -0
- package/dist/hooks/containerReveal.pool.stylex.js +281 -0
- package/dist/hooks/index.d.ts +2 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +2 -1
- package/dist/hooks/useContainerReveal.d.ts +83 -0
- package/dist/hooks/useContainerReveal.d.ts.map +1 -0
- package/dist/hooks/useContainerReveal.js +123 -0
- package/dist/hooks/useInputStatusIcon.d.ts.map +1 -1
- package/dist/hooks/useInputStatusIcon.js +11 -0
- package/package.json +1 -1
- package/src/DropdownMenu/DropdownMenuSubMenu.test.tsx +29 -1
- package/src/DropdownMenu/DropdownMenuSubMenu.tsx +13 -5
- package/src/DropdownMenu/menuItemHover.ts +1 -0
- package/src/Field/Field.doc.mjs +4 -0
- package/src/FieldStatus/FieldStatus.doc.mjs +1 -0
- package/src/FieldStatus/FieldStatus.test.tsx +61 -0
- package/src/FieldStatus/FieldStatus.tsx +14 -3
- package/src/MultiSelector/MultiSelector.tsx +6 -5
- package/src/Thumbnail/Thumbnail.tsx +18 -32
- package/src/TopNav/TopNavMegaMenu.tsx +26 -2
- package/src/hooks/containerReveal.pool.stylex.ts +464 -0
- package/src/hooks/index.ts +7 -0
- package/src/hooks/useContainerReveal.test.tsx +116 -0
- package/src/hooks/useContainerReveal.ts +180 -0
- package/src/hooks/useInputStatusIcon.test.tsx +59 -0
- package/src/hooks/useInputStatusIcon.tsx +12 -1
- package/dist/Thumbnail/thumbnail.markers.stylex.d.ts +0 -9
- package/dist/Thumbnail/thumbnail.markers.stylex.d.ts.map +0 -1
- package/dist/Thumbnail/thumbnail.markers.stylex.js +0 -14
- 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];
|
package/src/hooks/index.ts
CHANGED
|
@@ -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
|
+
});
|