@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.
- package/CHANGELOG.md +104 -0
- package/dist/AlertDialog/AlertDialog.d.ts +1 -4
- package/dist/AlertDialog/AlertDialog.d.ts.map +1 -1
- package/dist/AlertDialog/AlertDialog.js +6 -36
- package/dist/BottomSheet/BottomSheetPanel.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheetPanel.js +4 -23
- package/dist/BottomSheet/useSheetGestures.d.ts.map +1 -1
- package/dist/BottomSheet/useSheetGestures.js +1 -3
- package/dist/Button/Button.d.ts.map +1 -1
- package/dist/Button/Button.js +1 -17
- package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
- package/dist/CheckboxInput/CheckboxInput.js +2 -13
- package/dist/ComplexSelector/ComplexSelector.d.ts +1 -4
- package/dist/ComplexSelector/ComplexSelector.d.ts.map +1 -1
- package/dist/ComplexSelector/ComplexSelector.js +4 -14
- package/dist/ContextMenu/ContextMenu.js +2 -2
- package/dist/Dialog/Dialog.d.ts.map +1 -1
- package/dist/Dialog/Dialog.js +31 -7
- package/dist/Field/Field.js +2 -2
- package/dist/HoverCard/HoverCard.d.ts +3 -4
- package/dist/HoverCard/HoverCard.d.ts.map +1 -1
- package/dist/HoverCard/HoverCard.js +2 -27
- package/dist/HoverCard/useHoverCard.d.ts +4 -5
- package/dist/HoverCard/useHoverCard.d.ts.map +1 -1
- package/dist/HoverCard/useHoverCard.js +31 -17
- package/dist/Icon/Icon.d.ts +1 -5
- package/dist/Icon/Icon.d.ts.map +1 -1
- package/dist/Icon/Icon.js +2 -5
- package/dist/Layer/usePressPin.d.ts +5 -17
- package/dist/Layer/usePressPin.d.ts.map +1 -1
- package/dist/Layer/usePressPin.js +12 -62
- package/dist/Lightbox/Lightbox.d.ts.map +1 -1
- package/dist/Lightbox/Lightbox.js +0 -18
- package/dist/PowerSearch/PowerSearch.d.ts +3 -36
- package/dist/PowerSearch/PowerSearch.d.ts.map +1 -1
- package/dist/PowerSearch/PowerSearch.js +127 -433
- package/dist/PowerSearch/PowerSearchEditPopover.d.ts +1 -32
- package/dist/PowerSearch/PowerSearchEditPopover.d.ts.map +1 -1
- package/dist/PowerSearch/PowerSearchEditPopover.js +6 -17
- package/dist/PowerSearch/PowerSearchValueEditor.d.ts +3 -38
- package/dist/PowerSearch/PowerSearchValueEditor.d.ts.map +1 -1
- package/dist/PowerSearch/PowerSearchValueEditor.js +61 -170
- package/dist/PowerSearch/formatFilterValue.d.ts +0 -7
- package/dist/PowerSearch/formatFilterValue.d.ts.map +1 -1
- package/dist/PowerSearch/formatFilterValue.js +1 -11
- package/dist/RadioList/RadioList.d.ts.map +1 -1
- package/dist/RadioList/RadioList.js +2 -3
- package/dist/RadioList/RadioListItem.d.ts +1 -1
- package/dist/RadioList/RadioListItem.d.ts.map +1 -1
- package/dist/RadioList/RadioListItem.js +3 -5
- package/dist/Resizable/ResizeHandle.d.ts +1 -2
- package/dist/Resizable/ResizeHandle.d.ts.map +1 -1
- package/dist/Resizable/ResizeHandle.js +77 -99
- 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 +58 -60
- package/dist/Switch/useSwitchTouch.js +4 -4
- package/dist/Table/BaseTable.d.ts.map +1 -1
- package/dist/Table/BaseTable.js +8 -29
- package/dist/TextInput/TextInput.d.ts +2 -2
- package/dist/TextInput/TextInput.d.ts.map +1 -1
- package/dist/TextInput/TextInput.js +7 -10
- package/dist/Toast/Toast.d.ts +1 -11
- package/dist/Toast/Toast.d.ts.map +1 -1
- package/dist/Toast/Toast.js +15 -25
- package/dist/Toast/ToastViewport.d.ts.map +1 -1
- package/dist/Toast/ToastViewport.js +34 -94
- package/dist/Toast/types.d.ts +3 -9
- package/dist/Toast/types.d.ts.map +1 -1
- package/dist/Tooltip/useTooltip.d.ts +1 -19
- package/dist/Tooltip/useTooltip.d.ts.map +1 -1
- package/dist/Tooltip/useTooltip.js +53 -34
- package/dist/Typeahead/TypeaheadItem.d.ts +1 -2
- package/dist/Typeahead/TypeaheadItem.d.ts.map +1 -1
- package/dist/astryx.css +6 -35
- package/dist/gestures/PressTracker.d.ts.map +1 -1
- package/dist/gestures/PressTracker.js +3 -4
- package/dist/hooks/actionGroupTracker.d.ts +1 -4
- package/dist/hooks/actionGroupTracker.d.ts.map +1 -1
- package/dist/hooks/actionGroupTracker.js +3 -13
- package/dist/hooks/index.d.ts +0 -1
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +0 -1
- 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 +2 -6
- package/dist/hooks/useLongPress.d.ts.map +1 -1
- package/dist/hooks/useLongPress.js +15 -95
- package/dist/i18n/generated-english/PowerSearch/PowerSearchValueEditor.generated.d.ts.map +1 -1
- package/dist/i18n/generated-english/PowerSearch/PowerSearchValueEditor.generated.js +0 -6
- package/dist/i18n/generated-locales/en.generated.d.ts.map +1 -1
- package/dist/i18n/generated-locales/en.generated.js +0 -30
- package/dist/i18n/generated-locales/pseudo.generated.d.ts.map +1 -1
- package/dist/i18n/generated-locales/pseudo.generated.js +0 -30
- package/dist/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -4
- package/dist/motion/animatedLayout.d.ts.map +1 -1
- package/dist/motion/animatedLayout.js +6 -5
- package/dist/motion/animator.d.ts +14 -14
- package/dist/motion/animator.d.ts.map +1 -1
- package/dist/motion/animator.js +18 -41
- package/dist/utils/dateParser.d.ts.map +1 -1
- package/dist/utils/dateParser.js +0 -12
- package/dist/utils/focusReturn.d.ts +0 -15
- package/dist/utils/focusReturn.d.ts.map +1 -1
- package/dist/utils/focusReturn.js +0 -28
- package/dist/utils/interactionOverlay.stylex.d.ts +2 -3
- package/dist/utils/interactionOverlay.stylex.d.ts.map +1 -1
- package/dist/utils/interactionOverlay.stylex.js +2 -3
- package/dist/utils/pressFeedback.d.ts.map +1 -1
- package/dist/utils/pressFeedback.js +6 -19
- package/locales/en.json +0 -120
- package/locales/pseudo.json +0 -90
- package/package.json +5 -10
- package/src/AlertDialog/AlertDialog.doc.mjs +2 -2
- package/src/AlertDialog/AlertDialog.tsx +5 -37
- package/src/BottomSheet/BottomSheetPanel.tsx +3 -37
- package/src/BottomSheet/useSheetGestures.ts +1 -3
- package/src/Button/Button.tsx +4 -26
- package/src/CheckboxInput/CheckboxInput.doc.mjs +0 -18
- package/src/CheckboxInput/CheckboxInput.tsx +9 -36
- package/src/ComplexSelector/ComplexSelector.tsx +5 -20
- package/src/ContextMenu/ContextMenu.tsx +2 -2
- package/src/Dialog/Dialog.tsx +31 -10
- package/src/Field/Field.tsx +2 -2
- package/src/HoverCard/HoverCard.doc.mjs +2 -4
- package/src/HoverCard/HoverCard.tsx +4 -35
- package/src/HoverCard/useHoverCard.doc.mjs +36 -178
- package/src/HoverCard/useHoverCard.tsx +52 -46
- package/src/Icon/Icon.doc.mjs +8 -49
- package/src/Icon/Icon.tsx +7 -15
- package/src/Layer/usePressPin.ts +15 -81
- package/src/Lightbox/Lightbox.tsx +0 -18
- package/src/PowerSearch/PowerSearch.doc.mjs +5 -54
- package/src/PowerSearch/PowerSearch.tsx +155 -609
- package/src/PowerSearch/PowerSearchEditPopover.tsx +6 -28
- package/src/PowerSearch/PowerSearchValueEditor.tsx +38 -203
- package/src/PowerSearch/formatFilterValue.ts +1 -18
- package/src/RadioList/RadioList.tsx +2 -3
- package/src/RadioList/RadioListItem.tsx +3 -5
- package/src/Resizable/Resizable.doc.mjs +4 -5
- package/src/Resizable/ResizeHandle.tsx +109 -167
- package/src/SegmentedControl/useSegmentedSelectionMotion.ts +3 -6
- package/src/Slider/sliderTouchController.ts +2 -2
- package/src/Spinner/Spinner.tsx +63 -77
- package/src/Switch/useSwitchTouch.ts +4 -4
- package/src/Table/BaseTable.tsx +12 -46
- package/src/TextInput/TextInput.tsx +10 -11
- package/src/Toast/Toast.doc.mjs +3 -4
- package/src/Toast/Toast.tsx +32 -52
- package/src/Toast/ToastViewport.tsx +35 -121
- package/src/Toast/types.ts +3 -9
- package/src/Tooltip/useTooltip.doc.mjs +0 -24
- package/src/Tooltip/useTooltip.tsx +56 -61
- package/src/Typeahead/TypeaheadItem.doc.mjs +3 -6
- package/src/Typeahead/TypeaheadItem.tsx +1 -2
- package/src/gestures/PressTracker.ts +3 -4
- package/src/gestures/README.md +1 -7
- package/src/hooks/actionGroupTracker.ts +5 -20
- package/src/hooks/index.ts +0 -2
- package/src/hooks/useAttachment.ts +1 -1
- package/src/hooks/useInputStatusIcon.tsx +2 -3
- package/src/hooks/useLongPress.doc.mjs +2 -2
- package/src/hooks/useLongPress.ts +15 -136
- package/src/i18n/generated-english/PowerSearch/PowerSearchValueEditor.generated.ts +0 -6
- package/src/i18n/generated-locales/en.generated.ts +0 -41
- package/src/i18n/generated-locales/pseudo.generated.ts +0 -42
- package/src/index.ts +0 -13
- package/src/motion/README.md +5 -9
- package/src/motion/animatedLayout.ts +4 -5
- package/src/motion/animator.ts +29 -61
- package/src/utils/dateParser.ts +0 -16
- package/src/utils/focusReturn.ts +0 -30
- package/src/utils/interactionOverlay.stylex.ts +2 -3
- package/src/utils/pressFeedback.ts +6 -19
- package/dist/PowerSearch/PowerSearchBottomSheet.d.ts +0 -100
- package/dist/PowerSearch/PowerSearchBottomSheet.d.ts.map +0 -1
- package/dist/PowerSearch/PowerSearchBottomSheet.js +0 -1913
- package/dist/PowerSearch/formatTokenValue.d.ts +0 -28
- package/dist/PowerSearch/formatTokenValue.d.ts.map +0 -1
- package/dist/PowerSearch/formatTokenValue.js +0 -160
- package/dist/PowerSearch/powerSearchSheetUtils.d.ts +0 -65
- package/dist/PowerSearch/powerSearchSheetUtils.d.ts.map +0 -1
- package/dist/PowerSearch/powerSearchSheetUtils.js +0 -168
- package/dist/animator/createAnimator.d.ts +0 -98
- package/dist/animator/createAnimator.d.ts.map +0 -1
- package/dist/animator/createAnimator.js +0 -259
- package/dist/hooks/pressGroupContext.d.ts +0 -6
- package/dist/hooks/pressGroupContext.d.ts.map +0 -1
- package/dist/hooks/pressGroupContext.js +0 -20
- package/dist/hooks/useActionGroupTracking.d.ts +0 -53
- package/dist/hooks/useActionGroupTracking.d.ts.map +0 -1
- package/dist/hooks/useActionGroupTracking.js +0 -174
- package/dist/hooks/useAnimator.d.ts +0 -41
- package/dist/hooks/useAnimator.d.ts.map +0 -1
- package/dist/hooks/useAnimator.js +0 -70
- package/dist/i18n/generated-english/PowerSearch/PowerSearchBottomSheet.generated.d.ts +0 -8
- package/dist/i18n/generated-english/PowerSearch/PowerSearchBottomSheet.generated.d.ts.map +0 -1
- package/dist/i18n/generated-english/PowerSearch/PowerSearchBottomSheet.generated.js +0 -42
- package/dist/i18n/generated-english/PowerSearch/formatTokenValue.generated.d.ts +0 -8
- package/dist/i18n/generated-english/PowerSearch/formatTokenValue.generated.d.ts.map +0 -1
- package/dist/i18n/generated-english/PowerSearch/formatTokenValue.generated.js +0 -14
- package/dist/internal/indicatorPress.stylex.d.ts +0 -20
- package/dist/internal/indicatorPress.stylex.d.ts.map +0 -1
- package/dist/internal/indicatorPress.stylex.js +0 -26
- package/dist/motion/progress.d.ts +0 -88
- package/dist/motion/progress.d.ts.map +0 -1
- package/dist/motion/progress.js +0 -128
- package/dist/motion/uikit/alert.d.ts +0 -41
- package/dist/motion/uikit/alert.d.ts.map +0 -1
- package/dist/motion/uikit/alert.js +0 -43
- package/dist/privateAttributes/pressGroupItem.stylex.d.ts +0 -15
- package/dist/privateAttributes/pressGroupItem.stylex.d.ts.map +0 -1
- package/dist/privateAttributes/pressGroupItem.stylex.js +0 -23
- package/dist/utils/triggerFocus.d.ts +0 -36
- package/dist/utils/triggerFocus.d.ts.map +0 -1
- package/dist/utils/triggerFocus.js +0 -65
- package/src/PowerSearch/PowerSearchBottomSheet.tsx +0 -2515
- package/src/PowerSearch/formatTokenValue.ts +0 -215
- package/src/PowerSearch/powerSearchSheetUtils.ts +0 -224
- package/src/animator/createAnimator.doc.mjs +0 -153
- package/src/animator/createAnimator.ts +0 -324
- package/src/hooks/pressGroupContext.ts +0 -21
- package/src/hooks/useActionGroupTracking.ts +0 -224
- package/src/hooks/useAnimator.doc.mjs +0 -109
- package/src/hooks/useAnimator.ts +0 -67
- package/src/i18n/generated-english/PowerSearch/PowerSearchBottomSheet.generated.ts +0 -43
- package/src/i18n/generated-english/PowerSearch/formatTokenValue.generated.ts +0 -15
- package/src/internal/indicatorPress.stylex.ts +0 -24
- package/src/motion/progress.ts +0 -155
- package/src/motion/uikit/alert.ts +0 -43
- package/src/privateAttributes/pressGroupItem.stylex.ts +0 -23
- package/src/utils/triggerFocus.ts +0 -80
|
@@ -1,1913 +0,0 @@
|
|
|
1
|
-
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
-
|
|
3
|
-
'use client';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* @file PowerSearchBottomSheet.tsx
|
|
7
|
-
* @input The field's InternalConfig, filters, onChange, the field typeahead
|
|
8
|
-
* source, and the sheet options PowerSearch forwards; BottomSheet,
|
|
9
|
-
* Button, EmptyState, Heading, Icon, IconButton, Indicator, List,
|
|
10
|
-
* RadioList, Text, TextInput, PowerSearchValueEditor, the internal sheet
|
|
11
|
-
* confirmation and search-drag owners, i18n
|
|
12
|
-
* @output Exports PowerSearchBottomSheet — the touch surface of
|
|
13
|
-
* `PowerSearch experimental.presentation="bottom-sheet"`: a Filters sheet
|
|
14
|
-
* with editable rows and trailing remove actions, Add filter in its fixed
|
|
15
|
-
* header and Clear all / Done in its fixed footer, an Add filter sheet with
|
|
16
|
-
* grouped field rows or the content search with inline suggestions, and a
|
|
17
|
-
* value step that stages field → operator → value and confirms from its
|
|
18
|
-
* footer
|
|
19
|
-
* @position Internal presentation surface of PowerSearch. Loaded on demand by
|
|
20
|
-
* PowerSearch.tsx so the popover presentation never carries it. The
|
|
21
|
-
* trigger lives in PowerSearch.tsx.
|
|
22
|
-
*
|
|
23
|
-
* The three steps are sibling standalone BottomSheets that stack: each step
|
|
24
|
-
* opens above the sheet that led to it, which stays open beneath as a covered
|
|
25
|
-
* sheet, so no sheet needs a Back action. A dismissal closes only the top
|
|
26
|
-
* sheet and reveals the step beneath; dismissing the bottom sheet ends the
|
|
27
|
-
* flow. A list value's Tokenizer opens its own sheet above the value step, as
|
|
28
|
-
* the top sheet of the stack.
|
|
29
|
-
*
|
|
30
|
-
* The sheets pad their own content (`padding={0}` on BottomSheet). The
|
|
31
|
-
* Filters and Add filter sheets keep their header and footer fixed around a
|
|
32
|
-
* scroll viewport that reaches the sheet edge; its content publishes the
|
|
33
|
-
* padding as container padding so `edge="bleed"` lists run edge to edge with
|
|
34
|
-
* row text on the header's inset. The sheets are pinned tall: the field list
|
|
35
|
-
* resizes as it is searched and the editor changes with the operator, and
|
|
36
|
-
* only a fully expanded tall sheet gives mobile-keyboard accommodation.
|
|
37
|
-
*
|
|
38
|
-
* SYNC: When modified, update:
|
|
39
|
-
* - /packages/core/src/PowerSearch/PowerSearch.tsx (the trigger and the lazy host)
|
|
40
|
-
* - /docs/specs/components/PowerSearch/PowerSearch.spec.md (FR10–FR21, AR4–AR6, PR5)
|
|
41
|
-
*/
|
|
42
|
-
import React, { useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
|
43
|
-
import { flushSync } from 'react-dom';
|
|
44
|
-
import * as stylex from '@stylexjs/stylex';
|
|
45
|
-
import { BottomSheet } from "../BottomSheet/BottomSheet.js";
|
|
46
|
-
import { Button } from "../Button/Button.js";
|
|
47
|
-
import { EmptyState } from "../EmptyState/EmptyState.js";
|
|
48
|
-
import { Heading } from "../Heading/Heading.js";
|
|
49
|
-
import { Icon } from "../Icon/Icon.js";
|
|
50
|
-
import { IconButton } from "../IconButton/IconButton.js";
|
|
51
|
-
import { CheckboxIndicator } from "../Indicator/CheckboxIndicator.js";
|
|
52
|
-
import { RadioIndicator } from "../Indicator/RadioIndicator.js";
|
|
53
|
-
import { focusOutlineStyles } from "../utils/focusOutline.stylex.js";
|
|
54
|
-
import { isRenderable } from "../utils/isRenderable.js";
|
|
55
|
-
import { containerPaddingInlineVarStyles } from "../Layout/padding.stylex.js";
|
|
56
|
-
import { List } from "../List/List.js";
|
|
57
|
-
import { ListItem } from "../List/ListItem.js";
|
|
58
|
-
import { RadioList } from "../RadioList/RadioList.js";
|
|
59
|
-
import { RadioListItem } from "../RadioList/RadioListItem.js";
|
|
60
|
-
import { Text } from "../Text/Text.js";
|
|
61
|
-
import { TextInput } from "../TextInput/TextInput.js";
|
|
62
|
-
import { useAnnounce } from "../hooks/useAnnounce.js";
|
|
63
|
-
import { useListFormatter } from "../i18n/useListFormatter.js";
|
|
64
|
-
import { useLocale } from "../i18n/useLocale.js";
|
|
65
|
-
import { SheetConfirmation } from "../internal/SheetConfirmation.js";
|
|
66
|
-
import { useDismissSearchOnTouchDrag } from "../internal/useDismissSearchOnTouchDrag.js";
|
|
67
|
-
import "../theme/tokens.stylex.js";
|
|
68
|
-
import { colorVars, fontWeightVars, radiusVars, spacingVars } from "../theme/tokens.stylex.js";
|
|
69
|
-
import { isImeKeyEvent } from "../utils/ime.js";
|
|
70
|
-
import { rtlStyles } from "../utils/rtlStyles.js";
|
|
71
|
-
import { formatTokenValueText } from "./formatTokenValue.js";
|
|
72
|
-
function resolveRelativeDateLabel(operatorValue, value, t, buildRelativeDateOptions) {
|
|
73
|
-
return buildRelativeDateOptions(operatorValue, t).find(option => option.value === value)?.label ?? value;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// A placeholder the Filters row formats `labelValue` with, then splits on, so
|
|
77
|
-
// the value segment can render bold inside the catalog's ordering. A private
|
|
78
|
-
// use code point: never part of a label or value.
|
|
79
|
-
const ROW_VALUE_SLOT = '\uE000';
|
|
80
|
-
import { useAnnounceRenderedText } from "../hooks/useAnnounceRenderedText.js";
|
|
81
|
-
import { useInternalListFocus } from "../hooks/useListFocus.js";
|
|
82
|
-
import { useMergedRefs } from "../hooks/useMergedRefs.js";
|
|
83
|
-
import { groupItems } from "../utils/groupItems.js";
|
|
84
|
-
import { createSheetSuggestionSource, fieldMatchesQuery, createFilterIdentities, filterSignature, isSupportedOperator, nearestEditableIndex, resolveFilterIdentityIndex } from "./powerSearchSheetUtils.js";
|
|
85
|
-
import { resolveOperatorLabel } from "./resolveOperatorLabel.js";
|
|
86
|
-
import { registerEnglishFallback, useComponentTranslator } from "../i18n/useComponentTranslator.js";
|
|
87
|
-
import english from "../i18n/generated-english/PowerSearch/PowerSearchBottomSheet.generated.js";
|
|
88
|
-
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
89
|
-
registerEnglishFallback(english);
|
|
90
|
-
|
|
91
|
-
// =============================================================================
|
|
92
|
-
// Props
|
|
93
|
-
// =============================================================================
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* The popover presentation's editors, which the value step reuses. They come
|
|
97
|
-
* from PowerSearch.tsx, whose module graph already loads them for the popover
|
|
98
|
-
* presentation. Imported here, they and every component they reach would join
|
|
99
|
-
* this lazily imported graph, whose modules must import Core directly and
|
|
100
|
-
* never through a barrel.
|
|
101
|
-
*/
|
|
102
|
-
|
|
103
|
-
// =============================================================================
|
|
104
|
-
// Styles
|
|
105
|
-
// =============================================================================
|
|
106
|
-
|
|
107
|
-
// The trailing plus a field row shows when choosing it adds the filter at
|
|
108
|
-
// once, and the Add filter actions' glyph. Core's default icon set has no plus.
|
|
109
|
-
function PlusIcon(props) {
|
|
110
|
-
return /*#__PURE__*/_jsxs("svg", {
|
|
111
|
-
viewBox: "0 0 24 24",
|
|
112
|
-
fill: "none",
|
|
113
|
-
stroke: "currentColor",
|
|
114
|
-
strokeWidth: 1.5,
|
|
115
|
-
strokeLinecap: "round",
|
|
116
|
-
strokeLinejoin: "round",
|
|
117
|
-
...props,
|
|
118
|
-
children: [/*#__PURE__*/_jsx("path", {
|
|
119
|
-
d: "M5 12h14"
|
|
120
|
-
}), /*#__PURE__*/_jsx("path", {
|
|
121
|
-
d: "M12 5v14"
|
|
122
|
-
})]
|
|
123
|
-
});
|
|
124
|
-
}
|
|
125
|
-
const styles = {
|
|
126
|
-
touchAction: {
|
|
127
|
-
kAzted: "x1gg8mnh",
|
|
128
|
-
$$css: true
|
|
129
|
-
},
|
|
130
|
-
touchHitArea: {
|
|
131
|
-
"--_power-search-sheet-hit-inset": "x1o3f7l6 xhx5icg",
|
|
132
|
-
"--_power-search-sheet-hit-content": "x1bs45cr x73lxwv",
|
|
133
|
-
k5JduY: "x1oocgs0",
|
|
134
|
-
kwXMNM: "x1j6awrg",
|
|
135
|
-
kv0HGH: "xl7hj2y",
|
|
136
|
-
$$css: true
|
|
137
|
-
},
|
|
138
|
-
body: {
|
|
139
|
-
kzQI83: "x1iyjqo2",
|
|
140
|
-
k1xSpc: "x78zum5",
|
|
141
|
-
kXwgrk: "xdt5ytf",
|
|
142
|
-
kOIVth: "x18g69wz",
|
|
143
|
-
kg3NbH: "x1pzlopt",
|
|
144
|
-
kGO01o: "x1awphl8",
|
|
145
|
-
$$css: true
|
|
146
|
-
},
|
|
147
|
-
scrollContent: {
|
|
148
|
-
kAzted: "x1us19tq",
|
|
149
|
-
k1xSpc: "x78zum5",
|
|
150
|
-
kXwgrk: "xdt5ytf",
|
|
151
|
-
kOIVth: "x18g69wz",
|
|
152
|
-
kg3NbH: "x1pzlopt",
|
|
153
|
-
kGO01o: "x1awphl8",
|
|
154
|
-
$$css: true
|
|
155
|
-
},
|
|
156
|
-
footerEqualAction: {
|
|
157
|
-
kzQI83: "x1iyjqo2",
|
|
158
|
-
kCS8Yb: "x1r8uery",
|
|
159
|
-
k7Eaqz: "xeuugli",
|
|
160
|
-
kAzted: "x1gg8mnh",
|
|
161
|
-
$$css: true
|
|
162
|
-
},
|
|
163
|
-
footerSoleAction: {
|
|
164
|
-
kzQI83: "x1iyjqo2",
|
|
165
|
-
$$css: true
|
|
166
|
-
},
|
|
167
|
-
emptyState: {
|
|
168
|
-
kzQI83: "x1iyjqo2",
|
|
169
|
-
k1xSpc: "x78zum5",
|
|
170
|
-
kGNEyG: "x6s0dn4",
|
|
171
|
-
kjj79g: "xl56j7k",
|
|
172
|
-
$$css: true
|
|
173
|
-
},
|
|
174
|
-
controlSection: {
|
|
175
|
-
kmVPX3: "x1b2ylru",
|
|
176
|
-
kaIpWk: "x1hviunn",
|
|
177
|
-
kWkggS: "xwmxj5m",
|
|
178
|
-
$$css: true
|
|
179
|
-
},
|
|
180
|
-
trailingControlRow: {
|
|
181
|
-
kXwgrk: "x15zctf7",
|
|
182
|
-
$$css: true
|
|
183
|
-
},
|
|
184
|
-
nestedEditorContainer: {
|
|
185
|
-
k9g6sI: "x12h1iku",
|
|
186
|
-
kVQacm: "xb3r6kr",
|
|
187
|
-
$$css: true
|
|
188
|
-
},
|
|
189
|
-
choiceRow: {
|
|
190
|
-
k1xSpc: "x78zum5",
|
|
191
|
-
kGNEyG: "x6s0dn4",
|
|
192
|
-
kOIVth: "xjcht0a",
|
|
193
|
-
kzqmXN: "xh8yej3",
|
|
194
|
-
kAzted: "x1gg8mnh",
|
|
195
|
-
k8WAf4: "x8o8v82",
|
|
196
|
-
kZCmMZ: "x1ew2rer",
|
|
197
|
-
kwRFfy: "x19m2zy8",
|
|
198
|
-
kMzoRj: "xc342km",
|
|
199
|
-
kaIpWk: "x2u8bby",
|
|
200
|
-
kWkggS: "xjbqb8w",
|
|
201
|
-
kMwMTN: "x1tgivj0",
|
|
202
|
-
kMv6JI: "xjb2p0i",
|
|
203
|
-
k9WMMc: "x1yc453h",
|
|
204
|
-
kkrTdU: "x1ypdohk xvivl9a",
|
|
205
|
-
$$css: true
|
|
206
|
-
},
|
|
207
|
-
choiceRowDisabled: {
|
|
208
|
-
kMwMTN: "xv1l7n4 x1bhcspo",
|
|
209
|
-
$$css: true
|
|
210
|
-
},
|
|
211
|
-
choiceLabel: {
|
|
212
|
-
kzQI83: "x1iyjqo2",
|
|
213
|
-
k7Eaqz: "xeuugli",
|
|
214
|
-
$$css: true
|
|
215
|
-
}
|
|
216
|
-
};
|
|
217
|
-
|
|
218
|
-
// =============================================================================
|
|
219
|
-
// Draft state
|
|
220
|
-
// =============================================================================
|
|
221
|
-
|
|
222
|
-
function resolveDraftFilterIndex(filters, draft) {
|
|
223
|
-
if (draft.mode !== 'edit' || draft.filterIndex == null || draft.sourceFilter == null || draft.sourceSignature == null) {
|
|
224
|
-
return null;
|
|
225
|
-
}
|
|
226
|
-
return resolveFilterIdentityIndex(filters, draft.sourceFilter, draft.sourceSignature, draft.filterIndex);
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
// Below this many fields a search box is noise: the whole list is on screen.
|
|
230
|
-
const SEARCHABLE_FIELD_COUNT = 8;
|
|
231
|
-
// Empty-query browsing keeps the popover presentation's safety ceiling.
|
|
232
|
-
const MAX_BROWSE_MENU_ITEMS = 1000;
|
|
233
|
-
|
|
234
|
-
// =============================================================================
|
|
235
|
-
// Choice rows (enum, enum_list, date_relative)
|
|
236
|
-
// =============================================================================
|
|
237
|
-
|
|
238
|
-
/**
|
|
239
|
-
* Rendered inside a step's BottomSheet so that `useAnnounce` resolves the
|
|
240
|
-
* sheet's dialog as the enclosing modal; publishes that
|
|
241
|
-
* dialog's `announce` to the flow above through `onAnnounceChange` while
|
|
242
|
-
* mounted. Renders nothing.
|
|
243
|
-
*/
|
|
244
|
-
function SheetAnnouncer({
|
|
245
|
-
onAnnounceChange
|
|
246
|
-
}) {
|
|
247
|
-
const announce = useAnnounce();
|
|
248
|
-
// Hands this dialog's announce function up; the cleanup withdraws it when
|
|
249
|
-
// the step changes or the flow closes.
|
|
250
|
-
useEffect(() => {
|
|
251
|
-
onAnnounceChange(announce);
|
|
252
|
-
return () => {
|
|
253
|
-
onAnnounceChange(null);
|
|
254
|
-
};
|
|
255
|
-
}, [announce, onAnnounceChange]);
|
|
256
|
-
return null;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
/**
|
|
260
|
-
* The value step's "This filter is no longer available" message, rendered
|
|
261
|
-
* inside the step's dialog. The shared own-content module reads the rendered
|
|
262
|
-
* text and speaks it once per opened filter while the step is the open one.
|
|
263
|
-
* Called from inside the sheet's dialog, `useAnnounce` routes it into the
|
|
264
|
-
* dialog. The message is plain text, not an EmptyState, so nothing announces
|
|
265
|
-
* it a second time.
|
|
266
|
-
*/
|
|
267
|
-
function SheetEmptyMessage({
|
|
268
|
-
id,
|
|
269
|
-
message,
|
|
270
|
-
occasion,
|
|
271
|
-
isShown
|
|
272
|
-
}) {
|
|
273
|
-
const ref = useRef(null);
|
|
274
|
-
useAnnounceRenderedText(ref, isShown, occasion);
|
|
275
|
-
return /*#__PURE__*/_jsx("div", {
|
|
276
|
-
ref: ref,
|
|
277
|
-
id: id,
|
|
278
|
-
...{
|
|
279
|
-
className: "xq6koh6 x2b8uid"
|
|
280
|
-
},
|
|
281
|
-
children: /*#__PURE__*/_jsx(Text, {
|
|
282
|
-
type: "supporting",
|
|
283
|
-
color: "secondary",
|
|
284
|
-
children: message
|
|
285
|
-
})
|
|
286
|
-
});
|
|
287
|
-
}
|
|
288
|
-
// A radio group's one tab stop: the checked row, else the first.
|
|
289
|
-
function radioTabStopOrder(list) {
|
|
290
|
-
const rows = Array.from(list.querySelectorAll('[role="radio"]'));
|
|
291
|
-
const checked = rows.find(row => row.getAttribute('aria-checked') === 'true');
|
|
292
|
-
return checked == null ? rows : [checked, ...rows.filter(row => row !== checked)];
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
/**
|
|
296
|
-
* Closed vocabularies render as full-width rows with a trailing check instead
|
|
297
|
-
* of the popover presentation's Selector or Tokenizer: a dropdown layer inside
|
|
298
|
-
* a sheet stacks a second overlay on a surface the person is already inside.
|
|
299
|
-
*/
|
|
300
|
-
function ChoiceRows({
|
|
301
|
-
items,
|
|
302
|
-
selected,
|
|
303
|
-
isDisabled,
|
|
304
|
-
isMultiple,
|
|
305
|
-
label,
|
|
306
|
-
onToggle
|
|
307
|
-
}) {
|
|
308
|
-
// The rows carry their state as state, not as a name suffix: `radio` rows
|
|
309
|
-
// in a `radiogroup` for a single choice, `checkbox` rows in a `group` for
|
|
310
|
-
// `enum_list`, each with `aria-checked`, as the Tokenizer's sheet rows do.
|
|
311
|
-
// Disabled rows stay focusable with `aria-disabled`.
|
|
312
|
-
//
|
|
313
|
-
// Keyboard: a radio group is one tab stop — the checked row, else the
|
|
314
|
-
// first — and the arrow keys move focus and check the row they land on,
|
|
315
|
-
// wrapping; Home and End move to and check the first and last row. The
|
|
316
|
-
// shared list focus owns the tab stop and the moves; a row that takes focus
|
|
317
|
-
// from an arrow, Home or End is checked here. Space and Enter check the
|
|
318
|
-
// focused radio row and toggle a checkbox row: each row is a native
|
|
319
|
-
// `button`, whose activation fires `onClick` for both keys. A checkbox row
|
|
320
|
-
// is its own tab stop.
|
|
321
|
-
const isKeyMoveRef = useRef(false);
|
|
322
|
-
const {
|
|
323
|
-
listRef,
|
|
324
|
-
handleKeyDown,
|
|
325
|
-
handleFocus
|
|
326
|
-
} = useInternalListFocus({
|
|
327
|
-
itemSelector: '[role="radio"]',
|
|
328
|
-
orientation: 'both',
|
|
329
|
-
hasRovingTabIndex: !isMultiple,
|
|
330
|
-
tabStopOrder: radioTabStopOrder
|
|
331
|
-
});
|
|
332
|
-
const handleRadioKeyDown = event => {
|
|
333
|
-
isKeyMoveRef.current = true;
|
|
334
|
-
try {
|
|
335
|
-
handleKeyDown(event);
|
|
336
|
-
} finally {
|
|
337
|
-
isKeyMoveRef.current = false;
|
|
338
|
-
}
|
|
339
|
-
};
|
|
340
|
-
const handleRadioFocus = event => {
|
|
341
|
-
handleFocus(event);
|
|
342
|
-
if (!isKeyMoveRef.current || isDisabled) {
|
|
343
|
-
return;
|
|
344
|
-
}
|
|
345
|
-
const value = event.target.closest('[role="radio"]')?.dataset.value;
|
|
346
|
-
if (value != null && !selected.has(value)) {
|
|
347
|
-
onToggle(value, false);
|
|
348
|
-
}
|
|
349
|
-
};
|
|
350
|
-
return /*#__PURE__*/_jsx("div", {
|
|
351
|
-
ref: listRef,
|
|
352
|
-
role: isMultiple ? 'group' : 'radiogroup',
|
|
353
|
-
"aria-label": label,
|
|
354
|
-
onKeyDown: isMultiple ? undefined : handleRadioKeyDown,
|
|
355
|
-
onFocus: isMultiple ? undefined : handleRadioFocus,
|
|
356
|
-
...{
|
|
357
|
-
className: "x78zum5 xdt5ytf xojxgvx x1fcf3bl xx6qvi6"
|
|
358
|
-
},
|
|
359
|
-
children: items.map(item => {
|
|
360
|
-
const isSelected = selected.has(item.value);
|
|
361
|
-
return /*#__PURE__*/_jsxs("button", {
|
|
362
|
-
type: "button",
|
|
363
|
-
role: isMultiple ? 'checkbox' : 'radio',
|
|
364
|
-
"aria-checked": isSelected,
|
|
365
|
-
"aria-disabled": isDisabled || undefined,
|
|
366
|
-
"data-value": item.value,
|
|
367
|
-
onClick: () => {
|
|
368
|
-
if (!isDisabled) {
|
|
369
|
-
onToggle(item.value, isSelected);
|
|
370
|
-
}
|
|
371
|
-
},
|
|
372
|
-
...stylex.props(focusOutlineStyles.focusVisible, styles.choiceRow, isDisabled && styles.choiceRowDisabled),
|
|
373
|
-
children: [isRenderable(item.icon) && /*#__PURE__*/_jsx("span", {
|
|
374
|
-
"aria-hidden": "true",
|
|
375
|
-
...{
|
|
376
|
-
className: "x3nfvp2 x2lah0s"
|
|
377
|
-
},
|
|
378
|
-
children: item.icon
|
|
379
|
-
}), /*#__PURE__*/_jsx(Text, {
|
|
380
|
-
type: "body",
|
|
381
|
-
color: isDisabled ? 'inherit' : undefined,
|
|
382
|
-
xstyle: styles.choiceLabel,
|
|
383
|
-
children: item.label
|
|
384
|
-
}), isMultiple ? /*#__PURE__*/_jsx(CheckboxIndicator, {
|
|
385
|
-
state: isSelected ? 'checked' : 'unchecked',
|
|
386
|
-
size: "md",
|
|
387
|
-
isDisabled: isDisabled
|
|
388
|
-
}) : /*#__PURE__*/_jsx(RadioIndicator, {
|
|
389
|
-
state: isSelected ? 'checked' : 'unchecked',
|
|
390
|
-
size: "md",
|
|
391
|
-
isDisabled: isDisabled
|
|
392
|
-
})]
|
|
393
|
-
}, item.value);
|
|
394
|
-
})
|
|
395
|
-
});
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
/**
|
|
399
|
-
* The value control for one filter inside the value step. Closed vocabularies
|
|
400
|
-
* are rows here; every other type is the popover presentation's own editor in
|
|
401
|
-
* its sheet presentation.
|
|
402
|
-
*/
|
|
403
|
-
function SheetValueEditor({
|
|
404
|
-
operatorValue,
|
|
405
|
-
filterValue,
|
|
406
|
-
onChange,
|
|
407
|
-
onUnstage,
|
|
408
|
-
config,
|
|
409
|
-
fieldLabel,
|
|
410
|
-
isDisabled,
|
|
411
|
-
maxMenuItems,
|
|
412
|
-
editors
|
|
413
|
-
}) {
|
|
414
|
-
const {
|
|
415
|
-
buildRelativeDateOptions,
|
|
416
|
-
PowerSearchValueEditor
|
|
417
|
-
} = editors;
|
|
418
|
-
const t = useComponentTranslator();
|
|
419
|
-
const selectedValues = useMemo(() => {
|
|
420
|
-
if (filterValue?.type === 'enum' || filterValue?.type === 'date_relative') {
|
|
421
|
-
return new Set([filterValue.value]);
|
|
422
|
-
}
|
|
423
|
-
if (filterValue?.type === 'enum_list') {
|
|
424
|
-
return new Set(filterValue.value);
|
|
425
|
-
}
|
|
426
|
-
return new Set();
|
|
427
|
-
}, [filterValue]);
|
|
428
|
-
switch (operatorValue.type) {
|
|
429
|
-
case 'empty':
|
|
430
|
-
case 'nested':
|
|
431
|
-
return null;
|
|
432
|
-
case 'enum':
|
|
433
|
-
return /*#__PURE__*/_jsx(ChoiceRows, {
|
|
434
|
-
items: operatorValue.values,
|
|
435
|
-
selected: selectedValues,
|
|
436
|
-
isDisabled: isDisabled,
|
|
437
|
-
isMultiple: false,
|
|
438
|
-
label: fieldLabel,
|
|
439
|
-
onToggle: value => onChange({
|
|
440
|
-
type: 'enum',
|
|
441
|
-
value
|
|
442
|
-
})
|
|
443
|
-
});
|
|
444
|
-
case 'enum_list':
|
|
445
|
-
return /*#__PURE__*/_jsx(ChoiceRows, {
|
|
446
|
-
items: operatorValue.values,
|
|
447
|
-
selected: selectedValues,
|
|
448
|
-
isDisabled: isDisabled,
|
|
449
|
-
isMultiple: true,
|
|
450
|
-
label: fieldLabel,
|
|
451
|
-
onToggle: (value, isSelected) => {
|
|
452
|
-
const existing = filterValue?.type === 'enum_list' ? filterValue.value : [];
|
|
453
|
-
onChange({
|
|
454
|
-
type: 'enum_list',
|
|
455
|
-
value: isSelected ? existing.filter(v => v !== value) : [...existing, value]
|
|
456
|
-
});
|
|
457
|
-
}
|
|
458
|
-
});
|
|
459
|
-
case 'date_relative':
|
|
460
|
-
return /*#__PURE__*/_jsx(ChoiceRows, {
|
|
461
|
-
items: buildRelativeDateOptions(operatorValue, t),
|
|
462
|
-
selected: selectedValues,
|
|
463
|
-
isDisabled: isDisabled,
|
|
464
|
-
isMultiple: false,
|
|
465
|
-
label: fieldLabel,
|
|
466
|
-
onToggle: value => onChange({
|
|
467
|
-
type: 'date_relative',
|
|
468
|
-
value
|
|
469
|
-
})
|
|
470
|
-
});
|
|
471
|
-
case 'string':
|
|
472
|
-
case 'string_list':
|
|
473
|
-
case 'integer':
|
|
474
|
-
case 'float':
|
|
475
|
-
case 'time':
|
|
476
|
-
case 'date_absolute':
|
|
477
|
-
case 'date_range':
|
|
478
|
-
case 'entity_list':
|
|
479
|
-
case 'custom':
|
|
480
|
-
return /*#__PURE__*/_jsx("div", {
|
|
481
|
-
...{
|
|
482
|
-
className: "xfsso4q"
|
|
483
|
-
},
|
|
484
|
-
children: /*#__PURE__*/_jsx(PowerSearchValueEditor, {
|
|
485
|
-
operatorValue: operatorValue,
|
|
486
|
-
filterValue: filterValue,
|
|
487
|
-
onChange: onChange,
|
|
488
|
-
onUnstage: onUnstage,
|
|
489
|
-
config: config,
|
|
490
|
-
maxMenuItems: maxMenuItems,
|
|
491
|
-
isDisabled: isDisabled,
|
|
492
|
-
presentation: "bottom-sheet",
|
|
493
|
-
fieldLabel: fieldLabel
|
|
494
|
-
})
|
|
495
|
-
});
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
// =============================================================================
|
|
500
|
-
// Component
|
|
501
|
-
// =============================================================================
|
|
502
|
-
|
|
503
|
-
/**
|
|
504
|
-
* The sheet flow of PowerSearch's touch presentation: Filters, Add filter,
|
|
505
|
-
* and the value step, as standalone sheets that stack, each open beneath the
|
|
506
|
-
* step it led to.
|
|
507
|
-
*/
|
|
508
|
-
export function PowerSearchBottomSheet({
|
|
509
|
-
isOpen,
|
|
510
|
-
onOpenChange,
|
|
511
|
-
finalFocusRef,
|
|
512
|
-
resultCountText,
|
|
513
|
-
config,
|
|
514
|
-
configProp,
|
|
515
|
-
filters,
|
|
516
|
-
onChange,
|
|
517
|
-
searchSource,
|
|
518
|
-
placeholder,
|
|
519
|
-
isReadOnly,
|
|
520
|
-
isDisabled,
|
|
521
|
-
hasClear,
|
|
522
|
-
maxOperatorMenuItems,
|
|
523
|
-
maxSearchResults,
|
|
524
|
-
saveButtonLabel: saveButtonLabelFromProps,
|
|
525
|
-
components: componentOverrides,
|
|
526
|
-
editors
|
|
527
|
-
}) {
|
|
528
|
-
const {
|
|
529
|
-
NestedEditor
|
|
530
|
-
} = editors;
|
|
531
|
-
const t = useComponentTranslator();
|
|
532
|
-
const locale = useLocale();
|
|
533
|
-
const formatList = useListFormatter();
|
|
534
|
-
const isInteractive = !isDisabled && !isReadOnly;
|
|
535
|
-
const contentSearchFieldKey = config.config.contentSearchFieldKey;
|
|
536
|
-
const contentSearchOperator = contentSearchFieldKey == null ? undefined : config.getDefaultOperator(contentSearchFieldKey);
|
|
537
|
-
const hasContentSearch = contentSearchOperator?.value.type === 'string';
|
|
538
|
-
const suggestionSource = useMemo(() => createSheetSuggestionSource(config, searchSource), [config, searchSource]);
|
|
539
|
-
const contentResultsId = useId();
|
|
540
|
-
const managerAddButtonRef = useRef(null);
|
|
541
|
-
const contentSearchRef = useRef(null);
|
|
542
|
-
const fieldSearchRef = useRef(null);
|
|
543
|
-
// The Add filter sheet's panel: the fallback focus target when the control
|
|
544
|
-
// a step came from is gone — its first focusable control, which is the
|
|
545
|
-
// search field when one shows, else the first field row.
|
|
546
|
-
const fieldsSheetRef = useRef(null);
|
|
547
|
-
// The content search and its suggestion rows are one vertical list for the
|
|
548
|
-
// arrow keys: ArrowDown from the searchbox reaches the first suggestion,
|
|
549
|
-
// ArrowUp from the first suggestion returns to the searchbox, and the ends
|
|
550
|
-
// do not wrap. Home and End stay with the searchbox's caret.
|
|
551
|
-
const contentListFocus = useInternalListFocus({
|
|
552
|
-
itemSelector: `[role="searchbox"], [id="${contentResultsId}"] button`,
|
|
553
|
-
wrap: false,
|
|
554
|
-
hasHomeEnd: false
|
|
555
|
-
});
|
|
556
|
-
const fieldsSheetCallbackRef = useMergedRefs(fieldsSheetRef, contentListFocus.listRef);
|
|
557
|
-
const handleContentListKeyDown = event => {
|
|
558
|
-
if (!isImeKeyEvent(event.nativeEvent)) {
|
|
559
|
-
contentListFocus.handleKeyDown(event);
|
|
560
|
-
}
|
|
561
|
-
};
|
|
562
|
-
const manageSheetRef = useRef(null);
|
|
563
|
-
// Where focus was inside the Filters or Add filter sheet, read on every
|
|
564
|
-
// focus change there, so a controlled update that unmounts the focused
|
|
565
|
-
// control (a filter's row, a dropped field's row, Clear all after the last
|
|
566
|
-
// filter) can put focus on the nearest remaining row by that index, else
|
|
567
|
-
// the sheet's first control, else the sheet panel. A removed element
|
|
568
|
-
// fires no blur, so the position is what the effect below reads.
|
|
569
|
-
const lastSheetFocusRef = useRef(null);
|
|
570
|
-
// One index space for the record and the recovery: rows that are buttons
|
|
571
|
-
// (a plain read-only row is passed over, as the removal and Apply/Cancel
|
|
572
|
-
// rules pass it over), in document order.
|
|
573
|
-
const buttonRowsOf = panel => Array.from(panel.querySelectorAll('li')).map(row => row.querySelector('button')).filter(button => button != null);
|
|
574
|
-
const noteSheetFocus = useCallback(sheet => event => {
|
|
575
|
-
const row = event.target.closest('li');
|
|
576
|
-
const button = row?.querySelector('button') ?? null;
|
|
577
|
-
const rowIndex = button == null ? -1 : buttonRowsOf(event.currentTarget).indexOf(button);
|
|
578
|
-
lastSheetFocusRef.current = {
|
|
579
|
-
sheet,
|
|
580
|
-
rowIndex: rowIndex === -1 ? null : rowIndex
|
|
581
|
-
};
|
|
582
|
-
}, []);
|
|
583
|
-
const managerRowsRef = useRef(new Map());
|
|
584
|
-
const fieldRowsRef = useRef(new Map());
|
|
585
|
-
const latestFiltersRef = useRef(filters);
|
|
586
|
-
const pendingSheetFocusRef = useRef(null);
|
|
587
|
-
const pendingMutationFocusRef = useRef(false);
|
|
588
|
-
|
|
589
|
-
// The step an open starts on: Add filter directly when an interactive field
|
|
590
|
-
// has nothing to manage yet, otherwise Filters. Neither search field takes
|
|
591
|
-
// focus on open, so the software keyboard stays down until the person taps
|
|
592
|
-
// the search. Add filter opened directly is the flow's entry point: no
|
|
593
|
-
// Filters sheet beneath it, and Done in its footer.
|
|
594
|
-
const isEntryAddFilter = filters.length === 0 && isInteractive;
|
|
595
|
-
const [step, setStep] = useState(() => isOpen ? isEntryAddFilter ? 'fields' : 'manage' : null);
|
|
596
|
-
const [fieldPickerOrigin, setFieldPickerOrigin] = useState(() => isOpen && isEntryAddFilter ? 'trigger' : 'manage');
|
|
597
|
-
// The current step, read by the sheets' close reports and by a removal that
|
|
598
|
-
// decides where to speak.
|
|
599
|
-
const stepRef = useRef(step);
|
|
600
|
-
useLayoutEffect(() => {
|
|
601
|
-
stepRef.current = step;
|
|
602
|
-
}, [step]);
|
|
603
|
-
// The draft outlives a step change on purpose: a closing value step stays
|
|
604
|
-
// presented through its exit, and clearing the draft with the step would
|
|
605
|
-
// blank that sheet mid-transition.
|
|
606
|
-
const [draft, setDraft] = useState(null);
|
|
607
|
-
const draftGenerationRef = useRef(0);
|
|
608
|
-
const nextDraftGeneration = useCallback(() => {
|
|
609
|
-
draftGenerationRef.current += 1;
|
|
610
|
-
return draftGenerationRef.current;
|
|
611
|
-
}, []);
|
|
612
|
-
const [fieldQuery, setFieldQuery] = useState('');
|
|
613
|
-
const [contentQuery, setContentQuery] = useState('');
|
|
614
|
-
const [isClearConfirmationOpen, setIsClearConfirmationOpen] = useState(false);
|
|
615
|
-
const clearAllButtonRef = useRef(null);
|
|
616
|
-
// Set while Clear all commits, so the result count that changes in that
|
|
617
|
-
// commit is spoken with Clear all's outcome, as one message.
|
|
618
|
-
const isClearingRef = useRef(false);
|
|
619
|
-
|
|
620
|
-
// `useAnnounce` delivers inside the open Astryx modal that encloses its
|
|
621
|
-
// caller, and this component renders above the
|
|
622
|
-
// sheets, so it cannot call the hook itself. Each step's body renders a
|
|
623
|
-
// `SheetAnnouncer` inside its BottomSheet, which publishes the dialog's own
|
|
624
|
-
// `announce` here while that step is the active one; the effects below
|
|
625
|
-
// speak through it, so every message about a sheet's content is delivered
|
|
626
|
-
// in that sheet's dialog.
|
|
627
|
-
const announceRef = useRef(null);
|
|
628
|
-
const handleAnnounceChange = useCallback(next => {
|
|
629
|
-
announceRef.current = next;
|
|
630
|
-
}, []);
|
|
631
|
-
const announce = useCallback(message => {
|
|
632
|
-
announceRef.current?.(message);
|
|
633
|
-
}, []);
|
|
634
|
-
const pendingAnnouncementRef = useRef(null);
|
|
635
|
-
|
|
636
|
-
// The controlled `isOpen` opens and closes the flow; the step follows it in
|
|
637
|
-
// the render that sees the change, not an effect, so the entry step's sheet
|
|
638
|
-
// opens in the same commit as the flow (React's adjust-state-on-
|
|
639
|
-
// prop-change pattern). Closing from outside — the field became disabled —
|
|
640
|
-
// ends the flow wherever it was, the Clear all confirmation included.
|
|
641
|
-
const [wasOpen, setWasOpen] = useState(isOpen);
|
|
642
|
-
if (isOpen !== wasOpen) {
|
|
643
|
-
setWasOpen(isOpen);
|
|
644
|
-
if (isOpen) {
|
|
645
|
-
setFieldQuery('');
|
|
646
|
-
setContentQuery('');
|
|
647
|
-
setStep(isEntryAddFilter ? 'fields' : 'manage');
|
|
648
|
-
setFieldPickerOrigin(isEntryAddFilter ? 'trigger' : 'manage');
|
|
649
|
-
} else {
|
|
650
|
-
setStep(null);
|
|
651
|
-
setIsClearConfirmationOpen(false);
|
|
652
|
-
}
|
|
653
|
-
}
|
|
654
|
-
const closeFlow = useCallback(() => {
|
|
655
|
-
pendingSheetFocusRef.current = null;
|
|
656
|
-
setStep(null);
|
|
657
|
-
onOpenChange(false);
|
|
658
|
-
}, [onOpenChange]);
|
|
659
|
-
|
|
660
|
-
// Whether a Filters row is a button: the field interactive and the
|
|
661
|
-
// filter not read-only. The row opens the value step — the editor for its
|
|
662
|
-
// operator's value type, the built-in nested editor for a `nested` one, or
|
|
663
|
-
// for a filter the config no longer knows the unavailable state — and its
|
|
664
|
-
// trailing remove action removes it, as the popover's token has its × for
|
|
665
|
-
// it.
|
|
666
|
-
const isRowButton = useCallback(filter => !filter.isReadOnly && isInteractive, [isInteractive]);
|
|
667
|
-
|
|
668
|
-
// ------------------------------------------------------------------ focus --
|
|
669
|
-
|
|
670
|
-
// The control a step change named also becomes where the closing step
|
|
671
|
-
// sheets return focus when their exit ends: a closing sheet that held focus
|
|
672
|
-
// keeps it until then, and BottomSheet then returns it, so the named control
|
|
673
|
-
// receives it even when the first move could not land. With no control
|
|
674
|
-
// named, the closing steps return focus as the flow does.
|
|
675
|
-
const stepReturnTargetRef = useRef(null);
|
|
676
|
-
const stepFinalFocusRef = useMemo(() => ({
|
|
677
|
-
get current() {
|
|
678
|
-
return stepReturnTargetRef.current ?? finalFocusRef.current;
|
|
679
|
-
}
|
|
680
|
-
}), [finalFocusRef]);
|
|
681
|
-
const placeFocus = element => {
|
|
682
|
-
if (element == null) {
|
|
683
|
-
return;
|
|
684
|
-
}
|
|
685
|
-
stepReturnTargetRef.current = element;
|
|
686
|
-
element.focus({
|
|
687
|
-
preventScroll: true
|
|
688
|
-
});
|
|
689
|
-
};
|
|
690
|
-
const focusPendingSheetTarget = useCallback(() => {
|
|
691
|
-
const target = pendingSheetFocusRef.current;
|
|
692
|
-
if (target == null) {
|
|
693
|
-
return;
|
|
694
|
-
}
|
|
695
|
-
pendingSheetFocusRef.current = null;
|
|
696
|
-
if (target.type === 'manager-add') {
|
|
697
|
-
placeFocus(managerAddButtonRef.current);
|
|
698
|
-
return;
|
|
699
|
-
}
|
|
700
|
-
// Add filter's fallback: a controlled update may have removed the field
|
|
701
|
-
// whose row opened the step, or the content-search field itself; the
|
|
702
|
-
// sheet's first focusable control then takes focus — the search field
|
|
703
|
-
// when one shows, else the first field row.
|
|
704
|
-
const firstFieldsControl = () => {
|
|
705
|
-
const panel = fieldsSheetRef.current;
|
|
706
|
-
return panel?.querySelector('input') ?? panel?.querySelector('li button') ?? panel?.querySelector('button') ?? null;
|
|
707
|
-
};
|
|
708
|
-
if (target.type === 'content-search') {
|
|
709
|
-
placeFocus(contentSearchRef.current?.querySelector('input') ?? firstFieldsControl());
|
|
710
|
-
return;
|
|
711
|
-
}
|
|
712
|
-
if (target.type === 'field') {
|
|
713
|
-
placeFocus(fieldRowsRef.current.get(target.fieldKey)?.querySelector('button') ?? firstFieldsControl());
|
|
714
|
-
return;
|
|
715
|
-
}
|
|
716
|
-
// The row the step came from, followed across a controlled reorder or
|
|
717
|
-
// clone; when a controlled update removed it, or left two filters it
|
|
718
|
-
// could be, the nearest remaining row that is a button, as after a
|
|
719
|
-
// removal; else Add filter.
|
|
720
|
-
const index = resolveFilterIdentityIndex(latestFiltersRef.current, target.sourceFilter, target.sourceSignature, target.preferredIndex) ?? nearestEditableIndex(latestFiltersRef.current, target.preferredIndex, isRowButton);
|
|
721
|
-
const row = index == null ? null : managerRowsRef.current.get(index);
|
|
722
|
-
const rowButton = target.part === 'remove' ? row?.querySelector('[data-sheet-remove]') ?? row?.querySelector('button') : row?.querySelector('button');
|
|
723
|
-
placeFocus(rowButton ?? managerAddButtonRef.current);
|
|
724
|
-
}, [isRowButton]);
|
|
725
|
-
|
|
726
|
-
// A controlled update that unmounts the focused control of the Filters or
|
|
727
|
-
// Add filter sheet leaves focus on the page body inside the open modal.
|
|
728
|
-
// Read before paint on every commit of an open step: when focus is no
|
|
729
|
-
// longer inside the sheet's dialog, the nearest remaining row by the last
|
|
730
|
-
// focused index takes it, else the sheet's first control, else its panel.
|
|
731
|
-
// The pending-target effects below run after and win when a step
|
|
732
|
-
// change named a control.
|
|
733
|
-
useLayoutEffect(() => {
|
|
734
|
-
const last = lastSheetFocusRef.current;
|
|
735
|
-
if (step !== 'manage' && step !== 'fields') {
|
|
736
|
-
return;
|
|
737
|
-
}
|
|
738
|
-
if (last == null || last.sheet !== step) {
|
|
739
|
-
return;
|
|
740
|
-
}
|
|
741
|
-
const panel = step === 'manage' ? manageSheetRef.current : fieldsSheetRef.current;
|
|
742
|
-
const dialog = panel?.closest('dialog');
|
|
743
|
-
const active = document.activeElement;
|
|
744
|
-
if (panel == null || dialog == null || active != null && active !== document.body && dialog.contains(active)) {
|
|
745
|
-
return;
|
|
746
|
-
}
|
|
747
|
-
const rowButtons = buttonRowsOf(panel);
|
|
748
|
-
let target = null;
|
|
749
|
-
if (last.rowIndex != null && rowButtons.length > 0) {
|
|
750
|
-
// The following button row, else the last.
|
|
751
|
-
target = rowButtons[Math.min(last.rowIndex, rowButtons.length - 1)];
|
|
752
|
-
}
|
|
753
|
-
target ??= panel.querySelector('input') ?? rowButtons[0] ?? panel.querySelector('button') ?? null;
|
|
754
|
-
if (target != null) {
|
|
755
|
-
target.focus({
|
|
756
|
-
preventScroll: true
|
|
757
|
-
});
|
|
758
|
-
} else {
|
|
759
|
-
const focusable = dialog.querySelector('[tabindex="-1"]');
|
|
760
|
-
(focusable ?? dialog).focus({
|
|
761
|
-
preventScroll: true
|
|
762
|
-
});
|
|
763
|
-
}
|
|
764
|
-
});
|
|
765
|
-
|
|
766
|
-
// Both Effects move focus inside the entering sheet — the external system
|
|
767
|
-
// is the document's focus — to the control the step named. They run after
|
|
768
|
-
// the entering sheet's own
|
|
769
|
-
// panel focus (child effects commit first), so the intended control wins,
|
|
770
|
-
// and they acquire nothing, so they need no cleanup. The filters effect is
|
|
771
|
-
// declared first so a mutation that lands in the same commit as the step
|
|
772
|
-
// change resolves its row against the new filters.
|
|
773
|
-
useEffect(() => {
|
|
774
|
-
latestFiltersRef.current = filters;
|
|
775
|
-
if (step === 'manage' && pendingMutationFocusRef.current) {
|
|
776
|
-
pendingMutationFocusRef.current = false;
|
|
777
|
-
focusPendingSheetTarget();
|
|
778
|
-
}
|
|
779
|
-
}, [filters, focusPendingSheetTarget, step]);
|
|
780
|
-
|
|
781
|
-
// A target still pending when the flow closes, such as a commit that
|
|
782
|
-
// disabled the field, never carries into the next open.
|
|
783
|
-
useEffect(() => {
|
|
784
|
-
if (step == null) {
|
|
785
|
-
stepReturnTargetRef.current = null;
|
|
786
|
-
pendingSheetFocusRef.current = null;
|
|
787
|
-
return;
|
|
788
|
-
}
|
|
789
|
-
// A step that opens starts with no named return target; a return to
|
|
790
|
-
// Filters keeps the one its commit, or its removal, has just named.
|
|
791
|
-
if (step !== 'manage') {
|
|
792
|
-
stepReturnTargetRef.current = null;
|
|
793
|
-
}
|
|
794
|
-
focusPendingSheetTarget();
|
|
795
|
-
}, [focusPendingSheetTarget, step]);
|
|
796
|
-
|
|
797
|
-
// An announcement queued by a step change — a suggestion or a removal that
|
|
798
|
-
// changed the filters — is spoken in the commit that shows the next step,
|
|
799
|
-
// so it lands in that step's dialog region rather than the one about to
|
|
800
|
-
// close. It synchronizes the live region named above and acquires nothing.
|
|
801
|
-
useEffect(() => {
|
|
802
|
-
const message = pendingAnnouncementRef.current;
|
|
803
|
-
if (step != null && message != null) {
|
|
804
|
-
pendingAnnouncementRef.current = null;
|
|
805
|
-
announce(message);
|
|
806
|
-
}
|
|
807
|
-
}, [announce, step]);
|
|
808
|
-
|
|
809
|
-
// ----------------------------------------------------------------- fields --
|
|
810
|
-
|
|
811
|
-
// Every visible field lists: the value step edits every operator's value,
|
|
812
|
-
// `nested` through the popover's built-in editor.
|
|
813
|
-
const fields = useMemo(() => config.getVisibleFields(), [config]);
|
|
814
|
-
const normalizedFieldQuery = fieldQuery.trim().toLowerCase();
|
|
815
|
-
const matchingFields = useMemo(() => {
|
|
816
|
-
const matches = fields.filter(field => fieldMatchesQuery(field, normalizedFieldQuery));
|
|
817
|
-
return normalizedFieldQuery === '' ? matches : matches.slice(0, maxSearchResults);
|
|
818
|
-
}, [fields, maxSearchResults, normalizedFieldQuery]);
|
|
819
|
-
|
|
820
|
-
// Ungrouped fields first, then each group in first-appearance order — the
|
|
821
|
-
// same grouping the popover presentation's field suggestions use
|
|
822
|
-
// (`groupItems` with `ungroupedFirst`), so the two presentations
|
|
823
|
-
// list a config's fields in one order.
|
|
824
|
-
const groupedFields = useMemo(() => matchingFields.length === 0 ? [] : groupItems(matchingFields.map(field => ({
|
|
825
|
-
id: field.key,
|
|
826
|
-
label: field.label,
|
|
827
|
-
auxiliaryData: {
|
|
828
|
-
group: field.group
|
|
829
|
-
},
|
|
830
|
-
field
|
|
831
|
-
})), {
|
|
832
|
-
ungroupedFirst: true
|
|
833
|
-
}).map(group => [group.heading ?? '', group.items.map(item => item.field)]), [matchingFields]);
|
|
834
|
-
const isFieldSearchShown = fields.length >= SEARCHABLE_FIELD_COUNT;
|
|
835
|
-
|
|
836
|
-
// A field whose only operator takes an `empty` value is a one-tap filter:
|
|
837
|
-
// its row adds it with no value step, and shows a plus, not a chevron.
|
|
838
|
-
const isImmediateAddField = useCallback(field => {
|
|
839
|
-
const operators = config.getVisibleOperators(field.key);
|
|
840
|
-
return operators.length === 1 && operators[0].value.type === 'empty';
|
|
841
|
-
}, [config]);
|
|
842
|
-
|
|
843
|
-
// The steps stack: each opens above the sheet that led to it, which stays
|
|
844
|
-
// open beneath. Add filter entered directly has no Filters sheet beneath.
|
|
845
|
-
const isFieldPickerDirectEntry = fieldPickerOrigin === 'trigger';
|
|
846
|
-
const isManageSheetOpen = step === 'manage' || step === 'fields' && !isFieldPickerDirectEntry || step === 'value' && (draft?.mode === 'edit' || !isFieldPickerDirectEntry);
|
|
847
|
-
const isFieldsSheetOpen = step === 'fields' || step === 'value' && draft?.mode === 'create';
|
|
848
|
-
|
|
849
|
-
// The suggestions derive synchronously from the config, so a list never
|
|
850
|
-
// waits on a request: an empty list means an empty result, not a pending
|
|
851
|
-
// one. They stay listed while the value step covers Add filter.
|
|
852
|
-
const contentResults = useMemo(() => {
|
|
853
|
-
if (!isFieldsSheetOpen || !hasContentSearch || !isInteractive) {
|
|
854
|
-
return [];
|
|
855
|
-
}
|
|
856
|
-
const results = contentQuery.trim() === '' ? suggestionSource.bootstrap() : suggestionSource.search(contentQuery);
|
|
857
|
-
return results.slice(0, MAX_BROWSE_MENU_ITEMS);
|
|
858
|
-
}, [contentQuery, hasContentSearch, isFieldsSheetOpen, isInteractive, suggestionSource]);
|
|
859
|
-
|
|
860
|
-
// The field's result count, while a sheet is open: the owner's own Effect
|
|
861
|
-
// is quiet then, so the change is spoken here, inside the dialog. Only a
|
|
862
|
-
// change after the flow opened is spoken.
|
|
863
|
-
const announcedResultCountRef = useRef(resultCountText);
|
|
864
|
-
// The latest result count, for Clear all's handler to read after its
|
|
865
|
-
// commit has flushed.
|
|
866
|
-
const latestResultCountRef = useRef(resultCountText);
|
|
867
|
-
useLayoutEffect(() => {
|
|
868
|
-
latestResultCountRef.current = resultCountText;
|
|
869
|
-
}, [resultCountText]);
|
|
870
|
-
useEffect(() => {
|
|
871
|
-
if (isClearingRef.current || resultCountText === announcedResultCountRef.current) {
|
|
872
|
-
return;
|
|
873
|
-
}
|
|
874
|
-
announcedResultCountRef.current = resultCountText;
|
|
875
|
-
if (step != null && resultCountText != null) {
|
|
876
|
-
announce(resultCountText);
|
|
877
|
-
}
|
|
878
|
-
}, [announce, resultCountText, step]);
|
|
879
|
-
|
|
880
|
-
// ------------------------------------------------------------------ draft --
|
|
881
|
-
|
|
882
|
-
const draftField = draft ? config.getField(draft.field) : undefined;
|
|
883
|
-
const draftOperator = draft?.operator != null ? config.getOperator(draft.field, draft.operator) : undefined;
|
|
884
|
-
const draftOperators = useMemo(() => draft ? config.getVisibleOperators(draft.field) : [], [config, draft]);
|
|
885
|
-
|
|
886
|
-
// The two parts of a Filters row's text for `filter`: "{field} {operator}"
|
|
887
|
-
// as one catalog message, and the value as the popover presentation's
|
|
888
|
-
// token shows it — the same function, so the same words and the same zone
|
|
889
|
-
// handling, before the token's width budget. The rows render these, and
|
|
890
|
-
// the removal announcement joins them, so the three can never
|
|
891
|
-
// disagree.
|
|
892
|
-
const describeFilterParts = useCallback(filter => {
|
|
893
|
-
const field = config.getField(filter.field);
|
|
894
|
-
const operator = config.getOperator(filter.field, filter.operator);
|
|
895
|
-
// A filter whose field or operator the config no longer has still
|
|
896
|
-
// lists, by its raw keys, as its token does on the trigger; its row
|
|
897
|
-
// opens the unavailable step, and its trailing remove action removes
|
|
898
|
-
// it.
|
|
899
|
-
const fieldLabel = field?.label ?? filter.field;
|
|
900
|
-
const operatorLabel = operator == null ? filter.operator : resolveOperatorLabel(operator, t);
|
|
901
|
-
const label = operatorLabel ? t('@astryx.powersearch.sheet.fieldOperator', {
|
|
902
|
-
field: fieldLabel,
|
|
903
|
-
operator: operatorLabel
|
|
904
|
-
}) : fieldLabel;
|
|
905
|
-
if (operator == null) {
|
|
906
|
-
return {
|
|
907
|
-
label,
|
|
908
|
-
value: null
|
|
909
|
-
};
|
|
910
|
-
}
|
|
911
|
-
// A relative-date value is stored as a key (`3d_ago`); the row reads it
|
|
912
|
-
// as the value step's choice row does ("3 days ago"), through the
|
|
913
|
-
// catalog's relative-date messages, so the sheet agrees with itself. The popover token
|
|
914
|
-
// shows the stored key (a known gap of the record).
|
|
915
|
-
const value = operator.value.type === 'date_relative' && filter.value.type === 'date_relative' ? resolveRelativeDateLabel(operator.value, filter.value.value, t, editors.buildRelativeDateOptions) : formatTokenValueText(operator.value, filter.value, Number.MAX_SAFE_INTEGER, locale, t, formatList);
|
|
916
|
-
return {
|
|
917
|
-
label,
|
|
918
|
-
value
|
|
919
|
-
};
|
|
920
|
-
}, [config, editors, formatList, locale, t]);
|
|
921
|
-
const describeFilter = useCallback(filter => {
|
|
922
|
-
const parts = describeFilterParts(filter);
|
|
923
|
-
return parts.value ? t('@astryx.powersearch.sheet.labelValue', {
|
|
924
|
-
label: parts.label,
|
|
925
|
-
value: parts.value
|
|
926
|
-
}) : parts.label;
|
|
927
|
-
}, [describeFilterParts, t]);
|
|
928
|
-
const openFieldList = useCallback(() => {
|
|
929
|
-
if (!isInteractive) {
|
|
930
|
-
return;
|
|
931
|
-
}
|
|
932
|
-
setFieldPickerOrigin('manage');
|
|
933
|
-
setFieldQuery('');
|
|
934
|
-
setContentQuery('');
|
|
935
|
-
pendingSheetFocusRef.current = null;
|
|
936
|
-
setStep('fields');
|
|
937
|
-
}, [isInteractive]);
|
|
938
|
-
const queueManagerAddFocus = useCallback(() => {
|
|
939
|
-
pendingSheetFocusRef.current = {
|
|
940
|
-
type: 'manager-add'
|
|
941
|
-
};
|
|
942
|
-
}, []);
|
|
943
|
-
const commitSavedFilter = useCallback((next, filter) => {
|
|
944
|
-
if (!isInteractive) {
|
|
945
|
-
queueManagerAddFocus();
|
|
946
|
-
setStep('manage');
|
|
947
|
-
return;
|
|
948
|
-
}
|
|
949
|
-
if (next.mode === 'edit') {
|
|
950
|
-
const filterIndex = resolveDraftFilterIndex(filters, next);
|
|
951
|
-
const currentFilter = filterIndex == null ? undefined : filters[filterIndex];
|
|
952
|
-
if (filterIndex == null || currentFilter == null || currentFilter.isReadOnly) {
|
|
953
|
-
// A controlled update removed the edited filter, or left two it
|
|
954
|
-
// could be, or made it read-only: nothing is written, and focus
|
|
955
|
-
// returns to the nearest row that is a button around where it was,
|
|
956
|
-
// else Add filter.
|
|
957
|
-
pendingSheetFocusRef.current = next.sourceFilter != null && next.sourceSignature != null && next.filterIndex != null ? {
|
|
958
|
-
type: 'manager-filter',
|
|
959
|
-
sourceFilter: next.sourceFilter,
|
|
960
|
-
sourceSignature: next.sourceSignature,
|
|
961
|
-
preferredIndex: next.filterIndex
|
|
962
|
-
} : {
|
|
963
|
-
type: 'manager-add'
|
|
964
|
-
};
|
|
965
|
-
setStep('manage');
|
|
966
|
-
return;
|
|
967
|
-
}
|
|
968
|
-
pendingSheetFocusRef.current = {
|
|
969
|
-
type: 'manager-filter',
|
|
970
|
-
sourceFilter: filter,
|
|
971
|
-
sourceSignature: filterSignature(filter),
|
|
972
|
-
preferredIndex: filterIndex
|
|
973
|
-
};
|
|
974
|
-
const updated = [...filters];
|
|
975
|
-
updated[filterIndex] = filter;
|
|
976
|
-
onChange(updated, 'edit', filterIndex);
|
|
977
|
-
} else {
|
|
978
|
-
queueManagerAddFocus();
|
|
979
|
-
onChange([...filters, filter], 'add', filters.length);
|
|
980
|
-
}
|
|
981
|
-
setStep('manage');
|
|
982
|
-
}, [filters, isInteractive, onChange, queueManagerAddFocus]);
|
|
983
|
-
|
|
984
|
-
// Opening a new filter can come from a field row or a content-search
|
|
985
|
-
// suggestion. The origin is kept so Cancel returns to the control the person
|
|
986
|
-
// came from.
|
|
987
|
-
const openCreateEditor = useCallback((field, operator, returnFocus = 'field') => {
|
|
988
|
-
setDraft({
|
|
989
|
-
mode: 'create',
|
|
990
|
-
generation: nextDraftGeneration(),
|
|
991
|
-
returnFocus,
|
|
992
|
-
field: field.key,
|
|
993
|
-
operator: operator.key,
|
|
994
|
-
value: operator.value.type === 'empty' ? {
|
|
995
|
-
type: 'empty'
|
|
996
|
-
} : undefined
|
|
997
|
-
});
|
|
998
|
-
pendingSheetFocusRef.current = null;
|
|
999
|
-
setStep('value');
|
|
1000
|
-
}, [nextDraftGeneration]);
|
|
1001
|
-
const handleFieldSelect = useCallback(field => {
|
|
1002
|
-
if (!isInteractive) {
|
|
1003
|
-
return;
|
|
1004
|
-
}
|
|
1005
|
-
// A one-tap filter has nothing to stage: choosing its field adds it
|
|
1006
|
-
// and returns to Filters.
|
|
1007
|
-
if (isImmediateAddField(field)) {
|
|
1008
|
-
const [operator] = config.getVisibleOperators(field.key);
|
|
1009
|
-
const nextFilter = {
|
|
1010
|
-
field: field.key,
|
|
1011
|
-
operator: operator.key,
|
|
1012
|
-
value: {
|
|
1013
|
-
type: 'empty'
|
|
1014
|
-
}
|
|
1015
|
-
};
|
|
1016
|
-
pendingSheetFocusRef.current = {
|
|
1017
|
-
type: 'manager-filter',
|
|
1018
|
-
sourceFilter: nextFilter,
|
|
1019
|
-
sourceSignature: filterSignature(nextFilter),
|
|
1020
|
-
preferredIndex: filters.length
|
|
1021
|
-
};
|
|
1022
|
-
pendingMutationFocusRef.current = true;
|
|
1023
|
-
onChange([...filters, nextFilter], 'add', filters.length);
|
|
1024
|
-
pendingAnnouncementRef.current = t('@astryx.tokenizer.tokenAdded', {
|
|
1025
|
-
label: describeFilter(nextFilter)
|
|
1026
|
-
});
|
|
1027
|
-
setStep('manage');
|
|
1028
|
-
return;
|
|
1029
|
-
}
|
|
1030
|
-
const operator = config.getDefaultOperator(field.key) ?? field.operators[0];
|
|
1031
|
-
if (operator == null) {
|
|
1032
|
-
return;
|
|
1033
|
-
}
|
|
1034
|
-
openCreateEditor(field, operator);
|
|
1035
|
-
}, [config, describeFilter, filters, isImmediateAddField, isInteractive, onChange, openCreateEditor, t]);
|
|
1036
|
-
const handleContentSuggestionSelect = useCallback(item => {
|
|
1037
|
-
if (!isInteractive || item?.auxiliaryData == null) {
|
|
1038
|
-
return;
|
|
1039
|
-
}
|
|
1040
|
-
const {
|
|
1041
|
-
fieldKey,
|
|
1042
|
-
filterValue,
|
|
1043
|
-
operatorKey
|
|
1044
|
-
} = item.auxiliaryData;
|
|
1045
|
-
const field = config.getField(fieldKey);
|
|
1046
|
-
const operator = operatorKey == null ? config.getDefaultOperator(fieldKey) : config.getOperator(fieldKey, operatorKey);
|
|
1047
|
-
if (field == null || operator == null) {
|
|
1048
|
-
return;
|
|
1049
|
-
}
|
|
1050
|
-
setContentQuery('');
|
|
1051
|
-
if (filterValue != null) {
|
|
1052
|
-
const nextFilter = {
|
|
1053
|
-
field: field.key,
|
|
1054
|
-
operator: operator.key,
|
|
1055
|
-
value: filterValue
|
|
1056
|
-
};
|
|
1057
|
-
pendingSheetFocusRef.current = {
|
|
1058
|
-
type: 'manager-filter',
|
|
1059
|
-
sourceFilter: nextFilter,
|
|
1060
|
-
sourceSignature: filterSignature(nextFilter),
|
|
1061
|
-
preferredIndex: filters.length
|
|
1062
|
-
};
|
|
1063
|
-
pendingMutationFocusRef.current = true;
|
|
1064
|
-
onChange([...filters, nextFilter], 'add', filters.length);
|
|
1065
|
-
// The same announcement the popover presentation's typeahead add
|
|
1066
|
-
// makes, so assistive technology hears one phrase for one outcome;
|
|
1067
|
-
// spoken once the Filters sheet is the open dialog.
|
|
1068
|
-
pendingAnnouncementRef.current = t('@astryx.tokenizer.tokenAdded', {
|
|
1069
|
-
label: item.label
|
|
1070
|
-
});
|
|
1071
|
-
setStep('manage');
|
|
1072
|
-
return;
|
|
1073
|
-
}
|
|
1074
|
-
openCreateEditor(field, operator, 'content-search');
|
|
1075
|
-
}, [config, filters, isInteractive, onChange, openCreateEditor, t]);
|
|
1076
|
-
const handleFilterEdit = useCallback(index => {
|
|
1077
|
-
const filter = filters[index];
|
|
1078
|
-
if (!isInteractive || filter == null || filter.isReadOnly) {
|
|
1079
|
-
return;
|
|
1080
|
-
}
|
|
1081
|
-
setDraft({
|
|
1082
|
-
mode: 'edit',
|
|
1083
|
-
generation: nextDraftGeneration(),
|
|
1084
|
-
filterIndex: index,
|
|
1085
|
-
sourceFilter: filter,
|
|
1086
|
-
sourceSignature: filterSignature(filter),
|
|
1087
|
-
field: filter.field,
|
|
1088
|
-
operator: filter.operator,
|
|
1089
|
-
value: filter.value
|
|
1090
|
-
});
|
|
1091
|
-
pendingSheetFocusRef.current = null;
|
|
1092
|
-
setStep('value');
|
|
1093
|
-
}, [filters, isInteractive, nextDraftGeneration]);
|
|
1094
|
-
const handleRemoveFilter = useCallback(index => {
|
|
1095
|
-
const filter = filters[index];
|
|
1096
|
-
if (!isInteractive || filter == null || filter.isReadOnly) {
|
|
1097
|
-
return;
|
|
1098
|
-
}
|
|
1099
|
-
const nextFilters = filters.filter((_, i) => i !== index);
|
|
1100
|
-
const nextIndex = nearestEditableIndex(nextFilters, index, isRowButton);
|
|
1101
|
-
if (nextIndex == null) {
|
|
1102
|
-
pendingSheetFocusRef.current = {
|
|
1103
|
-
type: 'manager-add'
|
|
1104
|
-
};
|
|
1105
|
-
} else {
|
|
1106
|
-
const nextFilter = nextFilters[nextIndex];
|
|
1107
|
-
pendingSheetFocusRef.current = {
|
|
1108
|
-
type: 'manager-filter',
|
|
1109
|
-
sourceFilter: nextFilter,
|
|
1110
|
-
sourceSignature: filterSignature(nextFilter),
|
|
1111
|
-
preferredIndex: nextIndex,
|
|
1112
|
-
part: 'remove'
|
|
1113
|
-
};
|
|
1114
|
-
}
|
|
1115
|
-
pendingMutationFocusRef.current = true;
|
|
1116
|
-
onChange(nextFilters, 'remove', index);
|
|
1117
|
-
// The popover presentation's token × announces the same phrase. A
|
|
1118
|
-
// removal from a Filters row stays in that sheet and speaks at once; a
|
|
1119
|
-
// caller's Editor that removes while returning to Filters speaks once
|
|
1120
|
-
// the Filters sheet is the open dialog.
|
|
1121
|
-
const message = t('@astryx.tokenizer.tokenRemoved', {
|
|
1122
|
-
label: describeFilter(filter)
|
|
1123
|
-
});
|
|
1124
|
-
if (stepRef.current === 'manage') {
|
|
1125
|
-
announce(message);
|
|
1126
|
-
} else {
|
|
1127
|
-
pendingAnnouncementRef.current = message;
|
|
1128
|
-
}
|
|
1129
|
-
}, [announce, describeFilter, filters, isInteractive, isRowButton, onChange, t]);
|
|
1130
|
-
|
|
1131
|
-
// Closing Clear all's confirmation. Kept, focus returns to Clear all.
|
|
1132
|
-
// Confirmed, Clear all removes the filters the person can edit and keeps
|
|
1133
|
-
// read-only ones, which are the consumer's: one `remove` at the first
|
|
1134
|
-
// removed index. The Filters sheet stays open for further editing. The
|
|
1135
|
-
// confirmation must be gone before focus moves into the sheet and the
|
|
1136
|
-
// outcome is routed into the sheet's region, so its close is flushed
|
|
1137
|
-
// first. The outcome is "Editable filters removed" while a read-only filter
|
|
1138
|
-
// remains, otherwise "All filters removed", joined with a result count that
|
|
1139
|
-
// changed in the same commit as one catalog message; focus moves to the
|
|
1140
|
-
// sheet's first remaining control: a kept read-only row has none, so Add
|
|
1141
|
-
// filter.
|
|
1142
|
-
const closeClearConfirmation = confirmed => {
|
|
1143
|
-
const kept = filters.filter(filter => filter.isReadOnly);
|
|
1144
|
-
const firstRemovedIndex = filters.findIndex(filter => !filter.isReadOnly);
|
|
1145
|
-
const isClearing = confirmed && isInteractive && firstRemovedIndex >= 0;
|
|
1146
|
-
isClearingRef.current = isClearing;
|
|
1147
|
-
try {
|
|
1148
|
-
// eslint-disable-next-line @eslint-react/dom-no-flush-sync -- the confirmation must be gone before focus moves into the sheet and the words are routed into it
|
|
1149
|
-
flushSync(() => {
|
|
1150
|
-
setIsClearConfirmationOpen(false);
|
|
1151
|
-
if (isClearing) {
|
|
1152
|
-
onChange(kept, 'remove', firstRemovedIndex);
|
|
1153
|
-
}
|
|
1154
|
-
});
|
|
1155
|
-
} finally {
|
|
1156
|
-
isClearingRef.current = false;
|
|
1157
|
-
}
|
|
1158
|
-
if (!isClearing) {
|
|
1159
|
-
clearAllButtonRef.current?.focus();
|
|
1160
|
-
return;
|
|
1161
|
-
}
|
|
1162
|
-
managerAddButtonRef.current?.focus({
|
|
1163
|
-
preventScroll: true
|
|
1164
|
-
});
|
|
1165
|
-
const resultCount = latestResultCountRef.current;
|
|
1166
|
-
const changedCount = resultCount !== announcedResultCountRef.current ? resultCount : null;
|
|
1167
|
-
announcedResultCountRef.current = resultCount;
|
|
1168
|
-
if (kept.length > 0) {
|
|
1169
|
-
announce(changedCount == null ? t('@astryx.powersearch.editableFiltersRemoved') : t('@astryx.powersearch.sheet.editableFiltersClearedWithCount', {
|
|
1170
|
-
resultCount: changedCount
|
|
1171
|
-
}));
|
|
1172
|
-
} else {
|
|
1173
|
-
announce(changedCount == null ? t('@astryx.powersearch.allFiltersRemoved') : t('@astryx.powersearch.sheet.filtersClearedWithCount', {
|
|
1174
|
-
resultCount: changedCount
|
|
1175
|
-
}));
|
|
1176
|
-
}
|
|
1177
|
-
};
|
|
1178
|
-
const handleOperatorSelect = useCallback(operator => {
|
|
1179
|
-
setDraft(current => {
|
|
1180
|
-
if (current == null) {
|
|
1181
|
-
return current;
|
|
1182
|
-
}
|
|
1183
|
-
// A value survives an operator change only when the new operator
|
|
1184
|
-
// reads the same value type.
|
|
1185
|
-
const keepsValue = current.value != null && current.value.type === operator.value.type;
|
|
1186
|
-
return {
|
|
1187
|
-
...current,
|
|
1188
|
-
operator: operator.key,
|
|
1189
|
-
value: operator.value.type === 'empty' ? {
|
|
1190
|
-
type: 'empty'
|
|
1191
|
-
} : keepsValue ? current.value : undefined
|
|
1192
|
-
};
|
|
1193
|
-
});
|
|
1194
|
-
}, []);
|
|
1195
|
-
const handleDraftValueChange = useCallback(value => {
|
|
1196
|
-
setDraft(current => current == null ? current : {
|
|
1197
|
-
...current,
|
|
1198
|
-
value
|
|
1199
|
-
});
|
|
1200
|
-
}, []);
|
|
1201
|
-
// The number input's draft was cleared or stopped parsing: nothing stands
|
|
1202
|
-
// to confirm, so the confirm action disables again.
|
|
1203
|
-
const handleDraftUnstage = useCallback(() => {
|
|
1204
|
-
setDraft(current => current == null ? current : {
|
|
1205
|
-
...current,
|
|
1206
|
-
value: undefined
|
|
1207
|
-
});
|
|
1208
|
-
}, []);
|
|
1209
|
-
const handleSave = useCallback(() => {
|
|
1210
|
-
if (draft?.value == null || draft.operator == null) {
|
|
1211
|
-
return;
|
|
1212
|
-
}
|
|
1213
|
-
commitSavedFilter(draft, {
|
|
1214
|
-
field: draft.field,
|
|
1215
|
-
operator: draft.operator,
|
|
1216
|
-
value: draft.value
|
|
1217
|
-
});
|
|
1218
|
-
}, [commitSavedFilter, draft]);
|
|
1219
|
-
const returnFromValue = useCallback(() => {
|
|
1220
|
-
if (draft == null) {
|
|
1221
|
-
queueManagerAddFocus();
|
|
1222
|
-
setStep('manage');
|
|
1223
|
-
return;
|
|
1224
|
-
}
|
|
1225
|
-
if (draft.mode === 'edit' && draft.sourceFilter != null && draft.sourceSignature != null && draft.filterIndex != null) {
|
|
1226
|
-
pendingSheetFocusRef.current = {
|
|
1227
|
-
type: 'manager-filter',
|
|
1228
|
-
sourceFilter: draft.sourceFilter,
|
|
1229
|
-
sourceSignature: draft.sourceSignature,
|
|
1230
|
-
preferredIndex: resolveDraftFilterIndex(filters, draft) ?? draft.filterIndex
|
|
1231
|
-
};
|
|
1232
|
-
setStep('manage');
|
|
1233
|
-
return;
|
|
1234
|
-
}
|
|
1235
|
-
if (draft.returnFocus === 'content-search') {
|
|
1236
|
-
pendingSheetFocusRef.current = {
|
|
1237
|
-
type: 'content-search'
|
|
1238
|
-
};
|
|
1239
|
-
setStep('fields');
|
|
1240
|
-
return;
|
|
1241
|
-
}
|
|
1242
|
-
pendingSheetFocusRef.current = {
|
|
1243
|
-
type: 'field',
|
|
1244
|
-
fieldKey: draft.field
|
|
1245
|
-
};
|
|
1246
|
-
setStep('fields');
|
|
1247
|
-
}, [draft, filters, queueManagerAddFocus]);
|
|
1248
|
-
const handleRemove = useCallback(() => {
|
|
1249
|
-
if (draft?.mode !== 'edit') {
|
|
1250
|
-
return;
|
|
1251
|
-
}
|
|
1252
|
-
const filterIndex = resolveDraftFilterIndex(filters, draft);
|
|
1253
|
-
if (filterIndex == null || filters[filterIndex]?.isReadOnly) {
|
|
1254
|
-
queueManagerAddFocus();
|
|
1255
|
-
setStep('manage');
|
|
1256
|
-
return;
|
|
1257
|
-
}
|
|
1258
|
-
handleRemoveFilter(filterIndex);
|
|
1259
|
-
setStep('manage');
|
|
1260
|
-
}, [draft, filters, handleRemoveFilter, queueManagerAddFocus]);
|
|
1261
|
-
|
|
1262
|
-
// Each step sheet is controlled by `step`; a sheet only ever asks to close
|
|
1263
|
-
// (a dismissal its `purpose` allows: Escape, the scrim, a swipe, a platform
|
|
1264
|
-
// close request), which only the top sheet of the stack receives. Closing
|
|
1265
|
-
// the top step reveals the step beneath it; closing the bottom one ends the
|
|
1266
|
-
// flow. A stale report from a step that is no longer current is ignored.
|
|
1267
|
-
// Every step change goes through the handlers above.
|
|
1268
|
-
const handleManageOpenChange = useCallback(isSheetOpen => {
|
|
1269
|
-
if (!isSheetOpen && stepRef.current === 'manage') {
|
|
1270
|
-
closeFlow();
|
|
1271
|
-
}
|
|
1272
|
-
}, [closeFlow]);
|
|
1273
|
-
const handleFieldsOpenChange = useCallback(isSheetOpen => {
|
|
1274
|
-
if (isSheetOpen || stepRef.current !== 'fields') {
|
|
1275
|
-
return;
|
|
1276
|
-
}
|
|
1277
|
-
if (fieldPickerOrigin === 'trigger') {
|
|
1278
|
-
closeFlow();
|
|
1279
|
-
return;
|
|
1280
|
-
}
|
|
1281
|
-
queueManagerAddFocus();
|
|
1282
|
-
setStep('manage');
|
|
1283
|
-
}, [closeFlow, fieldPickerOrigin, queueManagerAddFocus]);
|
|
1284
|
-
const handleValueOpenChange = useCallback(isSheetOpen => {
|
|
1285
|
-
if (!isSheetOpen && stepRef.current === 'value') {
|
|
1286
|
-
returnFromValue();
|
|
1287
|
-
}
|
|
1288
|
-
}, [returnFromValue]);
|
|
1289
|
-
|
|
1290
|
-
// ---------------------------------------------------------------- render --
|
|
1291
|
-
|
|
1292
|
-
// Rows are keyed by filter identity, not position: a parent's reorder or a
|
|
1293
|
-
// removal above the focused row then moves elements instead of remounting
|
|
1294
|
-
// them, and focus stays on the same filter. Only a filter that is
|
|
1295
|
-
// itself removed loses its element, and the re-home effect above takes
|
|
1296
|
-
// over.
|
|
1297
|
-
const [filterIdentities] = useState(createFilterIdentities);
|
|
1298
|
-
const filterIds = filterIdentities.next(filters);
|
|
1299
|
-
const filterRows = filters.map((filter, index) => {
|
|
1300
|
-
const {
|
|
1301
|
-
label,
|
|
1302
|
-
value
|
|
1303
|
-
} = describeFilterParts(filter);
|
|
1304
|
-
const isEditable = isRowButton(filter);
|
|
1305
|
-
// The row reads from the same catalog message the removal announcement
|
|
1306
|
-
// uses (`labelValue`), so a locale that reorders it reorders both: the
|
|
1307
|
-
// message is formatted with a placeholder for the value, and the text
|
|
1308
|
-
// either side of that placeholder wraps the bold value.
|
|
1309
|
-
let displayLabel = label;
|
|
1310
|
-
if (value) {
|
|
1311
|
-
const [before, after] = t('@astryx.powersearch.sheet.labelValue', {
|
|
1312
|
-
label,
|
|
1313
|
-
value: ROW_VALUE_SLOT
|
|
1314
|
-
}).split(ROW_VALUE_SLOT);
|
|
1315
|
-
displayLabel = /*#__PURE__*/_jsxs(_Fragment, {
|
|
1316
|
-
children: [before, /*#__PURE__*/_jsx("span", {
|
|
1317
|
-
...{
|
|
1318
|
-
className: "x1lvx875"
|
|
1319
|
-
},
|
|
1320
|
-
children: value
|
|
1321
|
-
}), after]
|
|
1322
|
-
});
|
|
1323
|
-
}
|
|
1324
|
-
return {
|
|
1325
|
-
displayLabel,
|
|
1326
|
-
filter,
|
|
1327
|
-
index,
|
|
1328
|
-
isEditable,
|
|
1329
|
-
key: `filter-${filterIds[index]}`
|
|
1330
|
-
};
|
|
1331
|
-
});
|
|
1332
|
-
const hasRemovableFilter = filters.some(filter => !filter.isReadOnly);
|
|
1333
|
-
const isClearAllShown = hasClear && isInteractive && hasRemovableFilter;
|
|
1334
|
-
const EditorOverride = draftOperator != null ? componentOverrides?.[draftOperator.value.type]?.Editor : undefined;
|
|
1335
|
-
const draftOperatorLabel = draftOperator == null ? '' : resolveOperatorLabel(draftOperator, t);
|
|
1336
|
-
// The title names the filter's own operator when the step offers no
|
|
1337
|
-
// Operator group: one visible operator, or an operator the config no longer
|
|
1338
|
-
// lists as visible. The step then never shows another operator's name.
|
|
1339
|
-
const showsOperatorGroup = draftOperators.length > 1 && draftOperator != null && draftOperators.includes(draftOperator);
|
|
1340
|
-
// The title — the dialog's name — reads the field and operator as
|
|
1341
|
-
// the Filters row does: by their labels, or, for a filter whose field or
|
|
1342
|
-
// operator the config no longer has, by the raw keys the row shows.
|
|
1343
|
-
const editorTitle = draft == null ? t('@astryx.powersearch.sheet.addFilter') : draftField == null || draftOperator == null && draft.operator != null ? t('@astryx.powersearch.sheet.fieldOperator', {
|
|
1344
|
-
field: draftField?.label ?? draft.field,
|
|
1345
|
-
operator: draftOperatorLabel !== '' ? draftOperatorLabel : draft.operator ?? ''
|
|
1346
|
-
}).trim() : !showsOperatorGroup && draftOperatorLabel !== '' ? t('@astryx.powersearch.sheet.fieldOperator', {
|
|
1347
|
-
field: draftField.label,
|
|
1348
|
-
operator: draftOperatorLabel
|
|
1349
|
-
}) : draftField.label;
|
|
1350
|
-
const saveButtonLabel = saveButtonLabelFromProps ?? (draft?.mode === 'edit' ? t('@astryx.powersearch.editor.apply') : t('@astryx.powersearch.sheet.addFilter'));
|
|
1351
|
-
|
|
1352
|
-
// Every sheet body: padded, and publishing that padding to its lists.
|
|
1353
|
-
const bodyProps = stylex.props(styles.body, containerPaddingInlineVarStyles[4]);
|
|
1354
|
-
const scrollContentProps = stylex.props(styles.scrollContent, containerPaddingInlineVarStyles[4]);
|
|
1355
|
-
const controlSectionProps = stylex.props(styles.controlSection, containerPaddingInlineVarStyles[3]);
|
|
1356
|
-
|
|
1357
|
-
// A vertical touch drag on the Add filter rows moves focus off a focused
|
|
1358
|
-
// search to the sheet, so the software keyboard leaves and the rows can be
|
|
1359
|
-
// read.
|
|
1360
|
-
const fieldPickerDragProps = useDismissSearchOnTouchDrag(() => [contentSearchRef.current?.querySelector('input'), fieldSearchRef.current?.querySelector('input')], () => fieldsSheetRef.current?.closest('[tabindex="-1"]'));
|
|
1361
|
-
const isEditorDisabled = isDisabled || isReadOnly;
|
|
1362
|
-
const isSaveDisabled = isEditorDisabled || draft?.operator == null || draft.value == null;
|
|
1363
|
-
// The step has something to edit when the field and operator still exist
|
|
1364
|
-
// (a controlled config update can remove either). A caller's `Editor`
|
|
1365
|
-
// override replaces the body for its value type with the props the popover
|
|
1366
|
-
// hands it; otherwise a `nested` value takes the popover's built-in
|
|
1367
|
-
// NestedEditor and every other type the sheet's own control.
|
|
1368
|
-
const isDraftRenderable = draft != null && draftField != null && draftOperator != null;
|
|
1369
|
-
const isDraftNested = isDraftRenderable && !isSupportedOperator(draftOperator);
|
|
1370
|
-
// A controlled config update that removes the field or operator being
|
|
1371
|
-
// edited replaces the step's body with the unavailable state while focus
|
|
1372
|
-
// may be on one of the body's controls, which unmount without a blur. Focus
|
|
1373
|
-
// then moves to Cancel, the way back to the Filters row whose remove action
|
|
1374
|
-
// is the removal path, so it never falls to the page body inside the open
|
|
1375
|
-
// sheet. The message itself is announced by its rendered element.
|
|
1376
|
-
const cancelButtonRef = useRef(null);
|
|
1377
|
-
const wasDraftRenderableRef = useRef(isDraftRenderable);
|
|
1378
|
-
useLayoutEffect(() => {
|
|
1379
|
-
const becameUnavailable = wasDraftRenderableRef.current && !isDraftRenderable && draft != null;
|
|
1380
|
-
wasDraftRenderableRef.current = isDraftRenderable;
|
|
1381
|
-
if (!becameUnavailable || step !== 'value') {
|
|
1382
|
-
return;
|
|
1383
|
-
}
|
|
1384
|
-
const active = document.activeElement;
|
|
1385
|
-
const dialog = cancelButtonRef.current?.closest('dialog');
|
|
1386
|
-
if (dialog != null && active != null && dialog.contains(active)) {
|
|
1387
|
-
return;
|
|
1388
|
-
}
|
|
1389
|
-
cancelButtonRef.current?.focus();
|
|
1390
|
-
}, [draft, isDraftRenderable, step]);
|
|
1391
|
-
// The operator group stays outside either editor, so switching operators
|
|
1392
|
-
// swaps the body — a `nested` operator beside a plain one included.
|
|
1393
|
-
const operatorGroup = isDraftRenderable && showsOperatorGroup && /*#__PURE__*/_jsx(RadioList, {
|
|
1394
|
-
label: t('@astryx.powersearch.editor.operator'),
|
|
1395
|
-
isLabelHidden: true,
|
|
1396
|
-
value: draft.operator ?? '',
|
|
1397
|
-
onChange: operatorKey => {
|
|
1398
|
-
const operator = draftOperators.find(candidate => candidate.key === operatorKey);
|
|
1399
|
-
if (operator != null) {
|
|
1400
|
-
handleOperatorSelect(operator);
|
|
1401
|
-
}
|
|
1402
|
-
},
|
|
1403
|
-
isDisabled: isEditorDisabled,
|
|
1404
|
-
children: draftOperators.map(operator => /*#__PURE__*/_jsx(RadioListItem, {
|
|
1405
|
-
label: t('@astryx.powersearch.sheet.fieldOperator', {
|
|
1406
|
-
field: draftField.label,
|
|
1407
|
-
operator: resolveOperatorLabel(operator, t)
|
|
1408
|
-
}),
|
|
1409
|
-
value: operator.key,
|
|
1410
|
-
xstyle: styles.trailingControlRow
|
|
1411
|
-
}, operator.key))
|
|
1412
|
-
});
|
|
1413
|
-
// The step's Operator group owns the operator choice, so the built-in
|
|
1414
|
-
// nested editor's root operator picker is hidden and its root shows the
|
|
1415
|
-
// checked operator's label.
|
|
1416
|
-
const nestedOperatorOptions = useMemo(() => draftOperator == null ? [] : [{
|
|
1417
|
-
value: draftOperator.key,
|
|
1418
|
-
label: resolveOperatorLabel(draftOperator, t)
|
|
1419
|
-
}], [draftOperator, t]);
|
|
1420
|
-
const handleNestedDraftChange = useCallback(next => {
|
|
1421
|
-
setDraft(current => current == null ? current : {
|
|
1422
|
-
...current,
|
|
1423
|
-
value: next.value
|
|
1424
|
-
});
|
|
1425
|
-
}, []);
|
|
1426
|
-
const valueEditor = isDraftRenderable && (isDraftNested ? /*#__PURE__*/_jsx(NestedEditor, {
|
|
1427
|
-
config: config,
|
|
1428
|
-
partialFilter: {
|
|
1429
|
-
field: draft.field,
|
|
1430
|
-
operator: draft.operator,
|
|
1431
|
-
value: draft.value
|
|
1432
|
-
},
|
|
1433
|
-
operatorOptions: nestedOperatorOptions,
|
|
1434
|
-
hasRootOperatorPicker: false,
|
|
1435
|
-
onOperatorChange: () => {},
|
|
1436
|
-
onPartialFilterChange: handleNestedDraftChange,
|
|
1437
|
-
maxMenuItems: maxOperatorMenuItems,
|
|
1438
|
-
isReadOnly: isEditorDisabled,
|
|
1439
|
-
xstyle: styles.nestedEditorContainer
|
|
1440
|
-
}, `${draft.generation}-${draft.field}-${draft.operator}`) : /*#__PURE__*/_jsx(SheetValueEditor, {
|
|
1441
|
-
operatorValue: draftOperator.value,
|
|
1442
|
-
filterValue: draft.value,
|
|
1443
|
-
onChange: handleDraftValueChange,
|
|
1444
|
-
onUnstage: handleDraftUnstage,
|
|
1445
|
-
config: config,
|
|
1446
|
-
fieldLabel: draftField.label,
|
|
1447
|
-
isDisabled: isEditorDisabled,
|
|
1448
|
-
maxMenuItems: maxOperatorMenuItems,
|
|
1449
|
-
editors: editors
|
|
1450
|
-
}, `${draft.generation}-${draft.field}-${draft.operator}`));
|
|
1451
|
-
return /*#__PURE__*/_jsxs(_Fragment, {
|
|
1452
|
-
children: [/*#__PURE__*/_jsx(BottomSheet, {
|
|
1453
|
-
isOpen: isManageSheetOpen,
|
|
1454
|
-
onOpenChange: handleManageOpenChange,
|
|
1455
|
-
finalFocusRef: finalFocusRef,
|
|
1456
|
-
label: t('@astryx.powersearch.sheet.filtersTitle'),
|
|
1457
|
-
height: "tall",
|
|
1458
|
-
padding: 0,
|
|
1459
|
-
children: /*#__PURE__*/_jsxs("div", {
|
|
1460
|
-
ref: manageSheetRef,
|
|
1461
|
-
onFocus: noteSheetFocus('manage'),
|
|
1462
|
-
...{
|
|
1463
|
-
className: "x78zum5 xdt5ytf x1us19tq xc8icb0 x5yr21d xb3r6kr"
|
|
1464
|
-
},
|
|
1465
|
-
children: [step === 'manage' && /*#__PURE__*/_jsx(SheetAnnouncer, {
|
|
1466
|
-
onAnnounceChange: handleAnnounceChange
|
|
1467
|
-
}), /*#__PURE__*/_jsx("div", {
|
|
1468
|
-
...{
|
|
1469
|
-
className: "x7wzq59 x13vifvy x1vjfegm x78zum5 xdt5ytf xjcht0a x1adxfkp xvmdzux x1pzlopt x10xzikg"
|
|
1470
|
-
},
|
|
1471
|
-
children: /*#__PURE__*/_jsxs("div", {
|
|
1472
|
-
...{
|
|
1473
|
-
className: "x78zum5 x6s0dn4 x1txdalj"
|
|
1474
|
-
},
|
|
1475
|
-
children: [/*#__PURE__*/_jsx("div", {
|
|
1476
|
-
...{
|
|
1477
|
-
className: "xeuugli x1iyjqo2 x78zum5 xdt5ytf"
|
|
1478
|
-
},
|
|
1479
|
-
children: /*#__PURE__*/_jsx(Heading, {
|
|
1480
|
-
level: 3,
|
|
1481
|
-
children: t('@astryx.powersearch.sheet.filtersTitle')
|
|
1482
|
-
})
|
|
1483
|
-
}), isInteractive && filterRows.length > 0 && /*#__PURE__*/_jsx(Button, {
|
|
1484
|
-
ref: managerAddButtonRef,
|
|
1485
|
-
label: t('@astryx.powersearch.sheet.addFilter'),
|
|
1486
|
-
icon: /*#__PURE__*/_jsx(Icon, {
|
|
1487
|
-
icon: PlusIcon,
|
|
1488
|
-
size: "sm"
|
|
1489
|
-
}),
|
|
1490
|
-
isIconOnly: true,
|
|
1491
|
-
variant: "secondary",
|
|
1492
|
-
size: "md",
|
|
1493
|
-
xstyle: styles.touchHitArea,
|
|
1494
|
-
onClick: openFieldList
|
|
1495
|
-
})]
|
|
1496
|
-
})
|
|
1497
|
-
}), /*#__PURE__*/_jsx("div", {
|
|
1498
|
-
...{
|
|
1499
|
-
className: "x1iyjqo2 x2lwn1j x1odjw0f x1e4zzel"
|
|
1500
|
-
},
|
|
1501
|
-
children: /*#__PURE__*/_jsx("div", {
|
|
1502
|
-
...scrollContentProps,
|
|
1503
|
-
children: filterRows.length === 0 ?
|
|
1504
|
-
// With no filter the single thing to do is the only visible
|
|
1505
|
-
// action. Mounted only while the sheet is open, so its text
|
|
1506
|
-
// is spoken in this sheet when it appears.
|
|
1507
|
-
isManageSheetOpen && /*#__PURE__*/_jsx(EmptyState, {
|
|
1508
|
-
title: t('@astryx.powersearch.sheet.noFilters'),
|
|
1509
|
-
description: t('@astryx.powersearch.sheet.noFiltersDescription'),
|
|
1510
|
-
headingLevel: 4,
|
|
1511
|
-
icon: /*#__PURE__*/_jsx(Icon, {
|
|
1512
|
-
icon: "funnel",
|
|
1513
|
-
size: "lg",
|
|
1514
|
-
color: "secondary"
|
|
1515
|
-
}),
|
|
1516
|
-
xstyle: styles.emptyState,
|
|
1517
|
-
actions: isInteractive ? /*#__PURE__*/_jsx(Button, {
|
|
1518
|
-
ref: managerAddButtonRef,
|
|
1519
|
-
label: t('@astryx.powersearch.sheet.addFilter'),
|
|
1520
|
-
icon: /*#__PURE__*/_jsx(Icon, {
|
|
1521
|
-
icon: PlusIcon,
|
|
1522
|
-
size: "sm"
|
|
1523
|
-
}),
|
|
1524
|
-
variant: "primary",
|
|
1525
|
-
size: "lg",
|
|
1526
|
-
xstyle: styles.touchHitArea,
|
|
1527
|
-
onClick: openFieldList
|
|
1528
|
-
}) : undefined
|
|
1529
|
-
}) : /*#__PURE__*/_jsx(List, {
|
|
1530
|
-
hasDividers: true,
|
|
1531
|
-
density: "spacious",
|
|
1532
|
-
edge: "bleed",
|
|
1533
|
-
children: filterRows.map(row => {
|
|
1534
|
-
const operator = config.getOperator(row.filter.field, row.filter.operator);
|
|
1535
|
-
const fieldLabel = config.getField(row.filter.field)?.label ?? row.filter.field;
|
|
1536
|
-
// A one-tap filter has no value to edit: its row removes
|
|
1537
|
-
// it.
|
|
1538
|
-
const isOneTap = operator?.value.type === 'empty';
|
|
1539
|
-
const removeLabel = t('@astryx.powersearch.sheet.removeFilter', {
|
|
1540
|
-
field: fieldLabel
|
|
1541
|
-
});
|
|
1542
|
-
return /*#__PURE__*/_jsx(ListItem, {
|
|
1543
|
-
ref: node => {
|
|
1544
|
-
if (node == null) {
|
|
1545
|
-
managerRowsRef.current.delete(row.index);
|
|
1546
|
-
} else {
|
|
1547
|
-
managerRowsRef.current.set(row.index, node);
|
|
1548
|
-
}
|
|
1549
|
-
},
|
|
1550
|
-
label: row.displayLabel,
|
|
1551
|
-
onClick: row.isEditable ? isOneTap ? () => handleRemoveFilter(row.index) : () => handleFilterEdit(row.index) : undefined,
|
|
1552
|
-
endContent: row.isEditable ? /*#__PURE__*/_jsx(IconButton, {
|
|
1553
|
-
"data-sheet-remove": "",
|
|
1554
|
-
label: removeLabel,
|
|
1555
|
-
tooltip: removeLabel,
|
|
1556
|
-
icon: /*#__PURE__*/_jsx(Icon, {
|
|
1557
|
-
icon: "close",
|
|
1558
|
-
size: "sm"
|
|
1559
|
-
}),
|
|
1560
|
-
variant: "ghost",
|
|
1561
|
-
size: "md",
|
|
1562
|
-
xstyle: styles.touchHitArea,
|
|
1563
|
-
onClick: event => {
|
|
1564
|
-
event.stopPropagation();
|
|
1565
|
-
handleRemoveFilter(row.index);
|
|
1566
|
-
}
|
|
1567
|
-
}) : undefined
|
|
1568
|
-
}, row.key);
|
|
1569
|
-
})
|
|
1570
|
-
})
|
|
1571
|
-
})
|
|
1572
|
-
}), filterRows.length > 0 && /*#__PURE__*/_jsxs("div", {
|
|
1573
|
-
...{
|
|
1574
|
-
className: "x1vjfegm x78zum5 x6s0dn4 x1qughib x1txdalj x8o8v82 x1pzlopt x10xzikg x1n2onr6 xdd4er5 x2lah0s"
|
|
1575
|
-
},
|
|
1576
|
-
children: [isClearAllShown && /*#__PURE__*/_jsx(Button, {
|
|
1577
|
-
ref: clearAllButtonRef,
|
|
1578
|
-
label: t('@astryx.powersearch.sheet.clearAll'),
|
|
1579
|
-
variant: "secondary",
|
|
1580
|
-
size: "lg",
|
|
1581
|
-
xstyle: styles.footerEqualAction,
|
|
1582
|
-
onClick: () => setIsClearConfirmationOpen(true)
|
|
1583
|
-
}), /*#__PURE__*/_jsx(Button, {
|
|
1584
|
-
label: t('@astryx.powersearch.sheet.done'),
|
|
1585
|
-
variant: "primary",
|
|
1586
|
-
size: "lg",
|
|
1587
|
-
xstyle: isClearAllShown ? styles.footerEqualAction : [styles.footerSoleAction, styles.touchHitArea],
|
|
1588
|
-
onClick: closeFlow,
|
|
1589
|
-
width: isClearAllShown ? undefined : '100%'
|
|
1590
|
-
})]
|
|
1591
|
-
})]
|
|
1592
|
-
})
|
|
1593
|
-
}), /*#__PURE__*/_jsx(BottomSheet, {
|
|
1594
|
-
isOpen: isFieldsSheetOpen,
|
|
1595
|
-
onOpenChange: handleFieldsOpenChange,
|
|
1596
|
-
finalFocusRef: stepFinalFocusRef,
|
|
1597
|
-
label: t('@astryx.powersearch.sheet.addFilter'),
|
|
1598
|
-
height: "tall",
|
|
1599
|
-
padding: 0,
|
|
1600
|
-
children: /*#__PURE__*/_jsxs("div", {
|
|
1601
|
-
ref: fieldsSheetCallbackRef,
|
|
1602
|
-
onFocus: noteSheetFocus('fields'),
|
|
1603
|
-
onKeyDown: hasContentSearch ? handleContentListKeyDown : undefined,
|
|
1604
|
-
...{
|
|
1605
|
-
className: "x78zum5 xdt5ytf x1us19tq xc8icb0 x5yr21d xb3r6kr"
|
|
1606
|
-
},
|
|
1607
|
-
children: [step === 'fields' && /*#__PURE__*/_jsx(SheetAnnouncer, {
|
|
1608
|
-
onAnnounceChange: handleAnnounceChange
|
|
1609
|
-
}), /*#__PURE__*/_jsxs("div", {
|
|
1610
|
-
...{
|
|
1611
|
-
className: "x7wzq59 x13vifvy x1vjfegm x78zum5 xdt5ytf xjcht0a x1adxfkp xvmdzux x1pzlopt x10xzikg"
|
|
1612
|
-
},
|
|
1613
|
-
children: [/*#__PURE__*/_jsx("div", {
|
|
1614
|
-
...{
|
|
1615
|
-
className: "x78zum5 x6s0dn4 x1txdalj"
|
|
1616
|
-
},
|
|
1617
|
-
children: /*#__PURE__*/_jsx("div", {
|
|
1618
|
-
...{
|
|
1619
|
-
className: "xeuugli x1iyjqo2 x78zum5 xdt5ytf"
|
|
1620
|
-
},
|
|
1621
|
-
children: /*#__PURE__*/_jsx(Heading, {
|
|
1622
|
-
level: 3,
|
|
1623
|
-
children: t('@astryx.powersearch.sheet.addFilter')
|
|
1624
|
-
})
|
|
1625
|
-
})
|
|
1626
|
-
}), hasContentSearch ? /*#__PURE__*/_jsx("div", {
|
|
1627
|
-
ref: contentSearchRef,
|
|
1628
|
-
children: /*#__PURE__*/_jsx(TextInput, {
|
|
1629
|
-
label: t('@astryx.powersearch.sheet.search'),
|
|
1630
|
-
isLabelHidden: true,
|
|
1631
|
-
role: "searchbox",
|
|
1632
|
-
"aria-controls": contentResultsId,
|
|
1633
|
-
value: contentQuery,
|
|
1634
|
-
onChange: setContentQuery,
|
|
1635
|
-
onKeyDown: event => {
|
|
1636
|
-
if (isImeKeyEvent(event.nativeEvent)) {
|
|
1637
|
-
return;
|
|
1638
|
-
}
|
|
1639
|
-
if (event.key === 'Enter' && contentResults[0] != null) {
|
|
1640
|
-
event.preventDefault();
|
|
1641
|
-
handleContentSuggestionSelect(contentResults[0]);
|
|
1642
|
-
}
|
|
1643
|
-
},
|
|
1644
|
-
placeholder: placeholder,
|
|
1645
|
-
hasClear: true,
|
|
1646
|
-
startIcon: /*#__PURE__*/_jsx(Icon, {
|
|
1647
|
-
icon: "search",
|
|
1648
|
-
size: "sm",
|
|
1649
|
-
color: "secondary"
|
|
1650
|
-
}),
|
|
1651
|
-
isDisabled: !isInteractive,
|
|
1652
|
-
size: "lg",
|
|
1653
|
-
width: "100%"
|
|
1654
|
-
})
|
|
1655
|
-
}) : isFieldSearchShown ? /*#__PURE__*/_jsx("div", {
|
|
1656
|
-
ref: fieldSearchRef,
|
|
1657
|
-
children: /*#__PURE__*/_jsx(TextInput, {
|
|
1658
|
-
label: t('@astryx.powersearch.sheet.searchFields'),
|
|
1659
|
-
isLabelHidden: true,
|
|
1660
|
-
placeholder: t('@astryx.powersearch.sheet.searchFieldsPlaceholder'),
|
|
1661
|
-
value: fieldQuery,
|
|
1662
|
-
onChange: setFieldQuery,
|
|
1663
|
-
startIcon: /*#__PURE__*/_jsx(Icon, {
|
|
1664
|
-
icon: "search",
|
|
1665
|
-
size: "sm",
|
|
1666
|
-
color: "secondary"
|
|
1667
|
-
}),
|
|
1668
|
-
isDisabled: !isInteractive,
|
|
1669
|
-
size: "lg",
|
|
1670
|
-
width: "100%"
|
|
1671
|
-
})
|
|
1672
|
-
}) : null]
|
|
1673
|
-
}), /*#__PURE__*/_jsx("div", {
|
|
1674
|
-
...{
|
|
1675
|
-
className: "x1iyjqo2 x2lwn1j x1odjw0f x1e4zzel"
|
|
1676
|
-
},
|
|
1677
|
-
...fieldPickerDragProps,
|
|
1678
|
-
children: /*#__PURE__*/_jsx("div", {
|
|
1679
|
-
...scrollContentProps,
|
|
1680
|
-
children: hasContentSearch ? contentResults.length > 0 ? /*#__PURE__*/_jsx(List, {
|
|
1681
|
-
id: contentResultsId,
|
|
1682
|
-
"aria-label": t('@astryx.powersearch.sheet.searchResults'),
|
|
1683
|
-
hasDividers: true,
|
|
1684
|
-
density: "spacious",
|
|
1685
|
-
edge: "bleed",
|
|
1686
|
-
children: contentResults.map(item => /*#__PURE__*/_jsx(ListItem, {
|
|
1687
|
-
label: item.label,
|
|
1688
|
-
isDisabled: !isInteractive,
|
|
1689
|
-
onClick: () => handleContentSuggestionSelect(item)
|
|
1690
|
-
}, item.id))
|
|
1691
|
-
}) : contentQuery.trim() !== '' ?
|
|
1692
|
-
/*#__PURE__*/
|
|
1693
|
-
// The searchbox's `aria-controls` target while nothing
|
|
1694
|
-
// lists. The empty state mounts only while this step is the
|
|
1695
|
-
// open one and is spoken from its rendered text, once per
|
|
1696
|
-
// query that matches nothing.
|
|
1697
|
-
_jsx("div", {
|
|
1698
|
-
id: contentResultsId,
|
|
1699
|
-
children: step === 'fields' && /*#__PURE__*/_jsx(EmptyState, {
|
|
1700
|
-
icon: /*#__PURE__*/_jsx(Icon, {
|
|
1701
|
-
icon: "search",
|
|
1702
|
-
size: "lg",
|
|
1703
|
-
color: "secondary"
|
|
1704
|
-
}),
|
|
1705
|
-
title: t('@astryx.powersearch.sheet.noResults'),
|
|
1706
|
-
description: t('@astryx.powersearch.sheet.tryDifferentSearch'),
|
|
1707
|
-
headingLevel: 4,
|
|
1708
|
-
isCompact: true,
|
|
1709
|
-
xstyle: styles.emptyState
|
|
1710
|
-
}, contentQuery)
|
|
1711
|
-
}) :
|
|
1712
|
-
/*#__PURE__*/
|
|
1713
|
-
// An empty query with no bootstrap suggestion: the results
|
|
1714
|
-
// region exists, empty, so the searchbox's `aria-controls`
|
|
1715
|
-
// always names an element in the document.
|
|
1716
|
-
_jsx("div", {
|
|
1717
|
-
id: contentResultsId
|
|
1718
|
-
}) : groupedFields.length === 0 ?
|
|
1719
|
-
// A search outcome: only a non-empty query that matches
|
|
1720
|
-
// nothing shows it; a config with no field lists nothing.
|
|
1721
|
-
normalizedFieldQuery !== '' && step === 'fields' && /*#__PURE__*/_jsx(EmptyState, {
|
|
1722
|
-
icon: /*#__PURE__*/_jsx(Icon, {
|
|
1723
|
-
icon: "search",
|
|
1724
|
-
size: "lg",
|
|
1725
|
-
color: "secondary"
|
|
1726
|
-
}),
|
|
1727
|
-
title: t('@astryx.powersearch.sheet.noMatchingFields'),
|
|
1728
|
-
description: t('@astryx.powersearch.sheet.tryDifferentSearch'),
|
|
1729
|
-
headingLevel: 4,
|
|
1730
|
-
isCompact: true,
|
|
1731
|
-
xstyle: styles.emptyState
|
|
1732
|
-
}, normalizedFieldQuery) : groupedFields.map(([group, groupFields]) => /*#__PURE__*/_jsx(List, {
|
|
1733
|
-
hasDividers: true,
|
|
1734
|
-
density: "spacious",
|
|
1735
|
-
header: /*#__PURE__*/_jsx(Heading, {
|
|
1736
|
-
level: 4,
|
|
1737
|
-
children: group === '' ? t('@astryx.powersearch.sheet.ungroupedFields') : group
|
|
1738
|
-
}),
|
|
1739
|
-
edge: "bleed",
|
|
1740
|
-
children: groupFields.map(field => /*#__PURE__*/_jsx(ListItem, {
|
|
1741
|
-
ref: node => {
|
|
1742
|
-
if (node == null) {
|
|
1743
|
-
fieldRowsRef.current.delete(field.key);
|
|
1744
|
-
} else {
|
|
1745
|
-
fieldRowsRef.current.set(field.key, node);
|
|
1746
|
-
}
|
|
1747
|
-
},
|
|
1748
|
-
label: field.label,
|
|
1749
|
-
isDisabled: !isInteractive,
|
|
1750
|
-
startContent: field.icon,
|
|
1751
|
-
endContent: isImmediateAddField(field) ? /*#__PURE__*/_jsx(Icon, {
|
|
1752
|
-
icon: PlusIcon,
|
|
1753
|
-
size: "sm",
|
|
1754
|
-
color: "secondary"
|
|
1755
|
-
}) : /*#__PURE__*/_jsx(Icon, {
|
|
1756
|
-
icon: "chevronRight",
|
|
1757
|
-
size: "sm",
|
|
1758
|
-
color: "secondary",
|
|
1759
|
-
xstyle: rtlStyles.mirror
|
|
1760
|
-
}),
|
|
1761
|
-
onClick: () => handleFieldSelect(field)
|
|
1762
|
-
}, field.key))
|
|
1763
|
-
}, group))
|
|
1764
|
-
})
|
|
1765
|
-
}), isFieldPickerDirectEntry && /*#__PURE__*/_jsx("div", {
|
|
1766
|
-
...{
|
|
1767
|
-
className: "x1vjfegm x78zum5 x6s0dn4 x1qughib x1txdalj x8o8v82 x1pzlopt x10xzikg x1n2onr6 xdd4er5 x2lah0s"
|
|
1768
|
-
},
|
|
1769
|
-
children: /*#__PURE__*/_jsx(Button, {
|
|
1770
|
-
label: t('@astryx.powersearch.sheet.done'),
|
|
1771
|
-
variant: "primary",
|
|
1772
|
-
width: "100%",
|
|
1773
|
-
xstyle: styles.touchAction,
|
|
1774
|
-
onClick: closeFlow
|
|
1775
|
-
})
|
|
1776
|
-
})]
|
|
1777
|
-
})
|
|
1778
|
-
}), /*#__PURE__*/_jsx(BottomSheet, {
|
|
1779
|
-
isOpen: step === 'value',
|
|
1780
|
-
onOpenChange: handleValueOpenChange,
|
|
1781
|
-
finalFocusRef: stepFinalFocusRef,
|
|
1782
|
-
label: editorTitle,
|
|
1783
|
-
height: "tall",
|
|
1784
|
-
padding: 0,
|
|
1785
|
-
purpose: "info",
|
|
1786
|
-
children: /*#__PURE__*/_jsxs("div", {
|
|
1787
|
-
...{
|
|
1788
|
-
className: "x78zum5 xdt5ytf x1us19tq xc8icb0"
|
|
1789
|
-
},
|
|
1790
|
-
children: [/*#__PURE__*/_jsx("div", {
|
|
1791
|
-
...{
|
|
1792
|
-
className: "x7wzq59 x13vifvy x1vjfegm x78zum5 xdt5ytf xjcht0a x1adxfkp xvmdzux x1pzlopt x10xzikg"
|
|
1793
|
-
},
|
|
1794
|
-
children: /*#__PURE__*/_jsx("div", {
|
|
1795
|
-
...{
|
|
1796
|
-
className: "x78zum5 x6s0dn4 x1txdalj"
|
|
1797
|
-
},
|
|
1798
|
-
children: /*#__PURE__*/_jsx("div", {
|
|
1799
|
-
...{
|
|
1800
|
-
className: "xeuugli x1iyjqo2 x78zum5 xdt5ytf"
|
|
1801
|
-
},
|
|
1802
|
-
children: /*#__PURE__*/_jsx(Heading, {
|
|
1803
|
-
level: 3,
|
|
1804
|
-
maxLines: 1,
|
|
1805
|
-
children: editorTitle
|
|
1806
|
-
})
|
|
1807
|
-
})
|
|
1808
|
-
})
|
|
1809
|
-
}), isDraftRenderable && EditorOverride != null ? /*#__PURE__*/_jsxs("div", {
|
|
1810
|
-
...bodyProps,
|
|
1811
|
-
children: [step === 'value' && /*#__PURE__*/_jsx(SheetAnnouncer, {
|
|
1812
|
-
onAnnounceChange: handleAnnounceChange
|
|
1813
|
-
}), operatorGroup && /*#__PURE__*/_jsx("div", {
|
|
1814
|
-
...controlSectionProps,
|
|
1815
|
-
children: operatorGroup
|
|
1816
|
-
}), /*#__PURE__*/_jsx(EditorOverride, {
|
|
1817
|
-
config: configProp,
|
|
1818
|
-
filter: {
|
|
1819
|
-
field: draft.field,
|
|
1820
|
-
operator: draft.operator,
|
|
1821
|
-
value: draft.value
|
|
1822
|
-
},
|
|
1823
|
-
mode: draft.mode,
|
|
1824
|
-
onSave: saved => {
|
|
1825
|
-
if (saved == null) {
|
|
1826
|
-
if (draft.mode === 'edit') {
|
|
1827
|
-
handleRemove();
|
|
1828
|
-
} else {
|
|
1829
|
-
queueManagerAddFocus();
|
|
1830
|
-
setStep('manage');
|
|
1831
|
-
}
|
|
1832
|
-
return;
|
|
1833
|
-
}
|
|
1834
|
-
commitSavedFilter(draft, saved);
|
|
1835
|
-
},
|
|
1836
|
-
onCancel: returnFromValue,
|
|
1837
|
-
saveButtonLabel: saveButtonLabel,
|
|
1838
|
-
isReadOnly: isEditorDisabled
|
|
1839
|
-
}, `${draft.generation}-${draft.field}-${draft.operator}`)]
|
|
1840
|
-
}) : /*#__PURE__*/_jsxs(_Fragment, {
|
|
1841
|
-
children: [/*#__PURE__*/_jsxs("div", {
|
|
1842
|
-
...bodyProps,
|
|
1843
|
-
children: [step === 'value' && /*#__PURE__*/_jsx(SheetAnnouncer, {
|
|
1844
|
-
onAnnounceChange: handleAnnounceChange
|
|
1845
|
-
}), isDraftRenderable ?
|
|
1846
|
-
// A lone editor sits on the sheet's surface; with the
|
|
1847
|
-
// Operator group above it, each section is a muted
|
|
1848
|
-
// container.
|
|
1849
|
-
operatorGroup ? /*#__PURE__*/_jsxs(_Fragment, {
|
|
1850
|
-
children: [/*#__PURE__*/_jsx("div", {
|
|
1851
|
-
...controlSectionProps,
|
|
1852
|
-
children: operatorGroup
|
|
1853
|
-
}), /*#__PURE__*/_jsx("div", {
|
|
1854
|
-
...controlSectionProps,
|
|
1855
|
-
children: valueEditor
|
|
1856
|
-
})]
|
|
1857
|
-
}) : valueEditor :
|
|
1858
|
-
// A controlled config update removed the field or operator
|
|
1859
|
-
// being edited: say so (the element's rendered text is
|
|
1860
|
-
// announced, once per opened filter, while this step is the
|
|
1861
|
-
// open one) and keep a way out.
|
|
1862
|
-
draft != null && /*#__PURE__*/_jsx(SheetEmptyMessage, {
|
|
1863
|
-
message: t('@astryx.powersearch.sheet.filterUnavailable'),
|
|
1864
|
-
occasion: String(draft.generation),
|
|
1865
|
-
isShown: step === 'value'
|
|
1866
|
-
})]
|
|
1867
|
-
}), /*#__PURE__*/_jsx("div", {
|
|
1868
|
-
...{
|
|
1869
|
-
className: "x7wzq59 x1ey2m1c x1vjfegm x78zum5 x6s0dn4 x1qughib x1txdalj x8o8v82 x1pzlopt x10xzikg"
|
|
1870
|
-
},
|
|
1871
|
-
children: isReadOnly ? /*#__PURE__*/_jsx(Button, {
|
|
1872
|
-
ref: cancelButtonRef,
|
|
1873
|
-
label: t('@astryx.powersearch.editor.cancel'),
|
|
1874
|
-
variant: "secondary",
|
|
1875
|
-
width: "100%",
|
|
1876
|
-
xstyle: [styles.footerSoleAction, styles.touchHitArea],
|
|
1877
|
-
onClick: returnFromValue
|
|
1878
|
-
}) : /*#__PURE__*/_jsxs(_Fragment, {
|
|
1879
|
-
children: [/*#__PURE__*/_jsx(Button, {
|
|
1880
|
-
ref: cancelButtonRef,
|
|
1881
|
-
label: t('@astryx.powersearch.editor.cancel'),
|
|
1882
|
-
variant: "secondary",
|
|
1883
|
-
size: "lg",
|
|
1884
|
-
xstyle: styles.footerEqualAction,
|
|
1885
|
-
onClick: returnFromValue
|
|
1886
|
-
}), isDraftRenderable && /*#__PURE__*/_jsx(Button, {
|
|
1887
|
-
label: saveButtonLabel,
|
|
1888
|
-
variant: "primary",
|
|
1889
|
-
size: "lg",
|
|
1890
|
-
isDisabled: isSaveDisabled,
|
|
1891
|
-
xstyle: styles.footerEqualAction,
|
|
1892
|
-
onClick: handleSave
|
|
1893
|
-
})]
|
|
1894
|
-
})
|
|
1895
|
-
})]
|
|
1896
|
-
})]
|
|
1897
|
-
})
|
|
1898
|
-
}), isClearConfirmationOpen && /*#__PURE__*/_jsx(SheetConfirmation, {
|
|
1899
|
-
isOpen: true,
|
|
1900
|
-
onOpenChange: isConfirmationOpen => {
|
|
1901
|
-
if (!isConfirmationOpen) {
|
|
1902
|
-
closeClearConfirmation(false);
|
|
1903
|
-
}
|
|
1904
|
-
},
|
|
1905
|
-
title: t('@astryx.powersearch.sheet.clearAllTitle'),
|
|
1906
|
-
description: t('@astryx.powersearch.sheet.clearAllDescription'),
|
|
1907
|
-
actionLabel: t('@astryx.powersearch.sheet.clearAllConfirm'),
|
|
1908
|
-
cancelLabel: t('@astryx.powersearch.sheet.clearAllCancel'),
|
|
1909
|
-
onAction: () => closeClearConfirmation(true)
|
|
1910
|
-
})]
|
|
1911
|
-
});
|
|
1912
|
-
}
|
|
1913
|
-
PowerSearchBottomSheet.displayName = 'PowerSearchBottomSheet';
|