@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.
@@ -1 +1 @@
1
- {"version":3,"file":"MobilePowerSearch.d.ts","sourceRoot":"","sources":["../../src/MobilePowerSearch/MobilePowerSearch.tsx"],"names":[],"mappings":"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"}
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 { MobileSheetConfirmation } from "../internal/MobileSheetConfirmation.js";
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
- const fieldPickerDragProps = useDismissSearchOnTouchDrag(() => [contentSearchRef.current?.querySelector('input'), fieldSearchRef.current?.querySelector('input')]);
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(MobileSheetConfirmation, {
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
- * mobile-sheet confirmation and search-drag dismissal owners
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;AAY/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;AAoMD,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"}
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
- * mobile-sheet confirmation and search-drag dismissal owners
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 { MobileSheetConfirmation } from "../internal/MobileSheetConfirmation.js";
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
- if (isDraftDirty) {
661
- const added = draftValue.find(item => !valueAtOpen.some(openItem => openItem.id === item.id));
662
- const removed = valueAtOpen.find(item => !draftValue.some(draftItem => draftItem.id === item.id));
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
- const listDragProps = useDismissSearchOnTouchDrag(() => [searchRef.current]);
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(MobileSheetConfirmation, {
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(MobileSheetConfirmation, {
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.f4edef8",
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.f4edef8",
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.f4edef8",
63
- "@astryxdesign/cli": "0.7.7-canary.f4edef8",
64
- "@astryxdesign/interaction-harness": "0.7.7-canary.f4edef8",
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
- import {MobileSheetConfirmation} from '../internal/MobileSheetConfirmation';
146
- import {useDismissSearchOnTouchDrag} from '../internal/useDismissSearchOnTouchDrag';
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
- const fieldPickerDragProps = useDismissSearchOnTouchDrag(() => [
893
- contentSearchRef.current?.querySelector('input'),
894
- fieldSearchRef.current?.querySelector('input'),
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
- <MobileSheetConfirmation
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(screen.getByRole('button', {name: /Tags/})).toHaveAttribute(
1268
- 'aria-expanded',
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
- * mobile-sheet confirmation and search-drag dismissal owners
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 {MobileSheetConfirmation} from '../internal/MobileSheetConfirmation';
76
- import {useDismissSearchOnTouchDrag} from '../internal/useDismissSearchOnTouchDrag';
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
- if (isDraftDirty) {
1022
- const added = draftValue.find(
1023
- item => !valueAtOpen.some(openItem => openItem.id === item.id),
1024
- );
1025
- const removed = valueAtOpen.find(
1026
- item => !draftValue.some(draftItem => draftItem.id === item.id),
1027
- );
1028
- const lastDraftChange = lastDraftChangeRef.current;
1029
- const lastChangeIsNet =
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
- const listDragProps = useDismissSearchOnTouchDrag(() => [searchRef.current]);
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
- <MobileSheetConfirmation
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
- <MobileSheetConfirmation
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
- }