@astryxdesign/lab 0.7.5-canary.45b0afd → 0.7.5-canary.4bcd84f

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":"ListInput.d.ts","sourceRoot":"","sources":["../../src/ListInput/ListInput.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ListInput.d.ts","sourceRoot":"","sources":["../../src/ListInput/ListInput.tsx"],"names":[],"mappings":"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"}
@@ -476,6 +476,25 @@ export function ListInput({
476
476
  announce(t('@astryx.listInput.announceReorderCancelled'));
477
477
  }
478
478
  }, [isDisabled, isLoading, announce, t]);
479
+
480
+ // A second pointer that lands anywhere during a pointer reorder (the second
481
+ // finger of a pinch) cancels the reorder. A pinch whose first finger slid
482
+ // first zooms without a pointercancel, so the drag would otherwise commit on
483
+ // release and drop the row at the end of the list.
484
+ useEffect(() => {
485
+ if (!isPointerReorderActive) {
486
+ return;
487
+ }
488
+ const onPointerDown = event => {
489
+ const activeState = reorderStateRef.current;
490
+ if (activeState?.mode === 'pointer' && activeState.pointerId !== event.pointerId) {
491
+ setReorderState(null);
492
+ announce(t('@astryx.listInput.announceReorderCancelled'));
493
+ }
494
+ };
495
+ document.addEventListener('pointerdown', onPointerDown, true);
496
+ return () => document.removeEventListener('pointerdown', onPointerDown, true);
497
+ }, [isPointerReorderActive, announce, t]);
479
498
  useEffect(() => {
480
499
  const activeAnimations = activeMutationAnimationsRef.current;
481
500
  return () => {
@@ -1 +1 @@
1
- {"version":3,"file":"TransferList.d.ts","sourceRoot":"","sources":["../../src/TransferList/TransferList.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"TransferList.d.ts","sourceRoot":"","sources":["../../src/TransferList/TransferList.tsx"],"names":[],"mappings":"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"}
@@ -655,6 +655,26 @@ export function TransferList({
655
655
  event.currentTarget.releasePointerCapture?.(event.pointerId);
656
656
  }
657
657
  }, [finishReorder]);
658
+
659
+ // A second pointer that lands anywhere during a pointer reorder (the second
660
+ // finger of a pinch) cancels the reorder. A pinch whose first finger slid
661
+ // first zooms without a pointercancel, so the drag would otherwise commit on
662
+ // release and drop the row at the end of the list.
663
+ const isPointerReorderActive = previewCloneValue != null;
664
+ useEffect(() => {
665
+ if (!isPointerReorderActive) {
666
+ return;
667
+ }
668
+ const onPointerDown = event => {
669
+ const session = reorderSessionRef.current;
670
+ if (session?.mode === 'pointer' && session.pointerId !== event.pointerId) {
671
+ suppressClickRef.current = false;
672
+ finishReorder(true);
673
+ }
674
+ };
675
+ document.addEventListener('pointerdown', onPointerDown, true);
676
+ return () => document.removeEventListener('pointerdown', onPointerDown, true);
677
+ }, [isPointerReorderActive, finishReorder]);
658
678
  const handleSearch = useCallback(nextQuery => {
659
679
  setQuery(nextQuery);
660
680
  const normalized = nextQuery.trim().toLowerCase();
package/dist/lab.css CHANGED
@@ -544,7 +544,7 @@
544
544
  .xkrqix3:not(#\#):not(#\#):not(#\#){text-decoration-line:none}
545
545
  .xlyipyv:not(#\#):not(#\#):not(#\#){text-overflow:ellipsis}
546
546
  .xtvhhri:not(#\#):not(#\#):not(#\#){text-transform:uppercase}
547
- .x5ve5x3:not(#\#):not(#\#):not(#\#){touch-action:none}
547
+ .x1j4r261:not(#\#):not(#\#):not(#\#){touch-action:pinch-zoom}
548
548
  .x1bd091l:not(#\#):not(#\#):not(#\#){transform-origin:left bottom}
549
549
  .x9tu13d:not(#\#):not(#\#):not(#\#){transform:rotate(-90deg)}
550
550
  .x19jd1h0:not(#\#):not(#\#):not(#\#){transform:rotate(180deg)}
@@ -59,11 +59,11 @@ export declare const reorderStyles: Readonly<{
59
59
  }>;
60
60
  readonly handle: Readonly<{
61
61
  readonly cursor: stylex.StyleXClassNameFor<"cursor", "default" | "grab">;
62
- readonly touchAction: stylex.StyleXClassNameFor<"touchAction", "none">;
62
+ readonly touchAction: stylex.StyleXClassNameFor<"touchAction", "pinch-zoom">;
63
63
  }>;
64
64
  readonly handleActive: Readonly<{
65
65
  readonly cursor: stylex.StyleXClassNameFor<"cursor", "default" | "grabbing">;
66
- readonly touchAction: stylex.StyleXClassNameFor<"touchAction", "none">;
66
+ readonly touchAction: stylex.StyleXClassNameFor<"touchAction", "pinch-zoom">;
67
67
  }>;
68
68
  }>;
69
69
  //# sourceMappingURL=reorderStyles.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"reorderStyles.d.ts","sourceRoot":"","sources":["../src/reorderStyles.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AAEH,OAAO,KAAK,MAAM,MAAM,kBAAkB,CAAC;AAoB3C;;;GAGG;AACH,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAgDxB,CAAC"}
1
+ {"version":3,"file":"reorderStyles.d.ts","sourceRoot":"","sources":["../src/reorderStyles.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AAEH,OAAO,KAAK,MAAM,MAAM,kBAAkB,CAAC;AAoB3C;;;GAGG;AACH,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAqDxB,CAAC"}
@@ -71,12 +71,12 @@ export const reorderStyles = {
71
71
  },
72
72
  handle: {
73
73
  kkrTdU: "x1jm3nie x16khyan",
74
- kFalU9: "x5ve5x3",
74
+ kFalU9: "x1j4r261",
75
75
  $$css: true
76
76
  },
77
77
  handleActive: {
78
78
  kkrTdU: "xi9pz9s x16khyan",
79
- kFalU9: "x5ve5x3",
79
+ kFalU9: "x1j4r261",
80
80
  $$css: true
81
81
  }
82
82
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astryxdesign/lab",
3
- "version": "0.7.5-canary.45b0afd",
3
+ "version": "0.7.5-canary.4bcd84f",
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.5-canary.45b0afd",
52
+ "@astryxdesign/core": "0.7.5-canary.4bcd84f",
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.5-canary.45b0afd",
63
- "@astryxdesign/cli": "0.7.5-canary.45b0afd",
62
+ "@astryxdesign/a11y-spec": "0.7.5-canary.4bcd84f",
63
+ "@astryxdesign/cli": "0.7.5-canary.4bcd84f",
64
64
  "@babel/cli": "^7.29.7",
65
65
  "@babel/core": "^7.29.7",
66
66
  "@babel/preset-react": "^7.29.7",
@@ -74,14 +74,14 @@
74
74
  },
75
75
  "scripts": {
76
76
  "build": "node ../../scripts/clean-dist.mjs && pnpm build:esm && tsc --project tsconfig.build.json && node ../../scripts/add-declaration-extensions.mjs && pnpm build:css && node ../../scripts/check-no-dev-jsx.mjs && node ../../scripts/check-fully-specified.mjs",
77
- "build:esm": "babel src --out-dir dist --extensions .ts,.tsx --out-file-extension .js --ignore \"**/*.test.ts,**/*.test.tsx,**/__tests__/**,**/*.perf.test.*,**/*.doc.mjs,**/*.d.ts,**/*.css,**/*.css.d.ts\" --config-file ./babel.config.json",
77
+ "build:esm": "babel src --out-dir dist --extensions .ts,.tsx --out-file-extension .js --ignore \"**/*.test.ts,**/*.test.tsx,**/__tests__/**,**/*.perf.test.*,**/*.doc.mjs,**/*.d.ts,**/*.css,**/*.css.d.ts\" --config-file ./babel.config.cjs",
78
78
  "build:css": "node ../../scripts/build-css.mjs --package lab",
79
- "dev": "babel src --watch --out-dir dist --extensions .ts,.tsx --out-file-extension .js --ignore \"**/*.test.*,**/*.doc.mjs,**/*.d.ts,**/*.css*\" --config-file ./babel.config.json",
79
+ "dev": "babel src --watch --out-dir dist --extensions .ts,.tsx --out-file-extension .js --ignore \"**/*.test.*,**/*.doc.mjs,**/*.d.ts,**/*.css*\" --config-file ./babel.config.cjs",
80
80
  "test": "vitest run --root ../..",
81
81
  "test:watch": "vitest --root ../..",
82
82
  "lint": "eslint src",
83
83
  "typecheck:docs": "tsc --project tsconfig.docs.json",
84
- "typecheck": "tsc --project tsconfig.json --noEmit",
84
+ "typecheck": "tsc --project tsconfig.json --noEmit && pnpm typecheck:docs",
85
85
  "generate:emoji-data": "node scripts/generate-emoji-data.mjs"
86
86
  }
87
87
  }
@@ -82,6 +82,12 @@ import type {
82
82
  CommentUser,
83
83
  } from './types';
84
84
 
85
+ // The page's shared live regions are not the component's error Banner.
86
+ const errorBanner = () =>
87
+ screen
88
+ .queryAllByRole('alert')
89
+ .find(element => !element.hasAttribute('data-astryx-live-region'));
90
+
85
91
  const VIEWER: CommentUser = {id: 'avery', name: 'Avery Kim'};
86
92
  const OTHER: CommentUser = {id: 'maya', name: 'Maya Chen'};
87
93
  const AGENT: CommentUser = {
@@ -900,12 +906,12 @@ describe('Comments threads', () => {
900
906
  }),
901
907
  );
902
908
  await flush();
903
- const banner = screen.getByRole('alert');
909
+ const banner = errorBanner()!;
904
910
  expect(banner.textContent).toContain("Your reaction couldn't be saved.");
905
911
  await click(within(banner).getByRole('button', {name: 'Retry'}));
906
912
  await flush();
907
913
  expect(onReact).toHaveBeenCalledTimes(2);
908
- expect(screen.queryByRole('alert')).toBeNull();
914
+ expect(errorBanner()).toBeUndefined();
909
915
  });
910
916
 
911
917
  it('copies a link to the comment', async () => {
@@ -999,7 +1005,7 @@ describe('Comments threads', () => {
999
1005
  />
1000
1006
  </Sized>,
1001
1007
  );
1002
- const banner = screen.getByRole('alert');
1008
+ const banner = errorBanner()!;
1003
1009
  expect(banner.textContent).toContain("Couldn't post this comment.");
1004
1010
  await click(within(banner).getByRole('button', {name: 'Retry'}));
1005
1011
  await flush();
@@ -1022,7 +1028,7 @@ describe('Comments threads', () => {
1022
1028
  );
1023
1029
  await click(screen.getByRole('button', {name: 'Load earlier comments'}));
1024
1030
  await flush();
1025
- const banner = screen.getByRole('alert');
1031
+ const banner = errorBanner()!;
1026
1032
  expect(banner.textContent).toContain("Earlier comments couldn't load");
1027
1033
  await click(within(banner).getByRole('button', {name: 'Retry'}));
1028
1034
  await flush();
@@ -167,12 +167,16 @@ describe('InfoTip', () => {
167
167
  expect(screen.getByTestId('rich-content')).toBeInTheDocument();
168
168
  });
169
169
  describe('touch', () => {
170
- /** A tap: the pointer sequence a finger produces before hover is faked. */
170
+ /**
171
+ * A tap, in the order a browser dispatches it: the finger's pointer events,
172
+ * the hover it synthesizes, then the `click` that says the touch was a tap.
173
+ */
171
174
  const tap = (element: HTMLElement) => {
172
175
  fireEvent.pointerEnter(element, {pointerType: 'touch'});
173
176
  fireEvent.pointerDown(element, {pointerType: 'touch'});
174
177
  fireEvent.pointerUp(element, {pointerType: 'touch'});
175
178
  fireEvent.mouseEnter(element);
179
+ fireEvent.click(element);
176
180
  };
177
181
 
178
182
  it('opens on a tap, though the trigger is a button', async () => {
@@ -221,9 +225,10 @@ describe('InfoTip', () => {
221
225
  expect(popoverOpenState.get(layer)).toBe(true);
222
226
  });
223
227
 
224
- fireEvent.pointerDown(screen.getByRole('button', {name: 'Elsewhere'}), {
225
- pointerType: 'touch',
226
- });
228
+ // The tap outside counts when the finger lifts.
229
+ const elsewhere = screen.getByRole('button', {name: 'Elsewhere'});
230
+ fireEvent.pointerDown(elsewhere, {pointerType: 'touch'});
231
+ fireEvent.pointerUp(elsewhere, {pointerType: 'touch'});
227
232
 
228
233
  await waitFor(() => {
229
234
  expect(popoverOpenState.get(layer)).toBe(false);
@@ -19,7 +19,7 @@ import {
19
19
  waitFor,
20
20
  within,
21
21
  } from '@testing-library/react';
22
- import {useContainerReveal} from '@astryxdesign/core/hooks';
22
+ import {useAnnounce, useContainerReveal} from '@astryxdesign/core/hooks';
23
23
  import {TextInput} from '@astryxdesign/core/TextInput';
24
24
  import {
25
25
  ListInput,
@@ -206,6 +206,13 @@ function mockMutationAnimations(events: string[] = []) {
206
206
 
207
207
  afterEach(() => {
208
208
  vi.restoreAllMocks();
209
+ // The shared live regions outlive each test, and a test that stubs
210
+ // requestAnimationFrame can strand an announcement's frame. An empty
211
+ // announcement clears what is still waiting, so it is not joined into the
212
+ // next test's announcements (spec:AST-086 FR9).
213
+ const announcer = renderHook(() => useAnnounce());
214
+ announcer.result.current('');
215
+ announcer.unmount();
209
216
  if (originalAnimateDescriptor == null) {
210
217
  Reflect.deleteProperty(HTMLElement.prototype, 'animate');
211
218
  } else {
@@ -653,10 +660,13 @@ describe('ListInput', () => {
653
660
  expect(animate).not.toHaveBeenCalled();
654
661
  expect(screen.queryByDisplayValue('Ada')).not.toBeInTheDocument();
655
662
  expect(screen.getByRole('button', {name: 'Add guest'})).toHaveFocus();
663
+ // The removal and the empty state that appears join in one frame (FR9).
656
664
  await waitFor(() => {
657
665
  expect(
658
666
  Array.from(document.querySelectorAll('[aria-live="polite"]')).some(
659
- node => node.textContent === 'Removed guest 1.',
667
+ node =>
668
+ node.textContent ===
669
+ 'Removed guest 1. No guests yet Add a guest to get started.',
660
670
  ),
661
671
  ).toBe(true);
662
672
  });
@@ -1195,6 +1205,32 @@ describe('ListInput', () => {
1195
1205
  ).not.toBeInTheDocument();
1196
1206
  });
1197
1207
 
1208
+ it('cancels a pointer reorder when a second pointer lands, as a pinch does', () => {
1209
+ const onChange = vi.fn();
1210
+ const {container} = renderListInput({isReorderable: true, onChange});
1211
+ const handle = screen.getByRole('button', {name: 'Reorder guest 1'});
1212
+
1213
+ fireEvent.pointerDown(handle, {
1214
+ button: 0,
1215
+ pointerId: 8,
1216
+ clientX: 10,
1217
+ clientY: 10,
1218
+ });
1219
+ fireEvent.pointerMove(handle, {pointerId: 8, clientX: 10, clientY: 34});
1220
+ expect(
1221
+ container.querySelector('[data-list-input-reorder-source]'),
1222
+ ).toBeInTheDocument();
1223
+
1224
+ // The second finger of a pinch lands elsewhere on the page.
1225
+ fireEvent.pointerDown(document.body, {pointerId: 9, clientX: 200});
1226
+ expect(
1227
+ container.querySelector('[data-list-input-reorder-source]'),
1228
+ ).not.toBeInTheDocument();
1229
+
1230
+ fireEvent.pointerUp(handle, {pointerId: 8, clientX: 10, clientY: 90});
1231
+ expect(onChange).not.toHaveBeenCalled();
1232
+ });
1233
+
1198
1234
  it('clears a sub-threshold pointer drag without reordering', () => {
1199
1235
  const onChange = vi.fn();
1200
1236
  const {container} = renderListInput({isReorderable: true, onChange});
@@ -1273,10 +1309,14 @@ describe('ListInput', () => {
1273
1309
  fireEvent.pointerUp(handle, {pointerId: 9, clientX: 15, clientY: 10});
1274
1310
 
1275
1311
  expect(onChange).not.toHaveBeenCalled();
1312
+ // The grab and the return land in the same frame, so they are spoken
1313
+ // together, in order (spec:AST-086 FR9).
1276
1314
  await waitFor(() => {
1277
1315
  expect(
1278
1316
  Array.from(document.querySelectorAll('[aria-live="polite"]')).some(
1279
- node => node.textContent === 'guest returned to position 1.',
1317
+ node =>
1318
+ node.textContent ===
1319
+ 'guest 1 grabbed. Use arrow keys to move, Space or Enter to drop, and Escape to cancel. guest returned to position 1.',
1280
1320
  ),
1281
1321
  ).toBe(true);
1282
1322
  });
@@ -809,6 +809,29 @@ export function ListInput<T>({
809
809
  }
810
810
  }, [isDisabled, isLoading, announce, t]);
811
811
 
812
+ // A second pointer that lands anywhere during a pointer reorder (the second
813
+ // finger of a pinch) cancels the reorder. A pinch whose first finger slid
814
+ // first zooms without a pointercancel, so the drag would otherwise commit on
815
+ // release and drop the row at the end of the list.
816
+ useEffect(() => {
817
+ if (!isPointerReorderActive) {
818
+ return;
819
+ }
820
+ const onPointerDown = (event: PointerEvent) => {
821
+ const activeState = reorderStateRef.current;
822
+ if (
823
+ activeState?.mode === 'pointer' &&
824
+ activeState.pointerId !== event.pointerId
825
+ ) {
826
+ setReorderState(null);
827
+ announce(t('@astryx.listInput.announceReorderCancelled'));
828
+ }
829
+ };
830
+ document.addEventListener('pointerdown', onPointerDown, true);
831
+ return () =>
832
+ document.removeEventListener('pointerdown', onPointerDown, true);
833
+ }, [isPointerReorderActive, announce, t]);
834
+
812
835
  useEffect(() => {
813
836
  const activeAnimations = activeMutationAnimationsRef.current;
814
837
  return () => {
@@ -56,7 +56,14 @@ function Harness({
56
56
  );
57
57
  }
58
58
 
59
- function CreateHarness({spy}: {spy: ReturnType<typeof vi.fn>}) {
59
+ function CreateHarness({
60
+ spy,
61
+ }: {
62
+ spy: (
63
+ items: SearchableItem[],
64
+ change: MobileTokenizerChange<SearchableItem>,
65
+ ) => void;
66
+ }) {
60
67
  const [value, setValue] = useState<SearchableItem[]>([]);
61
68
  return (
62
69
  <MobileTokenizer
@@ -105,8 +112,10 @@ beforeEach(() => {
105
112
  window.scrollTo = vi.fn();
106
113
  vi.stubGlobal(
107
114
  'requestAnimationFrame',
115
+ // Next microtask, not synchronously: a real frame never runs before
116
+ // requestAnimationFrame returns, and callers may rely on its id.
108
117
  vi.fn((cb: FrameRequestCallback) => {
109
- cb(0);
118
+ queueMicrotask(() => cb(0));
110
119
  return 1;
111
120
  }),
112
121
  );
@@ -302,7 +311,7 @@ describe('MobileTokenizer (Lab, single-sheet flow)', () => {
302
311
  const title = await screen.findByRole('heading', {
303
312
  name: 'No results found',
304
313
  });
305
- const emptyState = title.closest('[role="status"]');
314
+ const emptyState = title.closest('.astryx-empty-state');
306
315
  expect(emptyState).toHaveTextContent('Try a different search.');
307
316
  expect(emptyState?.querySelector('.astryx-icon')).toHaveAttribute(
308
317
  'data-size',
@@ -125,7 +125,7 @@ function StagedColumnSelector({
125
125
  }
126
126
 
127
127
  function openSelector(): void {
128
- fireEvent.keyDown(screen.getByRole('button', {name: 'View options'}), {
128
+ fireEvent.keyDown(screen.getByRole('combobox', {name: 'View options'}), {
129
129
  key: 'ArrowDown',
130
130
  });
131
131
  }
@@ -144,7 +144,7 @@ describe('ComplexSelector + TransferList composition', () => {
144
144
  openSelector();
145
145
  await waitFor(() => {
146
146
  expect(
147
- screen.getByRole('button', {name: 'View options'}),
147
+ screen.getByRole('combobox', {name: 'View options'}),
148
148
  ).toHaveAttribute('aria-expanded', 'true');
149
149
  });
150
150
 
@@ -173,7 +173,7 @@ describe('ComplexSelector + TransferList composition', () => {
173
173
  const user = userEvent.setup();
174
174
  const onCommit = vi.fn();
175
175
  render(<StagedColumnSelector onCommit={onCommit} />);
176
- const trigger = screen.getByRole('button', {name: 'View options'});
176
+ const trigger = screen.getByRole('combobox', {name: 'View options'});
177
177
 
178
178
  openSelector();
179
179
  await user.click(
@@ -255,7 +255,7 @@ describe('ComplexSelector + TransferList composition', () => {
255
255
  const user = userEvent.setup();
256
256
  const onCommit = vi.fn();
257
257
  render(<StagedColumnSelector onCommit={onCommit} />);
258
- const trigger = screen.getByRole('button', {name: 'View options'});
258
+ const trigger = screen.getByRole('combobox', {name: 'View options'});
259
259
 
260
260
  openSelector();
261
261
  await waitFor(() => {
@@ -534,6 +534,42 @@ describe('TransferList', () => {
534
534
  expect(screen.getByTestId('popover-host')).toContainElement(layer);
535
535
  });
536
536
 
537
+ it('cancels a pointer reorder when a second pointer lands, as a pinch does', () => {
538
+ const onValueChange = vi.fn();
539
+ const {container} = render(
540
+ <ControlledTransferList
541
+ initialValue={['name', 'owner', 'status']}
542
+ onValueChange={onValueChange}
543
+ />,
544
+ );
545
+ const rows = [
546
+ ...container.querySelectorAll<HTMLElement>('[data-transfer-list-row]'),
547
+ ];
548
+ mockRowBounds(rows);
549
+ const handle = screen.getByRole('button', {name: 'Reorder Name'});
550
+
551
+ fireEvent.pointerDown(handle, {
552
+ button: 0,
553
+ pointerId: 7,
554
+ clientX: 300,
555
+ clientY: 10,
556
+ });
557
+ fireEvent.pointerMove(handle, {pointerId: 7, clientX: 300, clientY: 34});
558
+ expect(
559
+ container.querySelector('[data-transfer-list-reorder-source]'),
560
+ ).toBeInTheDocument();
561
+
562
+ // The second finger of a pinch lands elsewhere on the page.
563
+ fireEvent.pointerDown(document.body, {pointerId: 8, clientX: 600});
564
+ expect(
565
+ container.querySelector('[data-transfer-list-reorder-source]'),
566
+ ).not.toBeInTheDocument();
567
+
568
+ fireEvent.pointerMove(handle, {pointerId: 7, clientX: 300, clientY: 140});
569
+ fireEvent.pointerUp(handle, {pointerId: 7, clientX: 300, clientY: 140});
570
+ expect(onValueChange).not.toHaveBeenCalled();
571
+ });
572
+
537
573
  it('keeps rows stationary, locks X, follows pointer Y, and commits once', () => {
538
574
  const onValueChange = vi.fn();
539
575
  const {container} = render(
@@ -994,6 +994,30 @@ export function TransferList<T extends string = string>({
994
994
  [finishReorder],
995
995
  );
996
996
 
997
+ // A second pointer that lands anywhere during a pointer reorder (the second
998
+ // finger of a pinch) cancels the reorder. A pinch whose first finger slid
999
+ // first zooms without a pointercancel, so the drag would otherwise commit on
1000
+ // release and drop the row at the end of the list.
1001
+ const isPointerReorderActive = previewCloneValue != null;
1002
+ useEffect(() => {
1003
+ if (!isPointerReorderActive) {
1004
+ return;
1005
+ }
1006
+ const onPointerDown = (event: PointerEvent) => {
1007
+ const session = reorderSessionRef.current;
1008
+ if (
1009
+ session?.mode === 'pointer' &&
1010
+ session.pointerId !== event.pointerId
1011
+ ) {
1012
+ suppressClickRef.current = false;
1013
+ finishReorder(true);
1014
+ }
1015
+ };
1016
+ document.addEventListener('pointerdown', onPointerDown, true);
1017
+ return () =>
1018
+ document.removeEventListener('pointerdown', onPointerDown, true);
1019
+ }, [isPointerReorderActive, finishReorder]);
1020
+
997
1021
  const handleSearch = useCallback(
998
1022
  (nextQuery: string) => {
999
1023
  setQuery(nextQuery);
@@ -106,7 +106,7 @@ function Harness({
106
106
  async function openSelector(
107
107
  name = 'Visible fields',
108
108
  ): Promise<HTMLButtonElement> {
109
- const trigger = screen.getByRole('button', {
109
+ const trigger = screen.getByRole('combobox', {
110
110
  name,
111
111
  }) as HTMLButtonElement;
112
112
  fireEvent.keyDown(trigger, {key: 'ArrowDown'});
@@ -416,7 +416,7 @@ describe('TransferListSelector', () => {
416
416
  );
417
417
 
418
418
  expect(
419
- screen.getByRole('button', {name: 'Visible fields'}),
419
+ screen.getByRole('combobox', {name: 'Visible fields'}),
420
420
  ).toHaveTextContent('3 selected');
421
421
  });
422
422
 
@@ -462,7 +462,7 @@ describe('TransferListSelector', () => {
462
462
  expect(fieldset).not.toContainElement(cancel);
463
463
  expect(apply).toBeDisabled();
464
464
  expect(
465
- screen.getByRole('button', {name: 'Visible fields'}),
465
+ screen.getByRole('combobox', {name: 'Visible fields'}),
466
466
  ).toHaveAttribute('aria-disabled', 'true');
467
467
 
468
468
  view.rerender(
@@ -564,7 +564,7 @@ describe('TransferListSelector', () => {
564
564
  screen.getByTestId('column-selector').closest('.astryx-complex-selector'),
565
565
  ).toHaveAttribute('data-size', 'sm');
566
566
  expect(
567
- screen.getByRole('button', {name: 'Table columns'}),
567
+ screen.getByRole('combobox', {name: 'Table columns'}),
568
568
  ).toHaveTextContent('Customize columns');
569
569
  expect(
570
570
  screen.getByTestId('column-selector').closest('.astryx-field'),
@@ -0,0 +1,177 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file ReorderGripPinchZoom.a11y.chromium.spec.ts
5
+ * @input The checked-in Lab/ListInput › Tag Options and Lab/TransferList ›
6
+ * Default stories, a built Storybook and real Chromium touch input (CDP) on
7
+ * a phone viewport
8
+ * @output Browser evidence that a pinch which starts on, or reaches, a reorder
9
+ * grip zooms the page and leaves the order as it was, while a one-finger
10
+ * drag on the grip still reorders (WCAG 1.4.4)
11
+ * @position Chromium proof for the shared Lab reorder grip's `touch-action`
12
+ * (packages/lab/src/reorderStyles.ts)
13
+ */
14
+
15
+ import {expect, test, type CDPSession, type Page} from '@playwright/test';
16
+ import {
17
+ DEFAULT_STORYBOOK_DIR,
18
+ serveStorybook,
19
+ type StaticServer,
20
+ } from '@astryxdesign/a11y-spec/storybook';
21
+
22
+ let storybook: StaticServer;
23
+ test.beforeAll(async () => {
24
+ storybook = await serveStorybook(
25
+ process.env.ASTRYX_STORYBOOK_DIR ?? DEFAULT_STORYBOOK_DIR,
26
+ );
27
+ });
28
+ test.afterAll(async () => {
29
+ await storybook?.close();
30
+ });
31
+
32
+ // A phone: the viewport meta applies and the page can pinch-zoom.
33
+ test.use({
34
+ hasTouch: true,
35
+ isMobile: true,
36
+ viewport: {width: 390, height: 844},
37
+ deviceScaleFactor: 3,
38
+ });
39
+
40
+ const sleep = async (ms: number) =>
41
+ new Promise(resolve => setTimeout(resolve, ms));
42
+
43
+ type Point = {x: number; y: number};
44
+ type Box = {x: number; y: number; width: number; height: number};
45
+
46
+ interface Surface {
47
+ name: string;
48
+ story: string;
49
+ /** The first row's reorder grip. */
50
+ grip: string;
51
+ /** Every reorderable row, in display order, keyed by this attribute. */
52
+ rowAttribute: string;
53
+ }
54
+
55
+ const SURFACES: Surface[] = [
56
+ {
57
+ name: 'ListInput',
58
+ story: 'lab-listinput--tag-options',
59
+ grip: 'button[data-list-input-reorder]',
60
+ rowAttribute: 'data-list-input-row',
61
+ },
62
+ {
63
+ name: 'TransferList',
64
+ story: 'lab-transferlist--default',
65
+ grip: '[data-transfer-list-row]:not([data-transfer-list-row=""]) button[aria-pressed]',
66
+ rowAttribute: 'data-transfer-list-row',
67
+ },
68
+ ];
69
+
70
+ async function openStory(page: Page, surface: Surface) {
71
+ await page.goto(
72
+ `${storybook.origin}/iframe.html?id=${surface.story}&viewMode=story`,
73
+ );
74
+ const grip = page.locator(surface.grip).first();
75
+ await expect(grip).toBeVisible();
76
+ expect(await page.evaluate(() => window.visualViewport?.scale)).toBe(1);
77
+ const box = (await grip.boundingBox()) as Box;
78
+ const center = {x: box.x + box.width / 2, y: box.y + box.height / 2};
79
+ const cdp = await page.context().newCDPSession(page);
80
+ return {center, cdp, order: await readOrder(page, surface)};
81
+ }
82
+
83
+ async function readOrder(page: Page, surface: Surface) {
84
+ return page.evaluate(
85
+ attribute =>
86
+ Array.from(document.querySelectorAll(`[${attribute}]`))
87
+ .map(row => row.getAttribute(attribute))
88
+ .filter((key): key is string => key != null && key !== ''),
89
+ surface.rowAttribute,
90
+ );
91
+ }
92
+
93
+ const zoom = async (page: Page) =>
94
+ page.evaluate(() => window.visualViewport?.scale ?? 1);
95
+
96
+ async function touch(
97
+ cdp: CDPSession,
98
+ type: 'touchStart' | 'touchMove' | 'touchEnd',
99
+ points: Point[],
100
+ ) {
101
+ await cdp.send('Input.dispatchTouchEvent', {
102
+ type,
103
+ touchPoints: points.map((point, index) => ({...point, id: index + 1})),
104
+ });
105
+ }
106
+
107
+ // Two fingers land one after the other and spread apart, as a person pinches.
108
+ // The spread is wide enough to clear the browser's minimum pinch span.
109
+ async function pinch(cdp: CDPSession, first: Point, second: Point) {
110
+ const steps = 12;
111
+ const spread = 2.5;
112
+ const mid = {x: (first.x + second.x) / 2, y: (first.y + second.y) / 2};
113
+ const at = (p: Point, k: number) => ({
114
+ x: mid.x + (p.x - mid.x) * k,
115
+ y: mid.y + (p.y - mid.y) * k,
116
+ });
117
+ await touch(cdp, 'touchStart', [first]);
118
+ await sleep(30);
119
+ await touch(cdp, 'touchStart', [first, second]);
120
+ for (let i = 1; i <= steps; i++) {
121
+ await sleep(16);
122
+ const k = 1 + ((spread - 1) * i) / steps;
123
+ await touch(cdp, 'touchMove', [at(first, k), at(second, k)]);
124
+ }
125
+ await sleep(16);
126
+ await touch(cdp, 'touchEnd', []);
127
+ await sleep(600);
128
+ }
129
+
130
+ for (const surface of SURFACES) {
131
+ test.describe(surface.name, () => {
132
+ test('a pinch whose first finger lands on a grip zooms the page', async ({
133
+ page,
134
+ }) => {
135
+ const {center, cdp, order} = await openStory(page, surface);
136
+ await pinch(cdp, center, {x: center.x + 40, y: center.y + 140});
137
+ expect(await zoom(page)).toBeGreaterThan(1.2);
138
+ expect(await readOrder(page, surface)).toEqual(order);
139
+ });
140
+
141
+ test('a pinch whose second finger lands on a grip zooms the page', async ({
142
+ page,
143
+ }) => {
144
+ const {center, cdp, order} = await openStory(page, surface);
145
+ await pinch(cdp, {x: center.x + 40, y: center.y + 140}, center);
146
+ expect(await zoom(page)).toBeGreaterThan(1.2);
147
+ expect(await readOrder(page, surface)).toEqual(order);
148
+ });
149
+
150
+ test('a one-finger drag on a grip still reorders', async ({page}) => {
151
+ const {center, cdp, order} = await openStory(page, surface);
152
+ const rows = await page.evaluate(
153
+ attribute =>
154
+ Array.from(document.querySelectorAll(`[${attribute}]`))
155
+ .filter(row => row.getAttribute(attribute))
156
+ .map(row => row.getBoundingClientRect().top),
157
+ surface.rowAttribute,
158
+ );
159
+ // Past the second row's centre: the first row lands second or later.
160
+ const distance = (rows[2] - rows[0]) * 0.9;
161
+ await touch(cdp, 'touchStart', [center]);
162
+ for (let i = 1; i <= 20; i++) {
163
+ await sleep(16);
164
+ await touch(cdp, 'touchMove', [
165
+ {x: center.x, y: center.y + (distance * i) / 20},
166
+ ]);
167
+ }
168
+ await sleep(100);
169
+ await touch(cdp, 'touchEnd', []);
170
+ await sleep(600);
171
+ expect(await zoom(page)).toBe(1);
172
+ const after = await readOrder(page, surface);
173
+ expect(after).not.toEqual(order);
174
+ expect(after.indexOf(order[0])).toBeGreaterThan(0);
175
+ });
176
+ });
177
+ }
@@ -65,18 +65,23 @@ export const reorderStyles = stylex.create({
65
65
  insetBlockEnd: `calc(-1 * ${spacingVars['--spacing-0-5']})`,
66
66
  },
67
67
  },
68
+ // A finger on a grip drags its row from the first move, so the grip refuses
69
+ // panning. It still allows pinch-zoom: a pinch whose first finger lands on a
70
+ // grip zooms the page (WCAG 1.4.4); the browser's pointercancel then ends
71
+ // the drag, as it would any drag the browser takes. `touch-action` is read
72
+ // when the touch starts, so the active state keeps the same value.
68
73
  handle: {
69
74
  cursor: {
70
75
  default: 'grab',
71
76
  ':is(:disabled,[aria-disabled="true"])': 'default',
72
77
  },
73
- touchAction: 'none',
78
+ touchAction: 'pinch-zoom',
74
79
  },
75
80
  handleActive: {
76
81
  cursor: {
77
82
  default: 'grabbing',
78
83
  ':is(:disabled,[aria-disabled="true"])': 'default',
79
84
  },
80
- touchAction: 'none',
85
+ touchAction: 'pinch-zoom',
81
86
  },
82
87
  });