@astryxdesign/core 0.7.7-canary.36f8ea1 → 0.7.7-canary.550ff21

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.
Files changed (67) hide show
  1. package/dist/CommandPalette/CommandPaletteInput.d.ts.map +1 -1
  2. package/dist/CommandPalette/CommandPaletteInput.js +8 -1
  3. package/dist/DateInput/MonthScroller.d.ts.map +1 -1
  4. package/dist/DateInput/MonthScroller.js +72 -2
  5. package/dist/Field/PanelSearchInput.d.ts.map +1 -1
  6. package/dist/Field/PanelSearchInput.js +8 -1
  7. package/dist/Field/textEntryTraits.d.ts +6 -6
  8. package/dist/Field/textEntryTraits.js +6 -6
  9. package/dist/ProgressBar/ProgressBar.d.ts.map +1 -1
  10. package/dist/ProgressBar/ProgressBar.js +20 -9
  11. package/dist/ProgressBar/ProgressBarTouch.d.ts +29 -0
  12. package/dist/ProgressBar/ProgressBarTouch.d.ts.map +1 -0
  13. package/dist/ProgressBar/ProgressBarTouch.js +91 -0
  14. package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
  15. package/dist/Typeahead/BaseTypeahead.js +9 -1
  16. package/dist/astryx.css +7 -0
  17. package/dist/gestures/GestureArena.d.ts +21 -2
  18. package/dist/gestures/GestureArena.d.ts.map +1 -1
  19. package/dist/gestures/GestureArena.js +19 -2
  20. package/dist/gestures/GestureHost.d.ts +51 -1
  21. package/dist/gestures/GestureHost.d.ts.map +1 -1
  22. package/dist/gestures/GestureHost.js +148 -7
  23. package/dist/motion/animator.d.ts.map +1 -1
  24. package/dist/motion/animator.js +63 -5
  25. package/dist/motion/paging.d.ts +58 -0
  26. package/dist/motion/paging.d.ts.map +1 -0
  27. package/dist/motion/paging.js +128 -0
  28. package/dist/motion/uikit/index.d.ts +4 -3
  29. package/dist/motion/uikit/index.d.ts.map +1 -1
  30. package/dist/motion/uikit/index.js +4 -3
  31. package/dist/motion/uikit/paging.d.ts +160 -0
  32. package/dist/motion/uikit/paging.d.ts.map +1 -0
  33. package/dist/motion/uikit/paging.js +162 -0
  34. package/dist/motion/uikit/progressBar.d.ts +59 -0
  35. package/dist/motion/uikit/progressBar.d.ts.map +1 -0
  36. package/dist/motion/uikit/progressBar.js +82 -0
  37. package/dist/motion/uikit/scroll.d.ts +0 -19
  38. package/dist/motion/uikit/scroll.d.ts.map +1 -1
  39. package/dist/motion/uikit/scroll.js +2 -21
  40. package/dist/utils/selectionLock.d.ts +43 -0
  41. package/dist/utils/selectionLock.d.ts.map +1 -1
  42. package/dist/utils/selectionLock.js +394 -6
  43. package/package.json +4 -4
  44. package/src/CommandPalette/CommandPaletteInput.tsx +6 -0
  45. package/src/DateInput/MonthScroller.tsx +63 -5
  46. package/src/Field/PanelSearchInput.tsx +6 -0
  47. package/src/Field/textEntryTraits.ts +6 -6
  48. package/src/ProgressBar/ProgressBar.doc.mjs +3 -3
  49. package/src/ProgressBar/ProgressBar.tsx +82 -11
  50. package/src/ProgressBar/ProgressBarTouch.ts +109 -0
  51. package/src/Typeahead/BaseTypeahead.tsx +7 -0
  52. package/src/gestures/GestureArena.ts +44 -4
  53. package/src/gestures/GestureHost.ts +184 -7
  54. package/src/gestures/README.md +19 -3
  55. package/src/motion/MEASUREMENTS.md +8 -1
  56. package/src/motion/README.md +6 -3
  57. package/src/motion/animator.ts +78 -6
  58. package/src/motion/paging.ts +138 -0
  59. package/src/motion/uikit/index.ts +4 -3
  60. package/src/motion/uikit/paging.ts +162 -0
  61. package/src/motion/uikit/progressBar.ts +82 -0
  62. package/src/motion/uikit/scroll.ts +2 -18
  63. package/src/utils/selectionLock.ts +501 -6
  64. package/dist/motion/selectPage.d.ts +0 -26
  65. package/dist/motion/selectPage.d.ts.map +0 -1
  66. package/dist/motion/selectPage.js +0 -55
  67. package/src/motion/selectPage.ts +0 -55
@@ -57,6 +57,7 @@ import {
57
57
  typographyVars,
58
58
  } from '../theme/tokens.stylex';
59
59
  import type {BaseProps} from '../BaseProps';
60
+ import {textEntryTraitAttributes} from './textEntryTraits';
60
61
 
61
62
  const styles = stylex.create({
62
63
  // Gutter only. Matches the option list's own padding so the field below
@@ -273,6 +274,11 @@ export function PanelSearchInput({
273
274
  <input
274
275
  ref={mergedInputRef}
275
276
  type="text"
277
+ // Autocorrect would rewrite names and identifiers.
278
+ {...textEntryTraitAttributes({
279
+ enterKeyHint: 'search',
280
+ hasAutoCorrect: false,
281
+ })}
276
282
  aria-label={label}
277
283
  placeholder={placeholder}
278
284
  value={value}
@@ -3,16 +3,16 @@
3
3
  /**
4
4
  * @file textEntryTraits.ts
5
5
  * @input The keyboard-trait props a text field accepts
6
- * @output Exports the closed value unions EnterKeyHint, InputMode and
7
- * AutoCapitalize (the package entry exports these three), the fields' prop
8
- * mixin TextEntryTraitProps, and textEntryTraitAttributes, which maps the
9
- * props onto the native attributes of the editable element
10
- * @position Shared by TextInput and TextArea: the one place that says how a
11
- * field tells a touch keyboard what it holds
6
+ * @output EnterKeyHint, InputMode and AutoCapitalize (the package entry exports
7
+ * these), TextEntryTraitProps and textEntryTraitAttributes
8
+ * @position Shared by TextInput, TextArea and the search inputs: the one place
9
+ * that says how a field tells a touch keyboard what it holds
12
10
  *
13
11
  * SYNC: When modified, update:
14
12
  * - /packages/core/src/TextInput/TextInput.tsx and TextInput.doc.mjs
15
13
  * - /packages/core/src/TextArea/TextArea.tsx and TextArea.doc.mjs
14
+ * - /packages/core/src/Typeahead/BaseTypeahead.tsx, Field/PanelSearchInput.tsx
15
+ * and CommandPalette/CommandPaletteInput.tsx (the search inputs)
16
16
  * - /packages/core/src/Field/index.ts (type exports)
17
17
  */
18
18
 
@@ -160,7 +160,7 @@ export const docs = {
160
160
  usage: {
161
161
  anatomy,
162
162
  description:
163
- "A horizontal bar showing the completion progress of a task. Use it for operations where the duration is known, or as an animated indicator when progress can't be calculated. Supports semantic color variants, value labels, and custom formatting.",
163
+ "A horizontal bar showing the completion progress of a task. Use it for operations where the duration is known, or as an animated indicator when progress can't be calculated. Supports semantic color variants, value labels, and custom formatting. Under a finger a new value animates as on iOS: linearly, in proportion to how far it moves (a full track takes one second at the theme's default tempo), and the fill keeps a minimum visible length above zero.",
164
164
  bestPractices: [
165
165
  {
166
166
  guidance: true,
@@ -304,7 +304,7 @@ export const docsZh = {
304
304
  usage: {
305
305
  anatomy,
306
306
  description:
307
- "A horizontal bar showing the completion progress of a task. Use it for operations where the duration is known, or as an animated indicator when progress can't be calculated. Supports semantic color variants, value labels, and custom formatting.",
307
+ "A horizontal bar showing the completion progress of a task. Use it for operations where the duration is known, or as an animated indicator when progress can't be calculated. Supports semantic color variants, value labels, and custom formatting. Under a finger a new value animates as on iOS: linearly, in proportion to how far it moves (a full track takes one second at the theme's default tempo), and the fill keeps a minimum visible length above zero.",
308
308
  bestPractices: [
309
309
  {
310
310
  guidance: true,
@@ -347,7 +347,7 @@ export const docsDense = {
347
347
  usage: {
348
348
  anatomy,
349
349
  description:
350
- "A horizontal bar showing the completion progress of a task. Use it for operations where the duration is known, or as an animated indicator when progress can't be calculated. Supports semantic color variants, value labels, and custom formatting.",
350
+ "A horizontal bar showing the completion progress of a task. Use it for operations where the duration is known, or as an animated indicator when progress can't be calculated. Supports semantic color variants, value labels, and custom formatting. Under a finger a new value animates as on iOS: linearly, in proportion to how far it moves (a full track takes one second at the theme's default tempo), and the fill keeps a minimum visible length above zero.",
351
351
  bestPractices: [
352
352
  {
353
353
  guidance: true,
@@ -4,7 +4,7 @@
4
4
 
5
5
  /**
6
6
  * @file ProgressBar.tsx
7
- * @input Uses React, useId, stylex, color/spacing/radius/transition tokens
7
+ * @input Uses React, useId, stylex, tokens and ProgressBarTouch
8
8
  * @output Exports ProgressBar component, ProgressBarProps, ProgressBarVariant types
9
9
  * @position Core implementation; consumed by index.ts
10
10
  *
@@ -35,6 +35,7 @@ import type {BaseProps} from '../BaseProps';
35
35
  import {themeProps} from '../utils/themeProps';
36
36
  import {VisuallyHidden} from '../VisuallyHidden';
37
37
  import type {ProgressBarVariantMap} from './index';
38
+ import {useProgressBarTouch} from './ProgressBarTouch';
38
39
 
39
40
  // Every mark is wrapped in a Tooltip (marks always carry a label), so load it
40
41
  // lazily: a ProgressBar with no marks never bundles the Tooltip chunk. While it
@@ -249,10 +250,26 @@ const styles = stylex.create({
249
250
  // no transform), as before: the track's clip would cut the CanvasText
250
251
  // outline's start cap off a slid fill, and that outline is what keeps the
251
252
  // fill's shape visible there (architecture:theme-tokens INV11).
253
+ //
254
+ // Under a finger (TOUCH_QUERY, on every touch device), the fill moves as
255
+ // UIProgressView's setProgress(_:animated: true) does. It moves linearly
256
+ // from wherever it is drawn. Its duration is the `progress.fillTrackMs` role
257
+ // times the share of the track the value moved (`--_progress-bar-move`).
258
+ // Its curve is the `progress.curves.fill` role (`--_progress-bar-curve`).
259
+ // The touch layer (ProgressBarTouch) writes both, and this rule scales the
260
+ // duration by the theme's tempo. Above zero, the fill never draws shorter
261
+ // than the touch minimum (`--_progress-bar-min`, 16px on the 8px track). At
262
+ // zero it paints nothing (`fillEmpty`). Under reduced motion it lands at
263
+ // once, as everywhere (below). A mouse or trackpad keeps the theme's timing.
252
264
  fill: {
253
265
  width: {
254
266
  default: '100%',
255
- '@media (forced-colors: active)': 'var(--_progress-bar-fill, 0%)',
267
+ '@media (forced-colors: active)': {
268
+ default: 'var(--_progress-bar-fill, 0%)',
269
+ // Under a finger: the touch minimum and the flip offset (below).
270
+ '@media (pointer: coarse)':
271
+ 'calc(max(var(--_progress-bar-fill, 0%), var(--_progress-bar-min, 0px)) + var(--_progress-bar-flip, 0) * 0.01px)',
272
+ },
256
273
  },
257
274
  height: '100%',
258
275
  // Forced colors replace status fills with Canvas; this boundary keeps the
@@ -271,6 +288,19 @@ const styles = stylex.create({
271
288
  default: 'translateX(calc(100% - var(--_progress-bar-fill, 0%)))',
272
289
  '@media (forced-colors: active)': 'none',
273
290
  },
291
+ '@media (pointer: coarse)': {
292
+ // `--_progress-bar-flip` steps 0, 1, 2, 0… on every change. It moves
293
+ // the fill by that many hundredths of a pixel. So a change back to the
294
+ // value it just left is never the running transition's own start
295
+ // value. CSS shortens a transition that reverses to its start, which
296
+ // would halve a return mid-flight (10 → 90 → 10). It takes three
297
+ // values because two would repeat on that return.
298
+ default:
299
+ 'translateX(calc(max(var(--_progress-bar-fill, 0%), var(--_progress-bar-min, 0px)) - 100% + var(--_progress-bar-flip, 0) * 0.01px))',
300
+ ':dir(rtl)':
301
+ 'translateX(calc(min(calc(100% - var(--_progress-bar-fill, 0%)), calc(100% - var(--_progress-bar-min, 0px))) - var(--_progress-bar-flip, 0) * 0.01px))',
302
+ },
303
+ // After the touch key, so StyleX negates that key under forced colors.
274
304
  '@media (forced-colors: active)': 'none',
275
305
  },
276
306
  willChange: {
@@ -281,17 +311,36 @@ const styles = stylex.create({
281
311
  default: 'transform',
282
312
  '@media (forced-colors: active)': 'width',
283
313
  },
284
- // Under reduced motion a new value lands at once, on every pointer: the
285
- // fill never travels (design:reduced-motion DR1). The keys are the shared
286
- // reduced-motion queries, a fine pointer's and a coarse pointer's, spelled
287
- // as the record's other transitions spell them.
314
+ // Under reduced motion a new value lands at once, with a mouse as with a
315
+ // finger: the fill never travels. The keys are the shared reduced-motion
316
+ // queries, as the other transitions spell them. The coarse key comes after
317
+ // the touch key, so it wins there.
288
318
  transitionDuration: {
289
319
  default: durationVars['--duration-medium'],
320
+ // The touch layer's move (the `progress.fillTrackMs` role times the
321
+ // share moved), scaled by the theme's tempo.
322
+ '@media (pointer: coarse)': {
323
+ default:
324
+ 'calc(var(--_progress-bar-move, 0ms) * var(--_astryx-motion-tempo, 1))',
325
+ // Under forced colors the fill is sized by `width` instead. Touch
326
+ // motion never animates a layout property, so with a finger the new
327
+ // size lands at once there.
328
+ '@media (forced-colors: active)': '0s',
329
+ },
290
330
  '@media (prefers-reduced-motion: reduce) and (not (pointer: coarse))':
291
331
  '0s',
292
332
  '@media (prefers-reduced-motion: reduce) and (pointer: coarse)': '0s',
293
333
  },
294
- transitionTimingFunction: easeVars['--ease-standard'],
334
+ transitionTimingFunction: {
335
+ default: easeVars['--ease-standard'],
336
+ '@media (pointer: coarse)': 'var(--_progress-bar-curve, linear)',
337
+ },
338
+ },
339
+ fillEmpty: {
340
+ opacity: {
341
+ default: null,
342
+ '@media (pointer: coarse)': 0,
343
+ },
295
344
  },
296
345
  indeterminateFill: {
297
346
  height: '100%',
@@ -428,7 +477,15 @@ const markOnTrackStyles = stylex.create({
428
477
  // `CSSProperties` has no index signature for custom properties, so the fill's
429
478
  // inline style — which publishes the filled share as the private
430
479
  // `--_progress-bar-fill` — needs this augmentation to typecheck.
431
- type FillStyle = CSSProperties & Record<'--_progress-bar-fill', string>;
480
+ type FillStyle = CSSProperties &
481
+ Record<
482
+ | '--_progress-bar-fill'
483
+ | '--_progress-bar-move'
484
+ | '--_progress-bar-curve'
485
+ | '--_progress-bar-flip'
486
+ | '--_progress-bar-min',
487
+ string
488
+ >;
432
489
 
433
490
  function defaultFormatValueLabel(value: number, max: number): string {
434
491
  const pct = max > 0 ? Math.round((value / max) * 100) : 0;
@@ -509,7 +566,16 @@ export function ProgressBar({
509
566
  const showValueLabel = hasValueLabel && !isIndeterminate;
510
567
 
511
568
  const fillVariant = isDisabled ? 'disabled' : variant;
512
- const fillStyle: FillStyle = {'--_progress-bar-fill': `${percentage}%`};
569
+ // Under a finger the fill moves as UIProgressView's does and draws at least
570
+ // 16px; what that needs from script is the touch layer's (ProgressBarTouch).
571
+ const touch = useProgressBarTouch(
572
+ percentage,
573
+ !isIndeterminate && (marks?.length ?? 0) > 0,
574
+ );
575
+ const fillStyle: FillStyle = {
576
+ '--_progress-bar-fill': `${percentage}%`,
577
+ ...touch.fillVars,
578
+ };
513
579
 
514
580
  // Marks make no sense without a determinate value, so they are only drawn
515
581
  // in determinate mode. Non-finite mark values are dropped; the rest are
@@ -532,7 +598,7 @@ export function ProgressBar({
532
598
  value: mark.value,
533
599
  label: mark.label,
534
600
  pct,
535
- isOnFill: percentage > 0 && pct <= percentage,
601
+ isOnFill: percentage > 0 && pct <= touch.drawnPercentage,
536
602
  };
537
603
  })
538
604
  : [];
@@ -582,6 +648,7 @@ export function ProgressBar({
582
648
  aria-valuemax={isIndeterminate ? undefined : safeMax}
583
649
  aria-labelledby={labelId}
584
650
  aria-valuetext={isIndeterminate ? undefined : valueText}
651
+ ref={touch.trackRef}
585
652
  {...mergeProps(
586
653
  themeProps('progress-bar-track'),
587
654
  stylex.props(styles.track),
@@ -600,7 +667,11 @@ export function ProgressBar({
600
667
  <div
601
668
  {...mergeProps(
602
669
  themeProps('progress-bar-fill', {variant: fillVariant}),
603
- stylex.props(styles.fill, variantStyles[fillVariant]),
670
+ stylex.props(
671
+ styles.fill,
672
+ percentage === 0 && styles.fillEmpty,
673
+ variantStyles[fillVariant],
674
+ ),
604
675
  )}
605
676
  style={fillStyle}
606
677
  />
@@ -0,0 +1,109 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ 'use client';
4
+
5
+ /**
6
+ * @file ProgressBarTouch.ts
7
+ * @input Uses React, the touch presentation resolver, the shared resize
8
+ * observer, and the progress table's roles (through resolveMotion)
9
+ * @output useProgressBarTouch: the fill's touch variables (move length,
10
+ * curve, return offset, minimum length) and, under a finger with marks,
11
+ * the share of the track the fill draws
12
+ * @position ProgressBar's touch layer, imported only by ProgressBar.tsx
13
+ *
14
+ * SYNC: When modified, update /packages/core/src/ProgressBar/ProgressBar.tsx
15
+ * (its touch styles read these variables and the 16px minimum) and
16
+ * /packages/core/src/motion/uikit/progressBar.ts.
17
+ */
18
+
19
+ import {useEffect, useRef, useState, type RefObject} from 'react';
20
+ import {formatMotionRoleValue} from '../motion/roles';
21
+ import {resolveMotion} from '../motion/resolveMotion';
22
+ import {progressBar} from '../motion/uikit/progressBar';
23
+ import {observeResize} from '../utils/sharedResizeObserver';
24
+ import {useTouchPresentation} from '../utils/touchPresentation';
25
+
26
+ /** The track's height. */
27
+ const TRACK_HEIGHT_PX = 8;
28
+
29
+ /**
30
+ * Under a finger the fill never draws shorter than this above zero:
31
+ * UIProgressView's minimum, two track heights, on ProgressBar's 8px track.
32
+ */
33
+ export const TOUCH_MIN_FILL_PX =
34
+ progressBar.progressView.minimumFillHeights * TRACK_HEIGHT_PX;
35
+
36
+ export interface ProgressBarTouch {
37
+ /** The fill's touch timing variables, set inline on the fill. */
38
+ fillVars: Record<
39
+ | '--_progress-bar-move'
40
+ | '--_progress-bar-curve'
41
+ | '--_progress-bar-flip'
42
+ | '--_progress-bar-min',
43
+ string
44
+ >;
45
+ /** The track, measured under a finger when the bar has marks. */
46
+ trackRef: RefObject<HTMLDivElement | null>;
47
+ /** The share of the track the fill draws (0–100), for placing marks. */
48
+ drawnPercentage: number;
49
+ }
50
+
51
+ export function useProgressBarTouch(
52
+ percentage: number,
53
+ hasMarks: boolean,
54
+ ): ProgressBarTouch {
55
+ // The touch fill's duration comes from how far the value moved since the
56
+ // last value, the model change that UIKit measures. It holds until the next
57
+ // change, so a re-render with the same value keeps it.
58
+ const [motion, setMotion] = useState({percentage, change: 0, flip: 0});
59
+ const isNewValue = motion.percentage !== percentage;
60
+ // The share of the track the value moved (0–1). `flip` steps through three
61
+ // values on every change (see the fill's touch transform).
62
+ const change = isNewValue
63
+ ? Math.abs(percentage - motion.percentage) / 100
64
+ : motion.change;
65
+ const flip = isNewValue ? (motion.flip + 1) % 3 : motion.flip;
66
+ if (isNewValue) {
67
+ setMotion({percentage, change, flip});
68
+ }
69
+
70
+ // Under a finger the fill draws at least TOUCH_MIN_FILL_PX long, so a mark
71
+ // it covers there sits on the fill. That length is a share of the track's
72
+ // width, which only layout knows. So the track is measured, but only under
73
+ // a finger and only when the bar has marks.
74
+ const {isTouch} = useTouchPresentation();
75
+ const watchesTrack = isTouch && hasMarks;
76
+ const trackRef = useRef<HTMLDivElement | null>(null);
77
+ const [trackWidth, setTrackWidth] = useState(0);
78
+ useEffect(() => {
79
+ const element = trackRef.current;
80
+ if (!watchesTrack || element == null) {
81
+ return;
82
+ }
83
+ // The shared observer; its first call (on registration) carries no
84
+ // contentRect, so that one reads the box.
85
+ return observeResize(element, entry => {
86
+ setTrackWidth(
87
+ entry.contentRect?.width ?? element.getBoundingClientRect().width,
88
+ );
89
+ });
90
+ }, [watchesTrack]);
91
+ const drawnPercentage =
92
+ watchesTrack && percentage > 0 && trackWidth > 0
93
+ ? Math.max(percentage, (TOUCH_MIN_FILL_PX / trackWidth) * 100)
94
+ : percentage;
95
+
96
+ // CSS applies the theme's tempo to this duration.
97
+ const fillTrackMs = resolveMotion(null, progressBar, 'fillTrackMs');
98
+ const curve = resolveMotion(null, progressBar.curves.fill);
99
+ return {
100
+ fillVars: {
101
+ '--_progress-bar-move': `${Math.round(change * fillTrackMs * 1000) / 1000}ms`,
102
+ '--_progress-bar-curve': formatMotionRoleValue('curve', curve),
103
+ '--_progress-bar-flip': String(flip),
104
+ '--_progress-bar-min': `${TOUCH_MIN_FILL_PX}px`,
105
+ },
106
+ trackRef,
107
+ drawnPercentage,
108
+ };
109
+ }
@@ -79,6 +79,7 @@ import {
79
79
  useComponentTranslator,
80
80
  } from '../i18n/useComponentTranslator';
81
81
  import english from '../i18n/generated-english/Typeahead/BaseTypeahead.generated';
82
+ import {textEntryTraitAttributes} from '../Field/textEntryTraits';
82
83
 
83
84
  registerEnglishFallback(english);
84
85
  // =============================================================================
@@ -1723,6 +1724,12 @@ export const BaseTypeahead = function BaseTypeahead<T extends SearchableItem>({
1723
1724
  autoFocus={hasAutoFocus}
1724
1725
  data-autofocus={hasAutoFocus || undefined}
1725
1726
  autoComplete="off"
1727
+ // Autocorrect would rewrite names and identifiers. The key is only a
1728
+ // label: Enter still commits the highlighted option.
1729
+ {...textEntryTraitAttributes({
1730
+ enterKeyHint: 'search',
1731
+ hasAutoCorrect: false,
1732
+ })}
1726
1733
  {...mergeProps(
1727
1734
  stylex.props(
1728
1735
  styles.input,
@@ -90,8 +90,18 @@ export interface ArenaBeganInput extends TouchBeganInput {
90
90
  export interface GestureArenaOptions {
91
91
  clock?: GestureClock;
92
92
  environment?: GestureEnvironment;
93
- /** Touches a recognizer now owns: cancel the browser's default for them. */
94
- onClaim?: (touchIds: ReadonlyArray<number>) => void;
93
+ /**
94
+ * Touches a recognizer now owns, and the recognizer that claimed them. A
95
+ * claim comes in any state: a recognizer that waits on another's failure
96
+ * claims while `possible`, and a swipe claims as `ended`. Each claim is
97
+ * reported once.
98
+ */
99
+ onClaim?: (
100
+ touchIds: ReadonlyArray<number>,
101
+ claimer: GestureRecognizer,
102
+ ) => void;
103
+ /** Claimed touches given back to the browser (releaseClaim). */
104
+ onRelease?: (touchIds: ReadonlyArray<number>) => void;
95
105
  /** A click deferred at touch end resolved: replay it, or drop it. */
96
106
  onDeferredClick?: (touch: GestureTouch, allowed: boolean) => void;
97
107
  /**
@@ -100,6 +110,13 @@ export interface GestureArenaOptions {
100
110
  * listening now (`spec:AST-094` FR24).
101
111
  */
102
112
  onIdle?: () => void;
113
+ /**
114
+ * Whether the browser keeps a contact for itself while it is down. Then no
115
+ * claiming recognizer recognizes with it before its lift. So a mouse or pen
116
+ * drag that starts on selectable text selects that text. A recognition at
117
+ * the lift, such as a tap's, is unaffected.
118
+ */
119
+ refusesClaim?: (touch: GestureTouch) => boolean;
103
120
  /** Every recognizer transition, for debugging and tests. */
104
121
  onTransition?: (
105
122
  recognizer: GestureRecognizer,
@@ -553,7 +570,8 @@ export class GestureArena {
553
570
  ): void {
554
571
  if (
555
572
  !internals(recognizer).shouldBegin() ||
556
- this.isVetoedByConsumer(recognizer)
573
+ this.isVetoedByConsumer(recognizer) ||
574
+ this.isRefusedByBrowser(recognizer)
557
575
  ) {
558
576
  this.fail(recognizer);
559
577
  return;
@@ -635,6 +653,23 @@ export class GestureArena {
635
653
  return winner.canPrevent(other) && other.canBePrevented(winner);
636
654
  }
637
655
 
656
+ /**
657
+ * Whether a recognizer that claims touches would take a contact the
658
+ * browser keeps while it is down (`refusesClaim`).
659
+ */
660
+ private isRefusedByBrowser(recognizer: GestureRecognizer): boolean {
661
+ const refuses = this.options.refusesClaim;
662
+ if (refuses == null || !recognizer.claimsTouches) {
663
+ return false;
664
+ }
665
+ return recognizer.trackedTouches.some(
666
+ touch =>
667
+ touch.phase !== 'ended' &&
668
+ touch.phase !== 'cancelled' &&
669
+ refuses(touch),
670
+ );
671
+ }
672
+
638
673
  /** UIControl's rule: a control may veto an overlapping ancestor recognizer. */
639
674
  private isVetoedByConsumer(recognizer: GestureRecognizer): boolean {
640
675
  for (const id of internals(recognizer).seenTouchIds) {
@@ -674,6 +709,7 @@ export class GestureArena {
674
709
  recognizer: GestureRecognizer,
675
710
  keep: ReadonlySet<number> = new Set(),
676
711
  ): void {
712
+ const released: number[] = [];
677
713
  for (const touch of recognizer.trackedTouches) {
678
714
  const record = this.records.get(touch.id);
679
715
  if (record == null || !record.isClaimed || keep.has(touch.id)) {
@@ -692,8 +728,12 @@ export class GestureArena {
692
728
  );
693
729
  if (!isOwned) {
694
730
  record.isClaimed = false;
731
+ released.push(touch.id);
695
732
  }
696
733
  }
734
+ if (released.length > 0) {
735
+ this.options.onRelease?.(released);
736
+ }
697
737
  }
698
738
 
699
739
  private apply(recognizer: GestureRecognizer, next: GestureState): void {
@@ -768,7 +808,7 @@ export class GestureArena {
768
808
  }
769
809
  }
770
810
  if (claimed.length > 0) {
771
- this.options.onClaim?.(claimed);
811
+ this.options.onClaim?.(claimed, recognizer);
772
812
  }
773
813
  }
774
814