@astryxdesign/lab 0.7.7-canary.f4edef8 → 0.7.7-canary.f88f5dc
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/MobilePowerSearch/MobilePowerSearch.d.ts.map +1 -1
- package/dist/MobilePowerSearch/MobilePowerSearch.js +5 -4
- package/dist/MobileTokenizer/MobileTokenizer.d.ts +1 -1
- package/dist/MobileTokenizer/MobileTokenizer.d.ts.map +1 -1
- package/dist/MobileTokenizer/MobileTokenizer.js +10 -37
- package/package.json +5 -5
- package/src/MobilePowerSearch/MobilePowerSearch.test.tsx +8 -0
- package/src/MobilePowerSearch/MobilePowerSearch.tsx +18 -7
- package/src/MobileTokenizer/MobileTokenizer.test.tsx +3 -5
- package/src/MobileTokenizer/MobileTokenizer.tsx +25 -51
- package/dist/internal/MobileSheetConfirmation.d.ts +0 -12
- package/dist/internal/MobileSheetConfirmation.d.ts.map +0 -1
- package/dist/internal/MobileSheetConfirmation.js +0 -88
- package/dist/internal/useDismissSearchOnTouchDrag.d.ts +0 -21
- package/dist/internal/useDismissSearchOnTouchDrag.d.ts.map +0 -1
- package/dist/internal/useDismissSearchOnTouchDrag.js +0 -43
- package/src/internal/MobileSheetConfirmation.test.tsx +0 -68
- package/src/internal/MobileSheetConfirmation.tsx +0 -97
- package/src/internal/useDismissSearchOnTouchDrag.test.tsx +0 -94
- package/src/internal/useDismissSearchOnTouchDrag.ts +0 -57
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MobilePowerSearch.d.ts","sourceRoot":"","sources":["../../src/MobilePowerSearch/MobilePowerSearch.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsEG;AAEH,OAAO,KAAK,EAAE,EASZ,KAAK,SAAS,EAEf,MAAM,OAAO,CAAC;AAEf,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,oBAAoB,CAAC;AAIlD,OAAO,EAML,KAAK,kBAAkB,EACvB,KAAK,WAAW,EACjB,MAAM,0BAA0B,CAAC;AAElC,OAAO,EAAuB,KAAK,QAAQ,EAAC,MAAM,yBAAyB,CAAC;AAK5E,OAAO,KAAK,EAEV,qBAAqB,EACrB,qBAAqB,EACrB,iBAAiB,EAEjB,iBAAiB,EACjB,iBAAiB,EAEjB,eAAe,EAChB,MAAM,gCAAgC,CAAC;
|
|
1
|
+
{"version":3,"file":"MobilePowerSearch.d.ts","sourceRoot":"","sources":["../../src/MobilePowerSearch/MobilePowerSearch.tsx"],"names":[],"mappings":"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"}
|
|
@@ -97,8 +97,7 @@ import { useLocale, useTranslator } from '@astryxdesign/core/i18n';
|
|
|
97
97
|
import '@astryxdesign/core/theme/tokens.stylex';
|
|
98
98
|
import { colorVars, fontWeightVars, radiusVars, sizeVars, spacingVars, typeScaleVars, typographyVars } from '@astryxdesign/core/theme/tokens.stylex';
|
|
99
99
|
import { isImeKeyEvent, isRenderable, rtlStyles } from '@astryxdesign/core/utils';
|
|
100
|
-
import {
|
|
101
|
-
import { useDismissSearchOnTouchDrag } from "../internal/useDismissSearchOnTouchDrag.js";
|
|
100
|
+
import { SheetConfirmation, useDismissSearchOnTouchDrag } from '@astryxdesign/core/internal/touch-sheet';
|
|
102
101
|
import { MobilePowerSearchValueEditor } from "./MobilePowerSearchValueEditor.js";
|
|
103
102
|
import { createContentSearchSource, createFilterIdentities, fieldMatchesQuery, filterSignature, isSupportedOperator, nearestEditableIndex, resolveFilterIdentityIndex, useMobilePowerSearchConfig } from "./mobilePowerSearchUtils.js";
|
|
104
103
|
|
|
@@ -493,7 +492,9 @@ export function MobilePowerSearch({
|
|
|
493
492
|
|
|
494
493
|
// ------------------------------------------------------------------ focus --
|
|
495
494
|
|
|
496
|
-
|
|
495
|
+
// A drag moves focus to the sheet's panel, never to the page body behind
|
|
496
|
+
// the open modal.
|
|
497
|
+
const fieldPickerDragProps = useDismissSearchOnTouchDrag(() => [contentSearchRef.current?.querySelector('input'), fieldSearchRef.current?.querySelector('input')], () => (fieldSearchRef.current ?? contentSearchRef.current)?.closest('[tabindex="-1"]'));
|
|
497
498
|
const focusPendingSheetTarget = useCallback(() => {
|
|
498
499
|
const target = pendingSheetFocusRef.current;
|
|
499
500
|
if (target == null) {
|
|
@@ -1721,7 +1722,7 @@ export function MobilePowerSearch({
|
|
|
1721
1722
|
})]
|
|
1722
1723
|
})]
|
|
1723
1724
|
})
|
|
1724
|
-
}), /*#__PURE__*/_jsx(
|
|
1725
|
+
}), /*#__PURE__*/_jsx(SheetConfirmation, {
|
|
1725
1726
|
isOpen: isClearConfirmationOpen,
|
|
1726
1727
|
onOpenChange: setIsClearConfirmationOpen,
|
|
1727
1728
|
title: "Clear all filters?",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file MobileTokenizer.tsx
|
|
3
3
|
* @input Public @astryxdesign/core components and hooks, plus private Lab
|
|
4
|
-
*
|
|
4
|
+
* touch-sheet confirmation and search-drag dismissal from Core's internal entry
|
|
5
5
|
* @output Exports MobileTokenizer — Lab prototype of the touch Tokenizer
|
|
6
6
|
* flow with the inline-compatible closed trigger, a searchable draft-selection
|
|
7
7
|
* sheet, persistent Clear all / Save footer, dirty-dismiss confirmation,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MobileTokenizer.d.ts","sourceRoot":"","sources":["../../src/MobileTokenizer/MobileTokenizer.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,EASL,KAAK,SAAS,EAEf,MAAM,OAAO,CAAC;AAEf,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,oBAAoB,CAAC;AAclD,OAAO,KAAK,EAAC,cAAc,EAAE,YAAY,EAAC,MAAM,8BAA8B,CAAC;
|
|
1
|
+
{"version":3,"file":"MobileTokenizer.d.ts","sourceRoot":"","sources":["../../src/MobileTokenizer/MobileTokenizer.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,EASL,KAAK,SAAS,EAEf,MAAM,OAAO,CAAC;AAEf,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,oBAAoB,CAAC;AAclD,OAAO,KAAK,EAAC,cAAc,EAAE,YAAY,EAAC,MAAM,8BAA8B,CAAC;AAiB/E,MAAM,MAAM,qBAAqB,CAAC,CAAC,SAAS,cAAc,IACtD;IAAC,IAAI,EAAE,CAAC,CAAC;IAAC,IAAI,EAAE,KAAK,CAAA;CAAC,GACtB;IAAC,IAAI,EAAE,CAAC,CAAC;IAAC,IAAI,EAAE,QAAQ,CAAA;CAAC,GACzB;IAAC,IAAI,EAAE,CAAC,CAAC;IAAC,IAAI,EAAE,QAAQ,CAAA;CAAC,GACzB;IAAC,IAAI,EAAE,SAAS,CAAA;CAAC,CAAC;AAEtB,MAAM,WAAW,oBAAoB,CAAC,CAAC,SAAS,cAAc,CAAE,SAAQ,IAAI,CAC1E,SAAS,CAAC,cAAc,CAAC,EACzB,UAAU,CACX;IACC,KAAK,EAAE,MAAM,CAAC;IACd;;;OAGG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,YAAY,EAAE,YAAY,CAAC,CAAC,CAAC,CAAC;IAC9B,KAAK,EAAE,CAAC,EAAE,CAAC;IACX,QAAQ,EAAE,CAAC,KAAK,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,qBAAqB,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC;IACjE;;;OAGG;IACH,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,SAAS,CAAC;IACpC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,8DAA8D;IAC9D,MAAM,CAAC,EAAE;QAAC,IAAI,EAAE,OAAO,GAAG,SAAS,GAAG,SAAS,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAC,CAAC;IAClE;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,2EAA2E;IAC3E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACzC;AAwLD,wBAAgB,eAAe,CAAC,CAAC,SAAS,cAAc,EAAE,EACxD,KAAK,EACL,aAAa,EACb,YAAY,EACZ,KAAK,EACL,QAAQ,EACR,UAAU,EACV,WAAW,EACX,WAAW,EACX,MAAM,EACN,eAAe,EACf,QAAQ,EACR,UAAU,EACV,UAAkB,EAClB,SAAiB,EACjB,UAAU,EACV,YAAiB,EACjB,cAAkB,EAClB,UAAgB,EAChB,sBAAsB,EACtB,aAAa,EACb,MAAM,EACN,EAAE,EACF,SAAS,EACT,KAAK,EACL,YAAY,EAAE,aAAa,EAC3B,kBAAkB,EAAE,mBAAmB,EACvC,OAAO,EAAE,aAAa,EACtB,OAAO,EAAE,aAAa,EACtB,MAAM,EAAE,YAAY,EACpB,GAAG,IAAI,EACR,EAAE,oBAAoB,CAAC,CAAC,CAAC,qBAwPzB;yBAvRe,eAAe"}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
/**
|
|
6
6
|
* @file MobileTokenizer.tsx
|
|
7
7
|
* @input Public @astryxdesign/core components and hooks, plus private Lab
|
|
8
|
-
*
|
|
8
|
+
* touch-sheet confirmation and search-drag dismissal from Core's internal entry
|
|
9
9
|
* @output Exports MobileTokenizer — Lab prototype of the touch Tokenizer
|
|
10
10
|
* flow with the inline-compatible closed trigger, a searchable draft-selection
|
|
11
11
|
* sheet, persistent Clear all / Save footer, dirty-dismiss confirmation,
|
|
@@ -53,8 +53,7 @@ import { composeEventHandlers, mergeProps } from '@astryxdesign/core/utils';
|
|
|
53
53
|
import { VisuallyHidden } from '@astryxdesign/core/VisuallyHidden';
|
|
54
54
|
import '@astryxdesign/core/theme/tokens.stylex';
|
|
55
55
|
import { colorVars, radiusVars, sizeVars, spacingVars } from '@astryxdesign/core/theme/tokens.stylex';
|
|
56
|
-
import {
|
|
57
|
-
import { useDismissSearchOnTouchDrag } from "../internal/useDismissSearchOnTouchDrag.js";
|
|
56
|
+
import { SheetConfirmation, hasSameSelection, nameSheetDraftChange, useDismissSearchOnTouchDrag } from '@astryxdesign/core/internal/touch-sheet';
|
|
58
57
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
59
58
|
const CREATABLE_ID_PREFIX = '__xds_create__';
|
|
60
59
|
const LIST_RENDER_BATCH_SIZE = 50;
|
|
@@ -64,9 +63,6 @@ const LIST_LOAD_MORE_THRESHOLD_PX = 200;
|
|
|
64
63
|
// concentric with the field's (radius-2) when inset = radius-2 − radius-1 −
|
|
65
64
|
// border = 3px. The same number is the row gap, so wrapped rows read evenly.
|
|
66
65
|
const TOKEN_ROW_INSET = `calc(${spacingVars['--spacing-1']} - 1px)`;
|
|
67
|
-
function hasSameSelection(a, b) {
|
|
68
|
-
return a.length === b.length && a.every(item => b.some(candidate => candidate.id === item.id));
|
|
69
|
-
}
|
|
70
66
|
const styles = {
|
|
71
67
|
trigger: {
|
|
72
68
|
kzqmXN: "xh8yej3",
|
|
@@ -657,34 +653,9 @@ function MobileTokenizerSheetContent({
|
|
|
657
653
|
if (isDraftOverMax) {
|
|
658
654
|
return;
|
|
659
655
|
}
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
const lastDraftChange = lastDraftChangeRef.current;
|
|
664
|
-
const lastChangeIsNet = lastDraftChange != null && (lastDraftChange.type === 'remove' ? valueAtOpen.some(item => item.id === lastDraftChange.item.id) && !draftValue.some(item => item.id === lastDraftChange.item.id) : draftValue.some(item => item.id === lastDraftChange.item.id) && !valueAtOpen.some(item => item.id === lastDraftChange.item.id));
|
|
665
|
-
let change = null;
|
|
666
|
-
if (lastChangeIsNet) {
|
|
667
|
-
change = lastDraftChange;
|
|
668
|
-
} else if (lastDraftChange?.type === 'remove' && removed != null) {
|
|
669
|
-
change = {
|
|
670
|
-
item: removed,
|
|
671
|
-
type: 'remove'
|
|
672
|
-
};
|
|
673
|
-
} else if (added != null) {
|
|
674
|
-
const type = lastDraftChange?.item.id === added.id && lastDraftChange.type === 'create' ? 'create' : 'add';
|
|
675
|
-
change = {
|
|
676
|
-
item: added,
|
|
677
|
-
type
|
|
678
|
-
};
|
|
679
|
-
} else if (removed != null) {
|
|
680
|
-
change = {
|
|
681
|
-
item: removed,
|
|
682
|
-
type: 'remove'
|
|
683
|
-
};
|
|
684
|
-
}
|
|
685
|
-
if (change != null) {
|
|
686
|
-
onCommit([...draftValue], change);
|
|
687
|
-
}
|
|
656
|
+
const change = isDraftDirty ? nameSheetDraftChange(valueAtOpen, draftValue, lastDraftChangeRef.current) : null;
|
|
657
|
+
if (change != null) {
|
|
658
|
+
onCommit([...draftValue], change);
|
|
688
659
|
}
|
|
689
660
|
onCloseSheet();
|
|
690
661
|
};
|
|
@@ -692,7 +663,9 @@ function MobileTokenizerSheetContent({
|
|
|
692
663
|
label
|
|
693
664
|
});
|
|
694
665
|
const noResultsTitle = emptySearchResultsText ?? t('@astryx.typeahead.emptySearchResults');
|
|
695
|
-
|
|
666
|
+
// A drag moves focus to the sheet's panel, never to the page body behind
|
|
667
|
+
// the open modal.
|
|
668
|
+
const listDragProps = useDismissSearchOnTouchDrag(() => [searchRef.current], () => searchRef.current?.closest('[tabindex="-1"]'));
|
|
696
669
|
return /*#__PURE__*/_jsxs("div", {
|
|
697
670
|
...{
|
|
698
671
|
className: "x78zum5 xdt5ytf x5yr21d x2lwn1j xrrkdod xoxd1wu xvmdzux"
|
|
@@ -881,7 +854,7 @@ function MobileTokenizerSheetContent({
|
|
|
881
854
|
onClick: handleSave,
|
|
882
855
|
xstyle: styles.footerAction
|
|
883
856
|
})]
|
|
884
|
-
}), isClearConfirmationOpen && /*#__PURE__*/_jsx(
|
|
857
|
+
}), isClearConfirmationOpen && /*#__PURE__*/_jsx(SheetConfirmation, {
|
|
885
858
|
isOpen: true,
|
|
886
859
|
onOpenChange: next => {
|
|
887
860
|
if (!next) {
|
|
@@ -905,7 +878,7 @@ function MobileTokenizerSheetContent({
|
|
|
905
878
|
}
|
|
906
879
|
closeClearConfirmation(true);
|
|
907
880
|
}
|
|
908
|
-
}), isDiscardConfirmationOpen && /*#__PURE__*/_jsx(
|
|
881
|
+
}), isDiscardConfirmationOpen && /*#__PURE__*/_jsx(SheetConfirmation, {
|
|
909
882
|
isOpen: true,
|
|
910
883
|
onOpenChange: next => {
|
|
911
884
|
if (!next) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astryxdesign/lab",
|
|
3
|
-
"version": "0.7.7-canary.
|
|
3
|
+
"version": "0.7.7-canary.f88f5dc",
|
|
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.7-canary.
|
|
52
|
+
"@astryxdesign/core": "0.7.7-canary.f88f5dc",
|
|
53
53
|
"react": ">=19.0.0",
|
|
54
54
|
"react-dom": ">=19.0.0"
|
|
55
55
|
},
|
|
@@ -59,9 +59,9 @@
|
|
|
59
59
|
"d3-shape": "^3.2.0"
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
|
62
|
-
"@astryxdesign/a11y-spec": "0.7.7-canary.
|
|
63
|
-
"@astryxdesign/cli": "0.7.7-canary.
|
|
64
|
-
"@astryxdesign/interaction-harness": "0.7.7-canary.
|
|
62
|
+
"@astryxdesign/a11y-spec": "0.7.7-canary.f88f5dc",
|
|
63
|
+
"@astryxdesign/cli": "0.7.7-canary.f88f5dc",
|
|
64
|
+
"@astryxdesign/interaction-harness": "0.7.7-canary.f88f5dc",
|
|
65
65
|
"@babel/cli": "^7.29.7",
|
|
66
66
|
"@babel/core": "^7.29.7",
|
|
67
67
|
"@babel/preset-react": "^7.29.7",
|
|
@@ -1859,6 +1859,10 @@ describe('MobilePowerSearch content search', () => {
|
|
|
1859
1859
|
expect(
|
|
1860
1860
|
within(sheet()).getByRole('heading', {name: 'Filters'}),
|
|
1861
1861
|
).toBeTruthy();
|
|
1862
|
+
// The search sheet held focus as it closed, so words sent with the close
|
|
1863
|
+
// wait for its exit to end (component:BottomSheet FR9, spec:AST-086
|
|
1864
|
+
// FR14); settle that exit.
|
|
1865
|
+
act(() => settleSheets());
|
|
1862
1866
|
// The add is announced inside the sheet the flow is now on, not on the
|
|
1863
1867
|
// page body, which assistive technology does not expose under a native
|
|
1864
1868
|
// modal dialog.
|
|
@@ -1893,6 +1897,10 @@ describe('MobilePowerSearch content search', () => {
|
|
|
1893
1897
|
within(sheet()).getByRole('button', {name: '"release notes"'}),
|
|
1894
1898
|
);
|
|
1895
1899
|
await waitFor(() => expect(isSheetOpen()).toBe(true));
|
|
1900
|
+
// The search sheet held focus as it closed, so words sent with the close
|
|
1901
|
+
// wait for its exit to end (component:BottomSheet FR9, spec:AST-086
|
|
1902
|
+
// FR14); settle that exit.
|
|
1903
|
+
act(() => settleSheets());
|
|
1896
1904
|
await waitFor(() =>
|
|
1897
1905
|
expect(sheetRegion().textContent).toBe(
|
|
1898
1906
|
'Added "release notes" 20 results',
|
|
@@ -142,8 +142,11 @@ import {
|
|
|
142
142
|
typographyVars,
|
|
143
143
|
} from '@astryxdesign/core/theme/tokens.stylex';
|
|
144
144
|
import {isImeKeyEvent, isRenderable, rtlStyles} from '@astryxdesign/core/utils';
|
|
145
|
-
|
|
146
|
-
import {
|
|
145
|
+
|
|
146
|
+
import {
|
|
147
|
+
SheetConfirmation,
|
|
148
|
+
useDismissSearchOnTouchDrag,
|
|
149
|
+
} from '@astryxdesign/core/internal/touch-sheet';
|
|
147
150
|
import {MobilePowerSearchValueEditor} from './MobilePowerSearchValueEditor';
|
|
148
151
|
import {
|
|
149
152
|
createContentSearchSource,
|
|
@@ -889,10 +892,18 @@ export function MobilePowerSearch({
|
|
|
889
892
|
|
|
890
893
|
// ------------------------------------------------------------------ focus --
|
|
891
894
|
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
895
|
+
// A drag moves focus to the sheet's panel, never to the page body behind
|
|
896
|
+
// the open modal.
|
|
897
|
+
const fieldPickerDragProps = useDismissSearchOnTouchDrag(
|
|
898
|
+
() => [
|
|
899
|
+
contentSearchRef.current?.querySelector('input'),
|
|
900
|
+
fieldSearchRef.current?.querySelector('input'),
|
|
901
|
+
],
|
|
902
|
+
() =>
|
|
903
|
+
(
|
|
904
|
+
fieldSearchRef.current ?? contentSearchRef.current
|
|
905
|
+
)?.closest<HTMLElement>('[tabindex="-1"]'),
|
|
906
|
+
);
|
|
896
907
|
|
|
897
908
|
const focusPendingSheetTarget = useCallback(() => {
|
|
898
909
|
const target = pendingSheetFocusRef.current;
|
|
@@ -2356,7 +2367,7 @@ export function MobilePowerSearch({
|
|
|
2356
2367
|
</div>
|
|
2357
2368
|
</BottomSheet>
|
|
2358
2369
|
|
|
2359
|
-
<
|
|
2370
|
+
<SheetConfirmation
|
|
2360
2371
|
isOpen={isClearConfirmationOpen}
|
|
2361
2372
|
onOpenChange={setIsClearConfirmationOpen}
|
|
2362
2373
|
title="Clear all filters?"
|
|
@@ -1248,6 +1248,7 @@ describe('MobileTokenizer Core-sheet parity', () => {
|
|
|
1248
1248
|
);
|
|
1249
1249
|
}
|
|
1250
1250
|
render(<Flow />);
|
|
1251
|
+
const trigger = screen.getByRole('button', {name: /Tags/});
|
|
1251
1252
|
const sheet = await openSheet();
|
|
1252
1253
|
// Pending from open until the bootstrap settles.
|
|
1253
1254
|
expect(within(sheet).getByRole('img', {name: 'Loading'})).toBeTruthy();
|
|
@@ -1264,11 +1265,8 @@ describe('MobileTokenizer Core-sheet parity', () => {
|
|
|
1264
1265
|
// The field becomes disabled while open: the sheet closes and the query
|
|
1265
1266
|
// is kept; re-enabled and reopened, it starts from "en", settled.
|
|
1266
1267
|
fireEvent.click(screen.getByRole('button', {name: 'Toggle'}));
|
|
1267
|
-
expect(
|
|
1268
|
-
|
|
1269
|
-
'false',
|
|
1270
|
-
);
|
|
1271
|
-
act(() => screen.getByRole('button', {name: /Tags/}).focus());
|
|
1268
|
+
expect(trigger).toHaveAttribute('aria-expanded', 'false');
|
|
1269
|
+
act(() => trigger.focus());
|
|
1272
1270
|
await waitFor(() => expect(screen.queryByRole('dialog')).toBeNull());
|
|
1273
1271
|
fireEvent.click(screen.getByRole('button', {name: 'Toggle'}));
|
|
1274
1272
|
const reopened = await openSheet();
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
/**
|
|
6
6
|
* @file MobileTokenizer.tsx
|
|
7
7
|
* @input Public @astryxdesign/core components and hooks, plus private Lab
|
|
8
|
-
*
|
|
8
|
+
* touch-sheet confirmation and search-drag dismissal from Core's internal entry
|
|
9
9
|
* @output Exports MobileTokenizer — Lab prototype of the touch Tokenizer
|
|
10
10
|
* flow with the inline-compatible closed trigger, a searchable draft-selection
|
|
11
11
|
* sheet, persistent Clear all / Save footer, dirty-dismiss confirmation,
|
|
@@ -72,8 +72,13 @@ import {
|
|
|
72
72
|
sizeVars,
|
|
73
73
|
spacingVars,
|
|
74
74
|
} from '@astryxdesign/core/theme/tokens.stylex';
|
|
75
|
-
import {
|
|
76
|
-
|
|
75
|
+
import {
|
|
76
|
+
SheetConfirmation,
|
|
77
|
+
hasSameSelection,
|
|
78
|
+
nameSheetDraftChange,
|
|
79
|
+
useDismissSearchOnTouchDrag,
|
|
80
|
+
type SheetDraftChange,
|
|
81
|
+
} from '@astryxdesign/core/internal/touch-sheet';
|
|
77
82
|
|
|
78
83
|
export type MobileTokenizerChange<T extends SearchableItem> =
|
|
79
84
|
| {item: T; type: 'add'}
|
|
@@ -130,18 +135,6 @@ const LIST_LOAD_MORE_THRESHOLD_PX = 200;
|
|
|
130
135
|
// border = 3px. The same number is the row gap, so wrapped rows read evenly.
|
|
131
136
|
const TOKEN_ROW_INSET = `calc(${spacingVars['--spacing-1']} - 1px)`;
|
|
132
137
|
|
|
133
|
-
type SheetDraftChange<T> = {
|
|
134
|
-
item: T;
|
|
135
|
-
type: 'add' | 'create' | 'remove';
|
|
136
|
-
};
|
|
137
|
-
|
|
138
|
-
function hasSameSelection<T extends SearchableItem>(a: T[], b: T[]): boolean {
|
|
139
|
-
return (
|
|
140
|
-
a.length === b.length &&
|
|
141
|
-
a.every(item => b.some(candidate => candidate.id === item.id))
|
|
142
|
-
);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
138
|
const styles = stylex.create({
|
|
146
139
|
// The closed trigger is the inline Tokenizer field's own control box, as a
|
|
147
140
|
// button — the same `inputWrapperStyles.base` box Core's sheet trigger
|
|
@@ -1018,39 +1011,15 @@ function MobileTokenizerSheetContent<T extends SearchableItem>({
|
|
|
1018
1011
|
if (isDraftOverMax) {
|
|
1019
1012
|
return;
|
|
1020
1013
|
}
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
lastDraftChange != null &&
|
|
1031
|
-
(lastDraftChange.type === 'remove'
|
|
1032
|
-
? valueAtOpen.some(item => item.id === lastDraftChange.item.id) &&
|
|
1033
|
-
!draftValue.some(item => item.id === lastDraftChange.item.id)
|
|
1034
|
-
: draftValue.some(item => item.id === lastDraftChange.item.id) &&
|
|
1035
|
-
!valueAtOpen.some(item => item.id === lastDraftChange.item.id));
|
|
1036
|
-
let change: SheetDraftChange<T> | null = null;
|
|
1037
|
-
if (lastChangeIsNet) {
|
|
1038
|
-
change = lastDraftChange;
|
|
1039
|
-
} else if (lastDraftChange?.type === 'remove' && removed != null) {
|
|
1040
|
-
change = {item: removed, type: 'remove'};
|
|
1041
|
-
} else if (added != null) {
|
|
1042
|
-
const type: SheetDraftChange<T>['type'] =
|
|
1043
|
-
lastDraftChange?.item.id === added.id &&
|
|
1044
|
-
lastDraftChange.type === 'create'
|
|
1045
|
-
? 'create'
|
|
1046
|
-
: 'add';
|
|
1047
|
-
change = {item: added, type};
|
|
1048
|
-
} else if (removed != null) {
|
|
1049
|
-
change = {item: removed, type: 'remove'};
|
|
1050
|
-
}
|
|
1051
|
-
if (change != null) {
|
|
1052
|
-
onCommit([...draftValue], change);
|
|
1053
|
-
}
|
|
1014
|
+
const change = isDraftDirty
|
|
1015
|
+
? nameSheetDraftChange(
|
|
1016
|
+
valueAtOpen,
|
|
1017
|
+
draftValue,
|
|
1018
|
+
lastDraftChangeRef.current,
|
|
1019
|
+
)
|
|
1020
|
+
: null;
|
|
1021
|
+
if (change != null) {
|
|
1022
|
+
onCommit([...draftValue], change);
|
|
1054
1023
|
}
|
|
1055
1024
|
onCloseSheet();
|
|
1056
1025
|
};
|
|
@@ -1058,7 +1027,12 @@ function MobileTokenizerSheetContent<T extends SearchableItem>({
|
|
|
1058
1027
|
const searchLabel = t('@astryx.tokenizer.search', {label});
|
|
1059
1028
|
const noResultsTitle =
|
|
1060
1029
|
emptySearchResultsText ?? t('@astryx.typeahead.emptySearchResults');
|
|
1061
|
-
|
|
1030
|
+
// A drag moves focus to the sheet's panel, never to the page body behind
|
|
1031
|
+
// the open modal.
|
|
1032
|
+
const listDragProps = useDismissSearchOnTouchDrag(
|
|
1033
|
+
() => [searchRef.current],
|
|
1034
|
+
() => searchRef.current?.closest<HTMLElement>('[tabindex="-1"]'),
|
|
1035
|
+
);
|
|
1062
1036
|
|
|
1063
1037
|
return (
|
|
1064
1038
|
<div {...stylex.props(styles.sheetBody)}>
|
|
@@ -1245,7 +1219,7 @@ function MobileTokenizerSheetContent<T extends SearchableItem>({
|
|
|
1245
1219
|
/>
|
|
1246
1220
|
</div>
|
|
1247
1221
|
{isClearConfirmationOpen && (
|
|
1248
|
-
<
|
|
1222
|
+
<SheetConfirmation
|
|
1249
1223
|
isOpen
|
|
1250
1224
|
onOpenChange={next => {
|
|
1251
1225
|
if (!next) {
|
|
@@ -1270,7 +1244,7 @@ function MobileTokenizerSheetContent<T extends SearchableItem>({
|
|
|
1270
1244
|
/>
|
|
1271
1245
|
)}
|
|
1272
1246
|
{isDiscardConfirmationOpen && (
|
|
1273
|
-
<
|
|
1247
|
+
<SheetConfirmation
|
|
1274
1248
|
isOpen
|
|
1275
1249
|
onOpenChange={next => {
|
|
1276
1250
|
if (!next) {
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
interface MobileSheetConfirmationProps {
|
|
2
|
-
isOpen: boolean;
|
|
3
|
-
onOpenChange: (isOpen: boolean) => void;
|
|
4
|
-
title: string;
|
|
5
|
-
description: string;
|
|
6
|
-
cancelLabel: string;
|
|
7
|
-
actionLabel: string;
|
|
8
|
-
onAction: () => void;
|
|
9
|
-
}
|
|
10
|
-
export declare function MobileSheetConfirmation({ isOpen, onOpenChange, title, description, cancelLabel, actionLabel, onAction, }: MobileSheetConfirmationProps): import("react").JSX.Element;
|
|
11
|
-
export {};
|
|
12
|
-
//# sourceMappingURL=MobileSheetConfirmation.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"MobileSheetConfirmation.d.ts","sourceRoot":"","sources":["../../src/internal/MobileSheetConfirmation.tsx"],"names":[],"mappings":"AA0BA,UAAU,4BAA4B;IACpC,MAAM,EAAE,OAAO,CAAC;IAChB,YAAY,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;IACxC,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,IAAI,CAAC;CACtB;AAMD,wBAAgB,uBAAuB,CAAC,EACtC,MAAM,EACN,YAAY,EACZ,KAAK,EACL,WAAW,EACX,WAAW,EACX,WAAW,EACX,QAAQ,GACT,EAAE,4BAA4B,+BAgD9B"}
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
-
|
|
3
|
-
'use client';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* @file MobileSheetConfirmation.tsx
|
|
7
|
-
* @input Public Core Dialog, layout, text and action primitives; caller-owned copy
|
|
8
|
-
* @output Private confirmation shared by MobileTokenizer and MobilePowerSearch
|
|
9
|
-
* @position Lab-only mobile sheet confirmation owner; not a public export
|
|
10
|
-
*
|
|
11
|
-
* Core AlertDialog fixes its purpose to form and its safe action to ghost.
|
|
12
|
-
* These Lab flows instead allow scrim dismissal and use separated, full-width
|
|
13
|
-
* large actions with the secondary safe action focused first. Keep that
|
|
14
|
-
* difference here, while preserving the existing alert-dialog theme target.
|
|
15
|
-
*/
|
|
16
|
-
import { useId } from 'react';
|
|
17
|
-
import * as stylex from '@stylexjs/stylex';
|
|
18
|
-
import { Button } from '@astryxdesign/core/Button';
|
|
19
|
-
import { Dialog } from '@astryxdesign/core/Dialog';
|
|
20
|
-
import { Heading } from '@astryxdesign/core/Heading';
|
|
21
|
-
import { Layout, LayoutContent, LayoutFooter } from '@astryxdesign/core/Layout';
|
|
22
|
-
import { Stack } from '@astryxdesign/core/Stack';
|
|
23
|
-
import { Text } from '@astryxdesign/core/Text';
|
|
24
|
-
import { themeProps } from '@astryxdesign/core/utils';
|
|
25
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
26
|
-
const styles = {
|
|
27
|
-
action: {
|
|
28
|
-
kzqmXN: "xh8yej3",
|
|
29
|
-
$$css: true
|
|
30
|
-
}
|
|
31
|
-
};
|
|
32
|
-
export function MobileSheetConfirmation({
|
|
33
|
-
isOpen,
|
|
34
|
-
onOpenChange,
|
|
35
|
-
title,
|
|
36
|
-
description,
|
|
37
|
-
cancelLabel,
|
|
38
|
-
actionLabel,
|
|
39
|
-
onAction
|
|
40
|
-
}) {
|
|
41
|
-
const titleId = useId();
|
|
42
|
-
const descriptionId = useId();
|
|
43
|
-
return /*#__PURE__*/_jsx(Dialog, {
|
|
44
|
-
...themeProps('alert-dialog'),
|
|
45
|
-
isOpen: isOpen,
|
|
46
|
-
onOpenChange: onOpenChange,
|
|
47
|
-
purpose: "info",
|
|
48
|
-
role: "alertdialog",
|
|
49
|
-
"aria-labelledby": titleId,
|
|
50
|
-
"aria-describedby": descriptionId,
|
|
51
|
-
width: 400,
|
|
52
|
-
children: /*#__PURE__*/_jsx(Layout, {
|
|
53
|
-
content: /*#__PURE__*/_jsxs(LayoutContent, {
|
|
54
|
-
children: [/*#__PURE__*/_jsx(Heading, {
|
|
55
|
-
level: 2,
|
|
56
|
-
id: titleId,
|
|
57
|
-
children: title
|
|
58
|
-
}), /*#__PURE__*/_jsx(Text, {
|
|
59
|
-
type: "body",
|
|
60
|
-
color: "secondary",
|
|
61
|
-
id: descriptionId,
|
|
62
|
-
children: description
|
|
63
|
-
})]
|
|
64
|
-
}),
|
|
65
|
-
footer: /*#__PURE__*/_jsx(LayoutFooter, {
|
|
66
|
-
children: /*#__PURE__*/_jsxs(Stack, {
|
|
67
|
-
direction: "vertical",
|
|
68
|
-
gap: 4,
|
|
69
|
-
hAlign: "stretch",
|
|
70
|
-
children: [/*#__PURE__*/_jsx(Button, {
|
|
71
|
-
label: actionLabel,
|
|
72
|
-
variant: "destructive",
|
|
73
|
-
size: "lg",
|
|
74
|
-
onClick: onAction,
|
|
75
|
-
xstyle: styles.action
|
|
76
|
-
}), /*#__PURE__*/_jsx(Button, {
|
|
77
|
-
label: cancelLabel,
|
|
78
|
-
variant: "secondary",
|
|
79
|
-
size: "lg",
|
|
80
|
-
onClick: () => onOpenChange(false),
|
|
81
|
-
"data-autofocus": true,
|
|
82
|
-
xstyle: styles.action
|
|
83
|
-
})]
|
|
84
|
-
})
|
|
85
|
-
})
|
|
86
|
-
})
|
|
87
|
-
});
|
|
88
|
-
}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file useDismissSearchOnTouchDrag.ts
|
|
3
|
-
* @input Caller-owned search inputs and touch events from a separate results list
|
|
4
|
-
* @output Private touch handlers shared by the Lab mobile search sheets
|
|
5
|
-
* @position One owner for results-drag keyboard dismissal; not a public export
|
|
6
|
-
*
|
|
7
|
-
* A direct vertical touch drag dismisses only a focused caller-owned search.
|
|
8
|
-
* Scroll events are deliberately not observed: filtering can clamp scrollTop
|
|
9
|
-
* and fire scroll while someone is typing. Small moves, taps and multi-touch
|
|
10
|
-
* gestures leave focus alone. The hook acquires no external resource.
|
|
11
|
-
*/
|
|
12
|
-
import { type TouchEvent } from 'react';
|
|
13
|
-
type SearchInput = HTMLInputElement | null | undefined;
|
|
14
|
-
export declare function useDismissSearchOnTouchDrag(getSearchInputs: () => readonly SearchInput[]): {
|
|
15
|
-
onTouchStart: (event: TouchEvent<HTMLElement>) => void;
|
|
16
|
-
onTouchMove: (event: TouchEvent<HTMLElement>) => void;
|
|
17
|
-
onTouchEnd: () => void;
|
|
18
|
-
onTouchCancel: () => void;
|
|
19
|
-
};
|
|
20
|
-
export {};
|
|
21
|
-
//# sourceMappingURL=useDismissSearchOnTouchDrag.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"useDismissSearchOnTouchDrag.d.ts","sourceRoot":"","sources":["../../src/internal/useDismissSearchOnTouchDrag.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;GAUG;AAEH,OAAO,EAAS,KAAK,UAAU,EAAC,MAAM,OAAO,CAAC;AAI9C,KAAK,WAAW,GAAG,gBAAgB,GAAG,IAAI,GAAG,SAAS,CAAC;AAEvD,wBAAgB,2BAA2B,CACzC,eAAe,EAAE,MAAM,SAAS,WAAW,EAAE;0BAQrB,UAAU,CAAC,WAAW,CAAC;yBAIxB,UAAU,CAAC,WAAW,CAAC;;;EAqB/C"}
|
|
@@ -1,43 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
-
|
|
3
|
-
import {fireEvent, render, screen, within} from '@testing-library/react';
|
|
4
|
-
import {beforeEach, describe, expect, it, vi} from 'vitest';
|
|
5
|
-
import {MobileSheetConfirmation} from './MobileSheetConfirmation';
|
|
6
|
-
|
|
7
|
-
// jsdom has no native modal presentation; keep the public Dialog path active.
|
|
8
|
-
beforeEach(() => {
|
|
9
|
-
HTMLDialogElement.prototype.showModal = vi.fn(function (
|
|
10
|
-
this: HTMLDialogElement,
|
|
11
|
-
) {
|
|
12
|
-
this.setAttribute('open', '');
|
|
13
|
-
});
|
|
14
|
-
HTMLDialogElement.prototype.close = vi.fn(function (this: HTMLDialogElement) {
|
|
15
|
-
this.removeAttribute('open');
|
|
16
|
-
});
|
|
17
|
-
window.scrollTo = vi.fn();
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
function setup() {
|
|
21
|
-
const onAction = vi.fn();
|
|
22
|
-
const onOpenChange = vi.fn();
|
|
23
|
-
render(
|
|
24
|
-
<MobileSheetConfirmation
|
|
25
|
-
isOpen
|
|
26
|
-
onOpenChange={onOpenChange}
|
|
27
|
-
title="Discard changes?"
|
|
28
|
-
description="The draft will be removed."
|
|
29
|
-
actionLabel="Discard"
|
|
30
|
-
cancelLabel="Keep editing"
|
|
31
|
-
onAction={onAction}
|
|
32
|
-
/>,
|
|
33
|
-
);
|
|
34
|
-
const dialog = screen.getByRole('alertdialog', {name: 'Discard changes?'});
|
|
35
|
-
return {dialog, onAction, onOpenChange};
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
describe('MobileSheetConfirmation', () => {
|
|
39
|
-
it('keeps the alert-dialog theme target and accessible copy', () => {
|
|
40
|
-
const {dialog} = setup();
|
|
41
|
-
expect(dialog).toHaveClass('astryx-alert-dialog');
|
|
42
|
-
expect(dialog).toHaveAccessibleDescription('The draft will be removed.');
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it('stacks full-width large actions with the secondary safe action focused', () => {
|
|
46
|
-
const {dialog} = setup();
|
|
47
|
-
const [destructive, safe] = within(dialog).getAllByRole('button');
|
|
48
|
-
expect(destructive).toHaveAccessibleName('Discard');
|
|
49
|
-
expect(destructive).toHaveAttribute('data-variant', 'destructive');
|
|
50
|
-
expect(safe).toHaveAccessibleName('Keep editing');
|
|
51
|
-
expect(safe).toHaveAttribute('data-variant', 'secondary');
|
|
52
|
-
for (const button of [destructive, safe]) {
|
|
53
|
-
expect(button).toHaveAttribute('data-size', 'lg');
|
|
54
|
-
expect(getComputedStyle(button).width).toBe('100%');
|
|
55
|
-
}
|
|
56
|
-
expect(getComputedStyle(safe.parentElement!).flexDirection).toBe('column');
|
|
57
|
-
expect(safe).toHaveFocus();
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it('reports action and safe dismissal independently without committing on cancel', () => {
|
|
61
|
-
const {dialog, onAction, onOpenChange} = setup();
|
|
62
|
-
fireEvent.click(within(dialog).getByRole('button', {name: 'Keep editing'}));
|
|
63
|
-
expect(onOpenChange).toHaveBeenCalledWith(false);
|
|
64
|
-
expect(onAction).not.toHaveBeenCalled();
|
|
65
|
-
fireEvent.click(within(dialog).getByRole('button', {name: 'Discard'}));
|
|
66
|
-
expect(onAction).toHaveBeenCalledTimes(1);
|
|
67
|
-
});
|
|
68
|
-
});
|
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
-
|
|
3
|
-
'use client';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* @file MobileSheetConfirmation.tsx
|
|
7
|
-
* @input Public Core Dialog, layout, text and action primitives; caller-owned copy
|
|
8
|
-
* @output Private confirmation shared by MobileTokenizer and MobilePowerSearch
|
|
9
|
-
* @position Lab-only mobile sheet confirmation owner; not a public export
|
|
10
|
-
*
|
|
11
|
-
* Core AlertDialog fixes its purpose to form and its safe action to ghost.
|
|
12
|
-
* These Lab flows instead allow scrim dismissal and use separated, full-width
|
|
13
|
-
* large actions with the secondary safe action focused first. Keep that
|
|
14
|
-
* difference here, while preserving the existing alert-dialog theme target.
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import {useId} from 'react';
|
|
18
|
-
import * as stylex from '@stylexjs/stylex';
|
|
19
|
-
import {Button} from '@astryxdesign/core/Button';
|
|
20
|
-
import {Dialog} from '@astryxdesign/core/Dialog';
|
|
21
|
-
import {Heading} from '@astryxdesign/core/Heading';
|
|
22
|
-
import {Layout, LayoutContent, LayoutFooter} from '@astryxdesign/core/Layout';
|
|
23
|
-
import {Stack} from '@astryxdesign/core/Stack';
|
|
24
|
-
import {Text} from '@astryxdesign/core/Text';
|
|
25
|
-
import {themeProps} from '@astryxdesign/core/utils';
|
|
26
|
-
|
|
27
|
-
interface MobileSheetConfirmationProps {
|
|
28
|
-
isOpen: boolean;
|
|
29
|
-
onOpenChange: (isOpen: boolean) => void;
|
|
30
|
-
title: string;
|
|
31
|
-
description: string;
|
|
32
|
-
cancelLabel: string;
|
|
33
|
-
actionLabel: string;
|
|
34
|
-
onAction: () => void;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const styles = stylex.create({
|
|
38
|
-
action: {width: '100%'},
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
export function MobileSheetConfirmation({
|
|
42
|
-
isOpen,
|
|
43
|
-
onOpenChange,
|
|
44
|
-
title,
|
|
45
|
-
description,
|
|
46
|
-
cancelLabel,
|
|
47
|
-
actionLabel,
|
|
48
|
-
onAction,
|
|
49
|
-
}: MobileSheetConfirmationProps) {
|
|
50
|
-
const titleId = useId();
|
|
51
|
-
const descriptionId = useId();
|
|
52
|
-
return (
|
|
53
|
-
<Dialog
|
|
54
|
-
{...themeProps('alert-dialog')}
|
|
55
|
-
isOpen={isOpen}
|
|
56
|
-
onOpenChange={onOpenChange}
|
|
57
|
-
purpose="info"
|
|
58
|
-
role="alertdialog"
|
|
59
|
-
aria-labelledby={titleId}
|
|
60
|
-
aria-describedby={descriptionId}
|
|
61
|
-
width={400}>
|
|
62
|
-
<Layout
|
|
63
|
-
content={
|
|
64
|
-
<LayoutContent>
|
|
65
|
-
<Heading level={2} id={titleId}>
|
|
66
|
-
{title}
|
|
67
|
-
</Heading>
|
|
68
|
-
<Text type="body" color="secondary" id={descriptionId}>
|
|
69
|
-
{description}
|
|
70
|
-
</Text>
|
|
71
|
-
</LayoutContent>
|
|
72
|
-
}
|
|
73
|
-
footer={
|
|
74
|
-
<LayoutFooter>
|
|
75
|
-
<Stack direction="vertical" gap={4} hAlign="stretch">
|
|
76
|
-
<Button
|
|
77
|
-
label={actionLabel}
|
|
78
|
-
variant="destructive"
|
|
79
|
-
size="lg"
|
|
80
|
-
onClick={onAction}
|
|
81
|
-
xstyle={styles.action}
|
|
82
|
-
/>
|
|
83
|
-
<Button
|
|
84
|
-
label={cancelLabel}
|
|
85
|
-
variant="secondary"
|
|
86
|
-
size="lg"
|
|
87
|
-
onClick={() => onOpenChange(false)}
|
|
88
|
-
data-autofocus
|
|
89
|
-
xstyle={styles.action}
|
|
90
|
-
/>
|
|
91
|
-
</Stack>
|
|
92
|
-
</LayoutFooter>
|
|
93
|
-
}
|
|
94
|
-
/>
|
|
95
|
-
</Dialog>
|
|
96
|
-
);
|
|
97
|
-
}
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
-
|
|
3
|
-
import {useRef} from 'react';
|
|
4
|
-
import {fireEvent, render, screen} from '@testing-library/react';
|
|
5
|
-
import userEvent from '@testing-library/user-event';
|
|
6
|
-
import {describe, expect, it, vi} from 'vitest';
|
|
7
|
-
import {useDismissSearchOnTouchDrag} from './useDismissSearchOnTouchDrag';
|
|
8
|
-
|
|
9
|
-
function Harness({hasSecondSearch = false}: {hasSecondSearch?: boolean}) {
|
|
10
|
-
const firstRef = useRef<HTMLInputElement>(null);
|
|
11
|
-
const secondRef = useRef<HTMLInputElement>(null);
|
|
12
|
-
const dragProps = useDismissSearchOnTouchDrag(() => [
|
|
13
|
-
firstRef.current,
|
|
14
|
-
secondRef.current,
|
|
15
|
-
]);
|
|
16
|
-
return (
|
|
17
|
-
<>
|
|
18
|
-
<input ref={firstRef} aria-label="First search" />
|
|
19
|
-
{hasSecondSearch && <input ref={secondRef} aria-label="Second search" />}
|
|
20
|
-
<input aria-label="Unrelated field" />
|
|
21
|
-
<div role="group" aria-label="Results" {...dragProps} />
|
|
22
|
-
</>
|
|
23
|
-
);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const touches = (clientY: number) => [{clientY}];
|
|
27
|
-
|
|
28
|
-
function setup() {
|
|
29
|
-
render(<Harness hasSecondSearch />);
|
|
30
|
-
const search = screen.getByRole('textbox', {name: 'First search'});
|
|
31
|
-
const results = screen.getByRole('group', {name: 'Results'});
|
|
32
|
-
search.focus();
|
|
33
|
-
return {search, results};
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
describe('useDismissSearchOnTouchDrag', () => {
|
|
37
|
-
it('preserves typing across programmatic result scrolling', async () => {
|
|
38
|
-
const {search, results} = setup();
|
|
39
|
-
fireEvent.scroll(results, {target: {scrollTop: 300}});
|
|
40
|
-
await userEvent.setup().type(search, 'Eng');
|
|
41
|
-
fireEvent.scroll(results, {target: {scrollTop: 0}});
|
|
42
|
-
expect(search).toHaveFocus();
|
|
43
|
-
expect(search).toHaveValue('Eng');
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it('blurs a focused owned search once at the direct vertical drag threshold', () => {
|
|
47
|
-
const {search, results} = setup();
|
|
48
|
-
const blur = vi.spyOn(search, 'blur');
|
|
49
|
-
fireEvent.touchStart(results, {touches: touches(100)});
|
|
50
|
-
fireEvent.touchMove(results, {touches: touches(107)});
|
|
51
|
-
expect(search).toHaveFocus();
|
|
52
|
-
fireEvent.touchMove(results, {touches: touches(108)});
|
|
53
|
-
expect(search).not.toHaveFocus();
|
|
54
|
-
fireEvent.touchMove(results, {touches: touches(120)});
|
|
55
|
-
expect(blur).toHaveBeenCalledTimes(1);
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it('does not blur on an unowned touch move, tap, or horizontal motion', () => {
|
|
59
|
-
const {search, results} = setup();
|
|
60
|
-
fireEvent.touchMove(results, {touches: touches(120)});
|
|
61
|
-
expect(search).toHaveFocus();
|
|
62
|
-
fireEvent.touchStart(results, {touches: [{clientY: 100, clientX: 0}]});
|
|
63
|
-
fireEvent.touchMove(results, {touches: [{clientY: 100, clientX: 100}]});
|
|
64
|
-
fireEvent.touchEnd(results);
|
|
65
|
-
fireEvent.touchMove(results, {touches: touches(120)});
|
|
66
|
-
expect(search).toHaveFocus();
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it('resets ownership on cancellation and multi-touch without dismissing', () => {
|
|
70
|
-
const {search, results} = setup();
|
|
71
|
-
fireEvent.touchStart(results, {touches: touches(100)});
|
|
72
|
-
fireEvent.touchCancel(results);
|
|
73
|
-
fireEvent.touchMove(results, {touches: touches(120)});
|
|
74
|
-
expect(search).toHaveFocus();
|
|
75
|
-
fireEvent.touchStart(results, {touches: touches(100)});
|
|
76
|
-
fireEvent.touchMove(results, {touches: [{clientY: 120}, {clientY: 140}]});
|
|
77
|
-
fireEvent.touchMove(results, {touches: touches(150)});
|
|
78
|
-
expect(search).toHaveFocus();
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it('can dismiss either current owned input, but never an unrelated field', () => {
|
|
82
|
-
const {results} = setup();
|
|
83
|
-
const secondSearch = screen.getByRole('textbox', {name: 'Second search'});
|
|
84
|
-
secondSearch.focus();
|
|
85
|
-
fireEvent.touchStart(results, {touches: touches(100)});
|
|
86
|
-
fireEvent.touchMove(results, {touches: touches(90)});
|
|
87
|
-
expect(secondSearch).not.toHaveFocus();
|
|
88
|
-
const unrelated = screen.getByRole('textbox', {name: 'Unrelated field'});
|
|
89
|
-
unrelated.focus();
|
|
90
|
-
fireEvent.touchStart(results, {touches: touches(100)});
|
|
91
|
-
fireEvent.touchMove(results, {touches: touches(120)});
|
|
92
|
-
expect(unrelated).toHaveFocus();
|
|
93
|
-
});
|
|
94
|
-
});
|
|
@@ -1,57 +0,0 @@
|
|
|
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
|
-
|
|
17
|
-
import {useRef, type TouchEvent} from 'react';
|
|
18
|
-
|
|
19
|
-
const DRAG_THRESHOLD_PX = 8;
|
|
20
|
-
|
|
21
|
-
type SearchInput = HTMLInputElement | null | undefined;
|
|
22
|
-
|
|
23
|
-
export function useDismissSearchOnTouchDrag(
|
|
24
|
-
getSearchInputs: () => readonly SearchInput[],
|
|
25
|
-
) {
|
|
26
|
-
const startYRef = useRef<number | null>(null);
|
|
27
|
-
const reset = () => {
|
|
28
|
-
startYRef.current = null;
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
return {
|
|
32
|
-
onTouchStart: (event: TouchEvent<HTMLElement>) => {
|
|
33
|
-
startYRef.current =
|
|
34
|
-
event.touches.length === 1 ? event.touches[0].clientY : null;
|
|
35
|
-
},
|
|
36
|
-
onTouchMove: (event: TouchEvent<HTMLElement>) => {
|
|
37
|
-
if (event.touches.length !== 1) {
|
|
38
|
-
reset();
|
|
39
|
-
return;
|
|
40
|
-
}
|
|
41
|
-
const startY = startYRef.current;
|
|
42
|
-
if (
|
|
43
|
-
startY == null ||
|
|
44
|
-
Math.abs(event.touches[0].clientY - startY) < DRAG_THRESHOLD_PX
|
|
45
|
-
) {
|
|
46
|
-
return;
|
|
47
|
-
}
|
|
48
|
-
reset();
|
|
49
|
-
const focusedSearch = getSearchInputs().find(
|
|
50
|
-
input => input != null && input === document.activeElement,
|
|
51
|
-
);
|
|
52
|
-
focusedSearch?.blur();
|
|
53
|
-
},
|
|
54
|
-
onTouchEnd: reset,
|
|
55
|
-
onTouchCancel: reset,
|
|
56
|
-
};
|
|
57
|
-
}
|