@astryxdesign/core 0.5.1 → 0.5.2-canary.11abdc7

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 (74) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/dist/Toast/useToastGesture.d.ts.map +1 -1
  3. package/dist/Toast/useToastGesture.js +46 -9
  4. package/dist/i18n/InternationalizationContext.d.ts +2 -0
  5. package/dist/i18n/InternationalizationContext.d.ts.map +1 -1
  6. package/dist/i18n/InternationalizationContext.js +3 -1
  7. package/dist/i18n/InternationalizationProvider.d.ts.map +1 -1
  8. package/dist/i18n/InternationalizationProvider.js +3 -1
  9. package/dist/i18n/resolve.d.ts +1 -1
  10. package/dist/i18n/resolve.d.ts.map +1 -1
  11. package/dist/i18n/resolve.js +37 -38
  12. package/dist/i18n/useTranslator.d.ts.map +1 -1
  13. package/dist/i18n/useTranslator.js +2 -3
  14. package/locales/af-ZA.json +0 -4
  15. package/locales/ar-SA.json +0 -4
  16. package/locales/ca-ES.json +0 -4
  17. package/locales/cs-CZ.json +0 -4
  18. package/locales/da-DK.json +0 -4
  19. package/locales/de-DE.json +0 -4
  20. package/locales/el-GR.json +0 -4
  21. package/locales/es-ES.json +0 -4
  22. package/locales/fi-FI.json +0 -4
  23. package/locales/fr-FR.json +0 -4
  24. package/locales/he-IL.json +0 -4
  25. package/locales/hu-HU.json +0 -4
  26. package/locales/it-IT.json +0 -4
  27. package/locales/ja-JP.json +0 -4
  28. package/locales/ko-KR.json +0 -4
  29. package/locales/nl-NL.json +0 -4
  30. package/locales/no-NO.json +0 -4
  31. package/locales/pl-PL.json +0 -4
  32. package/locales/pt-BR.json +0 -4
  33. package/locales/pt-PT.json +0 -4
  34. package/locales/ro-RO.json +0 -4
  35. package/locales/ru-RU.json +0 -4
  36. package/locales/sr-SP.json +0 -4
  37. package/locales/sv-SE.json +0 -4
  38. package/locales/tr-TR.json +0 -4
  39. package/locales/uk-UA.json +0 -4
  40. package/locales/vi-VN.json +0 -4
  41. package/locales/zh-CN.json +0 -4
  42. package/locales/zh-TW.json +0 -4
  43. package/package.json +3 -3
  44. package/src/Calendar/Calendar.doc.mjs +1 -1
  45. package/src/CheckboxInput/CheckboxInput.doc.mjs +1 -1
  46. package/src/CheckboxList/CheckboxList.doc.mjs +1 -1
  47. package/src/ComplexSelector/ComplexSelector.doc.mjs +2 -2
  48. package/src/DateInput/DateInput.doc.mjs +1 -1
  49. package/src/DateRangeInput/DateRangeInput.doc.mjs +1 -1
  50. package/src/DateTimeInput/DateTimeInput.doc.mjs +1 -1
  51. package/src/Field/Field.doc.mjs +1 -1
  52. package/src/FieldStatus/FieldStatus.doc.mjs +1 -1
  53. package/src/FileInput/FileInput.doc.mjs +1 -1
  54. package/src/Indicator/Indicator.doc.mjs +1 -1
  55. package/src/InputGroup/InputGroup.doc.mjs +1 -1
  56. package/src/MultiSelector/MultiSelector.doc.mjs +1 -1
  57. package/src/NumberInput/NumberInput.doc.mjs +1 -1
  58. package/src/PowerSearch/PowerSearch.doc.mjs +1 -1
  59. package/src/RadioList/RadioList.doc.mjs +1 -1
  60. package/src/Selector/Selector.doc.mjs +1 -1
  61. package/src/Slider/Slider.doc.mjs +2 -2
  62. package/src/Switch/Switch.doc.mjs +1 -1
  63. package/src/TextArea/TextArea.doc.mjs +1 -1
  64. package/src/TextInput/TextInput.doc.mjs +1 -1
  65. package/src/TimeInput/TimeInput.doc.mjs +1 -1
  66. package/src/Toast/ToastViewport.test.tsx +118 -4
  67. package/src/Toast/useToastGesture.ts +61 -8
  68. package/src/Tokenizer/Tokenizer.doc.mjs +1 -1
  69. package/src/Typeahead/Typeahead.doc.mjs +1 -1
  70. package/src/i18n/InternationalizationContext.ts +3 -0
  71. package/src/i18n/InternationalizationProvider.tsx +8 -1
  72. package/src/i18n/__tests__/resolve.test.ts +23 -1
  73. package/src/i18n/resolve.ts +46 -49
  74. package/src/i18n/useTranslator.ts +2 -7
@@ -647,10 +647,6 @@
647
647
  "defaultMessage": "Выберите дату",
648
648
  "description": "Grey placeholder inside an empty DateTimeInput field. Note: the English says only \"date\" though the field is date+time — do not add \"and time\" unless natural."
649
649
  },
650
- "@astryx.dateTimeInput.dialogLabel": {
651
- "defaultMessage": "Выбор даты",
652
- "description": "Screen-reader-only accessible name for the DateTimeInput picker dialog. Same English-only \"date\" phrasing as the placeholder — don't add \"and time\" unless natural."
653
- },
654
650
  "@astryx.link.newTab": {
655
651
  "defaultMessage": "(откроется в новой вкладке)",
656
652
  "description": "Visually-hidden (screen-reader-only) suffix appended to a Link's accessible name when it opens in a new tab. Example: `Documentation (opens in new tab)`."
@@ -647,10 +647,6 @@
647
647
  "defaultMessage": "Изаберите датум",
648
648
  "description": "Grey placeholder inside an empty DateTimeInput field. Note: the English says only \"date\" though the field is date+time — do not add \"and time\" unless natural."
649
649
  },
650
- "@astryx.dateTimeInput.dialogLabel": {
651
- "defaultMessage": "Изаберите датум",
652
- "description": "Screen-reader-only accessible name for the DateTimeInput picker dialog. Same English-only \"date\" phrasing as the placeholder — don't add \"and time\" unless natural."
653
- },
654
650
  "@astryx.link.newTab": {
655
651
  "defaultMessage": "(отвара се у новој картици)",
656
652
  "description": "Visually-hidden (screen-reader-only) suffix appended to a Link's accessible name when it opens in a new tab. Example: `Documentation (opens in new tab)`."
@@ -643,10 +643,6 @@
643
643
  "defaultMessage": "Välj ett datum",
644
644
  "description": "Grey placeholder inside an empty DateTimeInput field. Note: the English says only \"date\" though the field is date+time — do not add \"and time\" unless natural."
645
645
  },
646
- "@astryx.dateTimeInput.dialogLabel": {
647
- "defaultMessage": "Välj datum",
648
- "description": "Screen-reader-only accessible name for the DateTimeInput picker dialog. Same English-only \"date\" phrasing as the placeholder — don't add \"and time\" unless natural."
649
- },
650
646
  "@astryx.link.newTab": {
651
647
  "defaultMessage": "(öppnas i ny flik)",
652
648
  "description": "Visually-hidden (screen-reader-only) suffix appended to a Link's accessible name when it opens in a new tab. Example: `Documentation (opens in new tab)`."
@@ -647,10 +647,6 @@
647
647
  "defaultMessage": "Bir tarih seçin",
648
648
  "description": "Grey placeholder inside an empty DateTimeInput field. Note: the English says only \"date\" though the field is date+time — do not add \"and time\" unless natural."
649
649
  },
650
- "@astryx.dateTimeInput.dialogLabel": {
651
- "defaultMessage": "Tarih seçin",
652
- "description": "Screen-reader-only accessible name for the DateTimeInput picker dialog. Same English-only \"date\" phrasing as the placeholder — don't add \"and time\" unless natural."
653
- },
654
650
  "@astryx.link.newTab": {
655
651
  "defaultMessage": "(yeni sekmede açılır)",
656
652
  "description": "Visually-hidden (screen-reader-only) suffix appended to a Link's accessible name when it opens in a new tab. Example: `Documentation (opens in new tab)`."
@@ -647,10 +647,6 @@
647
647
  "defaultMessage": "Виберіть дату",
648
648
  "description": "Grey placeholder inside an empty DateTimeInput field. Note: the English says only \"date\" though the field is date+time — do not add \"and time\" unless natural."
649
649
  },
650
- "@astryx.dateTimeInput.dialogLabel": {
651
- "defaultMessage": "Виберіть дату",
652
- "description": "Screen-reader-only accessible name for the DateTimeInput picker dialog. Same English-only \"date\" phrasing as the placeholder — don't add \"and time\" unless natural."
653
- },
654
650
  "@astryx.link.newTab": {
655
651
  "defaultMessage": "(відкривається в новій вкладці)",
656
652
  "description": "Visually-hidden (screen-reader-only) suffix appended to a Link's accessible name when it opens in a new tab. Example: `Documentation (opens in new tab)`."
@@ -643,10 +643,6 @@
643
643
  "defaultMessage": "Chọn ngày",
644
644
  "description": "Grey placeholder inside an empty DateTimeInput field. Note: the English says only \"date\" though the field is date+time — do not add \"and time\" unless natural."
645
645
  },
646
- "@astryx.dateTimeInput.dialogLabel": {
647
- "defaultMessage": "Chọn ngày",
648
- "description": "Screen-reader-only accessible name for the DateTimeInput picker dialog. Same English-only \"date\" phrasing as the placeholder — don't add \"and time\" unless natural."
649
- },
650
646
  "@astryx.link.newTab": {
651
647
  "defaultMessage": "(mở trong tab mới)",
652
648
  "description": "Visually-hidden (screen-reader-only) suffix appended to a Link's accessible name when it opens in a new tab. Example: `Documentation (opens in new tab)`."
@@ -651,10 +651,6 @@
651
651
  "defaultMessage": "选择日期",
652
652
  "description": "Grey placeholder inside an empty DateTimeInput field. Note: the English says only \"date\" though the field is date+time — do not add \"and time\" unless natural."
653
653
  },
654
- "@astryx.dateTimeInput.dialogLabel": {
655
- "defaultMessage": "选择日期",
656
- "description": "Screen-reader-only accessible name for the DateTimeInput picker dialog. Same English-only \"date\" phrasing as the placeholder — don't add \"and time\" unless natural."
657
- },
658
654
  "@astryx.link.newTab": {
659
655
  "defaultMessage": "(在新标签页中打开)",
660
656
  "description": "Visually-hidden (screen-reader-only) suffix appended to a Link's accessible name when it opens in a new tab. Example: `Documentation (opens in new tab)`."
@@ -651,10 +651,6 @@
651
651
  "defaultMessage": "選擇日期",
652
652
  "description": "Grey placeholder inside an empty DateTimeInput field. Note: the English says only \"date\" though the field is date+time — do not add \"and time\" unless natural."
653
653
  },
654
- "@astryx.dateTimeInput.dialogLabel": {
655
- "defaultMessage": "選擇日期",
656
- "description": "Screen-reader-only accessible name for the DateTimeInput picker dialog. Same English-only \"date\" phrasing as the placeholder — don't add \"and time\" unless natural."
657
- },
658
654
  "@astryx.link.newTab": {
659
655
  "defaultMessage": "(在新分頁中開啟)",
660
656
  "description": "Visually-hidden (screen-reader-only) suffix appended to a Link's accessible name when it opens in a new tab. Example: `Documentation (opens in new tab)`."
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astryxdesign/core",
3
- "version": "0.5.1",
3
+ "version": "0.5.2-canary.11abdc7",
4
4
  "displayName": "Astryx Core",
5
5
  "description": "The component library. Accessible, themeable React components with built-in spacing, dark mode, and StyleX styling.",
6
6
  "author": "Meta Open Source",
@@ -661,6 +661,7 @@
661
661
  "react-dom": ">=19.0.0"
662
662
  },
663
663
  "devDependencies": {
664
+ "@astryxdesign/cli": "0.5.2-canary.11abdc7",
664
665
  "@babel/cli": "^8.0.4",
665
666
  "@babel/core": "^7.29.7",
666
667
  "@babel/preset-react": "^8.0.1",
@@ -668,8 +669,7 @@
668
669
  "@stylexjs/babel-plugin": "^0.19.0",
669
670
  "@testing-library/dom": "^10.0.0",
670
671
  "@testing-library/jest-dom": "^6.6.0",
671
- "@testing-library/react": "^16.3.2",
672
- "@astryxdesign/cli": "0.5.1"
672
+ "@testing-library/react": "^16.3.2"
673
673
  },
674
674
  "dependencies": {
675
675
  "intl-messageformat": "^11.2.9"
@@ -5,7 +5,7 @@
5
5
  export const docs = {
6
6
  name: 'Calendar',
7
7
  displayName: 'Calendar',
8
- category: 'Data Input',
8
+ category: 'Form Controls',
9
9
  keywords: ["calendar","datepicker","date picker","rangepicker","date range","monthview","daypicker"],
10
10
  usage: {
11
11
  description:
@@ -6,7 +6,7 @@ export const docs = {
6
6
  name: 'CheckboxInput',
7
7
  displayName: 'Checkbox Input',
8
8
  group: 'Checkbox',
9
- category: 'Data Input',
9
+ category: 'Form Controls',
10
10
  keywords: ["checkbox","check","toggle","tick","indeterminate","boolean","tristate"],
11
11
  props: [
12
12
  {
@@ -6,7 +6,7 @@ export const docs = {
6
6
  name: 'CheckboxList',
7
7
  displayName: 'Checkbox List',
8
8
  group: 'Checkbox',
9
- category: 'Data Input',
9
+ category: 'Form Controls',
10
10
  isHiddenFromOverview: true,
11
11
  keywords: ["checkboxlist","checkbox","checkboxgroup","multichoice","multiselect","checklist"],
12
12
  description: 'Checkbox group container with field integration for label, description, and status.',
@@ -15,7 +15,7 @@ export const docs = {
15
15
  name: 'ComplexSelector',
16
16
  displayName: 'Complex Selector',
17
17
  group: 'Selector',
18
- category: 'Data Input',
18
+ category: 'Form Controls',
19
19
  keywords: [
20
20
  'selector',
21
21
  'picker',
@@ -214,7 +214,7 @@ export const docsDense = {
214
214
  name: 'ComplexSelector',
215
215
  displayName: 'Complex Selector',
216
216
  group: 'Selector',
217
- category: 'Data Input',
217
+ category: 'Form Controls',
218
218
  description:
219
219
  'Input/ghost trigger + dialog-popover shell for rich custom selectors. Content gets value/onChange/close/state; content owns semantics. Use focus hooks and evaluate custom content against WCAG 2.2.',
220
220
  usage: {
@@ -6,7 +6,7 @@ export const docs = {
6
6
  name: 'DateInput',
7
7
  displayName: 'Date Input',
8
8
  group: 'DateInput',
9
- category: 'Data Input',
9
+ category: 'Form Controls',
10
10
  keywords: [
11
11
  'dateinput',
12
12
  'datepicker',
@@ -6,7 +6,7 @@ export const docs = {
6
6
  name: 'DateRangeInput',
7
7
  displayName: 'Date Range Input',
8
8
  group: 'DateInput',
9
- category: 'Data Input',
9
+ category: 'Form Controls',
10
10
  keywords: [
11
11
  'daterangepicker',
12
12
  'daterange',
@@ -6,7 +6,7 @@ export const docs = {
6
6
  name: 'DateTimeInput',
7
7
  displayName: 'Date Time Input',
8
8
  group: 'DateInput',
9
- category: 'Data Input',
9
+ category: 'Form Controls',
10
10
  keywords: [
11
11
  'datetimepicker',
12
12
  'datetime',
@@ -6,7 +6,7 @@ export const docs = {
6
6
  name: 'Field',
7
7
  displayName: 'Field',
8
8
  group: 'Field',
9
- category: 'Data Input',
9
+ category: 'Form Controls',
10
10
  keywords: ["field","formfield","formgroup","formcontrol","label","input","required","optional","helpertext","hint"],
11
11
  playground: {
12
12
  defaults: {
@@ -5,7 +5,7 @@ export const docs = {
5
5
  name: 'FieldStatus',
6
6
  displayName: 'Field Status',
7
7
  group: 'Field',
8
- category: 'Data Input',
8
+ category: 'Form Controls',
9
9
  isHiddenFromOverview: true,
10
10
  description:
11
11
  'Status message component for form field validation feedback. Messages are announced to screen readers through persistent live regions (assertive for errors, polite otherwise), so conditional mounting is safe.',
@@ -5,7 +5,7 @@
5
5
  export const docs = {
6
6
  name: 'FileInput',
7
7
  displayName: 'File Input',
8
- category: 'Data Input',
8
+ category: 'Form Controls',
9
9
  keywords: ["fileinput","file","upload","drag","drop","dropzone","attachment","browse"],
10
10
  props: [
11
11
  {
@@ -6,7 +6,7 @@ export const docs = {
6
6
  name: 'Indicator',
7
7
  displayName: 'Indicator',
8
8
  group: 'Indicator',
9
- category: 'Data Input',
9
+ category: 'Form Controls',
10
10
  isHiddenFromOverview: true,
11
11
  keywords: [
12
12
  'indicator',
@@ -6,7 +6,7 @@ export const docs = {
6
6
  name: 'InputGroup',
7
7
  displayName: 'Input Group',
8
8
  group: 'Field',
9
- category: 'Data Input',
9
+ category: 'Form Controls',
10
10
  isHiddenFromOverview: true,
11
11
  keywords: [
12
12
  'inputgroup',
@@ -6,7 +6,7 @@ export const docs = {
6
6
  name: 'MultiSelector',
7
7
  displayName: 'Multi Selector',
8
8
  group: 'Selector',
9
- category: 'Data Input',
9
+ category: 'Form Controls',
10
10
  keywords: [
11
11
  'multiselect',
12
12
  'checkbox',
@@ -5,7 +5,7 @@
5
5
  export const docs = {
6
6
  name: 'NumberInput',
7
7
  displayName: 'Number Input',
8
- category: 'Data Input',
8
+ category: 'Form Controls',
9
9
  keywords: [
10
10
  'numberinput',
11
11
  'numberfield',
@@ -5,7 +5,7 @@
5
5
  export const docs = {
6
6
  name: 'PowerSearch',
7
7
  displayName: 'Power Search',
8
- category: 'Data Input',
8
+ category: 'Form Controls',
9
9
  keywords: [
10
10
  'powersearch',
11
11
  'search',
@@ -6,7 +6,7 @@ export const docs = {
6
6
  name: 'RadioList',
7
7
  displayName: 'Radio List',
8
8
  group: 'Radio',
9
- category: 'Data Input',
9
+ category: 'Form Controls',
10
10
  keywords: ["radiolist","radio","radiogroup","radiobutton","optionlist","singlechoice","choicelist"],
11
11
  theming: {
12
12
  targets: [
@@ -6,7 +6,7 @@ export const docs = {
6
6
  name: 'Selector',
7
7
  displayName: 'Selector',
8
8
  group: 'Selector',
9
- category: 'Data Input',
9
+ category: 'Form Controls',
10
10
  keywords: [
11
11
  'selector',
12
12
  'select',
@@ -5,7 +5,7 @@
5
5
  export const docs = {
6
6
  name: 'Slider',
7
7
  displayName: 'Slider',
8
- category: 'Data Input',
8
+ category: 'Form Controls',
9
9
  keywords: ["slider","range","slidebar","trackbar","scrubber","knob","thumb","rangeslider"],
10
10
  playground: {
11
11
  defaults: {
@@ -364,4 +364,4 @@ export const docsDense = {
364
364
  labelTooltip: 'Tooltip text for info icon next to label.',
365
365
  xstyle: 'StyleX layout styles; must be stylex.create() value.',
366
366
  },
367
- };
367
+ };
@@ -5,7 +5,7 @@
5
5
  export const docs = {
6
6
  name: 'Switch',
7
7
  displayName: 'Switch',
8
- category: 'Data Input',
8
+ category: 'Form Controls',
9
9
  keywords: [
10
10
  'switch',
11
11
  'toggle',
@@ -5,7 +5,7 @@
5
5
  export const docs = {
6
6
  name: 'TextArea',
7
7
  displayName: 'Text Area',
8
- category: 'Data Input',
8
+ category: 'Form Controls',
9
9
  keywords: ["textarea","textfield","multiline","comment","message","autoresize","autosize","charlimit"],
10
10
  props: [
11
11
  {
@@ -5,7 +5,7 @@
5
5
  export const docs = {
6
6
  name: 'TextInput',
7
7
  displayName: 'Text Input',
8
- category: 'Data Input',
8
+ category: 'Form Controls',
9
9
  keywords: ["textinput","textfield","input","search","clearable","prefix","suffix","adornment","validation"],
10
10
  props: [
11
11
  {
@@ -5,7 +5,7 @@
5
5
  export const docs = {
6
6
  name: 'TimeInput',
7
7
  displayName: 'Time Input',
8
- category: 'Data Input',
8
+ category: 'Form Controls',
9
9
  keywords: [
10
10
  'timeinput',
11
11
  'timepicker',
@@ -793,6 +793,11 @@ describe('Toast swipe dismissal', () => {
793
793
  expect(visualToast.style.getPropertyValue('--_toast-swipe-exit-y')).toBe(
794
794
  '120%',
795
795
  );
796
+ expect(visualToast.style.getPropertyValue('--_toast-swipe-y')).toBe('');
797
+ expect(visualToast.style.getPropertyValue('--_toast-swipe-opacity')).toBe(
798
+ '',
799
+ );
800
+ expect(visualToast.style.getPropertyValue('--_toast-swipe-scale')).toBe('');
796
801
  expect(getComputedStyle(visualToast).transform).not.toContain('translateX');
797
802
  });
798
803
 
@@ -1147,11 +1152,9 @@ describe('Toast swipe dismissal', () => {
1147
1152
 
1148
1153
  expect(onHide).toHaveBeenCalledWith('manual');
1149
1154
  expect(visualToast.style.getPropertyValue('--_toast-swipe-opacity')).toBe(
1150
- '0.700',
1151
- );
1152
- expect(visualToast.style.getPropertyValue('--_toast-swipe-scale')).toBe(
1153
- '0.985',
1155
+ '',
1154
1156
  );
1157
+ expect(visualToast.style.getPropertyValue('--_toast-swipe-scale')).toBe('');
1155
1158
  expect(visualToast.style.getPropertyValue('--_toast-swipe-exit-y')).toBe(
1156
1159
  'calc(-1 * 120%)',
1157
1160
  );
@@ -1248,6 +1251,117 @@ describe('Toast swipe dismissal', () => {
1248
1251
  );
1249
1252
  });
1250
1253
 
1254
+ it('abandons a one-finger swipe when a second touch starts', () => {
1255
+ const {visualToast, onHide} = renderSwipeToast();
1256
+ const dispatchTouch = (
1257
+ type: 'touchstart' | 'touchmove',
1258
+ touches: {identifier: number; clientX: number; clientY: number}[],
1259
+ changedTouches = touches,
1260
+ ) => {
1261
+ const event = new Event(type, {bubbles: true, cancelable: true});
1262
+ Object.defineProperty(event, 'touches', {value: touches});
1263
+ Object.defineProperty(event, 'changedTouches', {value: changedTouches});
1264
+ visualToast.dispatchEvent(event);
1265
+ return event;
1266
+ };
1267
+ const first = {identifier: 51, clientX: 10, clientY: 0};
1268
+ const second = {identifier: 52, clientX: 30, clientY: 40};
1269
+
1270
+ act(() => {
1271
+ dispatchTouch('touchstart', [first]);
1272
+ dispatchTouch('touchmove', [{...first, clientY: 40}]);
1273
+ });
1274
+ expect(visualToast.style.getPropertyValue('--_toast-swipe-y')).toBe('40px');
1275
+
1276
+ act(() => {
1277
+ dispatchTouch('touchstart', [first, second], [second]);
1278
+ });
1279
+ const multitouchMove = dispatchTouch('touchmove', [first, second]);
1280
+
1281
+ expect(multitouchMove.defaultPrevented).toBe(false);
1282
+ expect(visualToast.style.getPropertyValue('--_toast-swipe-y')).toBe('');
1283
+ expect(visualToast.style.getPropertyValue('--_toast-swipe-opacity')).toBe(
1284
+ '',
1285
+ );
1286
+ expect(onHide).not.toHaveBeenCalled();
1287
+ });
1288
+
1289
+ it('does not dismiss when another touch remains outside the Toast', () => {
1290
+ const {visualToast, onHide} = renderSwipeToast();
1291
+ const first = {identifier: 61, clientX: 10, clientY: 0};
1292
+ const second = {identifier: 62, clientX: 200, clientY: 200};
1293
+ const dispatchTouch = (
1294
+ type: 'touchstart' | 'touchmove' | 'touchend',
1295
+ touches: (typeof first)[],
1296
+ changedTouches: (typeof first)[],
1297
+ ) => {
1298
+ const event = new Event(type, {bubbles: true, cancelable: true});
1299
+ Object.defineProperty(event, 'touches', {value: touches});
1300
+ Object.defineProperty(event, 'changedTouches', {value: changedTouches});
1301
+ visualToast.dispatchEvent(event);
1302
+ return event;
1303
+ };
1304
+
1305
+ act(() => {
1306
+ dispatchTouch('touchstart', [first], [first]);
1307
+ dispatchTouch(
1308
+ 'touchmove',
1309
+ [{...first, clientY: 60}],
1310
+ [{...first, clientY: 60}],
1311
+ );
1312
+ });
1313
+ expect(visualToast.style.getPropertyValue('--_toast-swipe-y')).toBe('60px');
1314
+
1315
+ // The second contact began outside the Toast, so its touchstart did not
1316
+ // reach the Toast listener. Ending the tracked finger must still cancel
1317
+ // while that other contact remains active.
1318
+ act(() => {
1319
+ dispatchTouch('touchend', [second], [{...first, clientY: 60}]);
1320
+ });
1321
+
1322
+ expect(onHide).not.toHaveBeenCalled();
1323
+ expect(visualToast.style.getPropertyValue('--_toast-swipe-y')).toBe('');
1324
+ expect(visualToast.style.getPropertyValue('--_toast-swipe-opacity')).toBe(
1325
+ '',
1326
+ );
1327
+ });
1328
+
1329
+ it('does not let a pen event with the same numeric ID control a touch gesture', () => {
1330
+ const {visualToast, onHide} = renderSwipeToast();
1331
+ const touch = {identifier: 7, clientX: 10, clientY: 0};
1332
+ const touchStart = new Event('touchstart', {
1333
+ bubbles: true,
1334
+ cancelable: true,
1335
+ });
1336
+ Object.defineProperty(touchStart, 'touches', {value: [touch]});
1337
+ Object.defineProperty(touchStart, 'changedTouches', {value: [touch]});
1338
+
1339
+ act(() => {
1340
+ visualToast.dispatchEvent(touchStart);
1341
+ fireEvent.pointerMove(visualToast, {
1342
+ pointerId: 7,
1343
+ pointerType: 'pen',
1344
+ clientX: 10,
1345
+ clientY: 60,
1346
+ });
1347
+ fireEvent.pointerUp(visualToast, {
1348
+ pointerId: 7,
1349
+ pointerType: 'pen',
1350
+ clientX: 10,
1351
+ clientY: 60,
1352
+ });
1353
+ });
1354
+
1355
+ expect(onHide).not.toHaveBeenCalled();
1356
+ expect(visualToast.style.getPropertyValue('--_toast-swipe-y')).toBe('');
1357
+
1358
+ act(() => {
1359
+ visualToast.dispatchEvent(
1360
+ new Event('touchcancel', {bubbles: true, cancelable: true}),
1361
+ );
1362
+ });
1363
+ });
1364
+
1251
1365
  it('removes native touch listeners when a Toast unmounts', () => {
1252
1366
  const {visualToast, unmount} = renderSwipeToast();
1253
1367
  const removeListener = vi.spyOn(visualToast, 'removeEventListener');