@astryxdesign/core 0.7.7-canary.fc68123 → 0.7.7

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 (240) hide show
  1. package/CHANGELOG.md +104 -0
  2. package/dist/AlertDialog/AlertDialog.d.ts +1 -4
  3. package/dist/AlertDialog/AlertDialog.d.ts.map +1 -1
  4. package/dist/AlertDialog/AlertDialog.js +6 -36
  5. package/dist/BottomSheet/BottomSheetPanel.d.ts.map +1 -1
  6. package/dist/BottomSheet/BottomSheetPanel.js +4 -23
  7. package/dist/BottomSheet/useSheetGestures.d.ts.map +1 -1
  8. package/dist/BottomSheet/useSheetGestures.js +1 -3
  9. package/dist/Button/Button.d.ts.map +1 -1
  10. package/dist/Button/Button.js +1 -17
  11. package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
  12. package/dist/CheckboxInput/CheckboxInput.js +2 -13
  13. package/dist/ComplexSelector/ComplexSelector.d.ts +1 -4
  14. package/dist/ComplexSelector/ComplexSelector.d.ts.map +1 -1
  15. package/dist/ComplexSelector/ComplexSelector.js +4 -14
  16. package/dist/ContextMenu/ContextMenu.js +2 -2
  17. package/dist/Dialog/Dialog.d.ts.map +1 -1
  18. package/dist/Dialog/Dialog.js +31 -7
  19. package/dist/Field/Field.js +2 -2
  20. package/dist/HoverCard/HoverCard.d.ts +3 -4
  21. package/dist/HoverCard/HoverCard.d.ts.map +1 -1
  22. package/dist/HoverCard/HoverCard.js +2 -27
  23. package/dist/HoverCard/useHoverCard.d.ts +4 -5
  24. package/dist/HoverCard/useHoverCard.d.ts.map +1 -1
  25. package/dist/HoverCard/useHoverCard.js +31 -17
  26. package/dist/Icon/Icon.d.ts +1 -5
  27. package/dist/Icon/Icon.d.ts.map +1 -1
  28. package/dist/Icon/Icon.js +2 -5
  29. package/dist/Layer/usePressPin.d.ts +5 -17
  30. package/dist/Layer/usePressPin.d.ts.map +1 -1
  31. package/dist/Layer/usePressPin.js +12 -62
  32. package/dist/Lightbox/Lightbox.d.ts.map +1 -1
  33. package/dist/Lightbox/Lightbox.js +0 -18
  34. package/dist/PowerSearch/PowerSearch.d.ts +3 -36
  35. package/dist/PowerSearch/PowerSearch.d.ts.map +1 -1
  36. package/dist/PowerSearch/PowerSearch.js +127 -433
  37. package/dist/PowerSearch/PowerSearchEditPopover.d.ts +1 -32
  38. package/dist/PowerSearch/PowerSearchEditPopover.d.ts.map +1 -1
  39. package/dist/PowerSearch/PowerSearchEditPopover.js +6 -17
  40. package/dist/PowerSearch/PowerSearchValueEditor.d.ts +3 -38
  41. package/dist/PowerSearch/PowerSearchValueEditor.d.ts.map +1 -1
  42. package/dist/PowerSearch/PowerSearchValueEditor.js +61 -170
  43. package/dist/PowerSearch/formatFilterValue.d.ts +0 -7
  44. package/dist/PowerSearch/formatFilterValue.d.ts.map +1 -1
  45. package/dist/PowerSearch/formatFilterValue.js +1 -11
  46. package/dist/RadioList/RadioList.d.ts.map +1 -1
  47. package/dist/RadioList/RadioList.js +2 -3
  48. package/dist/RadioList/RadioListItem.d.ts +1 -1
  49. package/dist/RadioList/RadioListItem.d.ts.map +1 -1
  50. package/dist/RadioList/RadioListItem.js +3 -5
  51. package/dist/Resizable/ResizeHandle.d.ts +1 -2
  52. package/dist/Resizable/ResizeHandle.d.ts.map +1 -1
  53. package/dist/Resizable/ResizeHandle.js +77 -99
  54. package/dist/SegmentedControl/useSegmentedSelectionMotion.d.ts.map +1 -1
  55. package/dist/SegmentedControl/useSegmentedSelectionMotion.js +3 -3
  56. package/dist/Slider/sliderTouchController.js +2 -2
  57. package/dist/Spinner/Spinner.d.ts +1 -1
  58. package/dist/Spinner/Spinner.d.ts.map +1 -1
  59. package/dist/Spinner/Spinner.js +58 -60
  60. package/dist/Switch/useSwitchTouch.js +4 -4
  61. package/dist/Table/BaseTable.d.ts.map +1 -1
  62. package/dist/Table/BaseTable.js +8 -29
  63. package/dist/TextInput/TextInput.d.ts +2 -2
  64. package/dist/TextInput/TextInput.d.ts.map +1 -1
  65. package/dist/TextInput/TextInput.js +7 -10
  66. package/dist/Toast/Toast.d.ts +1 -11
  67. package/dist/Toast/Toast.d.ts.map +1 -1
  68. package/dist/Toast/Toast.js +15 -25
  69. package/dist/Toast/ToastViewport.d.ts.map +1 -1
  70. package/dist/Toast/ToastViewport.js +34 -94
  71. package/dist/Toast/types.d.ts +3 -9
  72. package/dist/Toast/types.d.ts.map +1 -1
  73. package/dist/Tooltip/useTooltip.d.ts +1 -19
  74. package/dist/Tooltip/useTooltip.d.ts.map +1 -1
  75. package/dist/Tooltip/useTooltip.js +53 -34
  76. package/dist/Typeahead/TypeaheadItem.d.ts +1 -2
  77. package/dist/Typeahead/TypeaheadItem.d.ts.map +1 -1
  78. package/dist/astryx.css +6 -35
  79. package/dist/gestures/PressTracker.d.ts.map +1 -1
  80. package/dist/gestures/PressTracker.js +3 -4
  81. package/dist/hooks/actionGroupTracker.d.ts +1 -4
  82. package/dist/hooks/actionGroupTracker.d.ts.map +1 -1
  83. package/dist/hooks/actionGroupTracker.js +3 -13
  84. package/dist/hooks/index.d.ts +0 -1
  85. package/dist/hooks/index.d.ts.map +1 -1
  86. package/dist/hooks/index.js +0 -1
  87. package/dist/hooks/useAttachment.d.ts +1 -1
  88. package/dist/hooks/useAttachment.js +1 -1
  89. package/dist/hooks/useInputStatusIcon.d.ts +1 -1
  90. package/dist/hooks/useInputStatusIcon.d.ts.map +1 -1
  91. package/dist/hooks/useInputStatusIcon.js +2 -2
  92. package/dist/hooks/useLongPress.d.ts +2 -6
  93. package/dist/hooks/useLongPress.d.ts.map +1 -1
  94. package/dist/hooks/useLongPress.js +15 -95
  95. package/dist/i18n/generated-english/PowerSearch/PowerSearchValueEditor.generated.d.ts.map +1 -1
  96. package/dist/i18n/generated-english/PowerSearch/PowerSearchValueEditor.generated.js +0 -6
  97. package/dist/i18n/generated-locales/en.generated.d.ts.map +1 -1
  98. package/dist/i18n/generated-locales/en.generated.js +0 -30
  99. package/dist/i18n/generated-locales/pseudo.generated.d.ts.map +1 -1
  100. package/dist/i18n/generated-locales/pseudo.generated.js +0 -30
  101. package/dist/index.d.ts +0 -1
  102. package/dist/index.d.ts.map +1 -1
  103. package/dist/index.js +0 -4
  104. package/dist/motion/animatedLayout.d.ts.map +1 -1
  105. package/dist/motion/animatedLayout.js +6 -5
  106. package/dist/motion/animator.d.ts +14 -14
  107. package/dist/motion/animator.d.ts.map +1 -1
  108. package/dist/motion/animator.js +18 -41
  109. package/dist/utils/dateParser.d.ts.map +1 -1
  110. package/dist/utils/dateParser.js +0 -12
  111. package/dist/utils/focusReturn.d.ts +0 -15
  112. package/dist/utils/focusReturn.d.ts.map +1 -1
  113. package/dist/utils/focusReturn.js +0 -28
  114. package/dist/utils/interactionOverlay.stylex.d.ts +2 -3
  115. package/dist/utils/interactionOverlay.stylex.d.ts.map +1 -1
  116. package/dist/utils/interactionOverlay.stylex.js +2 -3
  117. package/dist/utils/pressFeedback.d.ts.map +1 -1
  118. package/dist/utils/pressFeedback.js +6 -19
  119. package/locales/en.json +0 -120
  120. package/locales/pseudo.json +0 -90
  121. package/package.json +5 -10
  122. package/src/AlertDialog/AlertDialog.doc.mjs +2 -2
  123. package/src/AlertDialog/AlertDialog.tsx +5 -37
  124. package/src/BottomSheet/BottomSheetPanel.tsx +3 -37
  125. package/src/BottomSheet/useSheetGestures.ts +1 -3
  126. package/src/Button/Button.tsx +4 -26
  127. package/src/CheckboxInput/CheckboxInput.doc.mjs +0 -18
  128. package/src/CheckboxInput/CheckboxInput.tsx +9 -36
  129. package/src/ComplexSelector/ComplexSelector.tsx +5 -20
  130. package/src/ContextMenu/ContextMenu.tsx +2 -2
  131. package/src/Dialog/Dialog.tsx +31 -10
  132. package/src/Field/Field.tsx +2 -2
  133. package/src/HoverCard/HoverCard.doc.mjs +2 -4
  134. package/src/HoverCard/HoverCard.tsx +4 -35
  135. package/src/HoverCard/useHoverCard.doc.mjs +36 -178
  136. package/src/HoverCard/useHoverCard.tsx +52 -46
  137. package/src/Icon/Icon.doc.mjs +8 -49
  138. package/src/Icon/Icon.tsx +7 -15
  139. package/src/Layer/usePressPin.ts +15 -81
  140. package/src/Lightbox/Lightbox.tsx +0 -18
  141. package/src/PowerSearch/PowerSearch.doc.mjs +5 -54
  142. package/src/PowerSearch/PowerSearch.tsx +155 -609
  143. package/src/PowerSearch/PowerSearchEditPopover.tsx +6 -28
  144. package/src/PowerSearch/PowerSearchValueEditor.tsx +38 -203
  145. package/src/PowerSearch/formatFilterValue.ts +1 -18
  146. package/src/RadioList/RadioList.tsx +2 -3
  147. package/src/RadioList/RadioListItem.tsx +3 -5
  148. package/src/Resizable/Resizable.doc.mjs +4 -5
  149. package/src/Resizable/ResizeHandle.tsx +109 -167
  150. package/src/SegmentedControl/useSegmentedSelectionMotion.ts +3 -6
  151. package/src/Slider/sliderTouchController.ts +2 -2
  152. package/src/Spinner/Spinner.tsx +63 -77
  153. package/src/Switch/useSwitchTouch.ts +4 -4
  154. package/src/Table/BaseTable.tsx +12 -46
  155. package/src/TextInput/TextInput.tsx +10 -11
  156. package/src/Toast/Toast.doc.mjs +3 -4
  157. package/src/Toast/Toast.tsx +32 -52
  158. package/src/Toast/ToastViewport.tsx +35 -121
  159. package/src/Toast/types.ts +3 -9
  160. package/src/Tooltip/useTooltip.doc.mjs +0 -24
  161. package/src/Tooltip/useTooltip.tsx +56 -61
  162. package/src/Typeahead/TypeaheadItem.doc.mjs +3 -6
  163. package/src/Typeahead/TypeaheadItem.tsx +1 -2
  164. package/src/gestures/PressTracker.ts +3 -4
  165. package/src/gestures/README.md +1 -7
  166. package/src/hooks/actionGroupTracker.ts +5 -20
  167. package/src/hooks/index.ts +0 -2
  168. package/src/hooks/useAttachment.ts +1 -1
  169. package/src/hooks/useInputStatusIcon.tsx +2 -3
  170. package/src/hooks/useLongPress.doc.mjs +2 -2
  171. package/src/hooks/useLongPress.ts +15 -136
  172. package/src/i18n/generated-english/PowerSearch/PowerSearchValueEditor.generated.ts +0 -6
  173. package/src/i18n/generated-locales/en.generated.ts +0 -41
  174. package/src/i18n/generated-locales/pseudo.generated.ts +0 -42
  175. package/src/index.ts +0 -13
  176. package/src/motion/README.md +5 -9
  177. package/src/motion/animatedLayout.ts +4 -5
  178. package/src/motion/animator.ts +29 -61
  179. package/src/utils/dateParser.ts +0 -16
  180. package/src/utils/focusReturn.ts +0 -30
  181. package/src/utils/interactionOverlay.stylex.ts +2 -3
  182. package/src/utils/pressFeedback.ts +6 -19
  183. package/dist/PowerSearch/PowerSearchBottomSheet.d.ts +0 -100
  184. package/dist/PowerSearch/PowerSearchBottomSheet.d.ts.map +0 -1
  185. package/dist/PowerSearch/PowerSearchBottomSheet.js +0 -1913
  186. package/dist/PowerSearch/formatTokenValue.d.ts +0 -28
  187. package/dist/PowerSearch/formatTokenValue.d.ts.map +0 -1
  188. package/dist/PowerSearch/formatTokenValue.js +0 -160
  189. package/dist/PowerSearch/powerSearchSheetUtils.d.ts +0 -65
  190. package/dist/PowerSearch/powerSearchSheetUtils.d.ts.map +0 -1
  191. package/dist/PowerSearch/powerSearchSheetUtils.js +0 -168
  192. package/dist/animator/createAnimator.d.ts +0 -98
  193. package/dist/animator/createAnimator.d.ts.map +0 -1
  194. package/dist/animator/createAnimator.js +0 -259
  195. package/dist/hooks/pressGroupContext.d.ts +0 -6
  196. package/dist/hooks/pressGroupContext.d.ts.map +0 -1
  197. package/dist/hooks/pressGroupContext.js +0 -20
  198. package/dist/hooks/useActionGroupTracking.d.ts +0 -53
  199. package/dist/hooks/useActionGroupTracking.d.ts.map +0 -1
  200. package/dist/hooks/useActionGroupTracking.js +0 -174
  201. package/dist/hooks/useAnimator.d.ts +0 -41
  202. package/dist/hooks/useAnimator.d.ts.map +0 -1
  203. package/dist/hooks/useAnimator.js +0 -70
  204. package/dist/i18n/generated-english/PowerSearch/PowerSearchBottomSheet.generated.d.ts +0 -8
  205. package/dist/i18n/generated-english/PowerSearch/PowerSearchBottomSheet.generated.d.ts.map +0 -1
  206. package/dist/i18n/generated-english/PowerSearch/PowerSearchBottomSheet.generated.js +0 -42
  207. package/dist/i18n/generated-english/PowerSearch/formatTokenValue.generated.d.ts +0 -8
  208. package/dist/i18n/generated-english/PowerSearch/formatTokenValue.generated.d.ts.map +0 -1
  209. package/dist/i18n/generated-english/PowerSearch/formatTokenValue.generated.js +0 -14
  210. package/dist/internal/indicatorPress.stylex.d.ts +0 -20
  211. package/dist/internal/indicatorPress.stylex.d.ts.map +0 -1
  212. package/dist/internal/indicatorPress.stylex.js +0 -26
  213. package/dist/motion/progress.d.ts +0 -88
  214. package/dist/motion/progress.d.ts.map +0 -1
  215. package/dist/motion/progress.js +0 -128
  216. package/dist/motion/uikit/alert.d.ts +0 -41
  217. package/dist/motion/uikit/alert.d.ts.map +0 -1
  218. package/dist/motion/uikit/alert.js +0 -43
  219. package/dist/privateAttributes/pressGroupItem.stylex.d.ts +0 -15
  220. package/dist/privateAttributes/pressGroupItem.stylex.d.ts.map +0 -1
  221. package/dist/privateAttributes/pressGroupItem.stylex.js +0 -23
  222. package/dist/utils/triggerFocus.d.ts +0 -36
  223. package/dist/utils/triggerFocus.d.ts.map +0 -1
  224. package/dist/utils/triggerFocus.js +0 -65
  225. package/src/PowerSearch/PowerSearchBottomSheet.tsx +0 -2515
  226. package/src/PowerSearch/formatTokenValue.ts +0 -215
  227. package/src/PowerSearch/powerSearchSheetUtils.ts +0 -224
  228. package/src/animator/createAnimator.doc.mjs +0 -153
  229. package/src/animator/createAnimator.ts +0 -324
  230. package/src/hooks/pressGroupContext.ts +0 -21
  231. package/src/hooks/useActionGroupTracking.ts +0 -224
  232. package/src/hooks/useAnimator.doc.mjs +0 -109
  233. package/src/hooks/useAnimator.ts +0 -67
  234. package/src/i18n/generated-english/PowerSearch/PowerSearchBottomSheet.generated.ts +0 -43
  235. package/src/i18n/generated-english/PowerSearch/formatTokenValue.generated.ts +0 -15
  236. package/src/internal/indicatorPress.stylex.ts +0 -24
  237. package/src/motion/progress.ts +0 -155
  238. package/src/motion/uikit/alert.ts +0 -43
  239. package/src/privateAttributes/pressGroupItem.stylex.ts +0 -23
  240. package/src/utils/triggerFocus.ts +0 -80
@@ -4,25 +4,19 @@
4
4
 
5
5
  /**
6
6
  * @file PowerSearch.tsx
7
- * @input PowerSearchConfig, filters, onChange; Tokenizer, Field parts, Token,
8
- * useAdaptivePresentation, and PowerSearchBottomSheet (lazy)
7
+ * @input PowerSearchConfig, filters, onChange
9
8
  * @output Structured filter bar with token-based filter management
10
9
  * @position Main component; forwards DOM ref and exposes tokenizer control via
11
- * handleRef. The experimental `presentation` option swaps the tokenizer
12
- * field and editor popover for one trigger that opens PowerSearchBottomSheet.
10
+ * handleRef
13
11
  *
14
12
  * SYNC: When modified, update:
15
13
  * - /packages/core/src/PowerSearch/index.ts
16
- * - /packages/core/src/PowerSearch/PowerSearchBottomSheet.tsx (sheet presentation)
17
14
  * - /packages/cli/assets/templates/blocks/components/PowerSearch/ (showcase blocks)
18
15
  */
19
16
 
20
17
  import React, {
21
- lazy,
22
- Suspense,
23
18
  useCallback,
24
19
  useEffect,
25
- useId,
26
20
  useImperativeHandle,
27
21
  useMemo,
28
22
  useRef,
@@ -43,45 +37,30 @@ import {Token} from '../Token';
43
37
  import {TokenNamingContext} from '../Token/TokenNamingContext';
44
38
  import {Avatar} from '../Avatar';
45
39
  import {layerAnimations} from '../Layer/layerAnimations.stylex';
46
- import {Icon, renderIconSlot} from '../Icon';
40
+ import {Icon} from '../Icon';
47
41
  import type {IconType} from '../Icon';
48
42
  import type {IconName} from '../Icon/globalIconRegistry';
49
43
  import type {InputStatus} from '../Field';
50
- import {Field} from '../Field/Field';
51
- import {
52
- inputStatusBorderStyles,
53
- inputStatusFocusWithinStyles,
54
- inputStatusHoverShadowStyles,
55
- inputWrapperStyles,
56
- } from '../Field/inputStyles.stylex';
57
44
  import {usePopover} from '../Popover/usePopover';
58
- import {useTooltip} from '../Tooltip/useTooltip';
59
45
  import {layerViewportInset} from '../Layer/layerViewportInset.stylex';
60
46
  import {useAnnounce} from '../hooks/useAnnounce';
61
- import {useAdaptivePresentation} from '../hooks/useAdaptivePresentation';
62
- import {useDevWarning} from '../hooks/useDevWarning';
63
- import {useIsHydrating} from '../hooks/useIsHydrating';
64
47
  import {
65
48
  spacingVars,
66
49
  colorVars,
67
- sizeVars,
68
50
  typeScaleVars,
69
- typographyVars,
70
51
  fontWeightVars,
71
52
  } from '../theme/tokens.stylex';
72
53
  import {useSize} from '../SizeContext/SizeContext';
73
- import {isFocusDetached} from '../utils/focusReturn';
74
- import {isRenderable} from '../utils/isRenderable';
75
54
  import {useInternalConfig} from './useInternalConfig';
76
55
  import {usePowerSearchSource} from './usePowerSearchSource';
77
- import {formatFilterValue} from './formatFilterValue';
78
- import {NestedEditor, PowerSearchEditPopover} from './PowerSearchEditPopover';
79
56
  import {
80
- buildRelativeDateOptions,
81
- PowerSearchValueEditor,
82
- } from './PowerSearchValueEditor';
57
+ formatFilterValue,
58
+ formatDateAbsoluteCompact,
59
+ } from './formatFilterValue';
60
+ import {PowerSearchEditPopover} from './PowerSearchEditPopover';
83
61
  import {resolveOperatorLabel} from './resolveOperatorLabel';
84
62
  import {themeProps} from '../utils/themeProps';
63
+ import {truncateCharacters} from '../utils/characters';
85
64
  import {useLocale} from '../i18n/useLocale';
86
65
  import type {Locale} from '../i18n/types';
87
66
  import type {SearchableItem} from '../Typeahead/types';
@@ -95,14 +74,10 @@ import type {
95
74
  PowerSearchHandle,
96
75
  FilterValue,
97
76
  OperatorValue,
77
+ EnumItem,
98
78
  PowerSearchComponents,
99
79
  PowerSearchOperator,
100
80
  } from './types';
101
- import type {
102
- PowerSearchBottomSheetProps,
103
- PowerSearchSheetEditors,
104
- } from './PowerSearchBottomSheet';
105
- import {formatTokenValueText} from './formatTokenValue';
106
81
 
107
82
  import {useMergedRefs} from '../hooks/useMergedRefs';
108
83
  import type {FieldStatusPlacement} from '../Field/fieldStatusPlacement';
@@ -175,127 +150,130 @@ const resultCountStyles = stylex.create({
175
150
  },
176
151
  });
177
152
 
178
- // The sheet presentation's trigger field: the same control box as the
179
- // tokenizer field, with display-only tokens and one button as its only tab
180
- // stop.
181
- const sheetTriggerSizeStyles = stylex.create({
182
- sm: {minHeight: sizeVars['--size-element-sm']},
183
- md: {minHeight: sizeVars['--size-element-md']},
184
- lg: {minHeight: sizeVars['--size-element-lg']},
185
- });
186
-
187
- const sheetTriggerStyles = stylex.create({
188
- wrapper: {
189
- position: 'relative',
190
- flexWrap: 'nowrap',
191
- gap: spacingVars['--spacing-1'],
192
- height: 'auto',
193
- cursor: {
194
- default: 'pointer',
195
- ':is([aria-disabled="true"])': 'default',
196
- },
197
- },
198
- wrapperWithTokens: {
199
- paddingBlock: `calc(${spacingVars['--spacing-1']} - 1px)`,
200
- paddingInline: `calc(${spacingVars['--spacing-1']} - 1px)`,
201
- },
202
- contentSection: {
203
- position: 'relative',
204
- minWidth: 0,
205
- flexGrow: 1,
206
- display: 'flex',
207
- alignItems: 'center',
208
- flexWrap: 'wrap',
209
- columnGap: spacingVars['--spacing-1'],
210
- rowGap: `calc(${spacingVars['--spacing-1']} - 1px)`,
211
- },
212
- startIconWithTokens: {
213
- marginInlineStart: `calc(${spacingVars['--spacing-2']} - ${spacingVars['--spacing-1']} + 1px)`,
214
- },
215
- // The only tab stop in the field. It stays visually borderless while the
216
- // wrapper delegates taps on tokens and empty space to it.
217
- trigger: {
218
- appearance: 'none',
219
- flex: '1 1 40px',
220
- minWidth: '40px',
221
- alignSelf: 'stretch',
222
- display: 'flex',
223
- alignItems: 'center',
224
- paddingBlock: 0,
225
- paddingInline: spacingVars['--spacing-1'],
226
- margin: 0,
227
- borderWidth: 0,
228
- borderStyle: 'none',
229
- backgroundColor: 'transparent',
230
- color: colorVars['--color-text-secondary'],
231
- fontFamily: typographyVars['--font-family-body'],
232
- fontSize: typeScaleVars['--text-body-size'],
233
- lineHeight: typeScaleVars['--text-body-leading'],
234
- textAlign: 'start',
235
- whiteSpace: 'nowrap',
236
- cursor: {
237
- default: 'pointer',
238
- ':is([aria-disabled="true"])': 'default',
239
- },
240
- outline: 'none',
241
- },
242
- // Once a token is present the empty trigger covers the token lane instead
243
- // of reserving 40px or wrapping onto an otherwise blank row.
244
- triggerWithTokens: {
245
- position: 'absolute',
246
- inset: 0,
247
- minWidth: 0,
248
- padding: 0,
249
- },
250
- endSection: {
251
- display: 'flex',
252
- alignItems: 'center',
253
- gap: spacingVars['--spacing-2'],
254
- flexShrink: 0,
255
- paddingInlineEnd: spacingVars['--spacing-1'],
256
- },
257
- });
153
+ function truncateString(value: string, limit: number): string {
154
+ // Same semantics as before — strings within limit + 3 pass through, longer
155
+ // ones cut to limit + '...' — but counted in characters, not code units.
156
+ return truncateCharacters(value, limit + 3, '...');
157
+ }
258
158
 
259
- // The sheet flow (three standalone sheets, the sheet editors) only
260
- // renders for the bottom-sheet presentation. Loading it on demand keeps it out
261
- // of pages whose PowerSearch stays a tokenizer field.
262
- // One import promise, shared by the preload and the lazy element: the sheet
263
- // presentation loads the flow's chunk as soon as it is active, so the first
264
- // press does not wait for the network, while nothing of the flow is rendered
265
- // until a press.
266
- const loadPowerSearchBottomSheet = async () =>
267
- import('./PowerSearchBottomSheet');
268
- const LazyPowerSearchBottomSheet = lazy(async () =>
269
- loadPowerSearchBottomSheet().then(module => ({
270
- default: module.PowerSearchBottomSheet,
271
- })),
272
- );
273
- // The value step reuses the popover presentation's editors, which this
274
- // module already loads; see PowerSearchSheetEditors.
275
- const SHEET_EDITORS: PowerSearchSheetEditors = {
276
- NestedEditor,
277
- PowerSearchValueEditor,
278
- buildRelativeDateOptions,
279
- };
159
+ function getEnumLabel(values: ReadonlyArray<EnumItem>, value: string): string {
160
+ return values.find(v => v.value === value)?.label ?? value;
161
+ }
280
162
 
281
163
  /**
282
- * Hosts the lazily loaded sheet flow. Renders nothing on the server or in the
283
- * render that hydrates server HTML: a lazy component that has not loaded yet
284
- * would leave server markup incomplete and abort hydration. The closed flow
285
- * has no visible markup, so it first renders right after hydration.
164
+ * The visible text of a filter token's value. The token renders it, and the
165
+ * token's remove button and editor dialog are named by the same text.
286
166
  */
287
- function PowerSearchSheetHost(
288
- props: Omit<PowerSearchBottomSheetProps, 'editors'>,
289
- ) {
290
- const isHydrating = useIsHydrating();
291
- if (isHydrating) {
292
- return null;
167
+ function tokenValueText(
168
+ operatorValue: OperatorValue,
169
+ filterValue: FilterValue,
170
+ maxLength: number,
171
+ locale: Locale,
172
+ ): string | null {
173
+ switch (filterValue.type) {
174
+ case 'empty':
175
+ return null;
176
+
177
+ case 'string':
178
+ return truncateString(filterValue.value, maxLength);
179
+
180
+ case 'integer':
181
+ case 'float':
182
+ return String(filterValue.value);
183
+
184
+ case 'enum':
185
+ if (operatorValue.type === 'enum') {
186
+ return truncateString(
187
+ getEnumLabel(operatorValue.values, filterValue.value),
188
+ maxLength,
189
+ );
190
+ }
191
+ return truncateString(filterValue.value, maxLength);
192
+
193
+ case 'string_list': {
194
+ const items = filterValue.value;
195
+ if (items.length === 0) {
196
+ return null;
197
+ }
198
+ if (items.length === 1) {
199
+ return truncateString(items[0], maxLength);
200
+ }
201
+ const totalLength = items.reduce((sum, s) => sum + s.length, 0);
202
+ if (totalLength > maxLength) {
203
+ return `${items.length} items`;
204
+ }
205
+ return items.join(', ');
206
+ }
207
+
208
+ case 'enum_list': {
209
+ const items = filterValue.value;
210
+ if (items.length === 0) {
211
+ return null;
212
+ }
213
+ if (operatorValue.type === 'enum_list') {
214
+ const labels = items.map(v => getEnumLabel(operatorValue.values, v));
215
+ if (labels.length === 1) {
216
+ return truncateString(labels[0], maxLength);
217
+ }
218
+ const totalLength = labels.reduce((sum, s) => sum + s.length, 0);
219
+ if (totalLength > maxLength) {
220
+ return `${labels.length} items`;
221
+ }
222
+ return labels.join(', ');
223
+ }
224
+ if (items.length === 1) {
225
+ return truncateString(items[0], maxLength);
226
+ }
227
+ return `${items.length} items`;
228
+ }
229
+
230
+ case 'entity_list': {
231
+ const entities = filterValue.value;
232
+ if (entities.length === 0) {
233
+ return null;
234
+ }
235
+ if (entities.length === 1) {
236
+ return truncateString(entities[0].label, maxLength);
237
+ }
238
+ const totalLength = entities.reduce((sum, e) => sum + e.label.length, 0);
239
+ if (totalLength > maxLength) {
240
+ return `${entities.length} items`;
241
+ }
242
+ return entities.map(e => e.label).join(', ');
243
+ }
244
+
245
+ case 'time':
246
+ return filterValue.value;
247
+
248
+ case 'date_absolute':
249
+ return truncateString(
250
+ formatDateAbsoluteCompact(filterValue.unixSeconds, locale),
251
+ maxLength,
252
+ );
253
+
254
+ case 'date_relative':
255
+ return truncateString(filterValue.value, maxLength);
256
+
257
+ case 'date_range':
258
+ return 'date range';
259
+
260
+ case 'custom':
261
+ if (operatorValue.type === 'custom') {
262
+ return truncateString(
263
+ operatorValue.getString(filterValue.value),
264
+ maxLength,
265
+ );
266
+ }
267
+ return filterValue.value;
268
+
269
+ case 'nested': {
270
+ const count = filterValue.value.length;
271
+ return count === 1 ? '1 filter' : `${count} filters`;
272
+ }
273
+
274
+ default:
275
+ return null;
293
276
  }
294
- return (
295
- <Suspense fallback={null}>
296
- <LazyPowerSearchBottomSheet {...props} editors={SHEET_EDITORS} />
297
- </Suspense>
298
- );
299
277
  }
300
278
 
301
279
  function PowerSearchTokenValue({text}: {text: string}) {
@@ -410,37 +388,6 @@ export interface PowerSearchProps extends Omit<
410
388
  * Keys are operator value types (e.g. 'string', 'enum', 'date_absolute').
411
389
  */
412
390
  components?: PowerSearchComponents;
413
-
414
- /**
415
- * Experimental options, outside the stable compatibility promise: anything
416
- * in this object may change or be removed in a patch release. Omit it for
417
- * the stable behavior.
418
- * @experimental
419
- */
420
- experimental?: {
421
- /**
422
- * How the field presents filter editing.
423
- * - `'popover'`: the tokenizer field with its typeahead and the editor
424
- * popover — the stable behavior.
425
- * - `'bottom-sheet'`: the whole field is one trigger showing display-only
426
- * filter tokens; it opens a stack of sheets. Filters lists the filters
427
- * as rows that edit or remove one, with Add filter, Done, and Clear
428
- * all, which asks first and keeps read-only filters. Add filter opens
429
- * above it with the fields (or the content search with inline
430
- * suggestions), and a value step opens above that, stages operator →
431
- * value, and confirms from its footer. Dismissing a step reveals the
432
- * one beneath. Suited to compact touch screens, where the editor
433
- * popover competes with the software keyboard.
434
- * - `'adaptive'`: the bottom sheet on compact coarse-pointer screens,
435
- * otherwise the popover — the same resolution Selector uses.
436
- *
437
- * In the sheet presentation `menuWidth` and `tokenOverflowBehavior` have
438
- * no surface to act on and warn in development.
439
- * @experimental
440
- * @default 'popover'
441
- */
442
- presentation?: 'popover' | 'bottom-sheet' | 'adaptive';
443
- };
444
391
  }
445
392
 
446
393
  // =============================================================================
@@ -577,7 +524,6 @@ export function PowerSearch({
577
524
  className,
578
525
  style,
579
526
  components: componentOverrides,
580
- experimental,
581
527
  }: PowerSearchProps) {
582
528
  const size = useSize(sizeProp, 'md');
583
529
  const config = useInternalConfig(configProp);
@@ -591,105 +537,6 @@ export function PowerSearch({
591
537
  popoverSaveButtonLabelFromProps ?? t('@astryx.powersearch.editor.apply');
592
538
  const tokenizerRef = useRef<TokenizerHandle>(null);
593
539
 
594
- // Experimental presentation. `adaptive`
595
- // resolves through the policy the selection controls share; an open flow
596
- // keeps its presentation until it closes, so a resolution change mid-task
597
- // cannot pull the surface out from under the person.
598
- const presentation = experimental?.presentation ?? 'popover';
599
- const resolvedPresentation = useAdaptivePresentation(presentation);
600
- const [isSheetOpen, setIsSheetOpen] = useState(false);
601
- // Latched for the flow's lifetime: the flow stays presented after it closes
602
- // until the sheets have run their exit and handed focus back to the trigger.
603
- // Without the latch, `adaptive` resolving to `popover` mid-session (a phone
604
- // turned to landscape) would unmount the sheets and the trigger they return
605
- // focus to in the same render, dropping focus to the page body.
606
- const [isSheetPresented, setIsSheetPresented] = useState(false);
607
- const isSheetPresentation =
608
- isSheetOpen || isSheetPresented || resolvedPresentation === 'bottom-sheet';
609
- const triggerRef = useRef<HTMLButtonElement>(null);
610
- // `hasAutoFocus` applies only to the control the field first mounts. A
611
- // presentation swap remounts the control (combobox input or trigger), and
612
- // React's `autoFocus` on the new one would pull focus from wherever on the
613
- // page it was; the roots' callback refs hand over the focus the field held
614
- // instead. A field that never swaps keeps `autoFocus` on every render, so a
615
- // Dialog that keeps it mounted still finds its `data-autofocus` on open.
616
- const [autoFocusState, setAutoFocusState] = useState({
617
- presentation: isSheetPresentation,
618
- isFirstControl: true,
619
- });
620
- if (autoFocusState.presentation !== isSheetPresentation) {
621
- setAutoFocusState({
622
- presentation: isSheetPresentation,
623
- isFirstControl: false,
624
- });
625
- }
626
- const autoFocusOnMount =
627
- hasAutoFocus &&
628
- autoFocusState.isFirstControl &&
629
- autoFocusState.presentation === isSheetPresentation;
630
- // A focus the swap moves continues the same visit to the field: the
631
- // control that held focus is gone (no blur fires for a removed element), so
632
- // the caller is told of neither a blur nor a new focus.
633
- const isSwapFocusRef = useRef(false);
634
- const focusAfterSwap = useCallback((control: {focus(): void} | null) => {
635
- if (control == null) {
636
- return;
637
- }
638
- isSwapFocusRef.current = true;
639
- try {
640
- control.focus();
641
- } finally {
642
- isSwapFocusRef.current = false;
643
- }
644
- }, []);
645
- const handleFieldFocus = useCallback(
646
- (event: React.FocusEvent) => {
647
- if (!isSwapFocusRef.current) {
648
- onFocus?.(event);
649
- }
650
- },
651
- [onFocus],
652
- );
653
- const handleFieldBlur = useCallback(
654
- (event: React.FocusEvent) => {
655
- onBlur?.(event);
656
- },
657
- [onBlur],
658
- );
659
- // While the flow is open, the sheet announces its own content from inside
660
- // its dialog; this component announces only with the flow closed, and
661
- // `useAnnounce` routes those messages by where the field is — into a Dialog
662
- // or modal Drawer that holds it, or the shared channel when no modal does.
663
- // A result count that changed during the flow's exit is spoken once the
664
- // flow has fully closed and focus is back on the trigger: the closing
665
- // dialog stays modal through its exit, so neither its region nor the
666
- // page-body channel is heard before.
667
- const afterCloseResultCountRef = useRef<string | null>(null);
668
- const triggerId = useId();
669
- const triggerLabelId = useId();
670
- const triggerContentId = useId();
671
- const statusMessageId = useId();
672
- // A field that becomes disabled cannot keep its flow open, and must not
673
- // reopen it when re-enabled. Adjusted in the
674
- // render that sees the change, not an effect, so no frame shows both.
675
- const [wasDisabled, setWasDisabled] = useState(isDisabled);
676
- if (isDisabled !== wasDisabled) {
677
- setWasDisabled(isDisabled);
678
- if (isDisabled && isSheetOpen) {
679
- setIsSheetOpen(false);
680
- }
681
- }
682
- useDevWarning(
683
- 'PowerSearch',
684
- '`menuWidth` has no surface in the bottom-sheet presentation: the sheet flow has no typeahead popover to size. Scope it to the popover presentation or leave it out.',
685
- isSheetPresentation && menuWidth != null,
686
- );
687
- useDevWarning(
688
- 'PowerSearch',
689
- '`tokenOverflowBehavior` has no surface in the bottom-sheet presentation: the trigger wraps its display-only tokens. Scope it to the popover presentation or leave it out.',
690
- isSheetPresentation && tokenOverflowBehavior != null,
691
- );
692
-
693
540
  const [popoverState, setPopoverStateRaw] = useState<PopoverState>({
694
541
  type: 'idle',
695
542
  });
@@ -705,7 +552,7 @@ export function PowerSearch({
705
552
  const operatorLabel = operator ? resolveOperatorLabel(operator, t) : '';
706
553
  const label = `${fieldLabel}: ${operatorLabel}`.trim();
707
554
  const value = operator
708
- ? formatTokenValueText(
555
+ ? tokenValueText(
709
556
  operator.value,
710
557
  filter.value,
711
558
  Math.max(
@@ -734,21 +581,9 @@ export function PowerSearch({
734
581
  componentOverrides?.[editedOperator.value.type]?.Token == null
735
582
  ? editedFilter
736
583
  : undefined;
737
- // Whether the editor popover is open, for the hand-off below, which runs
738
- // in the commit that swaps presentations.
739
- const isPopoverOpenRef = useRef(false);
740
- // When `adaptive` flips to the sheet while the editor popover is open (a
741
- // phone rotated), the editing ends with the popover: its state returns to
742
- // idle (through the raw setter, since the tokenizer input is gone), in the
743
- // render that sees the swap, so a later switch back does not reopen a stale
744
- // editor.
745
- if (isSheetPresentation && popoverState.type !== 'idle') {
746
- setPopoverStateRaw({type: 'idle'});
747
- }
748
584
 
749
585
  // Layer for positioning the edit popover anchored to the tokenizer
750
586
  const handleLayerHide = useCallback(() => {
751
- isPopoverOpenRef.current = false;
752
587
  setPopoverStateRaw({type: 'idle'});
753
588
  }, []);
754
589
 
@@ -769,54 +604,9 @@ export function PowerSearch({
769
604
  : undefined,
770
605
  });
771
606
 
772
- // A presentation swap removes the focused control, so the other
773
- // presentation's primary control (the combobox input or the trigger) takes
774
- // focus and focus never falls to the page body. Each presentation keys its
775
- // root, so a swap mounts a new root. The detaching root notes that it held
776
- // focus, which also covers an input focused by server-rendered `autofocus`
777
- // before hydration. The editor popover is portaled outside the root, so an
778
- // open editor counts as held focus. The attaching root gives focus to its
779
- // primary control when focus has fallen to the page body; the sheet root
780
- // also hides a popover layer left open.
781
- const isFocusPendingHandOffRef = useRef(false);
782
- const hidePopover = popover.hide;
783
- const handOffFocusRef = useCallback(
784
- (node: HTMLDivElement | null) => {
785
- if (node == null) {
786
- return;
787
- }
788
- const trigger = triggerRef.current;
789
- const isSheetRoot = trigger != null && node.contains(trigger);
790
- // The sheet presentation preloads the flow's chunk, so the first press
791
- // opens without waiting for the network; nothing renders.
792
- if (isSheetRoot) {
793
- void loadPowerSearchBottomSheet();
794
- }
795
- if (isSheetRoot && isPopoverOpenRef.current) {
796
- isPopoverOpenRef.current = false;
797
- hidePopover();
798
- }
799
- const takesFocus = isFocusPendingHandOffRef.current;
800
- isFocusPendingHandOffRef.current = false;
801
- if (takesFocus && isFocusDetached(node.ownerDocument)) {
802
- focusAfterSwap(isSheetRoot ? trigger : tokenizerRef.current);
803
- }
804
- return () => {
805
- if (
806
- node.contains(node.ownerDocument.activeElement) ||
807
- isPopoverOpenRef.current
808
- ) {
809
- isFocusPendingHandOffRef.current = true;
810
- }
811
- };
812
- },
813
- [focusAfterSwap, hidePopover],
814
- );
815
-
816
607
  // Wrapper that manages layer visibility and tokenizer focus alongside state
817
608
  const setPopoverState = useCallback(
818
609
  (state: PopoverState) => {
819
- isPopoverOpenRef.current = state.type !== 'idle';
820
610
  setPopoverStateRaw(state);
821
611
  if (state.type !== 'idle') {
822
612
  // Schedule after the current frame so React can render the popover
@@ -833,22 +623,13 @@ export function PowerSearch({
833
623
  [popover],
834
624
  );
835
625
 
836
- // Expose imperative handle; in the sheet presentation the trigger is the
837
- // field's focusable control.
626
+ // Expose imperative handle
838
627
  useImperativeHandle(handleRef, () => ({
839
628
  focusTypeahead() {
840
- if (isSheetPresentation) {
841
- triggerRef.current?.focus();
842
- } else {
843
- tokenizerRef.current?.focus();
844
- }
629
+ tokenizerRef.current?.focus();
845
630
  },
846
631
  blurTypeahead() {
847
- if (isSheetPresentation) {
848
- triggerRef.current?.blur();
849
- } else {
850
- tokenizerRef.current?.blur();
851
- }
632
+ tokenizerRef.current?.blur();
852
633
  },
853
634
  }));
854
635
 
@@ -1052,12 +833,10 @@ export function PowerSearch({
1052
833
  setPopoverState({type: 'idle'});
1053
834
  }, [setPopoverState]);
1054
835
 
1055
- // Token renderer for both presentations. Given `onRemove`, the token is
1056
- // the tokenizer field's: click edits, × removes. Without it (the sheet
1057
- // presentation's trigger) the token is display-only; the field as a whole
1058
- // opens the flow.
1059
- const renderFilterToken = useCallback(
1060
- (item: PowerSearchItem, onRemove: (() => void) | undefined) => {
836
+ // Custom token renderer
837
+ // Custom token renderer
838
+ const renderToken = useCallback(
839
+ (item: PowerSearchItem, onRemove: () => void) => {
1061
840
  const auxData = item.auxiliaryData;
1062
841
  const filterIndex = auxData?.filterIndex ?? -1;
1063
842
  const filter = filterIndex >= 0 ? filters[filterIndex] : undefined;
@@ -1067,8 +846,7 @@ export function PowerSearch({
1067
846
  ? config.getOperator(auxData.fieldKey, auxData.operatorKey)
1068
847
  : undefined;
1069
848
 
1070
- const canInteract =
1071
- onRemove != null && !isReadOnly && !isDisabled && !filter?.isReadOnly;
849
+ const canInteract = !isReadOnly && !isDisabled && !filter?.isReadOnly;
1072
850
  const handleClick = canInteract
1073
851
  ? () => handleTokenClick(filterIndex)
1074
852
  : undefined;
@@ -1181,12 +959,6 @@ export function PowerSearch({
1181
959
  ],
1182
960
  );
1183
961
 
1184
- const renderToken = useCallback(
1185
- (item: PowerSearchItem, onRemove: () => void) =>
1186
- renderFilterToken(item, onRemove),
1187
- [renderFilterToken],
1188
- );
1189
-
1190
962
  // Custom typeahead item renderer — adds field icon and description
1191
963
  const renderItem = useCallback(
1192
964
  (item: PowerSearchItem) => {
@@ -1304,18 +1076,13 @@ export function PowerSearch({
1304
1076
  return resultCount;
1305
1077
  }, [resultCount, t]);
1306
1078
 
1307
- // This Effect synchronizes a polite live region with the result count,
1308
- // mirroring the way Typeahead announces its dropdown result count: the count
1309
- // is otherwise only shown visually and stays silent to assistive tech. The
1310
- // region follows where the field is: the modal that holds the field (a
1311
- // Dialog, a modal Drawer), else the shared channel. While a sheet of the flow is open the sheet itself announces the
1312
- // count from inside its dialog, and this Effect stays quiet. Only a
1313
- // change after mount is spoken — the count present on mount is not
1314
- // announced unprompted — and the guard compares against the last announced
1315
- // text rather than a mounted flag, so StrictMode's effect replay announces
1316
- // nothing either. A change that lands while the flow is closing waits for
1317
- // the trigger focus, since the dialog is still modal through its exit. The
1318
- // announcement acquires no resource, so there is no cleanup.
1079
+ // Announce result-count changes to screen readers through a polite live
1080
+ // region, mirroring the way Typeahead announces its dropdown result count.
1081
+ // The count is otherwise only shown visually and stays silent to assistive
1082
+ // tech. Only a change after mount is spoken: the guard compares against the
1083
+ // last announced text rather than a mounted flag, so StrictMode's effect
1084
+ // replay (which resets a flag and would speak the count present on mount
1085
+ // unprompted) announces nothing until the count changes.
1319
1086
  const announce = useAnnounce();
1320
1087
  // PowerSearch words its Clear all as its record does; the field announces
1321
1088
  // these words instead of Tokenizer's (component:Tokenizer AR7).
@@ -1328,20 +1095,15 @@ export function PowerSearch({
1328
1095
  [hasReadOnlyFilter, t],
1329
1096
  );
1330
1097
  const announcedResultCountRef = useRef(resultCountText);
1331
- const isSheetExiting = isSheetPresented && !isSheetOpen;
1332
1098
  useEffect(() => {
1333
1099
  if (resultCountText === announcedResultCountRef.current) {
1334
1100
  return;
1335
1101
  }
1336
1102
  announcedResultCountRef.current = resultCountText;
1337
1103
  if (resultCountText != null) {
1338
- if (isSheetExiting) {
1339
- afterCloseResultCountRef.current = resultCountText;
1340
- } else if (!isSheetOpen) {
1341
- announce(resultCountText);
1342
- }
1104
+ announce(resultCountText);
1343
1105
  }
1344
- }, [resultCountText, announce, isSheetExiting, isSheetOpen]);
1106
+ }, [resultCountText, announce]);
1345
1107
 
1346
1108
  // Build combined endContent from resultCount + endContent props
1347
1109
  const combinedEndContent = useMemo((): React.ReactNode => {
@@ -1362,230 +1124,14 @@ export function PowerSearch({
1362
1124
  return resultCountNode || endContent || undefined;
1363
1125
  }, [resultCountText, endContent]);
1364
1126
 
1365
- // ------------------------------------------------------- sheet trigger --
1366
-
1367
- // Disabled-reason tooltip for the sheet trigger. Disabled controls swallow
1368
- // pointer events, so the listeners attach to the field's control box; the
1369
- // trigger stays perceivable through aria-disabled.
1370
- const showsDisabledMessage = isDisabled && !!disabledMessage;
1371
- const disabledMessageTooltip = useTooltip({
1372
- placement: 'above',
1373
- focusTrigger: 'always',
1374
- isEnabled: isSheetPresentation && showsDisabledMessage,
1375
- });
1376
- const rootRef = useMergedRefs(
1377
- ref,
1378
- popover.triggerRef as React.Ref<HTMLDivElement>,
1379
- handOffFocusRef,
1380
- );
1381
-
1382
- const openSheet = useCallback(() => {
1383
- if (isDisabled) {
1384
- return;
1385
- }
1386
- // The flow restores focus to the element that was active when it opened;
1387
- // make sure that is the trigger even for a tap on a token.
1388
- triggerRef.current?.focus({preventScroll: true});
1389
- setIsSheetPresented(true);
1390
- setIsSheetOpen(true);
1391
- }, [isDisabled]);
1392
-
1393
- // The field's end lane — the result count and `endContent` — sits outside
1394
- // the trigger button: a caller's action button there keeps its own tab stop
1395
- // and taps, and an interactive control must not nest in a component
1396
- // button, so a tap on it must not open the flow.
1397
- const endLaneRef = useRef<HTMLDivElement>(null);
1398
- const handleTriggerWrapperClick = useCallback(
1399
- (event: React.MouseEvent<HTMLDivElement>) => {
1400
- // Tokens are display-only; a tap on them or the empty space around them
1401
- // opens the flow. The trigger button handles its own click, and the end
1402
- // lane handles its own.
1403
- if (
1404
- event.target instanceof Node &&
1405
- (triggerRef.current?.contains(event.target) ||
1406
- endLaneRef.current?.contains(event.target))
1407
- ) {
1408
- return;
1409
- }
1410
- openSheet();
1411
- },
1412
- [openSheet],
1413
- );
1414
-
1415
- const handleTriggerFocusWithin = useCallback(
1416
- (event: React.FocusEvent<HTMLDivElement>) => {
1417
- // The flow focuses the trigger only after its exit completes; that focus
1418
- // is the signal that the retained sheets can leave. If the resolution
1419
- // moved to the popover meanwhile, the tokenizer input takes over the
1420
- // focus once it mounts (the hand-off above).
1421
- if (!isSheetOpen && isSheetPresented) {
1422
- setIsSheetPresented(false);
1423
- // The dialogs have closed: the page-body channel is exposed again.
1424
- const resultCount = afterCloseResultCountRef.current;
1425
- afterCloseResultCountRef.current = null;
1426
- if (resultCount != null) {
1427
- announce(resultCount);
1428
- }
1429
- }
1430
- if (
1431
- !(event.relatedTarget instanceof Node) ||
1432
- !event.currentTarget.contains(event.relatedTarget)
1433
- ) {
1434
- handleFieldFocus(event);
1435
- }
1436
- },
1437
- [announce, handleFieldFocus, isSheetOpen, isSheetPresented],
1438
- );
1439
- const handleTriggerBlurWithin = useCallback(
1440
- (event: React.FocusEvent<HTMLDivElement>) => {
1441
- if (
1442
- !(event.relatedTarget instanceof Node) ||
1443
- !event.currentTarget.contains(event.relatedTarget)
1444
- ) {
1445
- handleFieldBlur(event);
1446
- }
1447
- },
1448
- [handleFieldBlur],
1449
- );
1450
-
1451
- if (isSheetPresentation) {
1452
- const triggerDescribedBy =
1453
- [
1454
- status?.message ? statusMessageId : null,
1455
- showsDisabledMessage ? disabledMessageTooltip.describedBy : null,
1456
- ]
1457
- .filter(Boolean)
1458
- .join(' ') || undefined;
1459
-
1460
- return (
1461
- <div key="sheet" ref={rootRef} {...themeProps('power-search')}>
1462
- <Field
1463
- label={label}
1464
- isLabelHidden={isLabelHidden}
1465
- inputID={triggerId}
1466
- labelID={triggerLabelId}
1467
- isDisabled={isDisabled}
1468
- status={
1469
- status
1470
- ? {
1471
- type: status.type,
1472
- message: status.message,
1473
- messageID: status.message ? statusMessageId : undefined,
1474
- }
1475
- : undefined
1476
- }
1477
- statusVariant={statusVariant}
1478
- xstyle={xstyle}
1479
- className={className}
1480
- style={style}>
1481
- <div
1482
- ref={disabledMessageTooltip.ref}
1483
- onClick={handleTriggerWrapperClick}
1484
- onFocus={handleTriggerFocusWithin}
1485
- onBlur={handleTriggerBlurWithin}
1486
- aria-disabled={isDisabled ? true : undefined}
1487
- {...stylex.props(
1488
- inputWrapperStyles.base,
1489
- sheetTriggerStyles.wrapper,
1490
- filters.length > 0 && sheetTriggerStyles.wrapperWithTokens,
1491
- sheetTriggerSizeStyles[size],
1492
- isDisabled && inputWrapperStyles.disabled,
1493
- status && inputStatusBorderStyles[status.type],
1494
- status &&
1495
- !isDisabled &&
1496
- inputStatusHoverShadowStyles[status.type],
1497
- status && inputStatusFocusWithinStyles[status.type],
1498
- )}>
1499
- {/* The trigger's name is the label plus everything visible here:
1500
- the placeholder, or the tokens, so speech input can say what
1501
- it sees and assistive technology hears the selected filters
1502
- (WCAG 2.5.3). */}
1503
- <div
1504
- id={triggerContentId}
1505
- {...stylex.props(sheetTriggerStyles.contentSection)}>
1506
- {isRenderable(startIcon as React.ReactNode) && (
1507
- <span
1508
- {...stylex.props(
1509
- filters.length > 0 &&
1510
- sheetTriggerStyles.startIconWithTokens,
1511
- )}>
1512
- {renderIconSlot(startIcon, {size: 'sm', color: 'secondary'})}
1513
- </span>
1514
- )}
1515
- {tokenizerValue.map(item => (
1516
- <React.Fragment key={item.id}>
1517
- {renderFilterToken(item, undefined)}
1518
- </React.Fragment>
1519
- ))}
1520
- <button
1521
- type="button"
1522
- id={triggerId}
1523
- ref={triggerRef}
1524
- // The field's primary control: `data-testid` lands here, as it
1525
- // lands on the combobox input in the popover presentation.
1526
- data-testid={testId}
1527
- autoFocus={autoFocusOnMount}
1528
- onClick={openSheet}
1529
- aria-disabled={isDisabled ? true : undefined}
1530
- aria-haspopup="dialog"
1531
- aria-expanded={isSheetOpen}
1532
- aria-labelledby={`${triggerLabelId} ${triggerContentId}`}
1533
- aria-describedby={triggerDescribedBy}
1534
- {...stylex.props(
1535
- sheetTriggerStyles.trigger,
1536
- filters.length > 0 && sheetTriggerStyles.triggerWithTokens,
1537
- )}>
1538
- {filters.length === 0 ? placeholder : null}
1539
- </button>
1540
- </div>
1541
- {(endContent || isRenderable(resultCountText)) && (
1542
- <div
1543
- ref={endLaneRef}
1544
- {...stylex.props(sheetTriggerStyles.endSection)}>
1545
- {isRenderable(resultCountText) && (
1546
- <span {...stylex.props(resultCountStyles.text)}>
1547
- {resultCountText}
1548
- </span>
1549
- )}
1550
- {endContent}
1551
- </div>
1552
- )}
1553
- </div>
1554
- {showsDisabledMessage &&
1555
- disabledMessageTooltip.renderTooltip(disabledMessage)}
1556
- </Field>
1557
- {/* Mounted from the press until the flow's exit has completed and
1558
- focus is back on the trigger (`isSheetPresented`): at rest the
1559
- field renders its trigger alone — no closed dialogs, rows, search
1560
- field or editors in the DOM. */}
1561
- {(isSheetOpen || isSheetPresented) && (
1562
- <PowerSearchSheetHost
1563
- isOpen={isSheetOpen}
1564
- onOpenChange={setIsSheetOpen}
1565
- finalFocusRef={triggerRef}
1566
- resultCountText={resultCountText}
1567
- config={config}
1568
- configProp={configProp}
1569
- filters={filters}
1570
- onChange={onChange}
1571
- searchSource={searchSource}
1572
- placeholder={placeholder}
1573
- isReadOnly={isReadOnly}
1574
- isDisabled={isDisabled}
1575
- hasClear={hasClear}
1576
- maxOperatorMenuItems={maxOperatorMenuItems}
1577
- maxSearchResults={maxSearchResults}
1578
- saveButtonLabel={popoverSaveButtonLabelFromProps}
1579
- components={componentOverrides}
1580
- />
1581
- )}
1582
- </div>
1583
- );
1584
- }
1585
-
1586
1127
  return (
1587
1128
  <>
1588
- <div key="popover" ref={rootRef} {...themeProps('power-search')}>
1129
+ <div
1130
+ ref={useMergedRefs(
1131
+ ref,
1132
+ popover.triggerRef as React.Ref<HTMLDivElement>,
1133
+ )}
1134
+ {...themeProps('power-search')}>
1589
1135
  {/* The field's Clear all announces PowerSearch's own words
1590
1136
  (component:PowerSearch; component:Tokenizer AR7). */}
1591
1137
  <TokenizerClearAllMessageContext value={clearAllMessage}>
@@ -1602,7 +1148,7 @@ export function PowerSearch({
1602
1148
  maxMenuItems={MAX_BROWSE_MENU_ITEMS}
1603
1149
  menuWidth={menuWidth}
1604
1150
  placeholder={filters.length === 0 ? placeholder : ''}
1605
- hasAutoFocus={autoFocusOnMount}
1151
+ hasAutoFocus={hasAutoFocus}
1606
1152
  hasClear={hasClear && !isReadOnly}
1607
1153
  startIcon={startIcon}
1608
1154
  endContent={combinedEndContent}
@@ -1614,8 +1160,8 @@ export function PowerSearch({
1614
1160
  debounceMs={0}
1615
1161
  status={status}
1616
1162
  statusVariant={statusVariant}
1617
- onFocus={handleFieldFocus}
1618
- onBlur={handleFieldBlur}
1163
+ onFocus={onFocus}
1164
+ onBlur={onBlur}
1619
1165
  containerXstyle={xstyle}
1620
1166
  containerClassName={className}
1621
1167
  containerStyle={style}