@astryxdesign/lab 0.7.6-canary.f73623d → 0.7.6-canary.f77aaf2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/CommentPin/CommentPin.js +1 -1
- package/dist/Comments/CommentComposer.d.ts.map +1 -1
- package/dist/Comments/CommentComposer.js +12 -4
- package/dist/MobilePowerSearch/MobilePowerSearch.d.ts +3 -6
- package/dist/MobilePowerSearch/MobilePowerSearch.d.ts.map +1 -1
- package/dist/MobilePowerSearch/MobilePowerSearch.js +12 -76
- package/dist/MobileTokenizer/MobileTokenizer.d.ts +10 -7
- package/dist/MobileTokenizer/MobileTokenizer.d.ts.map +1 -1
- package/dist/MobileTokenizer/MobileTokenizer.js +191 -114
- package/dist/internal/MobileSheetConfirmation.d.ts +12 -0
- package/dist/internal/MobileSheetConfirmation.d.ts.map +1 -0
- package/dist/internal/MobileSheetConfirmation.js +88 -0
- package/dist/internal/useDismissSearchOnTouchDrag.d.ts +21 -0
- package/dist/internal/useDismissSearchOnTouchDrag.d.ts.map +1 -0
- package/dist/internal/useDismissSearchOnTouchDrag.js +43 -0
- package/package.json +4 -4
- package/src/CommentPin/CommentPin.tsx +1 -1
- package/src/Comments/CommentComposer.test.tsx +56 -0
- package/src/Comments/CommentComposer.tsx +18 -13
- package/src/MobilePowerSearch/MobilePowerSearch.doc.mjs +1 -1
- package/src/MobilePowerSearch/MobilePowerSearch.test.tsx +7 -2
- package/src/MobilePowerSearch/MobilePowerSearch.tsx +16 -83
- package/src/MobileTokenizer/MobileTokenizer.doc.mjs +3 -3
- package/src/MobileTokenizer/MobileTokenizer.test.tsx +287 -31
- package/src/MobileTokenizer/MobileTokenizer.tsx +242 -137
- package/src/Schedule/__tests__/ScheduleTimeDirection.a11y.chromium.spec.ts +4 -0
- package/src/TransferList/TransferList.doc.mjs +1 -1
- package/src/internal/MobileSheetConfirmation.test.tsx +68 -0
- package/src/internal/MobileSheetConfirmation.tsx +97 -0
- package/src/internal/useDismissSearchOnTouchDrag.test.tsx +94 -0
- package/src/internal/useDismissSearchOnTouchDrag.ts +57 -0
- package/src/CommentPin/CommentPin.spec.md +0 -319
- package/src/Schedule/Schedule.spec.md +0 -590
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
'use client';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @file useDismissSearchOnTouchDrag.ts
|
|
7
|
+
* @input Caller-owned search inputs and touch events from a separate results list
|
|
8
|
+
* @output Private touch handlers shared by the Lab mobile search sheets
|
|
9
|
+
* @position One owner for results-drag keyboard dismissal; not a public export
|
|
10
|
+
*
|
|
11
|
+
* A direct vertical touch drag dismisses only a focused caller-owned search.
|
|
12
|
+
* Scroll events are deliberately not observed: filtering can clamp scrollTop
|
|
13
|
+
* and fire scroll while someone is typing. Small moves, taps and multi-touch
|
|
14
|
+
* gestures leave focus alone. The hook acquires no external resource.
|
|
15
|
+
*/
|
|
16
|
+
import { useRef } from 'react';
|
|
17
|
+
const DRAG_THRESHOLD_PX = 8;
|
|
18
|
+
export function useDismissSearchOnTouchDrag(getSearchInputs) {
|
|
19
|
+
const startYRef = useRef(null);
|
|
20
|
+
const reset = () => {
|
|
21
|
+
startYRef.current = null;
|
|
22
|
+
};
|
|
23
|
+
return {
|
|
24
|
+
onTouchStart: event => {
|
|
25
|
+
startYRef.current = event.touches.length === 1 ? event.touches[0].clientY : null;
|
|
26
|
+
},
|
|
27
|
+
onTouchMove: event => {
|
|
28
|
+
if (event.touches.length !== 1) {
|
|
29
|
+
reset();
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const startY = startYRef.current;
|
|
33
|
+
if (startY == null || Math.abs(event.touches[0].clientY - startY) < DRAG_THRESHOLD_PX) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
reset();
|
|
37
|
+
const focusedSearch = getSearchInputs().find(input => input != null && input === document.activeElement);
|
|
38
|
+
focusedSearch?.blur();
|
|
39
|
+
},
|
|
40
|
+
onTouchEnd: reset,
|
|
41
|
+
onTouchCancel: reset
|
|
42
|
+
};
|
|
43
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astryxdesign/lab",
|
|
3
|
-
"version": "0.7.6-canary.
|
|
3
|
+
"version": "0.7.6-canary.f77aaf2",
|
|
4
4
|
"description": "Experimental Astryx components — published to npm only under the @canary dist-tag for early testing; never released as a stable (latest) version.",
|
|
5
5
|
"author": "Meta Open Source",
|
|
6
6
|
"license": "MIT",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
},
|
|
50
50
|
"peerDependencies": {
|
|
51
51
|
"@stylexjs/stylex": ">=0.10.0",
|
|
52
|
-
"@astryxdesign/core": "0.7.6-canary.
|
|
52
|
+
"@astryxdesign/core": "0.7.6-canary.f77aaf2",
|
|
53
53
|
"react": ">=19.0.0",
|
|
54
54
|
"react-dom": ">=19.0.0"
|
|
55
55
|
},
|
|
@@ -59,8 +59,8 @@
|
|
|
59
59
|
"d3-shape": "^3.2.0"
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
|
62
|
-
"@astryxdesign/a11y-spec": "0.7.6-canary.
|
|
63
|
-
"@astryxdesign/cli": "0.7.6-canary.
|
|
62
|
+
"@astryxdesign/a11y-spec": "0.7.6-canary.f77aaf2",
|
|
63
|
+
"@astryxdesign/cli": "0.7.6-canary.f77aaf2",
|
|
64
64
|
"@babel/cli": "^7.29.7",
|
|
65
65
|
"@babel/core": "^7.29.7",
|
|
66
66
|
"@babel/preset-react": "^7.29.7",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*
|
|
16
16
|
* SYNC: When modified, update these files to stay in sync:
|
|
17
17
|
* - /packages/lab/src/CommentPin/CommentPin.doc.mjs
|
|
18
|
-
* - /
|
|
18
|
+
* - /docs/specs/components/CommentPin/CommentPin.spec.md
|
|
19
19
|
* - /packages/lab/src/CommentPin/CommentPin.test.tsx
|
|
20
20
|
* - /packages/lab/src/CommentPin/index.ts
|
|
21
21
|
* - /apps/storybook/stories/CommentPin.stories.tsx
|
|
@@ -200,6 +200,62 @@ describe('CommentComposer', () => {
|
|
|
200
200
|
expect(screen.getByTestId('draft').textContent).toBe('Ship it 🎉');
|
|
201
201
|
});
|
|
202
202
|
|
|
203
|
+
it('ends the upload busy state when onActionPendingChange is a new function each render', async () => {
|
|
204
|
+
let finishUpload: (images: {url: string; alt: string}[]) => void = () => {};
|
|
205
|
+
const onUploadImages = vi.fn(
|
|
206
|
+
() =>
|
|
207
|
+
new Promise<{url: string; alt: string}[]>(resolve => {
|
|
208
|
+
finishUpload = resolve;
|
|
209
|
+
}),
|
|
210
|
+
);
|
|
211
|
+
function Host() {
|
|
212
|
+
const [draft, setDraft] = useState('See this');
|
|
213
|
+
const [pending, setPending] = useState(false);
|
|
214
|
+
return (
|
|
215
|
+
<>
|
|
216
|
+
<CommentComposerBase
|
|
217
|
+
value={draft}
|
|
218
|
+
onChange={setDraft}
|
|
219
|
+
onSubmit={() => {}}
|
|
220
|
+
placeholder="Reply"
|
|
221
|
+
context={{kind: 'comment'}}
|
|
222
|
+
highlightColor="yellow"
|
|
223
|
+
triggers={[]}
|
|
224
|
+
agentCommands={[]}
|
|
225
|
+
hasEmojiPicker={false}
|
|
226
|
+
isSubmitting={false}
|
|
227
|
+
onUploadImages={onUploadImages}
|
|
228
|
+
// Inline: a new callback on every render of the host.
|
|
229
|
+
onActionPendingChange={isPending => setPending(isPending)}
|
|
230
|
+
/>
|
|
231
|
+
<output data-testid="pending">{String(pending)}</output>
|
|
232
|
+
</>
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
const {container} = render(<Host />);
|
|
236
|
+
const input = container.querySelector('input[type="file"]');
|
|
237
|
+
if (!(input instanceof HTMLInputElement)) {
|
|
238
|
+
throw new Error('File input not found');
|
|
239
|
+
}
|
|
240
|
+
const file = new File(['png'], 'chart.png', {type: 'image/png'});
|
|
241
|
+
await act(async () => {
|
|
242
|
+
fireEvent.change(input, {target: {files: [file]}});
|
|
243
|
+
await Promise.resolve();
|
|
244
|
+
});
|
|
245
|
+
await settle();
|
|
246
|
+
expect(onUploadImages).toHaveBeenCalledWith([file]);
|
|
247
|
+
expect(screen.getByTestId('pending').textContent).toBe('true');
|
|
248
|
+
|
|
249
|
+
await act(async () => {
|
|
250
|
+
finishUpload([{url: 'https://example.com/a.png', alt: 'Chart'}]);
|
|
251
|
+
await Promise.resolve();
|
|
252
|
+
});
|
|
253
|
+
await settle();
|
|
254
|
+
|
|
255
|
+
expect(screen.getByTestId('pending').textContent).toBe('false');
|
|
256
|
+
expect(container.querySelector('[aria-busy="true"]')).toBeNull();
|
|
257
|
+
});
|
|
258
|
+
|
|
203
259
|
it('uploads attached images and appends them as Markdown', async () => {
|
|
204
260
|
const onChange = vi.fn();
|
|
205
261
|
const onUploadImages = vi
|
|
@@ -239,6 +239,11 @@ export function CommentComposerBase({
|
|
|
239
239
|
latestValueRef.current = value;
|
|
240
240
|
const isMountedRef = useRef(true);
|
|
241
241
|
const actionPendingRef = useRef(false);
|
|
242
|
+
// The latest callback, read when reporting. An inline callback is new on
|
|
243
|
+
// every render; depending on it would re-run the unmount cleanup below on
|
|
244
|
+
// each render and leave the composer busy after its upload ends.
|
|
245
|
+
const onActionPendingChangeRef = useRef(onActionPendingChange);
|
|
246
|
+
onActionPendingChangeRef.current = onActionPendingChange;
|
|
242
247
|
const isSubmittingRef = useRef(isSubmitting);
|
|
243
248
|
isSubmittingRef.current = isSubmitting;
|
|
244
249
|
const imageInputRef = useRef<HTMLInputElement>(null);
|
|
@@ -269,27 +274,27 @@ export function CommentComposerBase({
|
|
|
269
274
|
const [isActionPending, setIsActionPending] = useState(false);
|
|
270
275
|
const [isPickerOpen, setIsPickerOpen] = useState(false);
|
|
271
276
|
|
|
272
|
-
const updateActionPending = useCallback(
|
|
273
|
-
(nextPending
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
277
|
+
const updateActionPending = useCallback((nextPending: boolean) => {
|
|
278
|
+
if (actionPendingRef.current === nextPending) {
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
actionPendingRef.current = nextPending;
|
|
282
|
+
setIsActionPending(nextPending);
|
|
283
|
+
onActionPendingChangeRef.current?.(nextPending);
|
|
284
|
+
}, []);
|
|
285
|
+
// Tracks mount for the upload's async completion, and on unmount releases
|
|
286
|
+
// the host's pending-action hold, which the upload's finally skips once
|
|
287
|
+
// unmounted.
|
|
283
288
|
useEffect(() => {
|
|
284
289
|
isMountedRef.current = true;
|
|
285
290
|
return () => {
|
|
286
291
|
isMountedRef.current = false;
|
|
287
292
|
if (actionPendingRef.current) {
|
|
288
293
|
actionPendingRef.current = false;
|
|
289
|
-
|
|
294
|
+
onActionPendingChangeRef.current?.(false);
|
|
290
295
|
}
|
|
291
296
|
};
|
|
292
|
-
}, [
|
|
297
|
+
}, []);
|
|
293
298
|
|
|
294
299
|
const uploadImages = async (files: readonly File[]) => {
|
|
295
300
|
if (onUploadImages == null || files.length === 0) {
|
|
@@ -107,7 +107,7 @@ export const docs = {
|
|
|
107
107
|
],
|
|
108
108
|
usage: {
|
|
109
109
|
description:
|
|
110
|
-
'Lab prototype for trying the touch PowerSearch flow. Choose it explicitly on touch layouts (for example `const Search = isTouch ? MobilePowerSearch : PowerSearch`); it never switches on pointer type by itself. The whole field is one Manage filters trigger showing display-only tokens. Tapping it opens a tall Filters sheet listing each filter with its value in bold and a direct trailing remove action, an Add filter icon button at the top right, and sticky Clear all and Done actions. One-tap boolean filters use the same trailing remove action as every other removable filter. When there are no filters, the centered Add filter action stands alone; the duplicate header add and footer Done controls are hidden. Clear all asks for confirmation, removes editable filters without closing the sheet, and preserves read-only filters. Opening the rest of a row edits that filter with Cancel and Apply. Add filter opens directly when no filter exists and leaves search unfocused until the user chooses it; when the config names a content-search field it carries that search with inline suggestions. Creating a filter drills field → operator (radios, only when a field has more than one) → value. A value editor that is the sheet’s only section sits directly on the sheet surface; multiple sections use separate muted containers. The previous step remains open beneath the current standalone BottomSheet, so Escape, a scrim press, or a downward swipe dismisses the current sheet and reveals that retained step; the Add filter header stays action-free. the stack keeps one scrim and blocks covered sheets. Single-value choices use persistent radio indicators and multi-value choices use persistent checkbox indicators. Searchable string and entity lists and date calendars remain inline in the value sheet; date-only and date-range selections honor timezoneID, while date-time operators keep their date-and-time input. Announcements made while a sheet is open are delivered inside that dialog. Filters remain keyed by identity across controlled reorder, stale filters stay visible and removable, and controlled removals re-home focus. The scroll viewport reaches the sheet edge while padded inner content aligns with the fixed header. Fields whose only operators are nested are left out with a development warning.',
|
|
110
|
+
'Lab prototype for trying the touch PowerSearch flow. Choose it explicitly on touch layouts (for example `const Search = isTouch ? MobilePowerSearch : PowerSearch`); it never switches on pointer type by itself. The whole field is one Manage filters trigger showing display-only tokens. Tapping it opens a tall Filters sheet listing each filter with its value in bold and a direct trailing remove action, an Add filter icon button at the top right, and sticky Clear all and Done actions. One-tap boolean filters use the same trailing remove action as every other removable filter. When there are no filters, the centered Add filter action stands alone; the duplicate header add and footer Done controls are hidden. Clear all asks for confirmation, removes editable filters without closing the sheet, and preserves read-only filters. Opening the rest of a row edits that filter with Cancel and Apply. Add filter opens directly when no filter exists and leaves search unfocused until the user chooses it; when the config names a content-search field it carries that search with inline suggestions. Creating a filter drills field → operator (radios, only when a field has more than one) → value. A value editor that is the sheet’s only section sits directly on the sheet surface; multiple sections use separate muted containers. The previous step remains open beneath the current standalone BottomSheet, so Escape, a scrim press, or a downward swipe dismisses the current sheet and reveals that retained step; the Add filter header stays action-free. the stack keeps one scrim and blocks covered sheets. Single-value choices use persistent radio indicators and multi-value choices use persistent checkbox indicators. Searchable string and entity lists and date calendars remain inline in the value sheet; date-only and date-range selections honor timezoneID, while date-time operators keep their date-and-time input. Announcements made while a sheet is open are delivered inside that dialog. Filters remain keyed by identity across controlled reorder, stale filters stay visible and removable, and controlled removals re-home focus. The scroll viewport reaches the sheet edge while padded inner content aligns with the fixed header. Dragging the results dismisses the search keyboard without clearing the query; filtering keeps search focused. Fields whose only operators are nested are left out with a development warning.',
|
|
111
111
|
bestPractices: [
|
|
112
112
|
{
|
|
113
113
|
guidance: true,
|
|
@@ -1009,11 +1009,16 @@ describe('MobilePowerSearch Add filter', () => {
|
|
|
1009
1009
|
sheet().querySelectorAll<HTMLElement>('div'),
|
|
1010
1010
|
).find(element => getComputedStyle(element).overflowY === 'auto')!;
|
|
1011
1011
|
fireEvent.scroll(scrollBody);
|
|
1012
|
+
expect(search).toHaveFocus();
|
|
1013
|
+
fireEvent.touchStart(scrollBody, {touches: [{clientY: 100}]});
|
|
1014
|
+
fireEvent.touchMove(scrollBody, {touches: [{clientY: 107}]});
|
|
1015
|
+
expect(search).toHaveFocus();
|
|
1016
|
+
fireEvent.touchMove(scrollBody, {touches: [{clientY: 109}]});
|
|
1012
1017
|
expect(search).not.toHaveFocus();
|
|
1013
1018
|
search.focus();
|
|
1014
|
-
fireEvent.touchMove(scrollBody);
|
|
1015
|
-
expect(search).not.toHaveFocus();
|
|
1016
1019
|
fireEvent.change(search, {target: {value: 'Field'}});
|
|
1020
|
+
fireEvent.scroll(scrollBody);
|
|
1021
|
+
expect(search).toHaveFocus();
|
|
1017
1022
|
expect(
|
|
1018
1023
|
within(sheet()).getAllByRole('button', {name: /^Field/}),
|
|
1019
1024
|
).toHaveLength(2);
|
|
@@ -5,11 +5,8 @@
|
|
|
5
5
|
/**
|
|
6
6
|
* @file MobilePowerSearch.tsx
|
|
7
7
|
* @input Core PowerSearch config, filters and onChange (the same props as
|
|
8
|
-
* @astryxdesign/core PowerSearch); public
|
|
9
|
-
*
|
|
10
|
-
* RadioList, Text, TextInput, Token, Tooltip hooks, i18n, hooks —
|
|
11
|
-
* useAnnounce for every announcement — utils, Layout container-padding
|
|
12
|
-
* styles)
|
|
8
|
+
* @astryxdesign/core PowerSearch); public Core primitives and private Lab
|
|
9
|
+
* mobile-sheet confirmation and search-drag dismissal owners
|
|
13
10
|
* @output Exports MobilePowerSearch — Lab prototype of the touch PowerSearch
|
|
14
11
|
* flow: the whole field is one "Manage filters" trigger with display-only
|
|
15
12
|
* tokens, a Filters sheet with editable rows and direct remove actions, Add
|
|
@@ -93,7 +90,6 @@ import * as stylex from '@stylexjs/stylex';
|
|
|
93
90
|
import type {BaseProps} from '@astryxdesign/core';
|
|
94
91
|
import {BottomSheet} from '@astryxdesign/core/BottomSheet';
|
|
95
92
|
import {Button} from '@astryxdesign/core/Button';
|
|
96
|
-
import {Dialog} from '@astryxdesign/core/Dialog';
|
|
97
93
|
import {EmptyState} from '@astryxdesign/core/EmptyState';
|
|
98
94
|
import {
|
|
99
95
|
Field,
|
|
@@ -107,7 +103,6 @@ import {
|
|
|
107
103
|
import {Heading} from '@astryxdesign/core/Heading';
|
|
108
104
|
import {Icon, renderIconSlot, type IconType} from '@astryxdesign/core/Icon';
|
|
109
105
|
import {IconButton} from '@astryxdesign/core/IconButton';
|
|
110
|
-
import {Layout, LayoutContent, LayoutFooter} from '@astryxdesign/core/Layout';
|
|
111
106
|
import {containerPaddingInlineVarStyles} from '@astryxdesign/core/Layout/padding.stylex';
|
|
112
107
|
import {List, ListItem} from '@astryxdesign/core/List';
|
|
113
108
|
import {resolveOperatorLabel} from '@astryxdesign/core/PowerSearch';
|
|
@@ -126,7 +121,6 @@ import {formatFilterValue} from '@astryxdesign/core/PowerSearch/utils';
|
|
|
126
121
|
import type {InternalConfig} from '@astryxdesign/core/PowerSearch/utils';
|
|
127
122
|
import {RadioList, RadioListItem} from '@astryxdesign/core/RadioList';
|
|
128
123
|
import {useSize} from '@astryxdesign/core/SizeContext';
|
|
129
|
-
import {Stack} from '@astryxdesign/core/Stack';
|
|
130
124
|
import {Text} from '@astryxdesign/core/Text';
|
|
131
125
|
import {TextInput} from '@astryxdesign/core/TextInput';
|
|
132
126
|
import {Token} from '@astryxdesign/core/Token';
|
|
@@ -148,6 +142,8 @@ import {
|
|
|
148
142
|
typographyVars,
|
|
149
143
|
} from '@astryxdesign/core/theme/tokens.stylex';
|
|
150
144
|
import {isImeKeyEvent, isRenderable, rtlStyles} from '@astryxdesign/core/utils';
|
|
145
|
+
import {MobileSheetConfirmation} from '../internal/MobileSheetConfirmation';
|
|
146
|
+
import {useDismissSearchOnTouchDrag} from '../internal/useDismissSearchOnTouchDrag';
|
|
151
147
|
import {MobilePowerSearchValueEditor} from './MobilePowerSearchValueEditor';
|
|
152
148
|
import {
|
|
153
149
|
createContentSearchSource,
|
|
@@ -207,7 +203,7 @@ export interface MobilePowerSearchProps extends Omit<
|
|
|
207
203
|
/** Validation status. */
|
|
208
204
|
status?: InputStatus;
|
|
209
205
|
/** How the status message is placed. @default 'attached' */
|
|
210
|
-
statusVariant?: FieldStatusVariant;
|
|
206
|
+
statusVariant?: FieldStatusVariant | 'tooltip';
|
|
211
207
|
/** Max display length for token values. @default 40 */
|
|
212
208
|
maxTokenLength?: number;
|
|
213
209
|
/** Max suggestions in string and entity value typeaheads. @default 10 */
|
|
@@ -257,63 +253,6 @@ function PlusIcon(props: SVGProps<SVGSVGElement>) {
|
|
|
257
253
|
);
|
|
258
254
|
}
|
|
259
255
|
|
|
260
|
-
function ClearFiltersConfirmation({
|
|
261
|
-
isOpen,
|
|
262
|
-
onOpenChange,
|
|
263
|
-
onConfirm,
|
|
264
|
-
}: {
|
|
265
|
-
isOpen: boolean;
|
|
266
|
-
onOpenChange: (isOpen: boolean) => void;
|
|
267
|
-
onConfirm: () => void;
|
|
268
|
-
}) {
|
|
269
|
-
const titleId = useId();
|
|
270
|
-
const descriptionId = useId();
|
|
271
|
-
return (
|
|
272
|
-
<Dialog
|
|
273
|
-
isOpen={isOpen}
|
|
274
|
-
onOpenChange={onOpenChange}
|
|
275
|
-
purpose="info"
|
|
276
|
-
role="alertdialog"
|
|
277
|
-
aria-labelledby={titleId}
|
|
278
|
-
aria-describedby={descriptionId}
|
|
279
|
-
width={400}>
|
|
280
|
-
<Layout
|
|
281
|
-
content={
|
|
282
|
-
<LayoutContent>
|
|
283
|
-
<Heading level={2} id={titleId}>
|
|
284
|
-
Clear all filters?
|
|
285
|
-
</Heading>
|
|
286
|
-
<Text type="body" color="secondary" id={descriptionId}>
|
|
287
|
-
This removes all editable filters from this search.
|
|
288
|
-
</Text>
|
|
289
|
-
</LayoutContent>
|
|
290
|
-
}
|
|
291
|
-
footer={
|
|
292
|
-
<LayoutFooter>
|
|
293
|
-
<Stack direction="vertical" gap={4} hAlign="stretch">
|
|
294
|
-
<Button
|
|
295
|
-
label="Clear filters"
|
|
296
|
-
variant="destructive"
|
|
297
|
-
size="lg"
|
|
298
|
-
onClick={onConfirm}
|
|
299
|
-
xstyle={styles.confirmationAction}
|
|
300
|
-
/>
|
|
301
|
-
<Button
|
|
302
|
-
label="Keep filters"
|
|
303
|
-
variant="secondary"
|
|
304
|
-
size="lg"
|
|
305
|
-
onClick={() => onOpenChange(false)}
|
|
306
|
-
data-autofocus
|
|
307
|
-
xstyle={styles.confirmationAction}
|
|
308
|
-
/>
|
|
309
|
-
</Stack>
|
|
310
|
-
</LayoutFooter>
|
|
311
|
-
}
|
|
312
|
-
/>
|
|
313
|
-
</Dialog>
|
|
314
|
-
);
|
|
315
|
-
}
|
|
316
|
-
|
|
317
256
|
const sizeStyles = stylex.create({
|
|
318
257
|
sm: {minHeight: sizeVars['--size-element-sm']},
|
|
319
258
|
md: {minHeight: sizeVars['--size-element-md']},
|
|
@@ -520,9 +459,6 @@ const styles = stylex.create({
|
|
|
520
459
|
trailingControlRow: {
|
|
521
460
|
flexDirection: 'row-reverse',
|
|
522
461
|
},
|
|
523
|
-
confirmationAction: {
|
|
524
|
-
width: '100%',
|
|
525
|
-
},
|
|
526
462
|
rowValue: {
|
|
527
463
|
fontWeight: fontWeightVars['--font-weight-bold'],
|
|
528
464
|
},
|
|
@@ -953,14 +889,10 @@ export function MobilePowerSearch({
|
|
|
953
889
|
|
|
954
890
|
// ------------------------------------------------------------------ focus --
|
|
955
891
|
|
|
956
|
-
const
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
if (activeElement === contentSearch || activeElement === fieldSearch) {
|
|
961
|
-
(activeElement as HTMLInputElement).blur();
|
|
962
|
-
}
|
|
963
|
-
}, []);
|
|
892
|
+
const fieldPickerDragProps = useDismissSearchOnTouchDrag(() => [
|
|
893
|
+
contentSearchRef.current?.querySelector('input'),
|
|
894
|
+
fieldSearchRef.current?.querySelector('input'),
|
|
895
|
+
]);
|
|
964
896
|
|
|
965
897
|
const focusPendingSheetTarget = useCallback(() => {
|
|
966
898
|
const target = pendingSheetFocusRef.current;
|
|
@@ -2132,10 +2064,7 @@ export function MobilePowerSearch({
|
|
|
2132
2064
|
</div>
|
|
2133
2065
|
) : null}
|
|
2134
2066
|
</div>
|
|
2135
|
-
<div
|
|
2136
|
-
{...scrollViewportProps}
|
|
2137
|
-
onScroll={blurFieldPickerSearch}
|
|
2138
|
-
onTouchMove={blurFieldPickerSearch}>
|
|
2067
|
+
<div {...scrollViewportProps} {...fieldPickerDragProps}>
|
|
2139
2068
|
<div {...scrollContentProps}>
|
|
2140
2069
|
{hasContentSearch ? (
|
|
2141
2070
|
contentResults.length > 0 ? (
|
|
@@ -2427,10 +2356,14 @@ export function MobilePowerSearch({
|
|
|
2427
2356
|
</div>
|
|
2428
2357
|
</BottomSheet>
|
|
2429
2358
|
|
|
2430
|
-
<
|
|
2359
|
+
<MobileSheetConfirmation
|
|
2431
2360
|
isOpen={isClearConfirmationOpen}
|
|
2432
2361
|
onOpenChange={setIsClearConfirmationOpen}
|
|
2433
|
-
|
|
2362
|
+
title="Clear all filters?"
|
|
2363
|
+
description="This removes all editable filters from this search."
|
|
2364
|
+
actionLabel="Clear filters"
|
|
2365
|
+
cancelLabel="Keep filters"
|
|
2366
|
+
onAction={() => {
|
|
2434
2367
|
handleClearAll();
|
|
2435
2368
|
setIsClearConfirmationOpen(false);
|
|
2436
2369
|
}}
|
|
@@ -11,7 +11,7 @@ export const docs = {
|
|
|
11
11
|
{name: 'isLabelHidden', type: 'boolean', description: 'Hide the visible label; the trigger and the sheet keep it as their accessible name.', default: 'false'},
|
|
12
12
|
{name: 'searchSource', type: 'SearchSource<T>', description: 'Core Typeahead search source (search + bootstrap).', required: true},
|
|
13
13
|
{name: 'value', type: 'T[]', description: 'Selected items (Core SearchableItem).', required: true},
|
|
14
|
-
{name: 'onChange', type: '(items: T[], change: MobileTokenizerChange<T>) => void', description: '
|
|
14
|
+
{name: 'onChange', type: '(items: T[], change: MobileTokenizerChange<T>) => void', description: 'Called by Save when the selection draft differs from the opening value. Receives the complete committed selection plus the latest add/create/remove metadata.', required: true},
|
|
15
15
|
{name: 'renderItem', type: '(item: T) => ReactNode', description: 'Custom content for a result row, shown beside its checkbox. Default: the item label.'},
|
|
16
16
|
{name: 'placeholder', type: 'string', description: 'Trigger text when nothing is selected.'},
|
|
17
17
|
{name: 'description', type: 'string', description: 'Help text under the field, read with the trigger.'},
|
|
@@ -24,14 +24,14 @@ export const docs = {
|
|
|
24
24
|
],
|
|
25
25
|
usage: {
|
|
26
26
|
description:
|
|
27
|
-
'Lab prototype for trying the touch Tokenizer flow: tap the field to open one searchable sheet where selected and available items share a full-row checkbox list. Selected items are grouped first when the sheet opens, while checkbox changes keep every existing row in place until the sheet closes; the latest selection is regrouped the next time it opens. Selected custom values stay in the list, and long result sets render progressively in 50-item batches. Custom text uses a trailing Add action, and
|
|
27
|
+
'Lab prototype for trying the touch Tokenizer flow: tap the field to open one searchable sheet where selected and available items share a full-row checkbox list. Selection changes remain in a draft until Save commits them; swipe, scrim, or Escape dismissal asks before discarding a dirty draft, while a clean dismissal closes directly. Selected items are grouped first when the sheet opens, while checkbox changes keep every existing row in place until the sheet closes; the latest selection is regrouped the next time it opens. Selected custom values stay in the list, and long result sets render progressively in 50-item batches. Custom text uses a trailing Add action. The bordered large search stays above the list, and scrolling or dragging results dismisses the software keyboard without clearing the query. Clear all and Save remain visible while searching. Clear all updates the draft only and requires confirmation; a confirmed clear returns focus to the search field and is announced inside the sheet. Every announcement — additions, removals, result counts — is delivered inside the open sheet. The trigger describes its committed selection to assistive technology (count, then the tokens) after the field description, status, and disabled reason. Strings come from the shared Astryx catalog.',
|
|
28
28
|
bestPractices: [
|
|
29
29
|
{guidance: true, description: 'Try this in Lab/canary to validate the flow; graduate via Core Tokenizer presentation="adaptive" when it ships.'},
|
|
30
30
|
{guidance: false, description: 'Do not ship stable product on this Lab API; it has no theming or spec contract yet.'},
|
|
31
31
|
],
|
|
32
32
|
anatomy: [
|
|
33
33
|
{name: 'Trigger field', required: true, description: 'Button showing tokens as a summary, in the inline Tokenizer field\'s own box: same border, radius, surface, block size and token inset as the desktop field.'},
|
|
34
|
-
{name: 'Management sheet', required: true, description: 'One searchable list with trailing checkboxes for existing items, a trailing Add action for custom text, and equal-width Clear all and
|
|
34
|
+
{name: 'Management sheet', required: true, description: 'One searchable list with trailing checkboxes for existing items, a trailing Add action for custom text, and persistent equal-width Clear all and Save footer actions. Save commits the draft; dirty dismissal asks before discarding. Nothing of the sheet is in the page while it is closed.'},
|
|
35
35
|
],
|
|
36
36
|
},
|
|
37
37
|
};
|