@astryxdesign/core 0.7.7-canary.bf14580 → 0.7.7-canary.c19ca12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/AlertDialog/AlertDialog.d.ts +4 -1
- package/dist/AlertDialog/AlertDialog.d.ts.map +1 -1
- package/dist/AlertDialog/AlertDialog.js +36 -6
- package/dist/BottomSheet/BottomSheet.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheet.js +76 -6
- package/dist/BottomSheet/BottomSheetPanel.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheetPanel.js +23 -4
- package/dist/BottomSheet/bottomSheetStack.d.ts.map +1 -1
- package/dist/BottomSheet/bottomSheetStack.js +5 -4
- package/dist/BottomSheet/useSheetGestures.d.ts.map +1 -1
- package/dist/BottomSheet/useSheetGestures.js +3 -1
- package/dist/Button/Button.d.ts.map +1 -1
- package/dist/Button/Button.js +47 -3
- package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
- package/dist/CheckboxInput/CheckboxInput.js +13 -2
- package/dist/ComplexSelector/ComplexSelector.d.ts +4 -1
- package/dist/ComplexSelector/ComplexSelector.d.ts.map +1 -1
- package/dist/ComplexSelector/ComplexSelector.js +14 -4
- package/dist/ContextMenu/ContextMenu.js +2 -2
- package/dist/DateInput/MonthScroller.d.ts.map +1 -1
- package/dist/DateInput/MonthScroller.js +13 -27
- package/dist/DateInput/Wheel.d.ts.map +1 -1
- package/dist/DateInput/Wheel.js +22 -41
- package/dist/Dialog/Dialog.d.ts.map +1 -1
- package/dist/Dialog/Dialog.js +7 -31
- package/dist/Field/Field.js +2 -2
- package/dist/HoverCard/HoverCard.d.ts +4 -3
- package/dist/HoverCard/HoverCard.d.ts.map +1 -1
- package/dist/HoverCard/HoverCard.js +27 -2
- package/dist/HoverCard/useHoverCard.d.ts +5 -4
- package/dist/HoverCard/useHoverCard.d.ts.map +1 -1
- package/dist/HoverCard/useHoverCard.js +17 -31
- package/dist/Icon/Icon.d.ts +5 -1
- package/dist/Icon/Icon.d.ts.map +1 -1
- package/dist/Icon/Icon.js +5 -2
- package/dist/Layer/layerAnimations.stylex.d.ts +7 -8
- package/dist/Layer/layerAnimations.stylex.d.ts.map +1 -1
- package/dist/Layer/layerAnimations.stylex.js +25 -20
- package/dist/Layer/usePressPin.d.ts +17 -5
- package/dist/Layer/usePressPin.d.ts.map +1 -1
- package/dist/Layer/usePressPin.js +62 -12
- package/dist/Lightbox/Lightbox.d.ts.map +1 -1
- package/dist/Lightbox/Lightbox.js +18 -0
- package/dist/PowerSearch/PowerSearch.d.ts +36 -3
- package/dist/PowerSearch/PowerSearch.d.ts.map +1 -1
- package/dist/PowerSearch/PowerSearch.js +522 -269
- package/dist/PowerSearch/PowerSearchBottomSheet.d.ts +100 -0
- package/dist/PowerSearch/PowerSearchBottomSheet.d.ts.map +1 -0
- package/dist/PowerSearch/PowerSearchBottomSheet.js +1913 -0
- package/dist/PowerSearch/PowerSearchEditPopover.d.ts +32 -1
- package/dist/PowerSearch/PowerSearchEditPopover.d.ts.map +1 -1
- package/dist/PowerSearch/PowerSearchEditPopover.js +17 -6
- package/dist/PowerSearch/PowerSearchValueEditor.d.ts +38 -3
- package/dist/PowerSearch/PowerSearchValueEditor.d.ts.map +1 -1
- package/dist/PowerSearch/PowerSearchValueEditor.js +170 -61
- package/dist/PowerSearch/formatFilterValue.d.ts +7 -0
- package/dist/PowerSearch/formatFilterValue.d.ts.map +1 -1
- package/dist/PowerSearch/formatFilterValue.js +11 -1
- package/dist/PowerSearch/formatTokenValue.d.ts +28 -0
- package/dist/PowerSearch/formatTokenValue.d.ts.map +1 -0
- package/dist/PowerSearch/formatTokenValue.js +160 -0
- package/dist/PowerSearch/powerSearchSheetUtils.d.ts +65 -0
- package/dist/PowerSearch/powerSearchSheetUtils.d.ts.map +1 -0
- package/dist/PowerSearch/powerSearchSheetUtils.js +168 -0
- package/dist/RadioList/RadioList.d.ts.map +1 -1
- package/dist/RadioList/RadioList.js +3 -2
- package/dist/RadioList/RadioListItem.d.ts +1 -1
- package/dist/RadioList/RadioListItem.d.ts.map +1 -1
- package/dist/RadioList/RadioListItem.js +5 -3
- package/dist/SegmentedControl/useSegmentedSelectionMotion.d.ts.map +1 -1
- package/dist/SegmentedControl/useSegmentedSelectionMotion.js +3 -3
- package/dist/Slider/sliderTouchController.js +2 -2
- package/dist/Spinner/Spinner.d.ts +1 -1
- package/dist/Spinner/Spinner.d.ts.map +1 -1
- package/dist/Spinner/Spinner.js +60 -58
- package/dist/Switch/useSwitchTouch.js +4 -4
- package/dist/Table/BaseTable.d.ts.map +1 -1
- package/dist/Table/BaseTable.js +29 -8
- package/dist/TextInput/TextInput.d.ts +2 -2
- package/dist/TextInput/TextInput.d.ts.map +1 -1
- package/dist/TextInput/TextInput.js +10 -7
- package/dist/Toast/Toast.d.ts +11 -1
- package/dist/Toast/Toast.d.ts.map +1 -1
- package/dist/Toast/Toast.js +25 -15
- package/dist/Toast/ToastViewport.d.ts.map +1 -1
- package/dist/Toast/ToastViewport.js +94 -34
- package/dist/Toast/types.d.ts +9 -3
- package/dist/Toast/types.d.ts.map +1 -1
- package/dist/Token/Token.d.ts +2 -2
- package/dist/Token/Token.d.ts.map +1 -1
- package/dist/Token/Token.js +12 -4
- package/dist/Token/TokenNamingContext.d.ts +14 -0
- package/dist/Token/TokenNamingContext.d.ts.map +1 -0
- package/dist/Token/TokenNamingContext.js +23 -0
- package/dist/Tooltip/useTooltip.d.ts +19 -1
- package/dist/Tooltip/useTooltip.d.ts.map +1 -1
- package/dist/Tooltip/useTooltip.js +34 -53
- package/dist/Typeahead/TypeaheadItem.d.ts +2 -1
- package/dist/Typeahead/TypeaheadItem.d.ts.map +1 -1
- package/dist/animator/createAnimator.d.ts +98 -0
- package/dist/animator/createAnimator.d.ts.map +1 -0
- package/dist/animator/createAnimator.js +259 -0
- package/dist/astryx.css +22 -6
- package/dist/gestures/GestureHost.d.ts +27 -1
- package/dist/gestures/GestureHost.d.ts.map +1 -1
- package/dist/gestures/GestureHost.js +90 -8
- package/dist/gestures/PressTracker.d.ts.map +1 -1
- package/dist/gestures/PressTracker.js +4 -3
- package/dist/hooks/actionGroupTracker.d.ts +4 -1
- package/dist/hooks/actionGroupTracker.d.ts.map +1 -1
- package/dist/hooks/actionGroupTracker.js +13 -3
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +1 -0
- package/dist/hooks/pressGroupContext.d.ts +6 -0
- package/dist/hooks/pressGroupContext.d.ts.map +1 -0
- package/dist/hooks/pressGroupContext.js +20 -0
- package/dist/hooks/useActionGroupTracking.d.ts +53 -0
- package/dist/hooks/useActionGroupTracking.d.ts.map +1 -0
- package/dist/hooks/useActionGroupTracking.js +174 -0
- package/dist/hooks/useAnimator.d.ts +41 -0
- package/dist/hooks/useAnimator.d.ts.map +1 -0
- package/dist/hooks/useAnimator.js +70 -0
- package/dist/hooks/useAttachment.d.ts +1 -1
- package/dist/hooks/useAttachment.js +1 -1
- package/dist/hooks/useInputStatusIcon.d.ts +1 -1
- package/dist/hooks/useInputStatusIcon.d.ts.map +1 -1
- package/dist/hooks/useInputStatusIcon.js +2 -2
- package/dist/hooks/useLongPress.d.ts +6 -2
- package/dist/hooks/useLongPress.d.ts.map +1 -1
- package/dist/hooks/useLongPress.js +95 -15
- package/dist/hooks/useMenuPress.d.ts.map +1 -1
- package/dist/hooks/useMenuPress.js +4 -2
- package/dist/i18n/generated-english/PowerSearch/PowerSearchBottomSheet.generated.d.ts +8 -0
- package/dist/i18n/generated-english/PowerSearch/PowerSearchBottomSheet.generated.d.ts.map +1 -0
- package/dist/i18n/generated-english/PowerSearch/PowerSearchBottomSheet.generated.js +42 -0
- package/dist/i18n/generated-english/PowerSearch/PowerSearchValueEditor.generated.d.ts.map +1 -1
- package/dist/i18n/generated-english/PowerSearch/PowerSearchValueEditor.generated.js +6 -0
- package/dist/i18n/generated-english/PowerSearch/formatTokenValue.generated.d.ts +8 -0
- package/dist/i18n/generated-english/PowerSearch/formatTokenValue.generated.d.ts.map +1 -0
- package/dist/i18n/generated-english/PowerSearch/formatTokenValue.generated.js +14 -0
- package/dist/i18n/generated-locales/en.generated.d.ts.map +1 -1
- package/dist/i18n/generated-locales/en.generated.js +30 -0
- package/dist/i18n/generated-locales/pseudo.generated.d.ts.map +1 -1
- package/dist/i18n/generated-locales/pseudo.generated.js +30 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/internal/indicatorPress.stylex.d.ts +20 -0
- package/dist/internal/indicatorPress.stylex.d.ts.map +1 -0
- package/dist/internal/indicatorPress.stylex.js +26 -0
- package/dist/motion/animator.d.ts +14 -14
- package/dist/motion/animator.d.ts.map +1 -1
- package/dist/motion/animator.js +41 -18
- package/dist/motion/reducedMotion.d.ts +24 -0
- package/dist/motion/reducedMotion.d.ts.map +1 -1
- package/dist/motion/reducedMotion.js +78 -1
- package/dist/motion/uikit/alert.d.ts +41 -0
- package/dist/motion/uikit/alert.d.ts.map +1 -0
- package/dist/motion/uikit/alert.js +43 -0
- package/dist/privateAttributes/pressGroupItem.stylex.d.ts +15 -0
- package/dist/privateAttributes/pressGroupItem.stylex.d.ts.map +1 -0
- package/dist/privateAttributes/pressGroupItem.stylex.js +23 -0
- package/dist/utils/clickGuard.d.ts +8 -1
- package/dist/utils/clickGuard.d.ts.map +1 -1
- package/dist/utils/clickGuard.js +24 -1
- package/dist/utils/dateParser.d.ts.map +1 -1
- package/dist/utils/dateParser.js +12 -0
- package/dist/utils/focusReturn.d.ts +15 -0
- package/dist/utils/focusReturn.d.ts.map +1 -1
- package/dist/utils/focusReturn.js +28 -0
- package/dist/utils/interactionOverlay.stylex.d.ts +3 -2
- package/dist/utils/interactionOverlay.stylex.d.ts.map +1 -1
- package/dist/utils/interactionOverlay.stylex.js +3 -2
- package/dist/utils/pressFeedback.d.ts.map +1 -1
- package/dist/utils/pressFeedback.js +19 -6
- package/dist/utils/triggerFocus.d.ts +36 -0
- package/dist/utils/triggerFocus.d.ts.map +1 -0
- package/dist/utils/triggerFocus.js +65 -0
- package/locales/en.json +120 -0
- package/locales/pseudo.json +90 -0
- package/package.json +9 -4
- package/src/AlertDialog/AlertDialog.doc.mjs +2 -2
- package/src/AlertDialog/AlertDialog.tsx +37 -5
- package/src/BottomSheet/BottomSheet.tsx +85 -5
- package/src/BottomSheet/BottomSheetPanel.tsx +40 -5
- package/src/BottomSheet/bottomSheetStack.ts +8 -4
- package/src/BottomSheet/useSheetGestures.ts +3 -1
- package/src/Button/Button.tsx +63 -5
- package/src/CheckboxInput/CheckboxInput.doc.mjs +18 -0
- package/src/CheckboxInput/CheckboxInput.tsx +36 -9
- package/src/ComplexSelector/ComplexSelector.tsx +20 -5
- package/src/ContextMenu/ContextMenu.tsx +2 -2
- package/src/DateInput/MonthScroller.tsx +18 -22
- package/src/DateInput/Wheel.tsx +30 -32
- package/src/Dialog/Dialog.tsx +10 -31
- package/src/Field/Field.tsx +2 -2
- package/src/HoverCard/HoverCard.doc.mjs +4 -2
- package/src/HoverCard/HoverCard.tsx +35 -4
- package/src/HoverCard/useHoverCard.doc.mjs +178 -36
- package/src/HoverCard/useHoverCard.tsx +46 -52
- package/src/Icon/Icon.doc.mjs +49 -8
- package/src/Icon/Icon.tsx +15 -7
- package/src/Layer/layerAnimations.stylex.ts +24 -16
- package/src/Layer/usePressPin.ts +81 -15
- package/src/Lightbox/Lightbox.tsx +18 -0
- package/src/PowerSearch/PowerSearch.doc.mjs +54 -5
- package/src/PowerSearch/PowerSearch.tsx +734 -288
- package/src/PowerSearch/PowerSearchBottomSheet.tsx +2515 -0
- package/src/PowerSearch/PowerSearchEditPopover.tsx +28 -6
- package/src/PowerSearch/PowerSearchValueEditor.tsx +203 -38
- package/src/PowerSearch/formatFilterValue.ts +18 -1
- package/src/PowerSearch/formatTokenValue.ts +215 -0
- package/src/PowerSearch/powerSearchSheetUtils.ts +224 -0
- package/src/RadioList/RadioList.tsx +3 -2
- package/src/RadioList/RadioListItem.tsx +5 -3
- package/src/SegmentedControl/useSegmentedSelectionMotion.ts +6 -3
- package/src/Slider/sliderTouchController.ts +2 -2
- package/src/Spinner/Spinner.tsx +77 -63
- package/src/Switch/useSwitchTouch.ts +4 -4
- package/src/Table/BaseTable.tsx +46 -12
- package/src/TextInput/TextInput.tsx +11 -10
- package/src/Toast/Toast.doc.mjs +4 -3
- package/src/Toast/Toast.tsx +52 -32
- package/src/Toast/ToastViewport.tsx +121 -35
- package/src/Toast/types.ts +9 -3
- package/src/Token/Token.tsx +13 -4
- package/src/Token/TokenNamingContext.ts +28 -0
- package/src/Tooltip/useTooltip.doc.mjs +24 -0
- package/src/Tooltip/useTooltip.tsx +61 -56
- package/src/Typeahead/TypeaheadItem.doc.mjs +6 -3
- package/src/Typeahead/TypeaheadItem.tsx +2 -1
- package/src/animator/createAnimator.doc.mjs +153 -0
- package/src/animator/createAnimator.ts +324 -0
- package/src/gestures/GestureHost.ts +105 -10
- package/src/gestures/PressTracker.ts +4 -3
- package/src/gestures/README.md +7 -1
- package/src/hooks/actionGroupTracker.ts +20 -5
- package/src/hooks/index.ts +2 -0
- package/src/hooks/pressGroupContext.ts +21 -0
- package/src/hooks/useActionGroupTracking.ts +224 -0
- package/src/hooks/useAnimator.doc.mjs +109 -0
- package/src/hooks/useAnimator.ts +67 -0
- package/src/hooks/useAttachment.ts +1 -1
- package/src/hooks/useInputStatusIcon.tsx +3 -2
- package/src/hooks/useLongPress.doc.mjs +2 -2
- package/src/hooks/useLongPress.ts +136 -15
- package/src/hooks/useMenuPress.ts +8 -2
- package/src/i18n/generated-english/PowerSearch/PowerSearchBottomSheet.generated.ts +43 -0
- package/src/i18n/generated-english/PowerSearch/PowerSearchValueEditor.generated.ts +6 -0
- package/src/i18n/generated-english/PowerSearch/formatTokenValue.generated.ts +15 -0
- package/src/i18n/generated-locales/en.generated.ts +41 -0
- package/src/i18n/generated-locales/pseudo.generated.ts +42 -0
- package/src/index.ts +13 -0
- package/src/internal/indicatorPress.stylex.ts +24 -0
- package/src/motion/README.md +8 -5
- package/src/motion/animator.ts +61 -29
- package/src/motion/reducedMotion.ts +79 -1
- package/src/motion/uikit/alert.ts +43 -0
- package/src/privateAttributes/pressGroupItem.stylex.ts +23 -0
- package/src/utils/clickGuard.ts +37 -1
- package/src/utils/dateParser.ts +16 -0
- package/src/utils/focusReturn.ts +30 -0
- package/src/utils/interactionOverlay.stylex.ts +3 -2
- package/src/utils/pressFeedback.ts +19 -6
- package/src/utils/triggerFocus.ts +80 -0
|
@@ -4,19 +4,25 @@
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* @file PowerSearch.tsx
|
|
7
|
-
* @input PowerSearchConfig, filters, onChange
|
|
7
|
+
* @input PowerSearchConfig, filters, onChange; Tokenizer, Field parts, Token,
|
|
8
|
+
* useAdaptivePresentation, and PowerSearchBottomSheet (lazy)
|
|
8
9
|
* @output Structured filter bar with token-based filter management
|
|
9
10
|
* @position Main component; forwards DOM ref and exposes tokenizer control via
|
|
10
|
-
* handleRef
|
|
11
|
+
* handleRef. The experimental `presentation` option swaps the tokenizer
|
|
12
|
+
* field and editor popover for one trigger that opens PowerSearchBottomSheet.
|
|
11
13
|
*
|
|
12
14
|
* SYNC: When modified, update:
|
|
13
15
|
* - /packages/core/src/PowerSearch/index.ts
|
|
16
|
+
* - /packages/core/src/PowerSearch/PowerSearchBottomSheet.tsx (sheet presentation)
|
|
14
17
|
* - /packages/cli/assets/templates/blocks/components/PowerSearch/ (showcase blocks)
|
|
15
18
|
*/
|
|
16
19
|
|
|
17
20
|
import React, {
|
|
21
|
+
lazy,
|
|
22
|
+
Suspense,
|
|
18
23
|
useCallback,
|
|
19
24
|
useEffect,
|
|
25
|
+
useId,
|
|
20
26
|
useImperativeHandle,
|
|
21
27
|
useMemo,
|
|
22
28
|
useRef,
|
|
@@ -34,32 +40,48 @@ import {TokenizerClearAllMessageContext} from '../Tokenizer/TokenizerClearAllMes
|
|
|
34
40
|
import {TokenizerItemRemovableContext} from '../Tokenizer/TokenizerItemRemovableContext';
|
|
35
41
|
import {TypeaheadItem} from '../Typeahead/TypeaheadItem';
|
|
36
42
|
import {Token} from '../Token';
|
|
43
|
+
import {TokenNamingContext} from '../Token/TokenNamingContext';
|
|
37
44
|
import {Avatar} from '../Avatar';
|
|
38
45
|
import {layerAnimations} from '../Layer/layerAnimations.stylex';
|
|
39
|
-
import {Icon} from '../Icon';
|
|
46
|
+
import {Icon, renderIconSlot} from '../Icon';
|
|
40
47
|
import type {IconType} from '../Icon';
|
|
41
48
|
import type {IconName} from '../Icon/globalIconRegistry';
|
|
42
49
|
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';
|
|
43
57
|
import {usePopover} from '../Popover/usePopover';
|
|
58
|
+
import {useTooltip} from '../Tooltip/useTooltip';
|
|
44
59
|
import {layerViewportInset} from '../Layer/layerViewportInset.stylex';
|
|
45
60
|
import {useAnnounce} from '../hooks/useAnnounce';
|
|
61
|
+
import {useAdaptivePresentation} from '../hooks/useAdaptivePresentation';
|
|
62
|
+
import {useDevWarning} from '../hooks/useDevWarning';
|
|
63
|
+
import {useIsHydrating} from '../hooks/useIsHydrating';
|
|
46
64
|
import {
|
|
47
65
|
spacingVars,
|
|
48
66
|
colorVars,
|
|
67
|
+
sizeVars,
|
|
49
68
|
typeScaleVars,
|
|
69
|
+
typographyVars,
|
|
50
70
|
fontWeightVars,
|
|
51
71
|
} from '../theme/tokens.stylex';
|
|
52
72
|
import {useSize} from '../SizeContext/SizeContext';
|
|
73
|
+
import {isFocusDetached} from '../utils/focusReturn';
|
|
74
|
+
import {isRenderable} from '../utils/isRenderable';
|
|
53
75
|
import {useInternalConfig} from './useInternalConfig';
|
|
54
76
|
import {usePowerSearchSource} from './usePowerSearchSource';
|
|
77
|
+
import {formatFilterValue} from './formatFilterValue';
|
|
78
|
+
import {NestedEditor, PowerSearchEditPopover} from './PowerSearchEditPopover';
|
|
55
79
|
import {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
} from './
|
|
59
|
-
import {PowerSearchEditPopover} from './PowerSearchEditPopover';
|
|
80
|
+
buildRelativeDateOptions,
|
|
81
|
+
PowerSearchValueEditor,
|
|
82
|
+
} from './PowerSearchValueEditor';
|
|
60
83
|
import {resolveOperatorLabel} from './resolveOperatorLabel';
|
|
61
84
|
import {themeProps} from '../utils/themeProps';
|
|
62
|
-
import {truncateCharacters} from '../utils/characters';
|
|
63
85
|
import {useLocale} from '../i18n/useLocale';
|
|
64
86
|
import type {Locale} from '../i18n/types';
|
|
65
87
|
import type {SearchableItem} from '../Typeahead/types';
|
|
@@ -73,9 +95,14 @@ import type {
|
|
|
73
95
|
PowerSearchHandle,
|
|
74
96
|
FilterValue,
|
|
75
97
|
OperatorValue,
|
|
76
|
-
EnumItem,
|
|
77
98
|
PowerSearchComponents,
|
|
99
|
+
PowerSearchOperator,
|
|
78
100
|
} from './types';
|
|
101
|
+
import type {
|
|
102
|
+
PowerSearchBottomSheetProps,
|
|
103
|
+
PowerSearchSheetEditors,
|
|
104
|
+
} from './PowerSearchBottomSheet';
|
|
105
|
+
import {formatTokenValueText} from './formatTokenValue';
|
|
79
106
|
|
|
80
107
|
import {useMergedRefs} from '../hooks/useMergedRefs';
|
|
81
108
|
import type {FieldStatusPlacement} from '../Field/fieldStatusPlacement';
|
|
@@ -148,217 +175,131 @@ const resultCountStyles = stylex.create({
|
|
|
148
175
|
},
|
|
149
176
|
});
|
|
150
177
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
function PowerSearchTokenValue({
|
|
162
|
-
operatorValue,
|
|
163
|
-
filterValue,
|
|
164
|
-
maxLength,
|
|
165
|
-
locale,
|
|
166
|
-
}: {
|
|
167
|
-
operatorValue: OperatorValue;
|
|
168
|
-
filterValue: FilterValue;
|
|
169
|
-
maxLength: number;
|
|
170
|
-
locale: Locale;
|
|
171
|
-
}) {
|
|
172
|
-
switch (filterValue.type) {
|
|
173
|
-
case 'empty':
|
|
174
|
-
return null;
|
|
175
|
-
|
|
176
|
-
case 'string':
|
|
177
|
-
return (
|
|
178
|
-
<span {...stylex.props(tokenValueStyles.value)}>
|
|
179
|
-
{truncateString(filterValue.value, maxLength)}
|
|
180
|
-
</span>
|
|
181
|
-
);
|
|
182
|
-
|
|
183
|
-
case 'integer':
|
|
184
|
-
case 'float':
|
|
185
|
-
return (
|
|
186
|
-
<span {...stylex.props(tokenValueStyles.value)}>
|
|
187
|
-
{filterValue.value}
|
|
188
|
-
</span>
|
|
189
|
-
);
|
|
190
|
-
|
|
191
|
-
case 'enum':
|
|
192
|
-
if (operatorValue.type === 'enum') {
|
|
193
|
-
return (
|
|
194
|
-
<span {...stylex.props(tokenValueStyles.value)}>
|
|
195
|
-
{truncateString(
|
|
196
|
-
getEnumLabel(operatorValue.values, filterValue.value),
|
|
197
|
-
maxLength,
|
|
198
|
-
)}
|
|
199
|
-
</span>
|
|
200
|
-
);
|
|
201
|
-
}
|
|
202
|
-
return (
|
|
203
|
-
<span {...stylex.props(tokenValueStyles.value)}>
|
|
204
|
-
{truncateString(filterValue.value, maxLength)}
|
|
205
|
-
</span>
|
|
206
|
-
);
|
|
207
|
-
|
|
208
|
-
case 'string_list': {
|
|
209
|
-
const items = filterValue.value;
|
|
210
|
-
if (items.length === 0) {
|
|
211
|
-
return null;
|
|
212
|
-
}
|
|
213
|
-
if (items.length === 1) {
|
|
214
|
-
return (
|
|
215
|
-
<span {...stylex.props(tokenValueStyles.value)}>
|
|
216
|
-
{truncateString(items[0], maxLength)}
|
|
217
|
-
</span>
|
|
218
|
-
);
|
|
219
|
-
}
|
|
220
|
-
const totalLength = items.reduce((sum, s) => sum + s.length, 0);
|
|
221
|
-
if (totalLength > maxLength) {
|
|
222
|
-
return (
|
|
223
|
-
<span {...stylex.props(tokenValueStyles.value)}>
|
|
224
|
-
{items.length} items
|
|
225
|
-
</span>
|
|
226
|
-
);
|
|
227
|
-
}
|
|
228
|
-
return (
|
|
229
|
-
<span {...stylex.props(tokenValueStyles.value)}>
|
|
230
|
-
{items.join(', ')}
|
|
231
|
-
</span>
|
|
232
|
-
);
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
case 'enum_list': {
|
|
236
|
-
const items = filterValue.value;
|
|
237
|
-
if (items.length === 0) {
|
|
238
|
-
return null;
|
|
239
|
-
}
|
|
240
|
-
if (operatorValue.type === 'enum_list') {
|
|
241
|
-
const labels = items.map(v => getEnumLabel(operatorValue.values, v));
|
|
242
|
-
if (labels.length === 1) {
|
|
243
|
-
return (
|
|
244
|
-
<span {...stylex.props(tokenValueStyles.value)}>
|
|
245
|
-
{truncateString(labels[0], maxLength)}
|
|
246
|
-
</span>
|
|
247
|
-
);
|
|
248
|
-
}
|
|
249
|
-
const totalLength = labels.reduce((sum, s) => sum + s.length, 0);
|
|
250
|
-
if (totalLength > maxLength) {
|
|
251
|
-
return (
|
|
252
|
-
<span {...stylex.props(tokenValueStyles.value)}>
|
|
253
|
-
{labels.length} items
|
|
254
|
-
</span>
|
|
255
|
-
);
|
|
256
|
-
}
|
|
257
|
-
return (
|
|
258
|
-
<span {...stylex.props(tokenValueStyles.value)}>
|
|
259
|
-
{labels.join(', ')}
|
|
260
|
-
</span>
|
|
261
|
-
);
|
|
262
|
-
}
|
|
263
|
-
if (items.length === 1) {
|
|
264
|
-
return (
|
|
265
|
-
<span {...stylex.props(tokenValueStyles.value)}>
|
|
266
|
-
{truncateString(items[0], maxLength)}
|
|
267
|
-
</span>
|
|
268
|
-
);
|
|
269
|
-
}
|
|
270
|
-
return (
|
|
271
|
-
<span {...stylex.props(tokenValueStyles.value)}>
|
|
272
|
-
{items.length} items
|
|
273
|
-
</span>
|
|
274
|
-
);
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
case 'entity_list': {
|
|
278
|
-
const entities = filterValue.value;
|
|
279
|
-
if (entities.length === 0) {
|
|
280
|
-
return null;
|
|
281
|
-
}
|
|
282
|
-
if (entities.length === 1) {
|
|
283
|
-
return (
|
|
284
|
-
<span {...stylex.props(tokenValueStyles.value)}>
|
|
285
|
-
{truncateString(entities[0].label, maxLength)}
|
|
286
|
-
</span>
|
|
287
|
-
);
|
|
288
|
-
}
|
|
289
|
-
const totalLength = entities.reduce((sum, e) => sum + e.label.length, 0);
|
|
290
|
-
if (totalLength > maxLength) {
|
|
291
|
-
return (
|
|
292
|
-
<span {...stylex.props(tokenValueStyles.value)}>
|
|
293
|
-
{entities.length} items
|
|
294
|
-
</span>
|
|
295
|
-
);
|
|
296
|
-
}
|
|
297
|
-
return (
|
|
298
|
-
<span {...stylex.props(tokenValueStyles.value)}>
|
|
299
|
-
{entities.map(e => e.label).join(', ')}
|
|
300
|
-
</span>
|
|
301
|
-
);
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
case 'time':
|
|
305
|
-
return (
|
|
306
|
-
<span {...stylex.props(tokenValueStyles.value)}>
|
|
307
|
-
{filterValue.value}
|
|
308
|
-
</span>
|
|
309
|
-
);
|
|
310
|
-
|
|
311
|
-
case 'date_absolute': {
|
|
312
|
-
const formatted = formatDateAbsoluteCompact(
|
|
313
|
-
filterValue.unixSeconds,
|
|
314
|
-
locale,
|
|
315
|
-
);
|
|
316
|
-
return (
|
|
317
|
-
<span {...stylex.props(tokenValueStyles.value)}>
|
|
318
|
-
{truncateString(formatted, maxLength)}
|
|
319
|
-
</span>
|
|
320
|
-
);
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
case 'date_relative':
|
|
324
|
-
return (
|
|
325
|
-
<span {...stylex.props(tokenValueStyles.value)}>
|
|
326
|
-
{truncateString(filterValue.value, maxLength)}
|
|
327
|
-
</span>
|
|
328
|
-
);
|
|
329
|
-
|
|
330
|
-
case 'date_range':
|
|
331
|
-
return <span {...stylex.props(tokenValueStyles.value)}>date range</span>;
|
|
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
|
+
});
|
|
332
186
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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
|
+
});
|
|
349
258
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
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
|
+
};
|
|
358
280
|
|
|
359
|
-
|
|
360
|
-
|
|
281
|
+
/**
|
|
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.
|
|
286
|
+
*/
|
|
287
|
+
function PowerSearchSheetHost(
|
|
288
|
+
props: Omit<PowerSearchBottomSheetProps, 'editors'>,
|
|
289
|
+
) {
|
|
290
|
+
const isHydrating = useIsHydrating();
|
|
291
|
+
if (isHydrating) {
|
|
292
|
+
return null;
|
|
361
293
|
}
|
|
294
|
+
return (
|
|
295
|
+
<Suspense fallback={null}>
|
|
296
|
+
<LazyPowerSearchBottomSheet {...props} editors={SHEET_EDITORS} />
|
|
297
|
+
</Suspense>
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function PowerSearchTokenValue({text}: {text: string}) {
|
|
302
|
+
return <span {...stylex.props(tokenValueStyles.value)}>{text}</span>;
|
|
362
303
|
}
|
|
363
304
|
|
|
364
305
|
// =============================================================================
|
|
@@ -469,6 +410,37 @@ export interface PowerSearchProps extends Omit<
|
|
|
469
410
|
* Keys are operator value types (e.g. 'string', 'enum', 'date_absolute').
|
|
470
411
|
*/
|
|
471
412
|
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
|
+
};
|
|
472
444
|
}
|
|
473
445
|
|
|
474
446
|
// =============================================================================
|
|
@@ -491,6 +463,20 @@ type PopoverState =
|
|
|
491
463
|
// Component
|
|
492
464
|
// =============================================================================
|
|
493
465
|
|
|
466
|
+
/**
|
|
467
|
+
* Whether a press on a token with this operator commits the filter instead of
|
|
468
|
+
* opening its editor: an `empty` operator with no `empty` Editor override.
|
|
469
|
+
*/
|
|
470
|
+
function commitsWithoutEditor(
|
|
471
|
+
operator: PowerSearchOperator | undefined,
|
|
472
|
+
componentOverrides: PowerSearchComponents | undefined,
|
|
473
|
+
): boolean {
|
|
474
|
+
return (
|
|
475
|
+
operator?.value.type === 'empty' &&
|
|
476
|
+
componentOverrides?.empty?.Editor == null
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
|
|
494
480
|
/**
|
|
495
481
|
* Structured filter bar where each token represents a filter (field + operator + value).
|
|
496
482
|
*
|
|
@@ -591,6 +577,7 @@ export function PowerSearch({
|
|
|
591
577
|
className,
|
|
592
578
|
style,
|
|
593
579
|
components: componentOverrides,
|
|
580
|
+
experimental,
|
|
594
581
|
}: PowerSearchProps) {
|
|
595
582
|
const size = useSize(sizeProp, 'md');
|
|
596
583
|
const config = useInternalConfig(configProp);
|
|
@@ -604,12 +591,164 @@ export function PowerSearch({
|
|
|
604
591
|
popoverSaveButtonLabelFromProps ?? t('@astryx.powersearch.editor.apply');
|
|
605
592
|
const tokenizerRef = useRef<TokenizerHandle>(null);
|
|
606
593
|
|
|
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
|
+
|
|
607
693
|
const [popoverState, setPopoverStateRaw] = useState<PopoverState>({
|
|
608
694
|
type: 'idle',
|
|
609
695
|
});
|
|
610
696
|
|
|
697
|
+
// A default filter token's visible text: its label (field and operator)
|
|
698
|
+
// and its value. The token's remove button and editor dialog are named by
|
|
699
|
+
// the same text.
|
|
700
|
+
const tokenText = useCallback(
|
|
701
|
+
(filter: PowerSearchFilter) => {
|
|
702
|
+
const field = config.getField(filter.field);
|
|
703
|
+
const operator = config.getOperator(filter.field, filter.operator);
|
|
704
|
+
const fieldLabel = field?.label ?? '';
|
|
705
|
+
const operatorLabel = operator ? resolveOperatorLabel(operator, t) : '';
|
|
706
|
+
const label = `${fieldLabel}: ${operatorLabel}`.trim();
|
|
707
|
+
const value = operator
|
|
708
|
+
? formatTokenValueText(
|
|
709
|
+
operator.value,
|
|
710
|
+
filter.value,
|
|
711
|
+
Math.max(
|
|
712
|
+
maxTokenLength - fieldLabel.length - operatorLabel.length,
|
|
713
|
+
10,
|
|
714
|
+
),
|
|
715
|
+
locale,
|
|
716
|
+
)
|
|
717
|
+
: null;
|
|
718
|
+
return {label, value, full: value != null ? `${label} ${value}` : label};
|
|
719
|
+
},
|
|
720
|
+
[config, maxTokenLength, t, locale],
|
|
721
|
+
);
|
|
722
|
+
// The built-in token being edited. A token from a `components` override
|
|
723
|
+
// names itself, so its editor keeps the plain popover.
|
|
724
|
+
const editedFilter =
|
|
725
|
+
popoverState.type === 'editing'
|
|
726
|
+
? filters[popoverState.filterIndex]
|
|
727
|
+
: undefined;
|
|
728
|
+
const editedOperator =
|
|
729
|
+
editedFilter != null
|
|
730
|
+
? config.getOperator(editedFilter.field, editedFilter.operator)
|
|
731
|
+
: undefined;
|
|
732
|
+
const editedBuiltInFilter =
|
|
733
|
+
editedOperator == null ||
|
|
734
|
+
componentOverrides?.[editedOperator.value.type]?.Token == null
|
|
735
|
+
? editedFilter
|
|
736
|
+
: 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
|
+
|
|
611
749
|
// Layer for positioning the edit popover anchored to the tokenizer
|
|
612
750
|
const handleLayerHide = useCallback(() => {
|
|
751
|
+
isPopoverOpenRef.current = false;
|
|
613
752
|
setPopoverStateRaw({type: 'idle'});
|
|
614
753
|
}, []);
|
|
615
754
|
|
|
@@ -618,14 +757,66 @@ export function PowerSearch({
|
|
|
618
757
|
hasLightDismiss: true,
|
|
619
758
|
hasCloseButton: false,
|
|
620
759
|
hasAutoFocus: false,
|
|
621
|
-
//
|
|
622
|
-
// stays on the tokenizer input, so a
|
|
623
|
-
|
|
760
|
+
// Adding: the popup's own listbox/menu content is the exposed semantics,
|
|
761
|
+
// and focus stays on the tokenizer input, so a dialog wrapper is
|
|
762
|
+
// incorrect. Editing a token: focus moves into the editor's controls, so
|
|
763
|
+
// the editor is a non-modal dialog named by the token's visible text.
|
|
764
|
+
role: editedBuiltInFilter != null ? 'dialog' : 'none',
|
|
765
|
+
isModal: false,
|
|
766
|
+
dialogLabel:
|
|
767
|
+
editedBuiltInFilter != null
|
|
768
|
+
? tokenText(editedBuiltInFilter).full
|
|
769
|
+
: undefined,
|
|
624
770
|
});
|
|
625
771
|
|
|
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
|
+
|
|
626
816
|
// Wrapper that manages layer visibility and tokenizer focus alongside state
|
|
627
817
|
const setPopoverState = useCallback(
|
|
628
818
|
(state: PopoverState) => {
|
|
819
|
+
isPopoverOpenRef.current = state.type !== 'idle';
|
|
629
820
|
setPopoverStateRaw(state);
|
|
630
821
|
if (state.type !== 'idle') {
|
|
631
822
|
// Schedule after the current frame so React can render the popover
|
|
@@ -642,13 +833,22 @@ export function PowerSearch({
|
|
|
642
833
|
[popover],
|
|
643
834
|
);
|
|
644
835
|
|
|
645
|
-
// Expose imperative handle
|
|
836
|
+
// Expose imperative handle; in the sheet presentation the trigger is the
|
|
837
|
+
// field's focusable control.
|
|
646
838
|
useImperativeHandle(handleRef, () => ({
|
|
647
839
|
focusTypeahead() {
|
|
648
|
-
|
|
840
|
+
if (isSheetPresentation) {
|
|
841
|
+
triggerRef.current?.focus();
|
|
842
|
+
} else {
|
|
843
|
+
tokenizerRef.current?.focus();
|
|
844
|
+
}
|
|
649
845
|
},
|
|
650
846
|
blurTypeahead() {
|
|
651
|
-
|
|
847
|
+
if (isSheetPresentation) {
|
|
848
|
+
triggerRef.current?.blur();
|
|
849
|
+
} else {
|
|
850
|
+
tokenizerRef.current?.blur();
|
|
851
|
+
}
|
|
652
852
|
},
|
|
653
853
|
}));
|
|
654
854
|
|
|
@@ -789,10 +989,7 @@ export function PowerSearch({
|
|
|
789
989
|
// edit: commit it as that editor's save would, the way adding one does,
|
|
790
990
|
// instead of opening it. A consumer's `empty` Editor override still opens.
|
|
791
991
|
const operator = config.getOperator(filter.field, filter.operator);
|
|
792
|
-
if (
|
|
793
|
-
operator?.value.type === 'empty' &&
|
|
794
|
-
componentOverrides?.empty?.Editor == null
|
|
795
|
-
) {
|
|
992
|
+
if (commitsWithoutEditor(operator, componentOverrides)) {
|
|
796
993
|
const newFilters = [...filters];
|
|
797
994
|
newFilters[index] = {
|
|
798
995
|
field: filter.field,
|
|
@@ -855,10 +1052,12 @@ export function PowerSearch({
|
|
|
855
1052
|
setPopoverState({type: 'idle'});
|
|
856
1053
|
}, [setPopoverState]);
|
|
857
1054
|
|
|
858
|
-
//
|
|
859
|
-
//
|
|
860
|
-
|
|
861
|
-
|
|
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) => {
|
|
862
1061
|
const auxData = item.auxiliaryData;
|
|
863
1062
|
const filterIndex = auxData?.filterIndex ?? -1;
|
|
864
1063
|
const filter = filterIndex >= 0 ? filters[filterIndex] : undefined;
|
|
@@ -868,7 +1067,8 @@ export function PowerSearch({
|
|
|
868
1067
|
? config.getOperator(auxData.fieldKey, auxData.operatorKey)
|
|
869
1068
|
: undefined;
|
|
870
1069
|
|
|
871
|
-
const canInteract =
|
|
1070
|
+
const canInteract =
|
|
1071
|
+
onRemove != null && !isReadOnly && !isDisabled && !filter?.isReadOnly;
|
|
872
1072
|
const handleClick = canInteract
|
|
873
1073
|
? () => handleTokenClick(filterIndex)
|
|
874
1074
|
: undefined;
|
|
@@ -897,20 +1097,16 @@ export function PowerSearch({
|
|
|
897
1097
|
// Default token rendering
|
|
898
1098
|
const fieldLabel = field?.label ?? '';
|
|
899
1099
|
const operatorLabel = operator ? resolveOperatorLabel(operator, t) : '';
|
|
900
|
-
const
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
1100
|
+
const text =
|
|
1101
|
+
filter != null
|
|
1102
|
+
? tokenText(filter)
|
|
1103
|
+
: {label: `${fieldLabel}: ${operatorLabel}`.trim(), value: null};
|
|
1104
|
+
const tokenLabel = text.label;
|
|
1105
|
+
const fullText =
|
|
1106
|
+
text.value != null ? `${text.label} ${text.value}` : text.label;
|
|
906
1107
|
const valueContent =
|
|
907
|
-
|
|
908
|
-
<PowerSearchTokenValue
|
|
909
|
-
operatorValue={operator.value}
|
|
910
|
-
filterValue={filter.value}
|
|
911
|
-
maxLength={adjustedMaxLength}
|
|
912
|
-
locale={locale}
|
|
913
|
-
/>
|
|
1108
|
+
text.value != null ? (
|
|
1109
|
+
<PowerSearchTokenValue text={text.value} />
|
|
914
1110
|
) : undefined;
|
|
915
1111
|
|
|
916
1112
|
// Show entity photo as token icon for single-entity filters
|
|
@@ -924,32 +1120,49 @@ export function PowerSearch({
|
|
|
924
1120
|
tokenIcon = <Avatar src={entity.photo} name={entity.label} size={16} />;
|
|
925
1121
|
}
|
|
926
1122
|
|
|
1123
|
+
// A token that opens its editor says that it opens a dialog, and the
|
|
1124
|
+
// naming context names it by all its visible text.
|
|
1125
|
+
const editorProps =
|
|
1126
|
+
handleClick != null &&
|
|
1127
|
+
!commitsWithoutEditor(operator, componentOverrides)
|
|
1128
|
+
? {
|
|
1129
|
+
'aria-haspopup': 'dialog' as const,
|
|
1130
|
+
'aria-expanded':
|
|
1131
|
+
popoverState.type === 'editing' &&
|
|
1132
|
+
popoverState.filterIndex === filterIndex,
|
|
1133
|
+
}
|
|
1134
|
+
: null;
|
|
1135
|
+
|
|
927
1136
|
return (
|
|
928
|
-
<
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
1137
|
+
<TokenNamingContext
|
|
1138
|
+
value={{actionName: fullText, removeName: fullText}}>
|
|
1139
|
+
<Token
|
|
1140
|
+
label={tokenLabel}
|
|
1141
|
+
size={size}
|
|
1142
|
+
icon={tokenIcon}
|
|
1143
|
+
{...editorProps}
|
|
1144
|
+
endContent={valueContent}
|
|
1145
|
+
onMouseDown={
|
|
1146
|
+
handleClick
|
|
1147
|
+
? e => {
|
|
1148
|
+
// Keep a token press from entering Tokenizer's input-focus
|
|
1149
|
+
// redirect. Click still edits/removes; Tab entry is unchanged.
|
|
1150
|
+
e.preventDefault();
|
|
1151
|
+
}
|
|
1152
|
+
: undefined
|
|
1153
|
+
}
|
|
1154
|
+
onClick={
|
|
1155
|
+
handleClick
|
|
1156
|
+
? (e: React.MouseEvent) => {
|
|
1157
|
+
e.stopPropagation();
|
|
1158
|
+
handleClick();
|
|
1159
|
+
}
|
|
1160
|
+
: undefined
|
|
1161
|
+
}
|
|
1162
|
+
onRemove={handleRemove}
|
|
1163
|
+
isDisabled={isDisabled}
|
|
1164
|
+
/>
|
|
1165
|
+
</TokenNamingContext>
|
|
953
1166
|
);
|
|
954
1167
|
},
|
|
955
1168
|
[
|
|
@@ -957,16 +1170,23 @@ export function PowerSearch({
|
|
|
957
1170
|
config,
|
|
958
1171
|
configProp,
|
|
959
1172
|
maxTokenLength,
|
|
960
|
-
locale,
|
|
961
1173
|
size,
|
|
962
1174
|
isReadOnly,
|
|
963
1175
|
isDisabled,
|
|
964
1176
|
handleTokenClick,
|
|
965
1177
|
componentOverrides,
|
|
966
1178
|
t,
|
|
1179
|
+
popoverState,
|
|
1180
|
+
tokenText,
|
|
967
1181
|
],
|
|
968
1182
|
);
|
|
969
1183
|
|
|
1184
|
+
const renderToken = useCallback(
|
|
1185
|
+
(item: PowerSearchItem, onRemove: () => void) =>
|
|
1186
|
+
renderFilterToken(item, onRemove),
|
|
1187
|
+
[renderFilterToken],
|
|
1188
|
+
);
|
|
1189
|
+
|
|
970
1190
|
// Custom typeahead item renderer — adds field icon and description
|
|
971
1191
|
const renderItem = useCallback(
|
|
972
1192
|
(item: PowerSearchItem) => {
|
|
@@ -1084,13 +1304,18 @@ export function PowerSearch({
|
|
|
1084
1304
|
return resultCount;
|
|
1085
1305
|
}, [resultCount, t]);
|
|
1086
1306
|
|
|
1087
|
-
//
|
|
1088
|
-
//
|
|
1089
|
-
//
|
|
1090
|
-
//
|
|
1091
|
-
//
|
|
1092
|
-
//
|
|
1093
|
-
//
|
|
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.
|
|
1094
1319
|
const announce = useAnnounce();
|
|
1095
1320
|
// PowerSearch words its Clear all as its record does; the field announces
|
|
1096
1321
|
// these words instead of Tokenizer's (component:Tokenizer AR7).
|
|
@@ -1103,15 +1328,20 @@ export function PowerSearch({
|
|
|
1103
1328
|
[hasReadOnlyFilter, t],
|
|
1104
1329
|
);
|
|
1105
1330
|
const announcedResultCountRef = useRef(resultCountText);
|
|
1331
|
+
const isSheetExiting = isSheetPresented && !isSheetOpen;
|
|
1106
1332
|
useEffect(() => {
|
|
1107
1333
|
if (resultCountText === announcedResultCountRef.current) {
|
|
1108
1334
|
return;
|
|
1109
1335
|
}
|
|
1110
1336
|
announcedResultCountRef.current = resultCountText;
|
|
1111
1337
|
if (resultCountText != null) {
|
|
1112
|
-
|
|
1338
|
+
if (isSheetExiting) {
|
|
1339
|
+
afterCloseResultCountRef.current = resultCountText;
|
|
1340
|
+
} else if (!isSheetOpen) {
|
|
1341
|
+
announce(resultCountText);
|
|
1342
|
+
}
|
|
1113
1343
|
}
|
|
1114
|
-
}, [resultCountText, announce]);
|
|
1344
|
+
}, [resultCountText, announce, isSheetExiting, isSheetOpen]);
|
|
1115
1345
|
|
|
1116
1346
|
// Build combined endContent from resultCount + endContent props
|
|
1117
1347
|
const combinedEndContent = useMemo((): React.ReactNode => {
|
|
@@ -1132,14 +1362,230 @@ export function PowerSearch({
|
|
|
1132
1362
|
return resultCountNode || endContent || undefined;
|
|
1133
1363
|
}, [resultCountText, endContent]);
|
|
1134
1364
|
|
|
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
|
+
|
|
1135
1586
|
return (
|
|
1136
1587
|
<>
|
|
1137
|
-
<div
|
|
1138
|
-
ref={useMergedRefs(
|
|
1139
|
-
ref,
|
|
1140
|
-
popover.triggerRef as React.Ref<HTMLDivElement>,
|
|
1141
|
-
)}
|
|
1142
|
-
{...themeProps('power-search')}>
|
|
1588
|
+
<div key="popover" ref={rootRef} {...themeProps('power-search')}>
|
|
1143
1589
|
{/* The field's Clear all announces PowerSearch's own words
|
|
1144
1590
|
(component:PowerSearch; component:Tokenizer AR7). */}
|
|
1145
1591
|
<TokenizerClearAllMessageContext value={clearAllMessage}>
|
|
@@ -1156,7 +1602,7 @@ export function PowerSearch({
|
|
|
1156
1602
|
maxMenuItems={MAX_BROWSE_MENU_ITEMS}
|
|
1157
1603
|
menuWidth={menuWidth}
|
|
1158
1604
|
placeholder={filters.length === 0 ? placeholder : ''}
|
|
1159
|
-
hasAutoFocus={
|
|
1605
|
+
hasAutoFocus={autoFocusOnMount}
|
|
1160
1606
|
hasClear={hasClear && !isReadOnly}
|
|
1161
1607
|
startIcon={startIcon}
|
|
1162
1608
|
endContent={combinedEndContent}
|
|
@@ -1168,8 +1614,8 @@ export function PowerSearch({
|
|
|
1168
1614
|
debounceMs={0}
|
|
1169
1615
|
status={status}
|
|
1170
1616
|
statusVariant={statusVariant}
|
|
1171
|
-
onFocus={
|
|
1172
|
-
onBlur={
|
|
1617
|
+
onFocus={handleFieldFocus}
|
|
1618
|
+
onBlur={handleFieldBlur}
|
|
1173
1619
|
containerXstyle={xstyle}
|
|
1174
1620
|
containerClassName={className}
|
|
1175
1621
|
containerStyle={style}
|