@astryxdesign/core 0.7.6 → 0.7.7-canary.36f8ea1
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/AppShell/AppShell.d.ts.map +1 -1
- package/dist/AppShell/AppShell.js +23 -3
- package/dist/BottomSheet/BottomSheetPanel.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheetPanel.js +16 -9
- package/dist/Chat/ChatComposerInput.d.ts +9 -0
- package/dist/Chat/ChatComposerInput.d.ts.map +1 -1
- package/dist/Chat/ChatComposerInput.js +2 -0
- package/dist/Chat/ChatMessage.js +2 -2
- package/dist/Chat/chatMessage.vars.stylex.d.ts +8 -8
- package/dist/Chat/chatMessage.vars.stylex.js +8 -8
- package/dist/Drawer/Drawer.d.ts.map +1 -1
- package/dist/Drawer/Drawer.js +6 -0
- package/dist/DropdownMenu/useMenuOverflow.d.ts +13 -3
- package/dist/DropdownMenu/useMenuOverflow.d.ts.map +1 -1
- package/dist/DropdownMenu/useMenuOverflow.js +31 -8
- package/dist/Link/Link.d.ts.map +1 -1
- package/dist/Link/Link.js +1 -0
- package/dist/Selector/Selector.d.ts.map +1 -1
- package/dist/Selector/Selector.js +25 -13
- package/dist/Slider/Slider.d.ts.map +1 -1
- package/dist/Slider/Slider.js +404 -131
- package/dist/Slider/sliderTouchController.d.ts +189 -0
- package/dist/Slider/sliderTouchController.d.ts.map +1 -0
- package/dist/Slider/sliderTouchController.js +1017 -0
- package/dist/Slider/useSliderTouch.d.ts +22 -0
- package/dist/Slider/useSliderTouch.d.ts.map +1 -0
- package/dist/Slider/useSliderTouch.js +56 -0
- package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
- package/dist/Typeahead/BaseTypeahead.js +10 -8
- package/dist/astryx.css +33 -6
- package/dist/gestures/GestureArena.d.ts.map +1 -1
- package/dist/gestures/GestureArena.js +4 -1
- package/dist/gestures/GestureHost.d.ts +72 -2
- package/dist/gestures/GestureHost.d.ts.map +1 -1
- package/dist/gestures/GestureHost.js +643 -5
- package/dist/gestures/GestureRecognizer.d.ts +15 -1
- package/dist/gestures/GestureRecognizer.d.ts.map +1 -1
- package/dist/gestures/GestureRecognizer.js +12 -0
- package/dist/gestures/PanGestureRecognizer.d.ts +8 -0
- package/dist/gestures/PanGestureRecognizer.d.ts.map +1 -1
- package/dist/gestures/PinchGestureRecognizer.d.ts +5 -1
- package/dist/gestures/PinchGestureRecognizer.d.ts.map +1 -1
- package/dist/gestures/PinchGestureRecognizer.js +10 -0
- package/dist/gestures/ScreenEdgePanGestureRecognizer.d.ts +3 -3
- package/dist/gestures/ScreenEdgePanGestureRecognizer.d.ts.map +1 -1
- package/dist/gestures/ScreenEdgePanGestureRecognizer.js +7 -5
- package/dist/gestures/nativeScroll.d.ts +16 -2
- package/dist/gestures/nativeScroll.d.ts.map +1 -1
- package/dist/gestures/nativeScroll.js +39 -8
- package/dist/gestures/recognizerInternals.d.ts +4 -2
- package/dist/gestures/recognizerInternals.d.ts.map +1 -1
- package/dist/gestures/types.d.ts +11 -0
- package/dist/gestures/types.d.ts.map +1 -1
- package/dist/gestures/types.js +7 -0
- package/dist/gestures/wheelSegmenter.d.ts +45 -0
- package/dist/gestures/wheelSegmenter.d.ts.map +1 -0
- package/dist/gestures/wheelSegmenter.js +98 -0
- package/dist/motion/animator.d.ts.map +1 -1
- package/dist/motion/animator.js +77 -14
- package/dist/motion/uikit/index.d.ts +2 -1
- package/dist/motion/uikit/index.d.ts.map +1 -1
- package/dist/motion/uikit/index.js +2 -1
- package/dist/motion/uikit/slider.d.ts +97 -0
- package/dist/motion/uikit/slider.d.ts.map +1 -0
- package/dist/motion/uikit/slider.js +111 -0
- package/dist/motion/uikit/springs.d.ts +46 -1
- package/dist/motion/uikit/springs.d.ts.map +1 -1
- package/dist/motion/uikit/springs.js +48 -4
- package/package.json +5 -5
- package/src/AppShell/AppShell.doc.mjs +10 -0
- package/src/AppShell/AppShell.tsx +48 -2
- package/src/BottomSheet/BottomSheetPanel.tsx +16 -9
- package/src/Chat/Chat.doc.mjs +1 -0
- package/src/Chat/ChatComposerInput.doc.mjs +11 -0
- package/src/Chat/ChatComposerInput.tsx +11 -0
- package/src/Chat/ChatMessage.tsx +2 -2
- package/src/Chat/chatMessage.vars.stylex.ts +8 -8
- package/src/Drawer/Drawer.tsx +6 -0
- package/src/DropdownMenu/useMenuOverflow.ts +44 -8
- package/src/Link/Link.tsx +3 -0
- package/src/Selector/Selector.tsx +29 -2
- package/src/Slider/Slider.doc.mjs +11 -7
- package/src/Slider/Slider.tsx +539 -132
- package/src/Slider/sliderTouchController.ts +1209 -0
- package/src/Slider/useSliderTouch.ts +78 -0
- package/src/Typeahead/BaseTypeahead.tsx +9 -8
- package/src/gestures/GestureArena.ts +1 -0
- package/src/gestures/GestureHost.ts +765 -5
- package/src/gestures/GestureRecognizer.ts +28 -0
- package/src/gestures/PanGestureRecognizer.ts +8 -0
- package/src/gestures/PinchGestureRecognizer.ts +16 -1
- package/src/gestures/README.md +63 -0
- package/src/gestures/ScreenEdgePanGestureRecognizer.ts +7 -5
- package/src/gestures/nativeScroll.ts +49 -9
- package/src/gestures/recognizerInternals.ts +9 -2
- package/src/gestures/types.ts +12 -0
- package/src/gestures/wheelSegmenter.ts +114 -0
- package/src/motion/animator.ts +115 -19
- package/src/motion/uikit/index.ts +2 -1
- package/src/motion/uikit/slider.ts +105 -0
- package/src/motion/uikit/springs.ts +37 -2
|
@@ -471,6 +471,38 @@ const styles = stylex.create({
|
|
|
471
471
|
top: 0,
|
|
472
472
|
zIndex: 1,
|
|
473
473
|
},
|
|
474
|
+
// The insets are zero unless the page sets `viewport-fit=cover`. The header
|
|
475
|
+
// background goes under the status bar and to the screen sides, as a UIKit
|
|
476
|
+
// bar does.
|
|
477
|
+
headerSafeAreaTop: {
|
|
478
|
+
paddingBlockStart: 'env(safe-area-inset-top, 0px)',
|
|
479
|
+
},
|
|
480
|
+
// The insets are physical sides, so a right-to-left context swaps them on
|
|
481
|
+
// the inline sides.
|
|
482
|
+
headerSafeAreaSides: {
|
|
483
|
+
paddingInlineStart: {
|
|
484
|
+
default: 'env(safe-area-inset-left, 0px)',
|
|
485
|
+
':is([dir="rtl"] *)': 'env(safe-area-inset-right, 0px)',
|
|
486
|
+
},
|
|
487
|
+
paddingInlineEnd: {
|
|
488
|
+
default: 'env(safe-area-inset-right, 0px)',
|
|
489
|
+
':is([dir="rtl"] *)': 'env(safe-area-inset-left, 0px)',
|
|
490
|
+
},
|
|
491
|
+
},
|
|
492
|
+
// In `auto`, the banner's wrapper and the phone top bar both stick. Below a
|
|
493
|
+
// banner, the bar sticks under the top inset, so its items stay clear of the
|
|
494
|
+
// status bar.
|
|
495
|
+
headerStickyBelowTopInset: {
|
|
496
|
+
top: 'env(safe-area-inset-top, 0px)',
|
|
497
|
+
},
|
|
498
|
+
// Main adds the bottom inset to the padding that LayoutContent publishes for
|
|
499
|
+
// `contentPadding`. The published value excludes the inset, so a bleed child
|
|
500
|
+
// subtracts only `contentPadding`. Main is border-box, so a child that fills
|
|
501
|
+
// it ends above the inset.
|
|
502
|
+
mainSafeAreaEnd: {
|
|
503
|
+
paddingBlockEnd:
|
|
504
|
+
'calc(var(--container-padding-block-end, 0px) + env(safe-area-inset-bottom, 0px))',
|
|
505
|
+
},
|
|
474
506
|
// Sticky sideNav for auto height mode — sticks within the wrapper.
|
|
475
507
|
// This div replaces the panel as the direct flex child of the middle
|
|
476
508
|
// (horizontal) container, so it needs the same flex properties that
|
|
@@ -891,7 +923,12 @@ export function AppShell({
|
|
|
891
923
|
role="banner"
|
|
892
924
|
{...mergeProps(
|
|
893
925
|
themeProps('app-shell-header', {variant}),
|
|
894
|
-
stylex.props(
|
|
926
|
+
stylex.props(
|
|
927
|
+
headerAreaStyle,
|
|
928
|
+
isAuto && styles.headerSticky,
|
|
929
|
+
styles.headerSafeAreaTop,
|
|
930
|
+
styles.headerSafeAreaSides,
|
|
931
|
+
),
|
|
895
932
|
)}>
|
|
896
933
|
{headerInner}
|
|
897
934
|
</div>
|
|
@@ -1184,6 +1221,7 @@ export function AppShell({
|
|
|
1184
1221
|
drawsMainFocusRing
|
|
1185
1222
|
? [focusOutlineStyles.focusVisible, styles.mainScrollRegionInset]
|
|
1186
1223
|
: styles.mainFocusTarget,
|
|
1224
|
+
styles.mainSafeAreaEnd,
|
|
1187
1225
|
]}>
|
|
1188
1226
|
{children}
|
|
1189
1227
|
</LayoutContent>
|
|
@@ -1230,7 +1268,15 @@ export function AppShell({
|
|
|
1230
1268
|
role={headerContent == null ? 'banner' : undefined}
|
|
1231
1269
|
{...mergeProps(
|
|
1232
1270
|
themeProps('app-shell-header', {variant}),
|
|
1233
|
-
stylex.props(
|
|
1271
|
+
stylex.props(
|
|
1272
|
+
headerAreaStyle,
|
|
1273
|
+
isAuto && styles.headerSticky,
|
|
1274
|
+
styles.headerSafeAreaSides,
|
|
1275
|
+
// Under a banner, the banner's wrapper pads the top inset. This top
|
|
1276
|
+
// bar takes only the side insets.
|
|
1277
|
+
headerContent == null && styles.headerSafeAreaTop,
|
|
1278
|
+
isAuto && headerContent != null && styles.headerStickyBelowTopInset,
|
|
1279
|
+
),
|
|
1234
1280
|
)}>
|
|
1235
1281
|
<LayoutHeader padding={0} hasDivider={navHasDividers}>
|
|
1236
1282
|
<div
|
|
@@ -675,10 +675,23 @@ export function BottomSheetPanel({
|
|
|
675
675
|
if (motion == null) {
|
|
676
676
|
return;
|
|
677
677
|
}
|
|
678
|
-
|
|
678
|
+
// React can run this effect again for the same motion, for example when
|
|
679
|
+
// a Suspense boundary shows this content again. The passive effect below
|
|
680
|
+
// does not run again then, so the motion it started stays started, and a
|
|
681
|
+
// completion completes at once instead of waiting for that effect.
|
|
682
|
+
if (startedMotionRef.current !== motion) {
|
|
683
|
+
startedMotionRef.current = null;
|
|
684
|
+
}
|
|
679
685
|
pendingMotionCompleteRef.current = null;
|
|
686
|
+
const complete = () => {
|
|
687
|
+
if (startedMotionRef.current === motion) {
|
|
688
|
+
onMotionCompleteRef.current?.(motion);
|
|
689
|
+
} else {
|
|
690
|
+
pendingMotionCompleteRef.current = motion;
|
|
691
|
+
}
|
|
692
|
+
};
|
|
680
693
|
if (motion === 'entering' && reactivatedEntranceRef.current) {
|
|
681
|
-
|
|
694
|
+
complete();
|
|
682
695
|
return;
|
|
683
696
|
}
|
|
684
697
|
// What ends a motion: the fade is opacity, and the entrance, alignment and
|
|
@@ -690,13 +703,7 @@ export function BottomSheetPanel({
|
|
|
690
703
|
return waitForTransition(
|
|
691
704
|
elementRef.current,
|
|
692
705
|
motion === 'fading' || crossFadeMotion ? 'opacity' : 'transform',
|
|
693
|
-
|
|
694
|
-
if (startedMotionRef.current === motion) {
|
|
695
|
-
onMotionCompleteRef.current?.(motion);
|
|
696
|
-
} else {
|
|
697
|
-
pendingMotionCompleteRef.current = motion;
|
|
698
|
-
}
|
|
699
|
-
},
|
|
706
|
+
complete,
|
|
700
707
|
);
|
|
701
708
|
}, [motion, isCrossFade]);
|
|
702
709
|
// The snap is transform-only; the layout height reconciles when it lands.
|
package/src/Chat/Chat.doc.mjs
CHANGED
|
@@ -35,6 +35,7 @@ export const docs = {
|
|
|
35
35
|
vars: [
|
|
36
36
|
{name: '--_chat-composer-radius', description: 'Border radius of the composer body. Inner elements derive their radius concentrically.', default: 'var(--radius-chat)', private: true},
|
|
37
37
|
{name: '--_chat-composer-padding', description: 'Padding of the composer body. Used in the concentric radius calculation.', default: 'var(--spacing-3)', private: true},
|
|
38
|
+
{name: '--_chat-avatar-offset', description: 'Top offset of a message avatar: the name row spacing when a bubble in the message shows a name, else 0px. Each message sets it from its own content. Not a theming lever.', default: '0px', private: true},
|
|
38
39
|
{name: '--_button-radius', description: 'Concentric button radius inside the composer.', default: 'max(var(--radius-element), calc(var(--_chat-composer-radius) - var(--_chat-composer-padding)))', private: true, derived: true, formula: 'max(var(--radius-element), calc(var(--_chat-composer-radius) - var(--_chat-composer-padding)))'},
|
|
39
40
|
],
|
|
40
41
|
derived: [
|
|
@@ -157,6 +157,13 @@ export const docs = {
|
|
|
157
157
|
description:
|
|
158
158
|
'Key-down handler invoked before the built-in Enter/history behavior (after any open trigger menu). The seam for platform-specific keys: call event.preventDefault() to suppress the default submit (e.g. newline on a touch keyboard), or act on the event yourself to add behavior (e.g. submit on Cmd/Ctrl+Enter). IME composition is always respected; Enter never submits mid-composition.',
|
|
159
159
|
},
|
|
160
|
+
{
|
|
161
|
+
name: 'enterKeyHint',
|
|
162
|
+
type: "'enter' | 'done' | 'go' | 'next' | 'previous' | 'search' | 'send'",
|
|
163
|
+
description:
|
|
164
|
+
'What the Return key of a touch keyboard says (the native enterkeyhint). Enter sends the message, so it defaults to send and iOS shows its Send key. When onKeyDown makes Enter insert a line break on touch keyboards, pass enter so the key shows Return again.',
|
|
165
|
+
default: "'send'",
|
|
166
|
+
},
|
|
160
167
|
],
|
|
161
168
|
};
|
|
162
169
|
|
|
@@ -187,6 +194,8 @@ export const docsZh = {
|
|
|
187
194
|
onSubmit: '用户不按 Shift 按 Enter 时调用。序列化值包含标记占位符。',
|
|
188
195
|
onKeyDown:
|
|
189
196
|
'在内置 Enter/历史处理之前(在打开的触发菜单之后)调用的按键处理器。平台特定按键的接缝:调用 event.preventDefault() 可抑制默认提交(例如触控键盘上插入换行),或自行处理事件以添加行为(例如 Cmd/Ctrl+Enter 提交)。始终尊重输入法组字状态——组字过程中 Enter 永不提交。',
|
|
197
|
+
enterKeyHint:
|
|
198
|
+
'触屏键盘回车键上显示的内容(原生 enterkeyhint)。Enter 会发送消息,因此默认为 send,iOS 显示“发送”键。当 onKeyDown 让触屏键盘上的 Enter 插入换行时,传入 enter 让按键恢复为回车。',
|
|
190
199
|
},
|
|
191
200
|
};
|
|
192
201
|
|
|
@@ -219,5 +228,7 @@ export const docsDense = {
|
|
|
219
228
|
'Enter w/o Shift handler; serialized value includes token placeholders',
|
|
220
229
|
onKeyDown:
|
|
221
230
|
'keydown handler before built-in Enter/history (after trigger menu); preventDefault() to suppress default submit (touch newline) or act on event to add behavior (Cmd/Ctrl+Enter). IME composition always respected',
|
|
231
|
+
enterKeyHint:
|
|
232
|
+
'touch keyboard Return key label (enterkeyhint); default send since Enter sends; pass enter when onKeyDown makes Enter a newline on touch',
|
|
222
233
|
},
|
|
223
234
|
};
|
|
@@ -68,6 +68,7 @@ import {
|
|
|
68
68
|
import {Badge, type BadgeProps} from '../Badge';
|
|
69
69
|
import {useChatComposerContext} from './ChatContext';
|
|
70
70
|
import {themeProps} from '../utils/themeProps';
|
|
71
|
+
import type {EnterKeyHint} from '../Field/textEntryTraits';
|
|
71
72
|
import {
|
|
72
73
|
registerEnglishFallback,
|
|
73
74
|
useComponentTranslator,
|
|
@@ -247,6 +248,14 @@ export interface ChatComposerInputProps extends Omit<
|
|
|
247
248
|
* never submits while a composition is in progress.
|
|
248
249
|
*/
|
|
249
250
|
onKeyDown?: (event: KeyboardEvent<HTMLDivElement>) => void;
|
|
251
|
+
/**
|
|
252
|
+
* What the Return key of a touch keyboard says (the native
|
|
253
|
+
* `enterkeyhint`). Enter sends the message, so the key says so: iOS shows
|
|
254
|
+
* its Send key. When `onKeyDown` makes Enter insert a line break on touch
|
|
255
|
+
* keyboards, pass `'enter'` so the key shows Return again.
|
|
256
|
+
* @default 'send'
|
|
257
|
+
*/
|
|
258
|
+
enterKeyHint?: EnterKeyHint;
|
|
250
259
|
}
|
|
251
260
|
|
|
252
261
|
// =============================================================================
|
|
@@ -373,6 +382,7 @@ export function ChatComposerInput(props: ChatComposerInputProps) {
|
|
|
373
382
|
onFiles,
|
|
374
383
|
onSubmit = composerCtx?.onSubmit,
|
|
375
384
|
onKeyDown: onKeyDownProp,
|
|
385
|
+
enterKeyHint = 'send',
|
|
376
386
|
xstyle,
|
|
377
387
|
className,
|
|
378
388
|
style,
|
|
@@ -941,6 +951,7 @@ export function ChatComposerInput(props: ChatComposerInputProps) {
|
|
|
941
951
|
// Explicit in both states: losing editability must not lose focus.
|
|
942
952
|
tabIndex={0}
|
|
943
953
|
contentEditable={!isDisabled}
|
|
954
|
+
enterKeyHint={enterKeyHint}
|
|
944
955
|
suppressContentEditableWarning
|
|
945
956
|
onInput={handleInput}
|
|
946
957
|
onClick={handleEditorClick}
|
package/src/Chat/ChatMessage.tsx
CHANGED
|
@@ -118,7 +118,7 @@ const styles = stylex.create({
|
|
|
118
118
|
// Judged from this article (parent-relative), never from siblings, so the
|
|
119
119
|
// global stylesheet keeps no sibling-relative :has().
|
|
120
120
|
avatarOffset: {
|
|
121
|
-
[chatMessageVars
|
|
121
|
+
[chatMessageVars['--_chat-avatar-offset']]: {
|
|
122
122
|
default: '0px',
|
|
123
123
|
':has([data-chat-name])': spacingVars['--spacing-5'],
|
|
124
124
|
},
|
|
@@ -128,7 +128,7 @@ const styles = stylex.create({
|
|
|
128
128
|
flexShrink: 0,
|
|
129
129
|
// The avatar lines up with a bubble's name row when the message holds
|
|
130
130
|
// one; the article sets the offset (see chatMessageVars).
|
|
131
|
-
marginBlockStart: chatMessageVars
|
|
131
|
+
marginBlockStart: chatMessageVars['--_chat-avatar-offset'],
|
|
132
132
|
},
|
|
133
133
|
contentColumn: {
|
|
134
134
|
display: 'flex',
|
|
@@ -5,14 +5,14 @@ import * as stylex from '@stylexjs/stylex';
|
|
|
5
5
|
/**
|
|
6
6
|
* Per-message values ChatMessage sets on its article for its own slots.
|
|
7
7
|
*
|
|
8
|
-
* `
|
|
9
|
-
* bubble in this message renders a name, else none. Each article sets
|
|
10
|
-
* its own `:has([data-chat-name])`, a parent-relative rule: a
|
|
11
|
-
* `:has(~ …)` in the global stylesheet makes WebKit re-check
|
|
12
|
-
* sibling on each insertion into any container, so filling one
|
|
13
|
-
* quadratically. A message nested in another sets its own value, so it
|
|
14
|
-
* inherits the outer message's.
|
|
8
|
+
* `--_chat-avatar-offset` is the avatar's top spacing: the name row's spacing
|
|
9
|
+
* when a bubble in this message renders a name, else none. Each article sets
|
|
10
|
+
* it from its own `:has([data-chat-name])`, a parent-relative rule: a
|
|
11
|
+
* sibling-relative `:has(~ …)` in the global stylesheet makes WebKit re-check
|
|
12
|
+
* every earlier sibling on each insertion into any container, so filling one
|
|
13
|
+
* grows quadratically. A message nested in another sets its own value, so it
|
|
14
|
+
* never inherits the outer message's.
|
|
15
15
|
*/
|
|
16
16
|
export const chatMessageVars = stylex.defineVars({
|
|
17
|
-
|
|
17
|
+
'--_chat-avatar-offset': '0px',
|
|
18
18
|
});
|
package/src/Drawer/Drawer.tsx
CHANGED
|
@@ -77,6 +77,7 @@ import {LayerDepthProvider, useLayerDismissal} from '../Layer';
|
|
|
77
77
|
import {layerTextReset} from '../Layer/layerTextReset.stylex';
|
|
78
78
|
import {layerStructureReset} from '../Layer/layerStructureReset.stylex';
|
|
79
79
|
import {LayerContentBoundary} from '../Layer/layerScopedContext';
|
|
80
|
+
import {useModalOutlet} from '../Layer/modalOutlet';
|
|
80
81
|
import {layerClickBoundary} from '../Layer/layerClickBoundary';
|
|
81
82
|
import {ModalAnnounceProvider} from '../hooks/ModalAnnounceProvider';
|
|
82
83
|
import {composeEventHandlers, mergeProps, themeProps} from '../utils';
|
|
@@ -497,6 +498,11 @@ export function Drawer({
|
|
|
497
498
|
setIsRendered,
|
|
498
499
|
});
|
|
499
500
|
|
|
501
|
+
// Toasts outside an open modal are inert, so the open drawer hosts them.
|
|
502
|
+
// It releases them when its exit starts.
|
|
503
|
+
// showModal() must run first, so keep this call after useDrawerDialogPresence.
|
|
504
|
+
useModalOutlet(dialogRef, isOpen && hasScrim);
|
|
505
|
+
|
|
500
506
|
const handleDismiss = useCallback(() => {
|
|
501
507
|
onOpenChange(false);
|
|
502
508
|
}, [onOpenChange]);
|
|
@@ -4,31 +4,62 @@
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* @file useMenuOverflow.ts
|
|
7
|
-
* @input Menu element ref, rendered content, and
|
|
7
|
+
* @input Menu element ref, rendered content, open state, and optionally the
|
|
8
|
+
* overflow its owner expects from the content alone
|
|
8
9
|
* @output Returns whether the open menu overflows its block-size constraint
|
|
9
10
|
* @position Shared responsive helper for DropdownMenu and DropdownMenuSubMenu
|
|
10
11
|
*/
|
|
11
12
|
|
|
12
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
useCallback,
|
|
15
|
+
useRef,
|
|
16
|
+
useState,
|
|
17
|
+
type ReactNode,
|
|
18
|
+
type RefObject,
|
|
19
|
+
} from 'react';
|
|
13
20
|
import {useIsomorphicLayoutEffect} from '../hooks/useIsomorphicLayoutEffect';
|
|
14
21
|
|
|
15
|
-
/**
|
|
22
|
+
/**
|
|
23
|
+
* Detect block-axis overflow without making a fitting menu a scroll container.
|
|
24
|
+
*
|
|
25
|
+
* `expectedOverflow` is the answer the owner can give before any measure (a
|
|
26
|
+
* long option list cannot fit). Until a measure of the same `content` says
|
|
27
|
+
* otherwise, it is the result, so the first open already renders the right
|
|
28
|
+
* class: a class that flips after the open measure restyles the whole list
|
|
29
|
+
* a second time. A measure still runs on every open and corrects a wrong
|
|
30
|
+
* guess.
|
|
31
|
+
*/
|
|
16
32
|
export function useMenuOverflow(
|
|
17
33
|
menuRef: RefObject<HTMLElement | null>,
|
|
18
34
|
content: ReactNode,
|
|
19
35
|
isOpen: boolean,
|
|
36
|
+
expectedOverflow?: boolean,
|
|
20
37
|
): boolean {
|
|
21
|
-
|
|
38
|
+
// The content each measure stands for, kept only for an owner that gives
|
|
39
|
+
// an expected answer (a caller whose content is a new element on every
|
|
40
|
+
// render compares nothing); set before the measuring effects run.
|
|
41
|
+
const contentRef = useRef<ReactNode>(null);
|
|
42
|
+
useIsomorphicLayoutEffect(() => {
|
|
43
|
+
contentRef.current = expectedOverflow === undefined ? null : content;
|
|
44
|
+
});
|
|
45
|
+
const [measured, setMeasured] = useState<{
|
|
46
|
+
content: ReactNode;
|
|
47
|
+
hasOverflow: boolean;
|
|
48
|
+
} | null>(null);
|
|
22
49
|
|
|
23
50
|
const measureOverflow = useCallback(() => {
|
|
24
51
|
const menu = menuRef.current;
|
|
25
52
|
if (!menu) {
|
|
26
53
|
return;
|
|
27
54
|
}
|
|
28
|
-
const
|
|
55
|
+
const hasOverflow = menu.scrollHeight > menu.clientHeight + 1;
|
|
29
56
|
// eslint-disable-next-line @eslint-react/set-state-in-effect -- DOM overflow measurement controls whether this menu becomes a scroll container
|
|
30
|
-
|
|
31
|
-
current
|
|
57
|
+
setMeasured(current =>
|
|
58
|
+
current != null &&
|
|
59
|
+
current.hasOverflow === hasOverflow &&
|
|
60
|
+
Object.is(current.content, contentRef.current)
|
|
61
|
+
? current
|
|
62
|
+
: {content: contentRef.current, hasOverflow},
|
|
32
63
|
);
|
|
33
64
|
}, [menuRef]);
|
|
34
65
|
|
|
@@ -73,5 +104,10 @@ export function useMenuOverflow(
|
|
|
73
104
|
}
|
|
74
105
|
}, [content, isOpen, measureOverflow]);
|
|
75
106
|
|
|
76
|
-
|
|
107
|
+
if (expectedOverflow === undefined) {
|
|
108
|
+
return measured?.hasOverflow ?? false;
|
|
109
|
+
}
|
|
110
|
+
return measured != null && Object.is(measured.content, content)
|
|
111
|
+
? measured.hasOverflow
|
|
112
|
+
: expectedOverflow;
|
|
77
113
|
}
|
package/src/Link/Link.tsx
CHANGED
|
@@ -98,6 +98,9 @@ const styles = stylex.create({
|
|
|
98
98
|
backgroundColor: 'transparent',
|
|
99
99
|
borderStyle: 'none',
|
|
100
100
|
padding: 0,
|
|
101
|
+
// Browsers centre a button's text; a wrapped label lines up as an
|
|
102
|
+
// anchor's does.
|
|
103
|
+
textAlign: 'inherit',
|
|
101
104
|
pointerEvents: 'auto',
|
|
102
105
|
position: 'relative',
|
|
103
106
|
},
|
|
@@ -46,6 +46,7 @@ import {
|
|
|
46
46
|
} from '../Field';
|
|
47
47
|
import {Divider} from '../Divider';
|
|
48
48
|
import {layerAnimations} from '../Layer/layerAnimations.stylex';
|
|
49
|
+
import {reducedMotionFadeConsts} from '../motion/reducedMotion.stylex';
|
|
49
50
|
import {useKeepLayerOpenProps, type LayerPlacement} from '../Layer/useLayer';
|
|
50
51
|
import {InternalInputClearButton} from '../Field/InputClearButton';
|
|
51
52
|
import {Spinner} from '../Spinner';
|
|
@@ -113,6 +114,14 @@ import english from '../i18n/generated-english/Selector/Selector.generated';
|
|
|
113
114
|
|
|
114
115
|
registerEnglishFallback(english);
|
|
115
116
|
|
|
117
|
+
/**
|
|
118
|
+
* A guess, confirmed by the open measure: more options than this overflow
|
|
119
|
+
* the popover listbox's 300 px block size even at 24 px rows, under the
|
|
120
|
+
* smallest default row (28 px). A wrong guess (a theme with shorter rows, a
|
|
121
|
+
* taller bottom sheet) costs only the measure's correction.
|
|
122
|
+
*/
|
|
123
|
+
const MOST_ROWS_THAT_FIT = 12;
|
|
124
|
+
|
|
116
125
|
const styles = stylex.create({
|
|
117
126
|
// Trigger container — the enhanced click target wrapping the combobox button and clear button as siblings
|
|
118
127
|
triggerContainer: {
|
|
@@ -350,7 +359,21 @@ const styles = stylex.create({
|
|
|
350
359
|
paddingBlock: spacingVars['--spacing-1'],
|
|
351
360
|
paddingInline: spacingVars['--spacing-1'],
|
|
352
361
|
opacity: 1,
|
|
353
|
-
|
|
362
|
+
// Under Reduce Motion the list appears at once on a fine pointer, and
|
|
363
|
+
// takes the shared cross-fade on a coarse one.
|
|
364
|
+
transitionProperty: 'opacity',
|
|
365
|
+
transitionDuration: {
|
|
366
|
+
default: durationVars['--duration-fast'],
|
|
367
|
+
'@media (prefers-reduced-motion: reduce) and (pointer: coarse)':
|
|
368
|
+
reducedMotionFadeConsts.duration,
|
|
369
|
+
'@media (prefers-reduced-motion: reduce) and (not (pointer: coarse))':
|
|
370
|
+
'0s',
|
|
371
|
+
},
|
|
372
|
+
transitionTimingFunction: {
|
|
373
|
+
default: 'ease',
|
|
374
|
+
'@media (prefers-reduced-motion: reduce) and (pointer: coarse)':
|
|
375
|
+
reducedMotionFadeConsts.easing,
|
|
376
|
+
},
|
|
354
377
|
},
|
|
355
378
|
dropdownInput: {
|
|
356
379
|
// The input trigger's text inset includes its border. Mirror that extra
|
|
@@ -376,7 +399,8 @@ const styles = stylex.create({
|
|
|
376
399
|
},
|
|
377
400
|
dropdownHidden: {
|
|
378
401
|
opacity: 0,
|
|
379
|
-
|
|
402
|
+
// A longhand: the shorthand would lose to the list's longhands.
|
|
403
|
+
transitionProperty: 'none',
|
|
380
404
|
},
|
|
381
405
|
|
|
382
406
|
// Popover container (for anchor positioning)
|
|
@@ -1407,10 +1431,13 @@ export function Selector<T extends SelectorOptionType>(
|
|
|
1407
1431
|
return match == null ? -1 : Number(match[1]);
|
|
1408
1432
|
}, []);
|
|
1409
1433
|
// Re-measured when the option set changes; the count is the dependency.
|
|
1434
|
+
// A list longer than the most rows that can fit cannot fit, so it starts
|
|
1435
|
+
// as a scroll container and the open measure only confirms it.
|
|
1410
1436
|
const listboxHasOverflow = useMenuOverflow(
|
|
1411
1437
|
listboxRef,
|
|
1412
1438
|
filteredItems.length,
|
|
1413
1439
|
surface.isOpen,
|
|
1440
|
+
filteredItems.length > MOST_ROWS_THAT_FIT,
|
|
1414
1441
|
);
|
|
1415
1442
|
const listboxPress = useMenuPress({
|
|
1416
1443
|
menuRef: listboxRef,
|
|
@@ -237,12 +237,14 @@ export const docs = {
|
|
|
237
237
|
{
|
|
238
238
|
name: 'containerClassName',
|
|
239
239
|
type: 'string',
|
|
240
|
-
description:
|
|
240
|
+
description:
|
|
241
|
+
'Class name for the field container. Same targeting as `containerXstyle`.',
|
|
241
242
|
},
|
|
242
243
|
{
|
|
243
244
|
name: 'containerStyle',
|
|
244
245
|
type: 'CSSProperties',
|
|
245
|
-
description:
|
|
246
|
+
description:
|
|
247
|
+
'Inline styles for the field container. Same targeting as `containerXstyle`.',
|
|
246
248
|
},
|
|
247
249
|
{
|
|
248
250
|
name: 'controlXstyle',
|
|
@@ -295,7 +297,7 @@ export const docs = {
|
|
|
295
297
|
],
|
|
296
298
|
anatomy,
|
|
297
299
|
description:
|
|
298
|
-
'A draggable control for selecting a numeric value or range within defined bounds. Supports single value and range selection, tick marks, custom value formatting, and vertical orientation. Use it when users need to explore a continuous range, such as volume, price, or percentage.',
|
|
300
|
+
'A draggable control for selecting a numeric value or range within defined bounds. Supports single value and range selection, tick marks, custom value formatting, and vertical orientation. Use it when users need to explore a continuous range, such as volume, price, or percentage. Under a finger, on any touch device, it tracks as UISlider does: only the thumb takes a drag, through a target larger than it, the value follows the finger’s travel rather than jumping to it, a release coasts on before `onChangeEnd`, a swipe from the track scrolls the page, and a tap on the track or a mark sets the value when the finger lifts (the thumb springs there). A mouse drag on a thumb tracks the same way, past the same 10 px in any direction, with no lift or coast; a mouse press on the track moves the thumb there, and only a primary click presses. A pen drags as a finger does, claiming only along the track, and its drag ends at the release with no coast. A wheel or a trackpad scroll over the slider goes to whatever scrolls under it.',
|
|
299
301
|
bestPractices: [
|
|
300
302
|
{
|
|
301
303
|
guidance: true,
|
|
@@ -508,7 +510,7 @@ export const docsZh = {
|
|
|
508
510
|
usage: {
|
|
509
511
|
anatomy,
|
|
510
512
|
description:
|
|
511
|
-
'A draggable control for selecting a numeric value or range within defined bounds. Supports single value and range selection, tick marks, custom value formatting, and vertical orientation. Use it when users need to explore a continuous range, such as volume, price, or percentage.',
|
|
513
|
+
'A draggable control for selecting a numeric value or range within defined bounds. Supports single value and range selection, tick marks, custom value formatting, and vertical orientation. Use it when users need to explore a continuous range, such as volume, price, or percentage. Under a finger, on any touch device, it tracks as UISlider does: only the thumb takes a drag, through a target larger than it, the value follows the finger’s travel rather than jumping to it, a release coasts on before `onChangeEnd`, a swipe from the track scrolls the page, and a tap on the track or a mark sets the value when the finger lifts (the thumb springs there). A mouse drag on a thumb tracks the same way, past the same 10 px in any direction, with no lift or coast; a mouse press on the track moves the thumb there, and only a primary click presses. A pen drags as a finger does, claiming only along the track, and its drag ends at the release with no coast. A wheel or a trackpad scroll over the slider goes to whatever scrolls under it.',
|
|
512
514
|
bestPractices: [
|
|
513
515
|
{
|
|
514
516
|
guidance: true,
|
|
@@ -544,7 +546,7 @@ export const docsDense = {
|
|
|
544
546
|
usage: {
|
|
545
547
|
anatomy,
|
|
546
548
|
description:
|
|
547
|
-
'A draggable control for selecting a numeric value or range within defined bounds. Supports single value and range selection, tick marks, custom value formatting, and vertical orientation. Use it when users need to explore a continuous range, such as volume, price, or percentage.',
|
|
549
|
+
'A draggable control for selecting a numeric value or range within defined bounds. Supports single value and range selection, tick marks, custom value formatting, and vertical orientation. Use it when users need to explore a continuous range, such as volume, price, or percentage. Under a finger, on any touch device, it tracks as UISlider does: only the thumb takes a drag, through a target larger than it, the value follows the finger’s travel rather than jumping to it, a release coasts on before `onChangeEnd`, a swipe from the track scrolls the page, and a tap on the track or a mark sets the value when the finger lifts (the thumb springs there). A mouse drag on a thumb tracks the same way, past the same 10 px in any direction, with no lift or coast; a mouse press on the track moves the thumb there, and only a primary click presses. A pen drags as a finger does, claiming only along the track, and its drag ends at the release with no coast. A wheel or a trackpad scroll over the slider goes to whatever scrolls under it.',
|
|
548
550
|
bestPractices: [
|
|
549
551
|
{
|
|
550
552
|
guidance: true,
|
|
@@ -574,10 +576,12 @@ export const docsDense = {
|
|
|
574
576
|
],
|
|
575
577
|
},
|
|
576
578
|
propDescriptions: {
|
|
577
|
-
containerXstyle:
|
|
579
|
+
containerXstyle:
|
|
580
|
+
'StyleX styles for the field container; no effect in InputGroup or horizontal-labels FormLayout. No root xstyle/className/style.',
|
|
578
581
|
containerClassName: 'Class name for the field container.',
|
|
579
582
|
containerStyle: 'Inline styles for the field container.',
|
|
580
|
-
controlXstyle:
|
|
583
|
+
controlXstyle:
|
|
584
|
+
'StyleX styles for the visual control box (border, background, icons); not the native input.',
|
|
581
585
|
controlClassName: 'Class name for the visual control box.',
|
|
582
586
|
controlStyle: 'Inline styles for the visual control box.',
|
|
583
587
|
label: 'Label text (always rendered for a11y).',
|