@astryxdesign/core 0.7.8-canary.ffd29c5 → 0.7.8

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 (189) hide show
  1. package/CHANGELOG.md +132 -0
  2. package/dist/BottomSheet/BottomSheetPanel.d.ts +1 -1
  3. package/dist/BottomSheet/BottomSheetPanel.d.ts.map +1 -1
  4. package/dist/BottomSheet/BottomSheetPanel.js +30 -14
  5. package/dist/BottomSheet/bottomSheetStack.js +1 -1
  6. package/dist/BottomSheet/useSheetGestures.d.ts +3 -1
  7. package/dist/BottomSheet/useSheetGestures.d.ts.map +1 -1
  8. package/dist/BottomSheet/useSheetGestures.js +4 -0
  9. package/dist/CheckboxList/CheckboxList.d.ts +13 -2
  10. package/dist/CheckboxList/CheckboxList.d.ts.map +1 -1
  11. package/dist/CheckboxList/CheckboxList.js +10 -4
  12. package/dist/CheckboxList/CheckboxListContext.d.ts +7 -0
  13. package/dist/CheckboxList/CheckboxListContext.d.ts.map +1 -1
  14. package/dist/CheckboxList/CheckboxListContext.js +2 -1
  15. package/dist/CheckboxList/CheckboxListItem.d.ts.map +1 -1
  16. package/dist/CheckboxList/CheckboxListItem.js +14 -2
  17. package/dist/CheckboxList/requiredGroup.d.ts +22 -0
  18. package/dist/CheckboxList/requiredGroup.d.ts.map +1 -0
  19. package/dist/CheckboxList/requiredGroup.js +52 -0
  20. package/dist/DateInput/PickerWheel.d.ts +1 -1
  21. package/dist/DateInput/PickerWheel.d.ts.map +1 -1
  22. package/dist/DateInput/PickerWheel.js +27 -6
  23. package/dist/DateInput/Wheel.d.ts +2 -2
  24. package/dist/DateInput/Wheel.d.ts.map +1 -1
  25. package/dist/DateInput/Wheel.js +20 -6
  26. package/dist/DateInput/datePickerTouch.d.ts +13 -0
  27. package/dist/DateInput/datePickerTouch.d.ts.map +1 -0
  28. package/dist/DateInput/datePickerTouch.js +14 -0
  29. package/dist/DateInput/wheelOptions.d.ts +22 -2
  30. package/dist/DateInput/wheelOptions.d.ts.map +1 -1
  31. package/dist/DateInput/wheelOptions.js +14 -2
  32. package/dist/DateTimeInput/TouchDateTimeField.d.ts.map +1 -1
  33. package/dist/DateTimeInput/TouchDateTimeField.js +62 -119
  34. package/dist/HoverCard/useHoverCard.d.ts.map +1 -1
  35. package/dist/HoverCard/useHoverCard.js +6 -1
  36. package/dist/Overlay/Overlay.d.ts.map +1 -1
  37. package/dist/Overlay/Overlay.js +7 -2
  38. package/dist/PowerSearch/PowerSearch.d.ts +4 -2
  39. package/dist/PowerSearch/PowerSearch.d.ts.map +1 -1
  40. package/dist/PowerSearch/PowerSearchBottomSheet.d.ts +24 -1
  41. package/dist/PowerSearch/PowerSearchBottomSheet.d.ts.map +1 -1
  42. package/dist/PowerSearch/PowerSearchBottomSheet.js +172 -50
  43. package/dist/Resizable/ResizeHandle.d.ts.map +1 -1
  44. package/dist/Resizable/ResizeHandle.js +15 -15
  45. package/dist/Resizable/createResizable.d.ts +70 -16
  46. package/dist/Resizable/createResizable.d.ts.map +1 -1
  47. package/dist/Resizable/createResizable.js +180 -22
  48. package/dist/Resizable/separatorAttributes.d.ts +45 -0
  49. package/dist/Resizable/separatorAttributes.d.ts.map +1 -0
  50. package/dist/Resizable/separatorAttributes.js +35 -0
  51. package/dist/Resizable/useResizable.js +2 -2
  52. package/dist/TimeInput/TimeWheels.d.ts +32 -0
  53. package/dist/TimeInput/TimeWheels.d.ts.map +1 -0
  54. package/dist/TimeInput/TimeWheels.js +144 -0
  55. package/dist/TimeInput/TouchTimeField.d.ts.map +1 -1
  56. package/dist/TimeInput/TouchTimeField.js +63 -118
  57. package/dist/TimeInput/timeWheelRules.d.ts +124 -0
  58. package/dist/TimeInput/timeWheelRules.d.ts.map +1 -0
  59. package/dist/TimeInput/timeWheelRules.js +277 -0
  60. package/dist/Tokenizer/TokenizerBottomSheet.js +8 -12
  61. package/dist/Tooltip/useTooltip.d.ts.map +1 -1
  62. package/dist/Tooltip/useTooltip.js +6 -0
  63. package/dist/astryx.css +3 -1
  64. package/dist/gestures/GestureHost.d.ts.map +1 -1
  65. package/dist/gestures/GestureHost.js +3 -0
  66. package/dist/i18n/generated-english/PowerSearch/PowerSearchBottomSheet.generated.d.ts.map +1 -1
  67. package/dist/i18n/generated-english/PowerSearch/PowerSearchBottomSheet.generated.js +5 -2
  68. package/dist/i18n/generated-english/Resizable/createResizable.generated.d.ts +8 -0
  69. package/dist/i18n/generated-english/Resizable/createResizable.generated.d.ts.map +1 -0
  70. package/dist/i18n/generated-english/Resizable/createResizable.generated.js +13 -0
  71. package/dist/i18n/generated-english/TimeInput/TimeWheels.generated.d.ts +8 -0
  72. package/dist/i18n/generated-english/TimeInput/TimeWheels.generated.d.ts.map +1 -0
  73. package/dist/i18n/generated-english/TimeInput/TimeWheels.generated.js +17 -0
  74. package/dist/i18n/generated-locales/en.generated.d.ts.map +1 -1
  75. package/dist/i18n/generated-locales/en.generated.js +5 -2
  76. package/dist/i18n/generated-locales/pseudo.generated.d.ts.map +1 -1
  77. package/dist/i18n/generated-locales/pseudo.generated.js +5 -2
  78. package/dist/keyboard/index.d.ts +17 -0
  79. package/dist/keyboard/index.d.ts.map +1 -0
  80. package/dist/keyboard/index.js +19 -0
  81. package/dist/keyboard/keyboardHeight.d.ts +149 -0
  82. package/dist/keyboard/keyboardHeight.d.ts.map +1 -0
  83. package/dist/keyboard/keyboardHeight.js +432 -0
  84. package/dist/keyboard/keyboardLayoutGuide.d.ts +59 -0
  85. package/dist/keyboard/keyboardLayoutGuide.d.ts.map +1 -0
  86. package/dist/keyboard/keyboardLayoutGuide.js +207 -0
  87. package/dist/keyboard/keyboardModel.d.ts +93 -0
  88. package/dist/keyboard/keyboardModel.d.ts.map +1 -0
  89. package/dist/keyboard/keyboardModel.js +220 -0
  90. package/dist/keyboard/rideKeyboard.d.ts +42 -0
  91. package/dist/keyboard/rideKeyboard.d.ts.map +1 -0
  92. package/dist/keyboard/rideKeyboard.js +77 -0
  93. package/dist/keyboard/useMobileKeyboard.d.ts +61 -0
  94. package/dist/keyboard/useMobileKeyboard.d.ts.map +1 -0
  95. package/dist/keyboard/useMobileKeyboard.js +699 -0
  96. package/dist/motion/android/index.d.ts +16 -0
  97. package/dist/motion/android/index.d.ts.map +1 -0
  98. package/dist/motion/android/index.js +18 -0
  99. package/dist/motion/android/keyboard.d.ts +83 -0
  100. package/dist/motion/android/keyboard.d.ts.map +1 -0
  101. package/dist/motion/android/keyboard.js +96 -0
  102. package/dist/motion/uikit/datePicker.d.ts +58 -0
  103. package/dist/motion/uikit/datePicker.d.ts.map +1 -0
  104. package/dist/motion/uikit/datePicker.js +60 -0
  105. package/dist/motion/uikit/index.d.ts +4 -3
  106. package/dist/motion/uikit/index.d.ts.map +1 -1
  107. package/dist/motion/uikit/index.js +4 -3
  108. package/dist/utils/interactionOverlay.stylex.d.ts +3 -2
  109. package/dist/utils/interactionOverlay.stylex.d.ts.map +1 -1
  110. package/dist/utils/interactionOverlay.stylex.js +3 -2
  111. package/dist/utils/layoutViewport.d.ts +25 -0
  112. package/dist/utils/layoutViewport.d.ts.map +1 -0
  113. package/dist/utils/layoutViewport.js +102 -0
  114. package/dist/utils/pressFeedback.d.ts.map +1 -1
  115. package/dist/utils/pressFeedback.js +74 -9
  116. package/dist/utils/pressGesture.d.ts +4 -2
  117. package/dist/utils/pressGesture.d.ts.map +1 -1
  118. package/dist/utils/pressGesture.js +3 -1
  119. package/dist/utils/selectionLock.d.ts +10 -1
  120. package/dist/utils/selectionLock.d.ts.map +1 -1
  121. package/dist/utils/selectionLock.js +31 -5
  122. package/dist/utils/sharedMutationObserver.d.ts +2 -1
  123. package/dist/utils/sharedMutationObserver.d.ts.map +1 -1
  124. package/dist/utils/sharedMutationObserver.js +2 -1
  125. package/dist/utils/timeParser.d.ts +2 -1
  126. package/dist/utils/timeParser.d.ts.map +1 -1
  127. package/dist/utils/timeParser.js +29 -3
  128. package/locales/en.json +21 -9
  129. package/locales/pseudo.json +15 -6
  130. package/package.json +5 -5
  131. package/src/BottomSheet/BottomSheet.doc.mjs +1 -1
  132. package/src/BottomSheet/BottomSheetPanel.tsx +41 -13
  133. package/src/BottomSheet/BottomSheetSwitcher.doc.mjs +5 -0
  134. package/src/BottomSheet/bottomSheetStack.ts +1 -1
  135. package/src/BottomSheet/useSheetGestures.ts +6 -0
  136. package/src/CheckboxList/CheckboxList.doc.mjs +7 -0
  137. package/src/CheckboxList/CheckboxList.tsx +34 -2
  138. package/src/CheckboxList/CheckboxListContext.tsx +9 -1
  139. package/src/CheckboxList/CheckboxListItem.tsx +23 -2
  140. package/src/CheckboxList/requiredGroup.ts +77 -0
  141. package/src/DateInput/PickerWheel.tsx +37 -4
  142. package/src/DateInput/Wheel.tsx +30 -3
  143. package/src/DateInput/datePickerTouch.ts +15 -0
  144. package/src/DateInput/wheelOptions.ts +31 -2
  145. package/src/DateTimeInput/TouchDateTimeField.tsx +94 -178
  146. package/src/HoverCard/useHoverCard.tsx +6 -1
  147. package/src/Overlay/Overlay.tsx +7 -2
  148. package/src/PowerSearch/PowerSearch.doc.mjs +2 -2
  149. package/src/PowerSearch/PowerSearch.tsx +4 -2
  150. package/src/PowerSearch/PowerSearchBottomSheet.tsx +237 -62
  151. package/src/Resizable/ResizeHandle.tsx +18 -26
  152. package/src/Resizable/createResizable.ts +242 -27
  153. package/src/Resizable/separatorAttributes.ts +73 -0
  154. package/src/Resizable/useResizable.ts +2 -2
  155. package/src/TimeInput/TimeWheels.tsx +218 -0
  156. package/src/TimeInput/TouchTimeField.tsx +93 -157
  157. package/src/TimeInput/timeWheelRules.ts +352 -0
  158. package/src/Tokenizer/TokenizerBottomSheet.tsx +8 -12
  159. package/src/Tooltip/useTooltip.tsx +6 -0
  160. package/src/gestures/GestureHost.ts +5 -4
  161. package/src/i18n/generated-english/PowerSearch/PowerSearchBottomSheet.generated.ts +5 -2
  162. package/src/i18n/generated-english/Resizable/createResizable.generated.ts +14 -0
  163. package/src/i18n/generated-english/TimeInput/TimeWheels.generated.ts +18 -0
  164. package/src/i18n/generated-locales/en.generated.ts +6 -4
  165. package/src/i18n/generated-locales/pseudo.generated.ts +6 -4
  166. package/src/keyboard/README.md +197 -0
  167. package/src/keyboard/index.ts +48 -0
  168. package/src/keyboard/keyboardHeight.ts +543 -0
  169. package/src/keyboard/keyboardLayoutGuide.ts +266 -0
  170. package/src/keyboard/keyboardModel.ts +301 -0
  171. package/src/keyboard/rideKeyboard.ts +100 -0
  172. package/src/keyboard/useMobileKeyboard.ts +866 -0
  173. package/src/motion/MEASUREMENTS.md +10 -0
  174. package/src/motion/README.md +2 -0
  175. package/src/motion/android/index.ts +18 -0
  176. package/src/motion/android/keyboard.ts +96 -0
  177. package/src/motion/uikit/datePicker.ts +60 -0
  178. package/src/motion/uikit/index.ts +4 -3
  179. package/src/utils/interactionOverlay.stylex.ts +3 -2
  180. package/src/utils/layoutViewport.ts +104 -0
  181. package/src/utils/pressFeedback.ts +99 -21
  182. package/src/utils/pressGesture.ts +4 -2
  183. package/src/utils/selectionLock.ts +40 -6
  184. package/src/utils/sharedMutationObserver.ts +2 -1
  185. package/src/utils/timeParser.ts +40 -3
  186. package/dist/BottomSheet/useMobileKeyboard.d.ts +0 -21
  187. package/dist/BottomSheet/useMobileKeyboard.d.ts.map +0 -1
  188. package/dist/BottomSheet/useMobileKeyboard.js +0 -413
  189. package/src/BottomSheet/useMobileKeyboard.ts +0 -542
@@ -341,7 +341,8 @@ export function isTimeInRange(
341
341
  }
342
342
 
343
343
  /**
344
- * Clamps a time to a min/max range.
344
+ * Clamps a time to a min/max range. Without seconds, a bound that falls
345
+ * between whole minutes gives the nearest whole minute inside the range.
345
346
  */
346
347
  export function clampTime(
347
348
  time: ISOTimeString,
@@ -357,19 +358,55 @@ export function clampTime(
357
358
  if (min && compareTime(time, min) < 0) {
358
359
  const parsedMin = parseISOTime(min);
359
360
  if (parsedMin) {
360
- return formatISOTime(parsedMin, includeSeconds);
361
+ return formatISOTime(
362
+ onWholeMinute(parsedMin, 1, includeSeconds, max),
363
+ includeSeconds,
364
+ );
361
365
  }
362
366
  }
363
367
  if (max && compareTime(time, max) > 0) {
364
368
  const parsedMax = parseISOTime(max);
365
369
  if (parsedMax) {
366
- return formatISOTime(parsedMax, includeSeconds);
370
+ return formatISOTime(
371
+ onWholeMinute(parsedMax, -1, includeSeconds, min),
372
+ includeSeconds,
373
+ );
367
374
  }
368
375
  }
369
376
 
370
377
  return time;
371
378
  }
372
379
 
380
+ /**
381
+ * A bound as a time without seconds reaches it: the next whole minute up from
382
+ * a minimum, or the whole minute down from a maximum. With no whole minute
383
+ * before the other bound, the bound stays as it is.
384
+ */
385
+ function onWholeMinute(
386
+ bound: ParsedTime,
387
+ direction: 1 | -1,
388
+ includeSeconds: boolean,
389
+ other: ISOTimeString | undefined,
390
+ ): ParsedTime {
391
+ if (includeSeconds || bound.second === 0) {
392
+ return bound;
393
+ }
394
+ const minutes = bound.hour * 60 + bound.minute + (direction === 1 ? 1 : 0);
395
+ if (minutes > 23 * 60 + 59) {
396
+ return bound;
397
+ }
398
+ const rounded = {
399
+ hour: Math.floor(minutes / 60),
400
+ minute: minutes % 60,
401
+ second: 0,
402
+ };
403
+ const order = compareTime(formatISOTime(rounded, true), other);
404
+ if (other && (direction === 1 ? order > 0 : order < 0)) {
405
+ return bound;
406
+ }
407
+ return rounded;
408
+ }
409
+
373
410
  /**
374
411
  * Adjusts a time by adding/subtracting minutes.
375
412
  *
@@ -1,21 +0,0 @@
1
- /**
2
- * @file useMobileKeyboard.ts
3
- * @input Uses React effects and refs supplied by BottomSheet
4
- * @output Exports internal useMobileKeyboard hook
5
- * @position Internal to BottomSheet; not exported from the core entry point
6
- */
7
- import { type RefObject } from 'react';
8
- interface UseMobileKeyboardOptions {
9
- bodyRef: RefObject<HTMLElement | null>;
10
- bottomClearance: number;
11
- isEnabled: boolean;
12
- isFullyExpanded: boolean;
13
- isPageScrollLocked: boolean;
14
- isSheetTraveling: boolean;
15
- isOpen: boolean;
16
- isPresented: boolean;
17
- sheetRef: RefObject<HTMLDivElement | null>;
18
- }
19
- export declare function useMobileKeyboard({ bodyRef, bottomClearance, isEnabled, isFullyExpanded, isPageScrollLocked, isSheetTraveling, isOpen, isPresented, sheetRef, }: UseMobileKeyboardOptions): void;
20
- export {};
21
- //# sourceMappingURL=useMobileKeyboard.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"useMobileKeyboard.d.ts","sourceRoot":"","sources":["../../src/BottomSheet/useMobileKeyboard.ts"],"names":[],"mappings":"AAIA;;;;;GAKG;AAcH,OAAO,EAAoB,KAAK,SAAS,EAAC,MAAM,OAAO,CAAC;AAgBxD,UAAU,wBAAwB;IAChC,OAAO,EAAE,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IACvC,eAAe,EAAE,MAAM,CAAC;IACxB,SAAS,EAAE,OAAO,CAAC;IACnB,eAAe,EAAE,OAAO,CAAC;IACzB,kBAAkB,EAAE,OAAO,CAAC;IAC5B,gBAAgB,EAAE,OAAO,CAAC;IAC1B,MAAM,EAAE,OAAO,CAAC;IAChB,WAAW,EAAE,OAAO,CAAC;IACrB,QAAQ,EAAE,SAAS,CAAC,cAAc,GAAG,IAAI,CAAC,CAAC;CAC5C;AA8DD,wBAAgB,iBAAiB,CAAC,EAChC,OAAO,EACP,eAAe,EACf,SAAS,EACT,eAAe,EACf,kBAAkB,EAClB,gBAAgB,EAChB,MAAM,EACN,WAAW,EACX,QAAQ,GACT,EAAE,wBAAwB,GAAG,IAAI,CAoajC"}
@@ -1,413 +0,0 @@
1
- // Copyright (c) Meta Platforms, Inc. and affiliates.
2
-
3
- 'use client';
4
-
5
- /**
6
- * @file useMobileKeyboard.ts
7
- * @input Uses React effects and refs supplied by BottomSheet
8
- * @output Exports internal useMobileKeyboard hook
9
- * @position Internal to BottomSheet; not exported from the core entry point
10
- */
11
-
12
- // A fully expanded, explicitly Tall sheet gets a keyboard-aware internal
13
- // scroll range, and the sheet itself stays still. Other detents and heights
14
- // opt out. Tall-sheet travel or a close blurs the field and dismisses the
15
- // keyboard.
16
- //
17
- // On iOS the browser reveals a focused field by scrolling the document, and
18
- // a fixed sheet moves with it, so the page lurches. The reveal belongs to the
19
- // focus operation, so refuse it there: take over the focus change on the
20
- // capture-phase blur, focus with preventScroll, and then scroll the control
21
- // into view in the sheet's own scroller. A tap, the keyboard's Next, Tab and
22
- // focus() all pass through that one change.
23
- import { useEffect, useRef } from 'react';
24
- const MOBILE_KEYBOARD_INSET_VAR = '--_sheet-keyboard-inset';
25
- const NON_TEXT_INPUT_TYPES = new Set(['button', 'checkbox', 'color', 'file', 'hidden', 'image', 'radio', 'range', 'reset', 'submit']);
26
- function getObstructionTop() {
27
- return window.visualViewport?.height ?? window.innerHeight;
28
- }
29
-
30
- // The keyboard is gone once the visible band reaches the layout viewport bottom.
31
- function isVisualViewportRecovered() {
32
- return getObstructionTop() >= window.innerHeight - 0.5;
33
- }
34
- function isIOSWebKit() {
35
- const userAgent = window.navigator.userAgent;
36
- return /iPad|iPhone|iPod/.test(userAgent) || userAgent.includes('Macintosh') && window.navigator.maxTouchPoints > 1;
37
- }
38
- function isTextEntryControl(element) {
39
- if (element instanceof HTMLTextAreaElement) {
40
- return !element.disabled && !element.readOnly;
41
- }
42
- if (element instanceof HTMLInputElement) {
43
- return !element.disabled && !element.readOnly && !NON_TEXT_INPUT_TYPES.has((element.getAttribute('type') ?? 'text').toLowerCase());
44
- }
45
- return element instanceof HTMLElement && element.matches('[contenteditable]:not([contenteditable="false"])');
46
- }
47
- function findTextEntryControl(target, body) {
48
- if (!(target instanceof Element)) {
49
- return null;
50
- }
51
- const direct = target.closest('input, textarea, [contenteditable]:not([contenteditable="false"])');
52
- if (body.contains(direct) && isTextEntryControl(direct)) {
53
- return direct;
54
- }
55
- const label = target.closest('label');
56
- const control = label instanceof HTMLLabelElement ? label.control : null;
57
- return body.contains(control) && isTextEntryControl(control) ? control : null;
58
- }
59
- export function useMobileKeyboard({
60
- bodyRef,
61
- bottomClearance,
62
- isEnabled,
63
- isFullyExpanded,
64
- isPageScrollLocked,
65
- isSheetTraveling,
66
- isOpen,
67
- isPresented,
68
- sheetRef
69
- }) {
70
- const hasKeyboardLayoutRef = useRef(false);
71
- const retainKeyboardLayoutRef = useRef(false);
72
- const isFullyExpandedRef = useRef(isFullyExpanded);
73
- const isOpenRef = useRef(isOpen);
74
- isFullyExpandedRef.current = isFullyExpanded;
75
- isOpenRef.current = isOpen;
76
- useEffect(() => {
77
- if (!isEnabled || !isSheetTraveling) {
78
- return;
79
- }
80
- const sheet = sheetRef.current;
81
- const activeElement = document.activeElement;
82
- if (sheet && activeElement instanceof HTMLElement && activeElement !== sheet && sheet.contains(activeElement)) {
83
- // Keep the current keyboard scroll range while focusing the sheet
84
- // dismisses the keyboard. Viewport resize events unwind that layout as
85
- // the visual viewport recovers, avoiding a content jump on the first
86
- // drag frame.
87
- retainKeyboardLayoutRef.current = hasKeyboardLayoutRef.current;
88
- sheet.focus({
89
- preventScroll: true
90
- });
91
- }
92
- }, [isEnabled, isSheetTraveling, sheetRef]);
93
-
94
- // Syncs the on-screen keyboard with the sheet. When a presented sheet closes,
95
- // the focused field blurs, so the keyboard closes.
96
- useEffect(() => {
97
- if (!isEnabled || !isPresented || isOpen) {
98
- return;
99
- }
100
- const sheet = sheetRef.current;
101
- const activeElement = document.activeElement;
102
- if (sheet && activeElement instanceof HTMLElement && activeElement !== sheet && sheet.contains(activeElement)) {
103
- retainKeyboardLayoutRef.current = hasKeyboardLayoutRef.current;
104
- activeElement.blur();
105
- }
106
- }, [isEnabled, isOpen, isPresented, sheetRef]);
107
-
108
- // Syncs the body's end inset with the visual viewport, so the on-screen
109
- // keyboard does not cover the body.
110
- useEffect(() => {
111
- const body = bodyRef.current;
112
- const sheet = sheetRef.current;
113
- if (!isEnabled || !isPresented || !body) {
114
- return;
115
- }
116
- let keyboardGeometry = null;
117
- let keyboardObstructionTop = null;
118
- let bodyScrollTopAtKeyboard = null;
119
- let documentScrollAtKeyboard = null;
120
- const ownsFocusTransitions = isIOSWebKit();
121
- const clearKeyboardLayout = () => {
122
- body.style.setProperty(MOBILE_KEYBOARD_INSET_VAR, '0px');
123
- if (bodyScrollTopAtKeyboard != null) {
124
- body.scrollTop = bodyScrollTopAtKeyboard;
125
- }
126
- keyboardGeometry = null;
127
- keyboardObstructionTop = null;
128
- bodyScrollTopAtKeyboard = null;
129
- documentScrollAtKeyboard = null;
130
- hasKeyboardLayoutRef.current = false;
131
- retainKeyboardLayoutRef.current = false;
132
- };
133
- const scrollBodyBy = (distance, smoothly) => {
134
- if (typeof body.scrollBy !== 'function') {
135
- body.scrollTop += distance;
136
- return;
137
- }
138
- const reduceMotion = typeof window.matchMedia === 'function' && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
139
- body.scrollBy({
140
- top: distance,
141
- // Not 'auto': that defers to the element's computed `scroll-behavior`,
142
- // so a consumer's `scroll-behavior: smooth` would animate a scroll this
143
- // hook needs to land in the same frame.
144
- behavior: smoothly && !reduceMotion ? 'smooth' : 'instant'
145
- });
146
- };
147
-
148
- // Scroll `control` into the part of the body the keyboard leaves visible,
149
- // reading live geometry each time.
150
- const scrollControlIntoSafeArea = (control, smoothly) => {
151
- const measuredBodyRect = body.getBoundingClientRect();
152
- const measuredControlRect = control.getBoundingClientRect();
153
- const obstructionTop = getObstructionTop();
154
- const overlap = Math.max(0, measuredBodyRect.bottom - obstructionTop);
155
- const clearance = overlap > 0 ? bottomClearance : 0;
156
- const safeTop = measuredBodyRect.top;
157
- const safeBottom = Math.min(measuredBodyRect.bottom, obstructionTop - clearance);
158
- if (safeBottom <= safeTop) {
159
- return;
160
- }
161
- if (measuredControlRect.bottom > safeBottom) {
162
- scrollBodyBy(measuredControlRect.bottom - safeBottom, smoothly);
163
- } else if (measuredControlRect.top < safeTop) {
164
- scrollBodyBy(measuredControlRect.top - safeTop, smoothly);
165
- }
166
- };
167
- const applyKeyboardGeometry = geometry => {
168
- const obstructionTop = getObstructionTop();
169
- // A collapsed detent can extend the body below the layout viewport even
170
- // after the keyboard closes, so body overlap alone cannot identify
171
- // recovery. Once the visual viewport is full height again, release the
172
- // retained keyboard layout unconditionally.
173
- if (isVisualViewportRecovered()) {
174
- clearKeyboardLayout();
175
- return;
176
- }
177
- const overlap = Math.max(0, geometry.bodyBottom - obstructionTop);
178
- if (overlap === 0) {
179
- clearKeyboardLayout();
180
- return;
181
- }
182
- const inset = Math.max(0, geometry.bodyBottom - (obstructionTop - bottomClearance));
183
- body.style.setProperty(MOBILE_KEYBOARD_INSET_VAR, `${inset}px`);
184
- };
185
-
186
- // Take the focus transition over before the browser performs it.
187
- //
188
- // `blur` is dispatched in the capture phase ahead of the browser's own
189
- // focus step, and it names the destination in `relatedTarget`. Focusing it
190
- // here with preventScroll settles the transition: the browser's step finds
191
- // the element already active, so it dispatches nothing further and has
192
- // nothing to reveal.
193
- const claimFocusTransition = event => {
194
- if (!isFullyExpandedRef.current) {
195
- return;
196
- }
197
- const destination = findTextEntryControl(event.relatedTarget, body);
198
- if (destination) {
199
- if (destination !== document.activeElement) {
200
- // Revealing the field is not this handler's job: focusing it raises
201
- // focusin, and the viewport resize that follows the keyboard raises
202
- // another — both already schedule the reveal below, which knows the
203
- // safe area and scrolls only the sheet's own body.
204
- destination.focus({
205
- preventScroll: true
206
- });
207
- }
208
- return;
209
- }
210
-
211
- // Focus left for nothing — the keyboard's Done button parks it on the
212
- // body. Park it on the sheet instead, so re-tapping the same field is
213
- // still a transition this handler sees. Left on the body, the field is
214
- // already `document.activeElement` on the next tap, no blur fires, and
215
- // the browser reveals it its own way.
216
- //
217
- // Only while the sheet is open: closing blurs the field too, and there
218
- // is no next tap to keep claimable — the host restores focus to whatever
219
- // opened the sheet.
220
- const origin = findTextEntryControl(event.target, body);
221
- if (isOpenRef.current && origin && !event.relatedTarget) {
222
- sheet?.focus({
223
- preventScroll: true
224
- });
225
- }
226
- };
227
- const revealFocusedControl = () => {
228
- const obstructionTop = getObstructionTop();
229
- const activeElement = document.activeElement;
230
- const focusedControl = activeElement instanceof HTMLElement && body.contains(activeElement) && isTextEntryControl(activeElement) ? activeElement : null;
231
- // The keyboard may close while the text field remains focused (for
232
- // example, a downward keyboard dismissal gesture on iOS). Run the full
233
- // cleanup path so the temporary spacer and the body scroll it enabled
234
- // are both restored. Then reveal the still-focused control against the
235
- // recovered sheet, because it may not be the field that opened the
236
- // keyboard and therefore may sit outside the restored scroll position.
237
- if (hasKeyboardLayoutRef.current && isVisualViewportRecovered()) {
238
- clearKeyboardLayout();
239
- if (isFullyExpandedRef.current && focusedControl) {
240
- scrollControlIntoSafeArea(focusedControl, false);
241
- }
242
- return;
243
- }
244
- if (!isFullyExpandedRef.current || focusedControl == null) {
245
- if (retainKeyboardLayoutRef.current && keyboardGeometry) {
246
- applyKeyboardGeometry(keyboardGeometry);
247
- return;
248
- }
249
- clearKeyboardLayout();
250
- return;
251
- }
252
-
253
- // An interactively dismissed iOS keyboard can leave the accessory bar
254
- // visible, so the visual viewport grows without reaching full height.
255
- // Restore the pre-keyboard body position as soon as that recovery starts;
256
- // the ordinary safe-area pass below may still scroll a low focused field
257
- // just enough for whatever obstruction remains. Advance the remembered
258
- // obstruction so later content mutations do not repeat this recovery.
259
- if (hasKeyboardLayoutRef.current && keyboardObstructionTop != null && obstructionTop > keyboardObstructionTop + 0.5 && bodyScrollTopAtKeyboard != null) {
260
- body.scrollTop = bodyScrollTopAtKeyboard;
261
- keyboardObstructionTop = obstructionTop;
262
- }
263
- retainKeyboardLayoutRef.current = false;
264
- const measuredBodyRect = body.getBoundingClientRect();
265
- const overlap = Math.max(0, measuredBodyRect.bottom - obstructionTop);
266
- // The extra clearance leaves room for mobile suggestion UI, but only
267
- // while the visual viewport actually overlaps the sheet. With no
268
- // obstruction, ordinary desktop and hardware-keyboard focus must not
269
- // shift an already visible control.
270
- const clearance = overlap > 0 ? bottomClearance : 0;
271
- keyboardGeometry = overlap > 0 ? {
272
- bodyBottom: measuredBodyRect.bottom
273
- } : null;
274
- // Where the document sits with the keyboard up and nothing yet shifted:
275
- // the position handleDocumentScroll returns to. Captured on the
276
- // transition only — a reveal that runs after the browser has already
277
- // scrolled would otherwise record the shifted position as correct.
278
- if (overlap > 0 && !hasKeyboardLayoutRef.current) {
279
- keyboardObstructionTop = obstructionTop;
280
- bodyScrollTopAtKeyboard = body.scrollTop;
281
- if (isPageScrollLocked) {
282
- documentScrollAtKeyboard = {
283
- x: window.scrollX,
284
- y: window.scrollY
285
- };
286
- }
287
- }
288
- hasKeyboardLayoutRef.current = overlap > 0;
289
- const inset = overlap > 0 ? Math.max(0, measuredBodyRect.bottom - (obstructionTop - clearance)) : 0;
290
- body.style.setProperty(MOBILE_KEYBOARD_INSET_VAR, `${inset}px`);
291
- scrollControlIntoSafeArea(focusedControl, overlap > 0);
292
- };
293
- let animationFrame = 0;
294
- const scheduleReveal = () => {
295
- cancelAnimationFrame(animationFrame);
296
- animationFrame = requestAnimationFrame(revealFocusedControl);
297
- };
298
-
299
- // Resuming the app re-reveals the focused field with no focus event to
300
- // claim, so the reveal above never runs and the document scrolls — taking
301
- // the fixed sheet with it. Put that scroll back on the event that reports
302
- // it and re-reveal inside the sheet.
303
- //
304
- // Only while the page is locked, which is the only state in which a
305
- // document scroll cannot be the user's own: behind a non-modal sheet the
306
- // page stays scrollable, and reverting there would fight them.
307
- const handleDocumentScroll = () => {
308
- const expected = documentScrollAtKeyboard;
309
- if (expected == null || window.scrollX === expected.x && window.scrollY === expected.y) {
310
- return;
311
- }
312
- window.scrollTo(expected.x, expected.y);
313
- const activeElement = document.activeElement;
314
- if (activeElement instanceof HTMLElement && body.contains(activeElement) && isTextEntryControl(activeElement)) {
315
- scrollControlIntoSafeArea(activeElement, false);
316
- }
317
- };
318
- const resizeObserver = typeof ResizeObserver === 'undefined' ? null : new ResizeObserver(scheduleReveal);
319
- const refreshObservedLayout = () => {
320
- if (!resizeObserver) {
321
- return;
322
- }
323
- resizeObserver.disconnect();
324
-
325
- // Body/sheet box changes include public height and xstyle updates. The
326
- // generated inset pseudo-element changes only scroll overflow, so these
327
- // observations do not feed the keyboard inset back into sheet geometry.
328
- const targets = new Set([body]);
329
- if (sheet) {
330
- targets.add(sheet);
331
- }
332
- for (const child of body.children) {
333
- targets.add(child);
334
- }
335
- const activeElement = document.activeElement;
336
- if (activeElement instanceof HTMLElement && body.contains(activeElement)) {
337
- for (let element = activeElement; element && element !== body; element = element.parentElement) {
338
- targets.add(element);
339
- }
340
- }
341
- for (const target of targets) {
342
- resizeObserver.observe(target);
343
- }
344
- };
345
- const mutationObserver = typeof MutationObserver === 'undefined' ? null : new MutationObserver(records => {
346
- // Ignore the internal inset written on the body. Consumer DOM,
347
- // text, class, style, or visibility changes can all move the
348
- // focused control without a viewport event.
349
- if (records.every(record => record.type === 'attributes' && record.target === body)) {
350
- return;
351
- }
352
- refreshObservedLayout();
353
- scheduleReveal();
354
- });
355
- const handleFocusIn = () => {
356
- refreshObservedLayout();
357
- scheduleReveal();
358
- };
359
- const handleFocusOut = () => {
360
- // Keep the added scroll range while the keyboard animates away. A focus
361
- // transition to another text-entry control cancels this in focusin.
362
- retainKeyboardLayoutRef.current = hasKeyboardLayoutRef.current;
363
- scheduleReveal();
364
- };
365
- const handleSheetTransitionEnd = event => {
366
- if (event.target === sheet && event.propertyName === 'transform') {
367
- scheduleReveal();
368
- }
369
- };
370
- const viewport = window.visualViewport;
371
- if (ownsFocusTransitions) {
372
- // `blur` does not bubble, so the capture phase is the only place to hear
373
- // every one of them — and it runs before the browser's own focus step,
374
- // which is the only window in which the transition can still be claimed.
375
- document.addEventListener('blur', claimFocusTransition, true);
376
- }
377
- body.addEventListener('focusin', handleFocusIn);
378
- body.addEventListener('focusout', handleFocusOut);
379
- sheet?.addEventListener('transitionend', handleSheetTransitionEnd);
380
- viewport?.addEventListener('resize', scheduleReveal);
381
- viewport?.addEventListener('scroll', scheduleReveal);
382
- window.addEventListener('scroll', handleDocumentScroll);
383
- window.addEventListener('resize', scheduleReveal);
384
- mutationObserver?.observe(body, {
385
- attributes: true,
386
- characterData: true,
387
- childList: true,
388
- subtree: true
389
- });
390
- refreshObservedLayout();
391
- // The dialog enters the top layer in a later effect. Wait until the next
392
- // frame so flex geometry is final before measuring the body.
393
- scheduleReveal();
394
- return () => {
395
- cancelAnimationFrame(animationFrame);
396
- if (ownsFocusTransitions) {
397
- document.removeEventListener('blur', claimFocusTransition, true);
398
- }
399
- body.removeEventListener('focusin', handleFocusIn);
400
- body.removeEventListener('focusout', handleFocusOut);
401
- sheet?.removeEventListener('transitionend', handleSheetTransitionEnd);
402
- viewport?.removeEventListener('resize', scheduleReveal);
403
- viewport?.removeEventListener('scroll', scheduleReveal);
404
- window.removeEventListener('scroll', handleDocumentScroll);
405
- window.removeEventListener('resize', scheduleReveal);
406
- resizeObserver?.disconnect();
407
- mutationObserver?.disconnect();
408
- body.style.removeProperty(MOBILE_KEYBOARD_INSET_VAR);
409
- hasKeyboardLayoutRef.current = false;
410
- retainKeyboardLayoutRef.current = false;
411
- };
412
- }, [bodyRef, bottomClearance, isEnabled, isPageScrollLocked, isPresented, sheetRef]);
413
- }