@astryxdesign/core 0.7.6 → 0.7.7-canary.1d04d40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (173) hide show
  1. package/dist/AppShell/AppShell.d.ts.map +1 -1
  2. package/dist/AppShell/AppShell.js +23 -3
  3. package/dist/BottomSheet/BottomSheetPanel.d.ts.map +1 -1
  4. package/dist/BottomSheet/BottomSheetPanel.js +16 -9
  5. package/dist/Chat/ChatComposerInput.d.ts +9 -0
  6. package/dist/Chat/ChatComposerInput.d.ts.map +1 -1
  7. package/dist/Chat/ChatComposerInput.js +2 -0
  8. package/dist/Chat/ChatMessage.js +2 -2
  9. package/dist/Chat/chatMessage.vars.stylex.d.ts +8 -8
  10. package/dist/Chat/chatMessage.vars.stylex.js +8 -8
  11. package/dist/CommandPalette/CommandPaletteInput.d.ts.map +1 -1
  12. package/dist/CommandPalette/CommandPaletteInput.js +8 -1
  13. package/dist/DateInput/MonthScroller.d.ts.map +1 -1
  14. package/dist/DateInput/MonthScroller.js +72 -2
  15. package/dist/Drawer/Drawer.d.ts.map +1 -1
  16. package/dist/Drawer/Drawer.js +6 -0
  17. package/dist/DropdownMenu/useMenuOverflow.d.ts +13 -3
  18. package/dist/DropdownMenu/useMenuOverflow.d.ts.map +1 -1
  19. package/dist/DropdownMenu/useMenuOverflow.js +31 -8
  20. package/dist/Field/PanelSearchInput.d.ts.map +1 -1
  21. package/dist/Field/PanelSearchInput.js +8 -1
  22. package/dist/Field/textEntryTraits.d.ts +6 -6
  23. package/dist/Field/textEntryTraits.js +6 -6
  24. package/dist/Layer/layerClickBoundary.d.ts +7 -1
  25. package/dist/Layer/layerClickBoundary.d.ts.map +1 -1
  26. package/dist/Layer/layerClickBoundary.js +2 -2
  27. package/dist/Layer/usePressPin.d.ts +95 -0
  28. package/dist/Layer/usePressPin.d.ts.map +1 -0
  29. package/dist/Layer/usePressPin.js +335 -0
  30. package/dist/Layer/useTouchTrigger.d.ts +23 -1
  31. package/dist/Layer/useTouchTrigger.d.ts.map +1 -1
  32. package/dist/Layer/useTouchTrigger.js +29 -2
  33. package/dist/Link/Link.d.ts.map +1 -1
  34. package/dist/Link/Link.js +1 -0
  35. package/dist/ProgressBar/ProgressBar.d.ts.map +1 -1
  36. package/dist/ProgressBar/ProgressBar.js +20 -9
  37. package/dist/ProgressBar/ProgressBarTouch.d.ts +29 -0
  38. package/dist/ProgressBar/ProgressBarTouch.d.ts.map +1 -0
  39. package/dist/ProgressBar/ProgressBarTouch.js +91 -0
  40. package/dist/Selector/Selector.d.ts.map +1 -1
  41. package/dist/Selector/Selector.js +25 -13
  42. package/dist/Slider/Slider.d.ts.map +1 -1
  43. package/dist/Slider/Slider.js +404 -131
  44. package/dist/Slider/sliderTouchController.d.ts +189 -0
  45. package/dist/Slider/sliderTouchController.d.ts.map +1 -0
  46. package/dist/Slider/sliderTouchController.js +1017 -0
  47. package/dist/Slider/useSliderTouch.d.ts +22 -0
  48. package/dist/Slider/useSliderTouch.d.ts.map +1 -0
  49. package/dist/Slider/useSliderTouch.js +56 -0
  50. package/dist/Tooltip/Tooltip.d.ts +30 -6
  51. package/dist/Tooltip/Tooltip.d.ts.map +1 -1
  52. package/dist/Tooltip/Tooltip.js +5 -0
  53. package/dist/Tooltip/index.d.ts +1 -1
  54. package/dist/Tooltip/index.d.ts.map +1 -1
  55. package/dist/Tooltip/useTooltip.d.ts +31 -5
  56. package/dist/Tooltip/useTooltip.d.ts.map +1 -1
  57. package/dist/Tooltip/useTooltip.js +59 -18
  58. package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
  59. package/dist/Typeahead/BaseTypeahead.js +19 -9
  60. package/dist/astryx.css +40 -6
  61. package/dist/gestures/GestureArena.d.ts +21 -2
  62. package/dist/gestures/GestureArena.d.ts.map +1 -1
  63. package/dist/gestures/GestureArena.js +23 -3
  64. package/dist/gestures/GestureHost.d.ts +123 -3
  65. package/dist/gestures/GestureHost.d.ts.map +1 -1
  66. package/dist/gestures/GestureHost.js +791 -12
  67. package/dist/gestures/GestureRecognizer.d.ts +15 -1
  68. package/dist/gestures/GestureRecognizer.d.ts.map +1 -1
  69. package/dist/gestures/GestureRecognizer.js +12 -0
  70. package/dist/gestures/PanGestureRecognizer.d.ts +8 -0
  71. package/dist/gestures/PanGestureRecognizer.d.ts.map +1 -1
  72. package/dist/gestures/PinchGestureRecognizer.d.ts +5 -1
  73. package/dist/gestures/PinchGestureRecognizer.d.ts.map +1 -1
  74. package/dist/gestures/PinchGestureRecognizer.js +10 -0
  75. package/dist/gestures/ScreenEdgePanGestureRecognizer.d.ts +3 -3
  76. package/dist/gestures/ScreenEdgePanGestureRecognizer.d.ts.map +1 -1
  77. package/dist/gestures/ScreenEdgePanGestureRecognizer.js +7 -5
  78. package/dist/gestures/nativeScroll.d.ts +16 -2
  79. package/dist/gestures/nativeScroll.d.ts.map +1 -1
  80. package/dist/gestures/nativeScroll.js +39 -8
  81. package/dist/gestures/recognizerInternals.d.ts +4 -2
  82. package/dist/gestures/recognizerInternals.d.ts.map +1 -1
  83. package/dist/gestures/types.d.ts +11 -0
  84. package/dist/gestures/types.d.ts.map +1 -1
  85. package/dist/gestures/types.js +7 -0
  86. package/dist/gestures/wheelSegmenter.d.ts +45 -0
  87. package/dist/gestures/wheelSegmenter.d.ts.map +1 -0
  88. package/dist/gestures/wheelSegmenter.js +98 -0
  89. package/dist/motion/animator.d.ts.map +1 -1
  90. package/dist/motion/animator.js +140 -19
  91. package/dist/motion/paging.d.ts +58 -0
  92. package/dist/motion/paging.d.ts.map +1 -0
  93. package/dist/motion/paging.js +128 -0
  94. package/dist/motion/uikit/index.d.ts +5 -3
  95. package/dist/motion/uikit/index.d.ts.map +1 -1
  96. package/dist/motion/uikit/index.js +5 -3
  97. package/dist/motion/uikit/paging.d.ts +160 -0
  98. package/dist/motion/uikit/paging.d.ts.map +1 -0
  99. package/dist/motion/uikit/paging.js +162 -0
  100. package/dist/motion/uikit/progressBar.d.ts +59 -0
  101. package/dist/motion/uikit/progressBar.d.ts.map +1 -0
  102. package/dist/motion/uikit/progressBar.js +82 -0
  103. package/dist/motion/uikit/scroll.d.ts +0 -19
  104. package/dist/motion/uikit/scroll.d.ts.map +1 -1
  105. package/dist/motion/uikit/scroll.js +2 -21
  106. package/dist/motion/uikit/slider.d.ts +97 -0
  107. package/dist/motion/uikit/slider.d.ts.map +1 -0
  108. package/dist/motion/uikit/slider.js +111 -0
  109. package/dist/motion/uikit/springs.d.ts +46 -1
  110. package/dist/motion/uikit/springs.d.ts.map +1 -1
  111. package/dist/motion/uikit/springs.js +48 -4
  112. package/dist/utils/selectionLock.d.ts +43 -0
  113. package/dist/utils/selectionLock.d.ts.map +1 -1
  114. package/dist/utils/selectionLock.js +394 -6
  115. package/package.json +5 -5
  116. package/src/AppShell/AppShell.doc.mjs +10 -0
  117. package/src/AppShell/AppShell.tsx +48 -2
  118. package/src/BottomSheet/BottomSheetPanel.tsx +16 -9
  119. package/src/Chat/Chat.doc.mjs +1 -0
  120. package/src/Chat/ChatComposerInput.doc.mjs +11 -0
  121. package/src/Chat/ChatComposerInput.tsx +11 -0
  122. package/src/Chat/ChatMessage.tsx +2 -2
  123. package/src/Chat/chatMessage.vars.stylex.ts +8 -8
  124. package/src/CommandPalette/CommandPaletteInput.tsx +6 -0
  125. package/src/DateInput/MonthScroller.tsx +63 -5
  126. package/src/Drawer/Drawer.tsx +6 -0
  127. package/src/DropdownMenu/useMenuOverflow.ts +44 -8
  128. package/src/Field/PanelSearchInput.tsx +6 -0
  129. package/src/Field/textEntryTraits.ts +6 -6
  130. package/src/Layer/layerClickBoundary.ts +2 -2
  131. package/src/Layer/usePressPin.ts +481 -0
  132. package/src/Layer/useTouchTrigger.ts +37 -1
  133. package/src/Link/Link.tsx +3 -0
  134. package/src/ProgressBar/ProgressBar.doc.mjs +3 -3
  135. package/src/ProgressBar/ProgressBar.tsx +82 -11
  136. package/src/ProgressBar/ProgressBarTouch.ts +109 -0
  137. package/src/Selector/Selector.tsx +29 -2
  138. package/src/Slider/Slider.doc.mjs +11 -7
  139. package/src/Slider/Slider.tsx +539 -132
  140. package/src/Slider/sliderTouchController.ts +1209 -0
  141. package/src/Slider/useSliderTouch.ts +78 -0
  142. package/src/Tooltip/Tooltip.doc.mjs +21 -6
  143. package/src/Tooltip/Tooltip.tsx +42 -5
  144. package/src/Tooltip/index.ts +1 -0
  145. package/src/Tooltip/useTooltip.doc.mjs +4 -2
  146. package/src/Tooltip/useTooltip.tsx +84 -21
  147. package/src/Typeahead/BaseTypeahead.tsx +16 -8
  148. package/src/gestures/GestureArena.ts +45 -4
  149. package/src/gestures/GestureHost.ts +949 -12
  150. package/src/gestures/GestureRecognizer.ts +28 -0
  151. package/src/gestures/PanGestureRecognizer.ts +8 -0
  152. package/src/gestures/PinchGestureRecognizer.ts +16 -1
  153. package/src/gestures/README.md +82 -3
  154. package/src/gestures/ScreenEdgePanGestureRecognizer.ts +7 -5
  155. package/src/gestures/nativeScroll.ts +49 -9
  156. package/src/gestures/recognizerInternals.ts +9 -2
  157. package/src/gestures/types.ts +12 -0
  158. package/src/gestures/wheelSegmenter.ts +114 -0
  159. package/src/motion/MEASUREMENTS.md +8 -1
  160. package/src/motion/README.md +6 -3
  161. package/src/motion/animator.ts +193 -25
  162. package/src/motion/paging.ts +138 -0
  163. package/src/motion/uikit/index.ts +5 -3
  164. package/src/motion/uikit/paging.ts +162 -0
  165. package/src/motion/uikit/progressBar.ts +82 -0
  166. package/src/motion/uikit/scroll.ts +2 -18
  167. package/src/motion/uikit/slider.ts +105 -0
  168. package/src/motion/uikit/springs.ts +37 -2
  169. package/src/utils/selectionLock.ts +501 -6
  170. package/dist/motion/selectPage.d.ts +0 -26
  171. package/dist/motion/selectPage.d.ts.map +0 -1
  172. package/dist/motion/selectPage.js +0 -55
  173. package/src/motion/selectPage.ts +0 -55
@@ -675,10 +675,23 @@ export function BottomSheetPanel({
675
675
  if (motion == null) {
676
676
  return;
677
677
  }
678
- startedMotionRef.current = null;
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
- pendingMotionCompleteRef.current = motion;
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.
@@ -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}
@@ -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.avatarOffset]: {
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.avatarOffset,
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
- * `avatarOffset` is the avatar's top spacing: the name row's spacing when a
9
- * bubble in this message renders a name, else none. Each article sets it from
10
- * its own `:has([data-chat-name])`, a parent-relative rule: a sibling-relative
11
- * `:has(~ …)` in the global stylesheet makes WebKit re-check every earlier
12
- * sibling on each insertion into any container, so filling one grows
13
- * quadratically. A message nested in another sets its own value, so it never
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
- avatarOffset: '0px',
17
+ '--_chat-avatar-offset': '0px',
18
18
  });
@@ -46,6 +46,7 @@ import {
46
46
  useComponentTranslator,
47
47
  } from '../i18n/useComponentTranslator';
48
48
  import english from '../i18n/generated-english/CommandPalette/CommandPaletteInput.generated';
49
+ import {textEntryTraitAttributes} from '../Field/textEntryTraits';
49
50
 
50
51
  registerEnglishFallback(english);
51
52
  const styles = stylex.create({
@@ -320,6 +321,11 @@ export function CommandPaletteInput({
320
321
  ? focusOutlineProps.focusVisible(styles.input)
321
322
  : focusOutlineProps.suppressed(styles.input))}
322
323
  type="text"
324
+ // Autocorrect would rewrite names and identifiers.
325
+ {...textEntryTraitAttributes({
326
+ enterKeyHint: 'search',
327
+ hasAutoCorrect: false,
328
+ })}
323
329
  role={ctx ? 'combobox' : standaloneRole}
324
330
  aria-expanded={
325
331
  ctx
@@ -62,6 +62,7 @@ import {
62
62
  plainDateToISO,
63
63
  DATE_FORMAT_WITH_WEEKDAY,
64
64
  } from '../utils';
65
+ import {reducedMotionFade} from '../motion/reducedMotion';
65
66
  import {dateInputTouchSizes, dateInputTouchGeometry} from './tokens.stylex';
66
67
  import {useOwnScrollGesture} from './useOwnScrollGesture';
67
68
  import {useScrollSettle} from './useScrollSettle';
@@ -431,8 +432,26 @@ export function MonthScroller({
431
432
  );
432
433
  }, [paneSize, initialMonthIndex, minMonthIndex, isRTL]);
433
434
 
435
+ // The Reduced Motion dip of a step on a touch screen, owned here so a
436
+ // second step or the unmount clears it.
437
+ const dipRef = useRef<{animation: Animation; timer: number} | null>(null);
438
+ const stopDip = useCallback(() => {
439
+ const dip = dipRef.current;
440
+ if (dip != null) {
441
+ window.clearTimeout(dip.timer);
442
+ dip.animation.cancel();
443
+ dipRef.current = null;
444
+ }
445
+ }, []);
446
+ // Synchronizes with the dip's timer and animation: neither outlives the
447
+ // scroller.
448
+ useEffect(() => stopDip, [stopDip]);
449
+
434
450
  const scrollToMonth = useCallback(
435
451
  (monthIndex: number, behavior: ScrollBehavior = 'smooth') => {
452
+ // Every step replaces a running dip, whichever way it moves. Otherwise
453
+ // the dip's landing would undo a jump or an immediate step made after it.
454
+ stopDip();
436
455
  const scroller = scrollerRef.current;
437
456
  if (scroller == null || paneSize === 0) {
438
457
  return;
@@ -458,13 +477,52 @@ export function MonthScroller({
458
477
  recenter(scroller, row);
459
478
  return;
460
479
  }
480
+ const left = scrollOffsetForRow(row, paneSize, isRTL);
481
+ // Under reduced motion the month changes without travel: at once on a fine
482
+ // primary pointer, under an opacity dip on a coarse one (reducedMotionFade).
483
+ const fade = behavior === 'smooth' ? reducedMotionFade(scroller) : null;
484
+ if (fade != null) {
485
+ // The window mounts around the target first, and the scroll lands in
486
+ // the layout effect below, as a far jump does. So nothing moves the
487
+ // window after the jump. If the window followed the jump, WebKit would
488
+ // re-snap it one pane further on every frame. A scroll that lands on
489
+ // the centre row reports nothing.
490
+ const land = () => {
491
+ // The landing is the newest steer. An earlier steer whose scroll
492
+ // never arrived would otherwise hide every scroll after it, a
493
+ // trackpad's included. Closing the wheels makes such a steer: it
494
+ // re-asserts the month already in view.
495
+ steeredRowRef.current = null;
496
+ if (row === centerRowRef.current) {
497
+ scroller.scrollTo({left, behavior: 'auto'});
498
+ return;
499
+ }
500
+ pendingScrollRef.current = row;
501
+ centerRowRef.current = row;
502
+ setCenterRow(row);
503
+ };
504
+ if (fade.duration === 0) {
505
+ land();
506
+ return;
507
+ }
508
+ // A second step within the dip has replaced the first (above): only
509
+ // its scroll lands, at its own dip's middle.
510
+ dipRef.current = {
511
+ animation: scroller.animate(
512
+ [{opacity: 1}, {opacity: 0}, {opacity: 1}],
513
+ {duration: fade.duration, easing: fade.easing},
514
+ ),
515
+ timer: window.setTimeout(() => {
516
+ dipRef.current = null;
517
+ land();
518
+ }, fade.duration / 2),
519
+ };
520
+ return;
521
+ }
461
522
  steeredRowRef.current = row;
462
- scroller.scrollTo({
463
- left: scrollOffsetForRow(row, paneSize, isRTL),
464
- behavior,
465
- });
523
+ scroller.scrollTo({left, behavior});
466
524
  },
467
- [paneSize, minMonthIndex, rowCount, isRTL, recenter],
525
+ [paneSize, minMonthIndex, rowCount, isRTL, recenter, stopDip],
468
526
  );
469
527
 
470
528
  // Deliberately un-keyed: it runs after every render, costs a null check, and
@@ -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 open state
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 {useCallback, useState, type ReactNode, type RefObject} from 'react';
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
- /** Detect block-axis overflow without making a fitting menu a scroll container. */
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
- const [hasOverflow, setHasOverflow] = useState(false);
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 nextHasOverflow = menu.scrollHeight > menu.clientHeight + 1;
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
- setHasOverflow(current =>
31
- current === nextHasOverflow ? current : nextHasOverflow,
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
- return hasOverflow;
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
  }
@@ -57,6 +57,7 @@ import {
57
57
  typographyVars,
58
58
  } from '../theme/tokens.stylex';
59
59
  import type {BaseProps} from '../BaseProps';
60
+ import {textEntryTraitAttributes} from './textEntryTraits';
60
61
 
61
62
  const styles = stylex.create({
62
63
  // Gutter only. Matches the option list's own padding so the field below
@@ -273,6 +274,11 @@ export function PanelSearchInput({
273
274
  <input
274
275
  ref={mergedInputRef}
275
276
  type="text"
277
+ // Autocorrect would rewrite names and identifiers.
278
+ {...textEntryTraitAttributes({
279
+ enterKeyHint: 'search',
280
+ hasAutoCorrect: false,
281
+ })}
276
282
  aria-label={label}
277
283
  placeholder={placeholder}
278
284
  value={value}
@@ -3,16 +3,16 @@
3
3
  /**
4
4
  * @file textEntryTraits.ts
5
5
  * @input The keyboard-trait props a text field accepts
6
- * @output Exports the closed value unions EnterKeyHint, InputMode and
7
- * AutoCapitalize (the package entry exports these three), the fields' prop
8
- * mixin TextEntryTraitProps, and textEntryTraitAttributes, which maps the
9
- * props onto the native attributes of the editable element
10
- * @position Shared by TextInput and TextArea: the one place that says how a
11
- * field tells a touch keyboard what it holds
6
+ * @output EnterKeyHint, InputMode and AutoCapitalize (the package entry exports
7
+ * these), TextEntryTraitProps and textEntryTraitAttributes
8
+ * @position Shared by TextInput, TextArea and the search inputs: the one place
9
+ * that says how a field tells a touch keyboard what it holds
12
10
  *
13
11
  * SYNC: When modified, update:
14
12
  * - /packages/core/src/TextInput/TextInput.tsx and TextInput.doc.mjs
15
13
  * - /packages/core/src/TextArea/TextArea.tsx and TextArea.doc.mjs
14
+ * - /packages/core/src/Typeahead/BaseTypeahead.tsx, Field/PanelSearchInput.tsx
15
+ * and CommandPalette/CommandPaletteInput.tsx (the search inputs)
16
16
  * - /packages/core/src/Field/index.ts (type exports)
17
17
  */
18
18
 
@@ -6,7 +6,7 @@
6
6
  * @file layerClickBoundary.ts
7
7
  * @input React pointer, mouse, touch and click events on a layer root
8
8
  * @output Exports layerClickBoundary, the props every layer content root
9
- * spreads, including the layer-root mark
9
+ * spreads, including the layer-root mark; also stopInReactTree
10
10
  * @position Internal to Layer; used by useLayer's renderers and the Dialog,
11
11
  * Drawer, Lightbox, MobileNav, BottomSheetPanel and ToastViewport roots.
12
12
  * Not exported from the package.
@@ -61,7 +61,7 @@ const returnTrue = () => true;
61
61
  * React's dispatch asks `isPropagationStopped()` before each further
62
62
  * listener; the native event is left alone.
63
63
  */
64
- function stopInReactTree(event: SyntheticEvent): void {
64
+ export function stopInReactTree(event: SyntheticEvent): void {
65
65
  event.isPropagationStopped = returnTrue;
66
66
  }
67
67