@astryxdesign/core 0.7.6-canary.2a905bb → 0.7.6-canary.2fa4cef
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/BottomSheet/BottomSheetPanel.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheetPanel.js +36 -9
- package/dist/BottomSheet/useSheetGestures.d.ts +36 -6
- package/dist/BottomSheet/useSheetGestures.d.ts.map +1 -1
- package/dist/BottomSheet/useSheetGestures.js +67 -10
- package/dist/FileInput/FileInput.d.ts.map +1 -1
- package/dist/FileInput/FileInput.js +27 -2
- package/dist/Layer/useTouchTrigger.d.ts +16 -2
- package/dist/Layer/useTouchTrigger.d.ts.map +1 -1
- package/dist/Layer/useTouchTrigger.js +56 -7
- package/dist/Toast/useToast.d.ts.map +1 -1
- package/dist/Toast/useToast.js +8 -2
- package/dist/ToggleButton/ToggleButton.d.ts.map +1 -1
- package/dist/ToggleButton/ToggleButton.js +8 -2
- package/dist/gestures/haptics.d.ts.map +1 -1
- package/dist/gestures/haptics.js +1 -5
- package/dist/hooks/useClickableContainer.d.ts +24 -4
- package/dist/hooks/useClickableContainer.d.ts.map +1 -1
- package/dist/hooks/useClickableContainer.js +41 -14
- package/dist/theme/Theme.d.ts +10 -0
- package/dist/theme/Theme.d.ts.map +1 -1
- package/dist/theme/Theme.js +8 -4
- package/dist/utils/pressDisabled.d.ts +21 -0
- package/dist/utils/pressDisabled.d.ts.map +1 -0
- package/dist/utils/pressDisabled.js +32 -0
- package/package.json +4 -4
- package/src/BottomSheet/BottomSheetPanel.tsx +39 -8
- package/src/BottomSheet/useSheetGestures.ts +92 -15
- package/src/FileInput/FileInput.tsx +29 -1
- package/src/HoverCard/HoverCard.doc.mjs +3 -3
- package/src/HoverCard/HoverCard.spec.md +16 -14
- package/src/HoverCard/useHoverCard.doc.mjs +1 -1
- package/src/Layer/useTouchTrigger.ts +70 -10
- package/src/Toast/useToast.tsx +8 -3
- package/src/ToggleButton/ToggleButton.tsx +9 -3
- package/src/Tooltip/Tooltip.doc.mjs +3 -3
- package/src/Tooltip/Tooltip.spec.md +17 -17
- package/src/Tooltip/useTooltip.doc.mjs +1 -1
- package/src/gestures/haptics.ts +1 -8
- package/src/hooks/useClickableContainer.ts +48 -17
- package/src/theme/Theme.tsx +8 -4
- package/src/utils/pressDisabled.ts +32 -0
|
@@ -26,7 +26,7 @@ import "../theme/tokens.stylex.js";
|
|
|
26
26
|
import { colorVars, fontWeightVars } from "../theme/tokens.stylex.js";
|
|
27
27
|
import { Button } from "../Button/index.js";
|
|
28
28
|
import { useToggleButtonGroup } from "./ToggleButtonGroup.js";
|
|
29
|
-
import { themeProps } from "../utils/themeProps.js";
|
|
29
|
+
import { themeDataAttributes, themeProps } from "../utils/themeProps.js";
|
|
30
30
|
import { mergeProps } from "../utils/mergeProps.js";
|
|
31
31
|
|
|
32
32
|
// =============================================================================
|
|
@@ -193,7 +193,6 @@ export function ToggleButton({
|
|
|
193
193
|
isLoading: isLoading,
|
|
194
194
|
isInterruptible: true,
|
|
195
195
|
isIconOnly: isIconOnly,
|
|
196
|
-
"aria-pressed": isPressed,
|
|
197
196
|
icon: resolvedIcon,
|
|
198
197
|
tooltip: tooltip,
|
|
199
198
|
...mergeProps(themeProps('toggle-button', {
|
|
@@ -205,6 +204,13 @@ export function ToggleButton({
|
|
|
205
204
|
onClick: handleClick,
|
|
206
205
|
clickAction: clickAction,
|
|
207
206
|
...props,
|
|
207
|
+
// The pressed state this component owns wins over a caller's
|
|
208
|
+
// same-named attributes (public-component-api INV5).
|
|
209
|
+
"aria-pressed": isPressed,
|
|
210
|
+
...themeDataAttributes({
|
|
211
|
+
isPressed: isPressed ? 'true' : 'false',
|
|
212
|
+
elevation
|
|
213
|
+
}),
|
|
208
214
|
children: labelContent
|
|
209
215
|
});
|
|
210
216
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"haptics.d.ts","sourceRoot":"","sources":["../../src/gestures/haptics.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,OAAO,EAEL,KAAK,kBAAkB,EACxB,MAAM,0BAA0B,CAAC;
|
|
1
|
+
{"version":3,"file":"haptics.d.ts","sourceRoot":"","sources":["../../src/gestures/haptics.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,OAAO,EAEL,KAAK,kBAAkB,EACxB,MAAM,0BAA0B,CAAC;AAGlC,8EAA8E;AAC9E,eAAO,MAAM,uBAAuB,8BAA8B,CAAC;AAEnE,4CAA4C;AAC5C,wBAAgB,UAAU,IAAI,OAAO,CAIpC;AAED,6EAA6E;AAC7E,wBAAgB,qBAAqB,CAAC,GAAG,CAAC,EAAE,QAAQ,GAAG,OAAO,CAQ7D;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,kBAAkB,GAAG,OAAO,CAK5D;AAkBD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,gBAAgB,GAAG,MAAM,IAAI,CAuFnE"}
|
package/dist/gestures/haptics.js
CHANGED
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
*/
|
|
36
36
|
|
|
37
37
|
import { HAPTIC_VIBRATION_PATTERNS } from "../motion/uikit/gestures.js";
|
|
38
|
+
import { isDisabledControl } from "../utils/pressDisabled.js";
|
|
38
39
|
|
|
39
40
|
/** Marks the hidden switch, so click handlers can recognize and ignore it. */
|
|
40
41
|
export const HAPTIC_SWITCH_ATTRIBUTE = 'data-astryx-haptic-switch';
|
|
@@ -69,11 +70,6 @@ export function playHaptic(kind) {
|
|
|
69
70
|
const VISUALLY_HIDDEN = 'position:absolute;width:1px;height:1px;margin:-1px;padding:0;border:0;' + 'overflow:hidden;clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap;' + 'opacity:0;pointer-events:none;';
|
|
70
71
|
let nextLabelId = 0;
|
|
71
72
|
|
|
72
|
-
/** A native disabled control, or one marked `aria-disabled`. */
|
|
73
|
-
function isDisabledControl(control) {
|
|
74
|
-
return control.matches(':disabled') || control.getAttribute('aria-disabled') === 'true';
|
|
75
|
-
}
|
|
76
|
-
|
|
77
73
|
/** Whether `input` is a switch checkbox, which plays the tap haptic itself. */
|
|
78
74
|
function isSwitch(input) {
|
|
79
75
|
return input.type === 'checkbox' && input.hasAttribute('switch');
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file useClickableContainer.ts
|
|
3
3
|
* @input Container ref, interactive element ref, click/href handlers
|
|
4
|
-
* @output onClick and onMouseUp handlers for the container; INTERACTIVE_SELECTORS list
|
|
4
|
+
* @output onClick and onMouseUp handlers for the container; INTERACTIVE_SELECTORS list;
|
|
5
|
+
* hasInteractiveAncestor and the shared innermostInteractive walk
|
|
5
6
|
* @position Core hook for clickable containers that safely handle nested interactive elements
|
|
6
7
|
*
|
|
7
8
|
* Solves the "nested interactive elements" problem: when a card is clickable
|
|
@@ -16,6 +17,12 @@
|
|
|
16
17
|
* - /packages/core/src/hooks/index.ts (export)
|
|
17
18
|
*/
|
|
18
19
|
import { type RefObject, type MouseEvent } from 'react';
|
|
20
|
+
/**
|
|
21
|
+
* The attribute this hook sets on the container it makes clickable. A click
|
|
22
|
+
* inside a nested marked container belongs to that container (see
|
|
23
|
+
* INTERACTIVE_SELECTORS).
|
|
24
|
+
*/
|
|
25
|
+
export declare const PRESSABLE_CONTAINER_ATTRIBUTE = "data-pressable-container";
|
|
19
26
|
/**
|
|
20
27
|
* Canonical list of interactive element selectors — native controls plus
|
|
21
28
|
* role-based interactive elements. Clicks on these (or their descendants)
|
|
@@ -29,9 +36,22 @@ import { type RefObject, type MouseEvent } from 'react';
|
|
|
29
36
|
*/
|
|
30
37
|
export declare const INTERACTIVE_SELECTORS: string;
|
|
31
38
|
/**
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
|
|
39
|
+
* Whether this element is interactive by INTERACTIVE_SELECTORS: a control,
|
|
40
|
+
* link, editor, or nested clickable container, and not marked read-only.
|
|
41
|
+
*/
|
|
42
|
+
export declare function matchesInteractiveSelectors(element: Element): boolean;
|
|
43
|
+
/**
|
|
44
|
+
* The innermost element, from `el` up to (not including) `rootEl` or the
|
|
45
|
+
* document body, that `isInteractive` accepts: by default the
|
|
46
|
+
* INTERACTIVE_SELECTORS match. The one walk every "does this event belong
|
|
47
|
+
* to a control" check shares; a hover layer's tap passes its own test
|
|
48
|
+
* (useTouchTrigger's `tapLandsOnDisabledControl`).
|
|
49
|
+
*/
|
|
50
|
+
export declare function innermostInteractive(el: Element, rootEl: Element | null, isInteractive?: (element: Element) => boolean): Element | null;
|
|
51
|
+
/**
|
|
52
|
+
* Check whether an element has an interactive ancestor between it and the
|
|
53
|
+
* root (the element itself included). If the click target is inside a nested
|
|
54
|
+
* button/link/etc., we should NOT handle it at the container level.
|
|
35
55
|
*
|
|
36
56
|
* Exported for containers that cannot use the hook itself — a `<tr>` whose
|
|
37
57
|
* props are assembled inside a Table plugin's `transformBodyRow`, for
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useClickableContainer.d.ts","sourceRoot":"","sources":["../../src/hooks/useClickableContainer.ts"],"names":[],"mappings":"AAIA
|
|
1
|
+
{"version":3,"file":"useClickableContainer.d.ts","sourceRoot":"","sources":["../../src/hooks/useClickableContainer.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAAyB,KAAK,SAAS,EAAE,KAAK,UAAU,EAAC,MAAM,OAAO,CAAC;AAG9E;;;;GAIG;AACH,eAAO,MAAM,6BAA6B,6BAA6B,CAAC;AAExE;;;;;;;;;;GAUG;AACH,eAAO,MAAM,qBAAqB,QAmBvB,CAAC;AAIZ;;;GAGG;AACH,wBAAgB,2BAA2B,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAKrE;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAClC,EAAE,EAAE,OAAO,EACX,MAAM,EAAE,OAAO,GAAG,IAAI,EACtB,aAAa,GAAE,CAAC,OAAO,EAAE,OAAO,KAAK,OAAqC,GACzE,OAAO,GAAG,IAAI,CAShB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,sBAAsB,CAAC,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,GAAG,OAAO,CAE5E;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CASvD;AAED,MAAM,WAAW,4BAA4B;IAC3C,yCAAyC;IACzC,YAAY,EAAE,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IAC5C,mEAAmE;IACnE,cAAc,CAAC,EAAE,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IAC/C,oBAAoB;IACpB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,CAAC,WAAW,CAAC,KAAK,IAAI,CAAC;IACnD;;;;OAIG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,kBAAkB;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,wCAAwC;IACxC,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,wBAAwB;IACvC,OAAO,EAAE,CAAC,KAAK,EAAE,UAAU,CAAC,WAAW,CAAC,KAAK,IAAI,CAAC;IAClD,SAAS,EAAE,CAAC,KAAK,EAAE,UAAU,CAAC,WAAW,CAAC,KAAK,IAAI,CAAC;CACrD;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,qBAAqB,CAAC,EACpC,YAAY,EACZ,cAAc,EACd,OAAO,EAAE,WAAW,EACpB,IAAI,EACJ,MAAM,EACN,QAAgB,GACjB,EAAE,4BAA4B,GAAG,wBAAwB,CAmHzD"}
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
/**
|
|
6
6
|
* @file useClickableContainer.ts
|
|
7
7
|
* @input Container ref, interactive element ref, click/href handlers
|
|
8
|
-
* @output onClick and onMouseUp handlers for the container; INTERACTIVE_SELECTORS list
|
|
8
|
+
* @output onClick and onMouseUp handlers for the container; INTERACTIVE_SELECTORS list;
|
|
9
|
+
* hasInteractiveAncestor and the shared innermostInteractive walk
|
|
9
10
|
* @position Core hook for clickable containers that safely handle nested interactive elements
|
|
10
11
|
*
|
|
11
12
|
* Solves the "nested interactive elements" problem: when a card is clickable
|
|
@@ -22,6 +23,13 @@
|
|
|
22
23
|
import { useCallback, useEffect } from 'react';
|
|
23
24
|
import { isSafeUrl } from "../utils/safeUrl.js";
|
|
24
25
|
|
|
26
|
+
/**
|
|
27
|
+
* The attribute this hook sets on the container it makes clickable. A click
|
|
28
|
+
* inside a nested marked container belongs to that container (see
|
|
29
|
+
* INTERACTIVE_SELECTORS).
|
|
30
|
+
*/
|
|
31
|
+
export const PRESSABLE_CONTAINER_ATTRIBUTE = 'data-pressable-container';
|
|
32
|
+
|
|
25
33
|
/**
|
|
26
34
|
* Canonical list of interactive element selectors — native controls plus
|
|
27
35
|
* role-based interactive elements. Clicks on these (or their descendants)
|
|
@@ -33,13 +41,39 @@ import { isSafeUrl } from "../utils/safeUrl.js";
|
|
|
33
41
|
* consumers that need focusable elements must additionally exclude
|
|
34
42
|
* unfocusable/disabled targets (e.g. `[tabindex="-1"]`, `:disabled`).
|
|
35
43
|
*/
|
|
36
|
-
export const INTERACTIVE_SELECTORS = ['button', 'a', 'input', 'select', 'textarea', '[role="button"]', '[role="link"]', '[role="checkbox"]', '[role="radio"]', '[role="switch"]', '[role="tab"]', '[role="menuitem"]', '[role="option"]', '[role="combobox"]', '[role="listbox"]', '[role="slider"]', '[role="spinbutton"]',
|
|
44
|
+
export const INTERACTIVE_SELECTORS = ['button', 'a', 'input', 'select', 'textarea', '[role="button"]', '[role="link"]', '[role="checkbox"]', '[role="radio"]', '[role="switch"]', '[role="tab"]', '[role="menuitem"]', '[role="option"]', '[role="combobox"]', '[role="listbox"]', '[role="slider"]', '[role="spinbutton"]', `[${PRESSABLE_CONTAINER_ATTRIBUTE}]`].join(',');
|
|
37
45
|
const NON_INTERACTIVE_SELECTORS = '[aria-readonly="true"]';
|
|
38
46
|
|
|
39
47
|
/**
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
|
|
48
|
+
* Whether this element is interactive by INTERACTIVE_SELECTORS: a control,
|
|
49
|
+
* link, editor, or nested clickable container, and not marked read-only.
|
|
50
|
+
*/
|
|
51
|
+
export function matchesInteractiveSelectors(element) {
|
|
52
|
+
return element.matches(INTERACTIVE_SELECTORS) && !element.matches(NON_INTERACTIVE_SELECTORS);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The innermost element, from `el` up to (not including) `rootEl` or the
|
|
57
|
+
* document body, that `isInteractive` accepts: by default the
|
|
58
|
+
* INTERACTIVE_SELECTORS match. The one walk every "does this event belong
|
|
59
|
+
* to a control" check shares; a hover layer's tap passes its own test
|
|
60
|
+
* (useTouchTrigger's `tapLandsOnDisabledControl`).
|
|
61
|
+
*/
|
|
62
|
+
export function innermostInteractive(el, rootEl, isInteractive = matchesInteractiveSelectors) {
|
|
63
|
+
let current = el;
|
|
64
|
+
while (current != null && current !== rootEl && current !== document.body) {
|
|
65
|
+
if (isInteractive(current)) {
|
|
66
|
+
return current;
|
|
67
|
+
}
|
|
68
|
+
current = current.parentElement;
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Check whether an element has an interactive ancestor between it and the
|
|
75
|
+
* root (the element itself included). If the click target is inside a nested
|
|
76
|
+
* button/link/etc., we should NOT handle it at the container level.
|
|
43
77
|
*
|
|
44
78
|
* Exported for containers that cannot use the hook itself — a `<tr>` whose
|
|
45
79
|
* props are assembled inside a Table plugin's `transformBodyRow`, for
|
|
@@ -48,14 +82,7 @@ const NON_INTERACTIVE_SELECTORS = '[aria-readonly="true"]';
|
|
|
48
82
|
* instead of a short hand-rolled selector list per caller.
|
|
49
83
|
*/
|
|
50
84
|
export function hasInteractiveAncestor(el, rootEl) {
|
|
51
|
-
|
|
52
|
-
while (current != null && current !== rootEl && current !== document.body) {
|
|
53
|
-
if (current.matches(INTERACTIVE_SELECTORS) && !current.matches(NON_INTERACTIVE_SELECTORS)) {
|
|
54
|
-
return true;
|
|
55
|
-
}
|
|
56
|
-
current = current.parentElement;
|
|
57
|
-
}
|
|
58
|
-
return false;
|
|
85
|
+
return innermostInteractive(el, rootEl) != null;
|
|
59
86
|
}
|
|
60
87
|
|
|
61
88
|
/**
|
|
@@ -115,7 +142,7 @@ export function useClickableContainer({
|
|
|
115
142
|
useEffect(() => {
|
|
116
143
|
const el = containerRef.current;
|
|
117
144
|
if (el) {
|
|
118
|
-
el.setAttribute(
|
|
145
|
+
el.setAttribute(PRESSABLE_CONTAINER_ATTRIBUTE, 'true');
|
|
119
146
|
}
|
|
120
147
|
}, [containerRef]);
|
|
121
148
|
const onClick = useCallback(event => {
|
package/dist/theme/Theme.d.ts
CHANGED
|
@@ -44,6 +44,16 @@ interface ThemeProps {
|
|
|
44
44
|
/** Children to render */
|
|
45
45
|
children: React.ReactNode;
|
|
46
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* True below a Theme; the root Theme (no parent context) syncs data-theme
|
|
49
|
+
* to <html>. A detached React root that conceptually sits inside the app
|
|
50
|
+
* (useToast's fallback viewport) provides it as `true` itself, so a Theme
|
|
51
|
+
* rendered in toast content themes its subtree without claiming <html> as
|
|
52
|
+
* if it were the app's root Theme.
|
|
53
|
+
* @internal Not exported from the theme barrel; useToast imports it from
|
|
54
|
+
* './theme/Theme' directly.
|
|
55
|
+
*/
|
|
56
|
+
export declare const ThemeNestingContext: React.Context<boolean>;
|
|
47
57
|
/**
|
|
48
58
|
* Theme provider component
|
|
49
59
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Theme.d.ts","sourceRoot":"","sources":["../../src/theme/Theme.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,OAAO,KAAgD,MAAM,OAAO,CAAC;AAGrE,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,SAAS,CAAC;AAEvC,OAAO,EAAmB,KAAK,YAAY,EAAC,MAAM,eAAe,CAAC;AAMlE;;GAEG;AACH,UAAU,UAAU;IAClB,+BAA+B;IAC/B,KAAK,EAAE,YAAY,CAAC;IACpB,kDAAkD;IAClD,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,yBAAyB;IACzB,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;CAC3B;
|
|
1
|
+
{"version":3,"file":"Theme.d.ts","sourceRoot":"","sources":["../../src/theme/Theme.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,OAAO,KAAgD,MAAM,OAAO,CAAC;AAGrE,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,SAAS,CAAC;AAEvC,OAAO,EAAmB,KAAK,YAAY,EAAC,MAAM,eAAe,CAAC;AAMlE;;GAEG;AACH,UAAU,UAAU;IAClB,+BAA+B;IAC/B,KAAK,EAAE,YAAY,CAAC;IACpB,kDAAkD;IAClD,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,yBAAyB;IACzB,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;CAC3B;AA0BD;;;;;;;;GAQG;AACH,eAAO,MAAM,mBAAmB,wBAA6B,CAAC;AA6I9D;;;;;;;;;;;;GAYG;AACH,wBAAgB,KAAK,CAAC,EACpB,KAAK,EACL,IAAe,EACf,QAAQ,GACT,EAAE,UAAU,GAAG,KAAK,CAAC,YAAY,CA+BjC;yBAnCe,KAAK"}
|
package/dist/theme/Theme.js
CHANGED
|
@@ -78,11 +78,15 @@ const wrapperStyles = {
|
|
|
78
78
|
// =============================================================================
|
|
79
79
|
|
|
80
80
|
/**
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
81
|
+
* True below a Theme; the root Theme (no parent context) syncs data-theme
|
|
82
|
+
* to <html>. A detached React root that conceptually sits inside the app
|
|
83
|
+
* (useToast's fallback viewport) provides it as `true` itself, so a Theme
|
|
84
|
+
* rendered in toast content themes its subtree without claiming <html> as
|
|
85
|
+
* if it were the app's root Theme.
|
|
86
|
+
* @internal Not exported from the theme barrel; useToast imports it from
|
|
87
|
+
* './theme/Theme' directly.
|
|
84
88
|
*/
|
|
85
|
-
const ThemeNestingContext = /*#__PURE__*/React.createContext(false);
|
|
89
|
+
export const ThemeNestingContext = /*#__PURE__*/React.createContext(false);
|
|
86
90
|
ThemeNestingContext.displayName = 'ThemeNestingContext';
|
|
87
91
|
|
|
88
92
|
// =============================================================================
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file pressDisabled.ts
|
|
3
|
+
* @input An element
|
|
4
|
+
* @output `isDisabledControl`: whether an element is a disabled control, so a
|
|
5
|
+
* press on it performs no action
|
|
6
|
+
* @position The one disabled-control check that press feedback
|
|
7
|
+
* (../gestures/haptics) and the hover layers' touch trigger
|
|
8
|
+
* (../Layer/useTouchTrigger) share, so the two never disagree about a
|
|
9
|
+
* control. It lives outside the gesture code, which a public import that
|
|
10
|
+
* uses no gesture never carries (architecture:react-component-runtime
|
|
11
|
+
* INV13).
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Whether `element` is a disabled control: `aria-disabled="true"`, or a form
|
|
15
|
+
* control the browser counts as `:disabled` (a disabled fieldset's controls
|
|
16
|
+
* included). A disabled control performs no action, so it gives no press
|
|
17
|
+
* feedback (spec:AST-094 FR23), and a tap on it opens its reason instead
|
|
18
|
+
* (component:Tooltip FR5).
|
|
19
|
+
*/
|
|
20
|
+
export declare function isDisabledControl(element: Element): boolean;
|
|
21
|
+
//# sourceMappingURL=pressDisabled.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pressDisabled.d.ts","sourceRoot":"","sources":["../../src/utils/pressDisabled.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;GAWG;AAEH;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAS3D"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file pressDisabled.ts
|
|
5
|
+
* @input An element
|
|
6
|
+
* @output `isDisabledControl`: whether an element is a disabled control, so a
|
|
7
|
+
* press on it performs no action
|
|
8
|
+
* @position The one disabled-control check that press feedback
|
|
9
|
+
* (../gestures/haptics) and the hover layers' touch trigger
|
|
10
|
+
* (../Layer/useTouchTrigger) share, so the two never disagree about a
|
|
11
|
+
* control. It lives outside the gesture code, which a public import that
|
|
12
|
+
* uses no gesture never carries (architecture:react-component-runtime
|
|
13
|
+
* INV13).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Whether `element` is a disabled control: `aria-disabled="true"`, or a form
|
|
18
|
+
* control the browser counts as `:disabled` (a disabled fieldset's controls
|
|
19
|
+
* included). A disabled control performs no action, so it gives no press
|
|
20
|
+
* feedback (spec:AST-094 FR23), and a tap on it opens its reason instead
|
|
21
|
+
* (component:Tooltip FR5).
|
|
22
|
+
*/
|
|
23
|
+
export function isDisabledControl(element) {
|
|
24
|
+
if (element.getAttribute('aria-disabled') === 'true') {
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
return element.matches(':disabled');
|
|
29
|
+
} catch {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astryxdesign/core",
|
|
3
|
-
"version": "0.7.6-canary.
|
|
3
|
+
"version": "0.7.6-canary.2fa4cef",
|
|
4
4
|
"displayName": "Astryx Core",
|
|
5
5
|
"description": "The component library. Accessible, themeable React components with built-in spacing, dark mode, and StyleX styling.",
|
|
6
6
|
"author": "Meta Open Source",
|
|
@@ -860,9 +860,9 @@
|
|
|
860
860
|
"react-dom": ">=19.0.0"
|
|
861
861
|
},
|
|
862
862
|
"devDependencies": {
|
|
863
|
-
"@astryxdesign/a11y-spec": "0.7.6-canary.
|
|
864
|
-
"@astryxdesign/cli": "0.7.6-canary.
|
|
865
|
-
"@astryxdesign/interaction-harness": "0.7.6-canary.
|
|
863
|
+
"@astryxdesign/a11y-spec": "0.7.6-canary.2fa4cef",
|
|
864
|
+
"@astryxdesign/cli": "0.7.6-canary.2fa4cef",
|
|
865
|
+
"@astryxdesign/interaction-harness": "0.7.6-canary.2fa4cef",
|
|
866
866
|
"@babel/cli": "^7.29.7",
|
|
867
867
|
"@babel/core": "^7.29.7",
|
|
868
868
|
"@babel/preset-react": "^7.29.7",
|
|
@@ -71,7 +71,11 @@ import {
|
|
|
71
71
|
type BottomSheetSnapPoint,
|
|
72
72
|
} from './snapOffsets';
|
|
73
73
|
import {useMobileKeyboard} from './useMobileKeyboard';
|
|
74
|
-
import {
|
|
74
|
+
import {
|
|
75
|
+
scrollPreservationPadding,
|
|
76
|
+
sheetLayoutHeight,
|
|
77
|
+
useSheetGestures,
|
|
78
|
+
} from './useSheetGestures';
|
|
75
79
|
|
|
76
80
|
const HEIGHT_BUDGETS = {
|
|
77
81
|
hug: '92dvh',
|
|
@@ -592,6 +596,8 @@ export function BottomSheetPanel({
|
|
|
592
596
|
ignoredSnapPointsMessage !== '',
|
|
593
597
|
);
|
|
594
598
|
|
|
599
|
+
// A caller's height owns the element's height: the drag never writes it.
|
|
600
|
+
const consumerHeight = style?.height;
|
|
595
601
|
const {
|
|
596
602
|
contentProps,
|
|
597
603
|
handleProps,
|
|
@@ -616,6 +622,7 @@ export function BottomSheetPanel({
|
|
|
616
622
|
onDismiss,
|
|
617
623
|
snapHeights,
|
|
618
624
|
onScrimOpacity,
|
|
625
|
+
hasFixedHeight: consumerHeight != null,
|
|
619
626
|
});
|
|
620
627
|
|
|
621
628
|
const {getViewportProps, getContentProps} = useScrollableArea({
|
|
@@ -758,6 +765,8 @@ export function BottomSheetPanel({
|
|
|
758
765
|
// transform — the layout height changes at rest, in one reconciling render
|
|
759
766
|
// whose visible geometry is identical (see useSheetGestures).
|
|
760
767
|
// - dragging above the base restores the full height below the viewport
|
|
768
|
+
// (the move that crosses writes that height to the element itself,
|
|
769
|
+
// so this render only catches up to it)
|
|
761
770
|
// - a peek settles with layout 0, so it slides rather than reflowing
|
|
762
771
|
const layoutOffset = isDragging
|
|
763
772
|
? dragOffset < settledOffset
|
|
@@ -774,7 +783,7 @@ export function BottomSheetPanel({
|
|
|
774
783
|
const isTraveling = isDragging || settlingLayoutOffset != null;
|
|
775
784
|
resizedHeight =
|
|
776
785
|
layoutOffset > 0 || isTraveling
|
|
777
|
-
?
|
|
786
|
+
? sheetLayoutHeight(sheetHeight, layoutOffset)
|
|
778
787
|
: undefined;
|
|
779
788
|
gestureTransform =
|
|
780
789
|
translation !== 0 ? `translateY(${translation}px)` : undefined;
|
|
@@ -791,6 +800,9 @@ export function BottomSheetPanel({
|
|
|
791
800
|
? retainedTransform
|
|
792
801
|
: undefined;
|
|
793
802
|
const consumerTransform = style?.transform;
|
|
803
|
+
const hostHeight =
|
|
804
|
+
isInteractive || isRetained || isClosing ? resizedHeight : undefined;
|
|
805
|
+
const bodyEndPadding = scrollPreservationPadding(scrollPreservationInset);
|
|
794
806
|
// The transform is written to the element here, not rendered through the
|
|
795
807
|
// style prop. A drag writes it straight to the element once per input
|
|
796
808
|
// sample (useSheetGestures) and renders nothing in between, so React never
|
|
@@ -799,20 +811,39 @@ export function BottomSheetPanel({
|
|
|
799
811
|
// the finger left it. Written after every commit instead, outside a drag,
|
|
800
812
|
// the resting value lands in the same style update as the restored
|
|
801
813
|
// transition, so a settle or a dismissal animates from the live position.
|
|
814
|
+
//
|
|
815
|
+
// The height and the scrolling area's end inset are rendered through props,
|
|
816
|
+
// but a drag that crosses its base detent writes them to the elements too.
|
|
817
|
+
// When the release is processed before that move's render commits, React
|
|
818
|
+
// compares the resting values with the ones it committed before the drag,
|
|
819
|
+
// finds them unchanged and writes nothing, so the sheet would stay drawn at
|
|
820
|
+
// the drag's height. They are written here after every commit, outside a
|
|
821
|
+
// drag, for the same reason as the transform. A caller's height is left to
|
|
822
|
+
// the style prop: the drag does not write over it, so there is nothing to
|
|
823
|
+
// restore.
|
|
802
824
|
useLayoutEffect(() => {
|
|
803
|
-
if (isDragging
|
|
825
|
+
if (isDragging) {
|
|
804
826
|
return;
|
|
805
827
|
}
|
|
806
828
|
const element = elementRef.current;
|
|
807
829
|
if (element != null) {
|
|
808
|
-
|
|
830
|
+
if (consumerTransform == null) {
|
|
831
|
+
element.style.transform = sheetTransform ?? '';
|
|
832
|
+
}
|
|
833
|
+
if (consumerHeight == null) {
|
|
834
|
+
element.style.height = hostHeight ?? '';
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
const body = bodyElementRef.current;
|
|
838
|
+
if (body != null) {
|
|
839
|
+
body.style.paddingBlockEnd = bodyEndPadding ?? '';
|
|
809
840
|
}
|
|
810
841
|
});
|
|
811
842
|
const gestureStyle = {
|
|
812
843
|
transition: contentProps.style.transition,
|
|
813
844
|
touchAction: contentProps.style.touchAction,
|
|
814
845
|
overscrollBehavior: contentProps.style.overscrollBehavior,
|
|
815
|
-
height:
|
|
846
|
+
height: hostHeight,
|
|
816
847
|
};
|
|
817
848
|
return (
|
|
818
849
|
<div
|
|
@@ -839,7 +870,7 @@ export function BottomSheetPanel({
|
|
|
839
870
|
...(isInteractive
|
|
840
871
|
? gestureStyle
|
|
841
872
|
: isRetained || isClosing
|
|
842
|
-
? {height:
|
|
873
|
+
? {height: hostHeight}
|
|
843
874
|
: {}),
|
|
844
875
|
...style,
|
|
845
876
|
},
|
|
@@ -860,8 +891,8 @@ export function BottomSheetPanel({
|
|
|
860
891
|
styles.body,
|
|
861
892
|
height === 'tall' && styles.tallKeyboardBody,
|
|
862
893
|
),
|
|
863
|
-
|
|
864
|
-
? {style: {paddingBlockEnd:
|
|
894
|
+
bodyEndPadding != null
|
|
895
|
+
? {style: {paddingBlockEnd: bodyEndPadding}}
|
|
865
896
|
: {},
|
|
866
897
|
),
|
|
867
898
|
}),
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
/**
|
|
6
6
|
* @file useSheetGestures.ts
|
|
7
7
|
* @input Uses React (useCallback, useEffect, useMemo, useRef, useState)
|
|
8
|
-
* @output Exports useSheetGestures hook
|
|
8
|
+
* @output Exports useSheetGestures hook, its option/result types, and the
|
|
9
|
+
* height and end-inset formats a host renders for the layout split
|
|
9
10
|
* @position Internal to BottomSheet; not exported from the core entry point
|
|
10
11
|
*
|
|
11
12
|
* Drag + snap machinery for the bottom sheet. Tracks a pointer drag down the
|
|
@@ -26,10 +27,18 @@
|
|
|
26
27
|
* A live drag writes that transform straight to the sheet element, once per
|
|
27
28
|
* input sample, and renders nothing: React state changes when the drag begins
|
|
28
29
|
* and ends, and in between only when the layout split changes (a drag that
|
|
29
|
-
* crosses above its base detent restores the full height).
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
30
|
+
* crosses above its base detent restores the full height). That crossing
|
|
31
|
+
* changes three things that only look right together: the transform, the
|
|
32
|
+
* sheet's height and the scrolling area's end inset. The move writes all
|
|
33
|
+
* three to the elements in the same task, so the browser paints them in one
|
|
34
|
+
* frame, and the render it schedules catches the host up to the same values
|
|
35
|
+
* whenever it commits; it never decides what the next frame shows. The live
|
|
36
|
+
* offset is readable through `activeOffsetRef`. The host owns the element's
|
|
37
|
+
* transform, height and end inset outside a drag and writes their resting
|
|
38
|
+
* values after each commit (a host with a fixed height of its own keeps that
|
|
39
|
+
* height, and the drag never writes it), so a settle animates from wherever the finger
|
|
40
|
+
* left the sheet, and a release processed before the crossing move's render
|
|
41
|
+
* commits still leaves the sheet drawn at its resting detent.
|
|
33
42
|
*
|
|
34
43
|
* Those are pixels, and the stops behind them are relative to the viewport, so
|
|
35
44
|
* a sheet at rest re-resolves its detent on `resize` / `orientationchange`,
|
|
@@ -206,6 +215,28 @@ function preservationInsetForOffset(
|
|
|
206
215
|
return Math.max(0, baseOffset - targetOffset - naturalEndGap);
|
|
207
216
|
}
|
|
208
217
|
|
|
218
|
+
/**
|
|
219
|
+
* The sheet's height, in px, while it is in motion or rests with a layout
|
|
220
|
+
* split: the fully open height less the part the scrolling area gives up.
|
|
221
|
+
* The host renders it and a live drag writes it; one format keeps the two
|
|
222
|
+
* identical, so the render that follows a written value changes nothing.
|
|
223
|
+
*/
|
|
224
|
+
export function sheetLayoutHeight(
|
|
225
|
+
sheetHeight: number,
|
|
226
|
+
layoutOffset: number,
|
|
227
|
+
): string {
|
|
228
|
+
return `${Math.max(0, sheetHeight - Math.max(0, layoutOffset))}px`;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* The scrolling area's end padding that preserves its scroll position across
|
|
233
|
+
* a height change, or nothing when there is none to add. Shared by the host's
|
|
234
|
+
* render and a live drag's write for the same reason as sheetLayoutHeight.
|
|
235
|
+
*/
|
|
236
|
+
export function scrollPreservationPadding(inset: number): string | undefined {
|
|
237
|
+
return inset > 0 ? `${inset}px` : undefined;
|
|
238
|
+
}
|
|
239
|
+
|
|
209
240
|
function renderedBlockEndPadding(element: HTMLElement): number {
|
|
210
241
|
const value = Number.parseFloat(getComputedStyle(element).paddingBlockEnd);
|
|
211
242
|
return Number.isFinite(value) ? value : 0;
|
|
@@ -368,6 +399,14 @@ export interface UseSheetGesturesOptions {
|
|
|
368
399
|
* on the dismiss overshoot. Lets the owner mirror it onto the scrim.
|
|
369
400
|
*/
|
|
370
401
|
onScrimOpacity?: (opacity: number) => void;
|
|
402
|
+
/**
|
|
403
|
+
* Whether the host draws the sheet at a fixed height of its own (a caller's
|
|
404
|
+
* height) rather than the layout-split height. A drag across the base
|
|
405
|
+
* detent then writes only the transform and the end inset, and leaves the
|
|
406
|
+
* element's height to the host.
|
|
407
|
+
* @default false
|
|
408
|
+
*/
|
|
409
|
+
hasFixedHeight?: boolean;
|
|
371
410
|
}
|
|
372
411
|
|
|
373
412
|
export interface SheetContentProps {
|
|
@@ -508,6 +547,7 @@ export function useSheetGestures({
|
|
|
508
547
|
snapHeights,
|
|
509
548
|
onSnap,
|
|
510
549
|
onScrimOpacity,
|
|
550
|
+
hasFixedHeight = false,
|
|
511
551
|
}: UseSheetGesturesOptions): UseSheetGesturesResult {
|
|
512
552
|
const [dragOffset, setDragOffset] = useState(0);
|
|
513
553
|
const [settledOffset, setSettledOffset] = useState(0);
|
|
@@ -657,11 +697,13 @@ export function useSheetGestures({
|
|
|
657
697
|
const canDismissRef = useRef(canDismiss);
|
|
658
698
|
const onSnapRef = useRef(onSnap);
|
|
659
699
|
const onScrimOpacityRef = useRef(onScrimOpacity);
|
|
700
|
+
const hasFixedHeightRef = useRef(hasFixedHeight);
|
|
660
701
|
useEffect(() => {
|
|
661
702
|
onDismissRef.current = onDismiss;
|
|
662
703
|
canDismissRef.current = canDismiss;
|
|
663
704
|
onSnapRef.current = onSnap;
|
|
664
705
|
onScrimOpacityRef.current = onScrimOpacity;
|
|
706
|
+
hasFixedHeightRef.current = hasFixedHeight;
|
|
665
707
|
});
|
|
666
708
|
|
|
667
709
|
// Live drag bookkeeping (refs so pointermove doesn't churn renders).
|
|
@@ -954,6 +996,31 @@ export function useSheetGestures({
|
|
|
954
996
|
[],
|
|
955
997
|
);
|
|
956
998
|
|
|
999
|
+
// A live drag's layout split moved. The transform written beside this call
|
|
1000
|
+
// is only correct on the new split, so the height and end inset that pair
|
|
1001
|
+
// with it go to the elements in the same task. They are the values the host
|
|
1002
|
+
// renders from the state this move also sets, in the same format, so its
|
|
1003
|
+
// commit, whenever it lands, leaves them as they are. If the drag ends
|
|
1004
|
+
// before that commit, the host's post-commit write puts the resting values
|
|
1005
|
+
// back, since React sees no change from what it committed before the drag.
|
|
1006
|
+
// A host with a fixed height of its own renders that height throughout and
|
|
1007
|
+
// never restores a written one, so its height is not written.
|
|
1008
|
+
const writeLiveLayout = useCallback(
|
|
1009
|
+
(sheetHeight: number, layoutOffset: number) => {
|
|
1010
|
+
const element = sheetElRef.current;
|
|
1011
|
+
// Unmeasured, the host renders no height to pair with.
|
|
1012
|
+
if (element != null && sheetHeight > 0 && !hasFixedHeightRef.current) {
|
|
1013
|
+
element.style.height = sheetLayoutHeight(sheetHeight, layoutOffset);
|
|
1014
|
+
}
|
|
1015
|
+
const body = bodyNodeRef.current;
|
|
1016
|
+
if (body != null) {
|
|
1017
|
+
body.style.paddingBlockEnd =
|
|
1018
|
+
scrollPreservationPadding(scrollPreservationInsetRef.current) ?? '';
|
|
1019
|
+
}
|
|
1020
|
+
},
|
|
1021
|
+
[],
|
|
1022
|
+
);
|
|
1023
|
+
|
|
957
1024
|
const cancelDrag = useCallback(
|
|
958
1025
|
(target?: HTMLElement) => {
|
|
959
1026
|
const state = dragStateRef.current;
|
|
@@ -1262,23 +1329,28 @@ export function useSheetGestures({
|
|
|
1262
1329
|
state.renderedOffset = next;
|
|
1263
1330
|
state.layoutOffset = layoutOffset;
|
|
1264
1331
|
activeOffsetRef.current = next;
|
|
1332
|
+
updateScrollPreservationInset(
|
|
1333
|
+
preservationInsetForOffset(
|
|
1334
|
+
state.baseLayoutOffset,
|
|
1335
|
+
layoutOffset,
|
|
1336
|
+
state.naturalEndGap,
|
|
1337
|
+
),
|
|
1338
|
+
);
|
|
1265
1339
|
writeLiveTransform(next, layoutOffset);
|
|
1266
1340
|
if (hasLayoutChanged) {
|
|
1267
|
-
// The layout split moved
|
|
1268
|
-
// the
|
|
1341
|
+
// The layout split moved. The new height and end inset are written
|
|
1342
|
+
// with the transform, in this task: a render scheduled normally can
|
|
1343
|
+
// commit after the next paint, and the transform alone on the old
|
|
1344
|
+
// height would put the sheet the whole split away from the finger
|
|
1345
|
+
// for that frame. The host still renders them from state, reading
|
|
1346
|
+
// the offset it has to pair with the height from here.
|
|
1347
|
+
writeLiveLayout(state.height, layoutOffset);
|
|
1269
1348
|
setDragOffset(next);
|
|
1270
1349
|
}
|
|
1271
1350
|
if (!state.hasTraveled && next !== state.baseOffset) {
|
|
1272
1351
|
state.hasTraveled = true;
|
|
1273
1352
|
setIsTraveling(true);
|
|
1274
1353
|
}
|
|
1275
|
-
updateScrollPreservationInset(
|
|
1276
|
-
preservationInsetForOffset(
|
|
1277
|
-
state.baseLayoutOffset,
|
|
1278
|
-
layoutOffset,
|
|
1279
|
-
state.naturalEndGap,
|
|
1280
|
-
),
|
|
1281
|
-
);
|
|
1282
1354
|
|
|
1283
1355
|
// Mirror the scrim to the live drag: full at/above the mid detent,
|
|
1284
1356
|
// fading to hidden as it collapses onto the peek detent and through the
|
|
@@ -1295,7 +1367,12 @@ export function useSheetGestures({
|
|
|
1295
1367
|
scrimOpacityForOffset(next, offsets, dismissOffset, peekOffset),
|
|
1296
1368
|
);
|
|
1297
1369
|
},
|
|
1298
|
-
[
|
|
1370
|
+
[
|
|
1371
|
+
resolveDetents,
|
|
1372
|
+
updateScrollPreservationInset,
|
|
1373
|
+
writeLiveLayout,
|
|
1374
|
+
writeLiveTransform,
|
|
1375
|
+
],
|
|
1299
1376
|
);
|
|
1300
1377
|
|
|
1301
1378
|
const endDrag = useCallback(
|