@astryxdesign/core 0.7.5 → 0.7.6-canary.0ef60dd
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/AlertDialog/AlertDialog.d.ts +1 -1
- package/dist/AlertDialog/AlertDialog.d.ts.map +1 -1
- package/dist/AlertDialog/AlertDialog.js +5 -5
- package/dist/Avatar/Avatar.d.ts.map +1 -1
- package/dist/Avatar/Avatar.js +12 -3
- package/dist/Badge/Badge.d.ts.map +1 -1
- package/dist/Badge/Badge.js +7 -1
- package/dist/BottomSheet/BottomSheet.js +1 -1
- package/dist/BottomSheet/BottomSheetPanel.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheetPanel.js +2 -1
- package/dist/BottomSheet/useSheetGestures.d.ts +12 -0
- package/dist/BottomSheet/useSheetGestures.d.ts.map +1 -1
- package/dist/BottomSheet/useSheetGestures.js +30 -3
- package/dist/Button/Button.js +4 -4
- package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
- package/dist/CheckboxInput/CheckboxInput.js +6 -5
- package/dist/ClickableCard/ClickableCard.d.ts.map +1 -1
- package/dist/ClickableCard/ClickableCard.js +37 -4
- package/dist/DateInput/DateInput.d.ts.map +1 -1
- package/dist/DateInput/DateInput.js +12 -4
- package/dist/DateInput/MonthScroller.d.ts.map +1 -1
- package/dist/DateInput/MonthScroller.js +101 -15
- package/dist/DateInput/TouchDateField.d.ts.map +1 -1
- package/dist/DateInput/TouchDateField.js +8 -3
- package/dist/DateRangeInput/DateRangeInput.d.ts.map +1 -1
- package/dist/DateRangeInput/DateRangeInput.js +1 -1
- package/dist/DateTimeInput/DateTimeInput.d.ts +5 -2
- package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
- package/dist/DateTimeInput/DateTimeInput.js +23 -12
- package/dist/DateTimeInput/NativeTimeSegment.d.ts.map +1 -1
- package/dist/DateTimeInput/NativeTimeSegment.js +5 -4
- package/dist/DateTimeInput/TouchDateTimeField.d.ts.map +1 -1
- package/dist/DateTimeInput/TouchDateTimeField.js +4 -3
- package/dist/Dialog/Dialog.d.ts.map +1 -1
- package/dist/Dialog/Dialog.js +34 -9
- package/dist/Dialog/DialogHeader.d.ts.map +1 -1
- package/dist/Dialog/DialogHeader.js +5 -4
- package/dist/Divider/Divider.d.ts.map +1 -1
- package/dist/Divider/Divider.js +2 -1
- package/dist/Drawer/Drawer.d.ts +1 -0
- package/dist/Drawer/Drawer.d.ts.map +1 -1
- package/dist/Drawer/Drawer.js +7 -2
- package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenu.js +7 -3
- package/dist/DropdownMenu/MenuBottomSheetActionList.d.ts.map +1 -1
- package/dist/DropdownMenu/MenuBottomSheetActionList.js +6 -5
- package/dist/DropdownMenu/lazyMenuBottomSheet.d.ts +20 -2
- package/dist/DropdownMenu/lazyMenuBottomSheet.d.ts.map +1 -1
- package/dist/DropdownMenu/lazyMenuBottomSheet.js +31 -3
- package/dist/DropdownMenu/renderDropdownItems.d.ts.map +1 -1
- package/dist/DropdownMenu/renderDropdownItems.js +21 -16
- package/dist/EmptyState/EmptyState.d.ts.map +1 -1
- package/dist/EmptyState/EmptyState.js +2 -1
- package/dist/Field/FieldLabel.d.ts +1 -1
- package/dist/Field/FieldLabel.d.ts.map +1 -1
- package/dist/Field/FieldLabel.js +4 -4
- package/dist/Field/InputClearButton.js +3 -3
- package/dist/Field/PanelSearchInput.js +2 -2
- package/dist/FieldStatus/FieldStatus.js +2 -2
- package/dist/FileInput/FileInput.d.ts.map +1 -1
- package/dist/FileInput/FileInput.js +7 -1
- package/dist/Heading/Heading.js +1 -1
- package/dist/Icon/Icon.js +1 -1
- package/dist/Indicator/CheckIndicator.d.ts.map +1 -1
- package/dist/Indicator/CheckIndicator.js +2 -1
- package/dist/Indicator/CheckboxIndicator.d.ts.map +1 -1
- package/dist/Indicator/CheckboxIndicator.js +3 -1
- package/dist/Indicator/RadioIndicator.d.ts.map +1 -1
- package/dist/Indicator/RadioIndicator.js +3 -1
- package/dist/Item/Item.d.ts.map +1 -1
- package/dist/Item/Item.js +2 -1
- package/dist/Item/ItemSwipeLayer.js +1 -1
- package/dist/Layer/popoverOperations.d.ts +27 -0
- package/dist/Layer/popoverOperations.d.ts.map +1 -0
- package/dist/Layer/popoverOperations.js +159 -0
- package/dist/Layer/useLayer.d.ts +1 -1
- package/dist/Layer/useLayer.d.ts.map +1 -1
- package/dist/Layer/useLayer.js +93 -59
- package/dist/Layout/Layout.js +1 -1
- package/dist/Layout/LayoutContent.js +1 -1
- package/dist/Layout/LayoutFooter.js +1 -1
- package/dist/Layout/LayoutHeader.js +1 -1
- package/dist/Lightbox/Lightbox.d.ts.map +1 -1
- package/dist/Lightbox/Lightbox.js +5 -0
- package/dist/Lightbox/LightboxViewport.a11y.browser.spec.js +5 -4
- package/dist/List/List.js +1 -1
- package/dist/List/ListItem.js +3 -3
- package/dist/MobileNav/MobileNav.d.ts.map +1 -1
- package/dist/MobileNav/MobileNav.js +7 -2
- package/dist/MultiSelector/CreateRow.a11y.chromium.spec.js +7 -6
- package/dist/MultiSelector/MultiSelector.d.ts +1 -1
- package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
- package/dist/MultiSelector/MultiSelector.js +26 -10
- package/dist/NumberInput/NumberInput.d.ts +3 -1
- package/dist/NumberInput/NumberInput.d.ts.map +1 -1
- package/dist/NumberInput/NumberInput.js +3 -3
- package/dist/NumberInput/numberParser.d.ts +3 -0
- package/dist/NumberInput/numberParser.d.ts.map +1 -1
- package/dist/NumberInput/numberParser.js +7 -44
- package/dist/ProgressBar/ProgressBar.d.ts.map +1 -1
- package/dist/ProgressBar/ProgressBar.js +16 -12
- package/dist/Resizable/useResizable.d.ts.map +1 -1
- package/dist/Resizable/useResizable.js +28 -2
- package/dist/SegmentedControl/SegmentedControlItem.d.ts.map +1 -1
- package/dist/SegmentedControl/SegmentedControlItem.js +4 -1
- package/dist/Selector/useSelectorPresentation.d.ts +8 -1
- package/dist/Selector/useSelectorPresentation.d.ts.map +1 -1
- package/dist/Selector/useSelectorPresentation.js +20 -4
- package/dist/Skeleton/Skeleton.d.ts.map +1 -1
- package/dist/Skeleton/Skeleton.js +1 -1
- package/dist/Slider/Slider.d.ts.map +1 -1
- package/dist/Slider/Slider.js +10 -2
- package/dist/Spinner/Spinner.d.ts.map +1 -1
- package/dist/Spinner/Spinner.js +3 -3
- package/dist/Stack/Stack.js +1 -1
- package/dist/Stack/StackItem.js +1 -1
- package/dist/TabList/TabList.d.ts.map +1 -1
- package/dist/TabList/TabList.js +16 -1
- package/dist/Table/plugins/columnResize/useTableColumnResize.d.ts.map +1 -1
- package/dist/Table/plugins/columnResize/useTableColumnResize.js +9 -1
- package/dist/Text/Text.js +1 -1
- package/dist/Text/useTruncation.d.ts.map +1 -1
- package/dist/Text/useTruncation.js +23 -5
- package/dist/TextArea/TextArea.d.ts +1 -1
- package/dist/TextArea/TextArea.d.ts.map +1 -1
- package/dist/TextArea/TextArea.js +13 -2
- package/dist/TextInput/TextInput.d.ts.map +1 -1
- package/dist/TextInput/TextInput.js +3 -3
- package/dist/TimeInput/TimeInput.d.ts +6 -2
- package/dist/TimeInput/TimeInput.d.ts.map +1 -1
- package/dist/TimeInput/TimeInput.js +21 -14
- package/dist/TimeInput/TouchTimeField.d.ts.map +1 -1
- package/dist/TimeInput/TouchTimeField.js +5 -4
- package/dist/Timestamp/Timestamp.d.ts.map +1 -1
- package/dist/Timestamp/Timestamp.js +2 -2
- package/dist/Timestamp/TimestampHoverCard.js +4 -4
- package/dist/Toast/Toast.d.ts.map +1 -1
- package/dist/Toast/Toast.js +60 -20
- package/dist/Toast/ToastViewport.d.ts.map +1 -1
- package/dist/Toast/ToastViewport.js +82 -51
- package/dist/Tokenizer/Tokenizer.d.ts.map +1 -1
- package/dist/Tokenizer/Tokenizer.js +13 -2
- package/dist/Tokenizer/TokenizerBottomSheet.js +3 -3
- package/dist/Toolbar/Toolbar.d.ts.map +1 -1
- package/dist/Toolbar/Toolbar.js +2 -1
- package/dist/TopNav/TopNavMegaMenu.d.ts.map +1 -1
- package/dist/TopNav/TopNavMegaMenu.js +3 -0
- package/dist/TopNav/TopNavMenu.d.ts.map +1 -1
- package/dist/TopNav/TopNavMenu.js +1 -0
- package/dist/Typeahead/Typeahead.d.ts +1 -1
- package/dist/Typeahead/Typeahead.d.ts.map +1 -1
- package/dist/Typeahead/Typeahead.js +9 -1
- package/dist/astryx.css +20 -4
- package/dist/gestures/GestureArena.d.ts +6 -0
- package/dist/gestures/GestureArena.d.ts.map +1 -1
- package/dist/gestures/GestureArena.js +39 -0
- package/dist/gestures/GestureHost.d.ts +352 -0
- package/dist/gestures/GestureHost.d.ts.map +1 -0
- package/dist/gestures/GestureHost.js +1773 -0
- package/dist/gestures/GestureRecognizer.d.ts +10 -0
- package/dist/gestures/GestureRecognizer.d.ts.map +1 -1
- package/dist/gestures/GestureRecognizer.js +16 -0
- package/dist/gestures/LongPressGestureRecognizer.d.ts +1 -0
- package/dist/gestures/LongPressGestureRecognizer.d.ts.map +1 -1
- package/dist/gestures/LongPressGestureRecognizer.js +3 -0
- package/dist/gestures/PanGestureRecognizer.d.ts +1 -0
- package/dist/gestures/PanGestureRecognizer.d.ts.map +1 -1
- package/dist/gestures/PanGestureRecognizer.js +3 -0
- package/dist/gestures/PinchGestureRecognizer.d.ts +31 -0
- package/dist/gestures/PinchGestureRecognizer.d.ts.map +1 -0
- package/dist/gestures/PinchGestureRecognizer.js +128 -0
- package/dist/gestures/ScreenEdgePanGestureRecognizer.d.ts +71 -0
- package/dist/gestures/ScreenEdgePanGestureRecognizer.d.ts.map +1 -0
- package/dist/gestures/ScreenEdgePanGestureRecognizer.js +223 -0
- package/dist/gestures/SwipeGestureRecognizer.d.ts +37 -0
- package/dist/gestures/SwipeGestureRecognizer.d.ts.map +1 -0
- package/dist/gestures/SwipeGestureRecognizer.js +148 -0
- package/dist/gestures/TapGestureRecognizer.d.ts +1 -0
- package/dist/gestures/TapGestureRecognizer.d.ts.map +1 -1
- package/dist/gestures/TapGestureRecognizer.js +3 -0
- package/dist/gestures/VelocityTracker.d.ts +6 -2
- package/dist/gestures/VelocityTracker.d.ts.map +1 -1
- package/dist/gestures/VelocityTracker.js +7 -3
- package/dist/gestures/haptics.d.ts +65 -0
- package/dist/gestures/haptics.d.ts.map +1 -0
- package/dist/gestures/haptics.js +182 -0
- package/dist/gestures/nativeScroll.d.ts +42 -0
- package/dist/gestures/nativeScroll.d.ts.map +1 -0
- package/dist/gestures/nativeScroll.js +228 -0
- package/dist/gestures/touchAction.d.ts +60 -0
- package/dist/gestures/touchAction.d.ts.map +1 -0
- package/dist/gestures/touchAction.js +157 -0
- package/dist/gestures/touchActionRule.d.ts +34 -0
- package/dist/gestures/touchActionRule.d.ts.map +1 -0
- package/dist/gestures/touchActionRule.js +140 -0
- package/dist/gestures/types.d.ts +18 -7
- package/dist/gestures/types.d.ts.map +1 -1
- package/dist/gestures/types.js +22 -2
- package/dist/hooks/index.d.ts +1 -1
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/modalAnnounce.d.ts.map +1 -1
- package/dist/hooks/modalAnnounce.js +107 -61
- package/dist/hooks/useAdaptivePresentation.d.ts.map +1 -1
- package/dist/hooks/useAdaptivePresentation.js +4 -1
- package/dist/hooks/useAnnounce.d.ts +101 -42
- package/dist/hooks/useAnnounce.d.ts.map +1 -1
- package/dist/hooks/useAnnounce.js +161 -75
- package/dist/hooks/useAnnounceRenderedText.d.ts +11 -3
- package/dist/hooks/useAnnounceRenderedText.d.ts.map +1 -1
- package/dist/hooks/useAnnounceRenderedText.js +5 -3
- package/dist/hooks/useBusyAnnouncement.d.ts.map +1 -1
- package/dist/hooks/useBusyAnnouncement.js +6 -12
- package/dist/hooks/useMenuHover.d.ts.map +1 -1
- package/dist/hooks/useMenuHover.js +12 -1
- package/dist/i18n/generated-locales/en.generated.d.ts.map +1 -1
- package/dist/i18n/generated-locales/en.generated.js +5 -0
- package/dist/i18n/generated-locales/pseudo.generated.d.ts.map +1 -1
- package/dist/i18n/generated-locales/pseudo.generated.js +5 -0
- package/dist/motion/animator.d.ts +7 -1
- package/dist/motion/animator.d.ts.map +1 -1
- package/dist/motion/animator.js +119 -27
- package/dist/theme/defineTheme.d.ts +19 -5
- package/dist/theme/defineTheme.d.ts.map +1 -1
- package/dist/theme/defineTheme.js +64 -4
- package/dist/theme/domainTokens/domainTokenDefaults.d.ts +3 -0
- package/dist/theme/domainTokens/domainTokenDefaults.d.ts.map +1 -0
- package/dist/theme/domainTokens/domainTokenDefaults.js +19 -0
- package/dist/theme/domainTokens/index.d.ts +5 -4
- package/dist/theme/domainTokens/index.d.ts.map +1 -1
- package/dist/theme/domainTokens/index.js +5 -9
- package/dist/theme/index.d.ts +1 -0
- package/dist/theme/index.d.ts.map +1 -1
- package/dist/theme/motionKnobs.d.ts +56 -0
- package/dist/theme/motionKnobs.d.ts.map +1 -0
- package/dist/theme/motionKnobs.js +48 -0
- package/dist/theme/resolveThemeValues.d.ts +11 -4
- package/dist/theme/resolveThemeValues.d.ts.map +1 -1
- package/dist/theme/resolveThemeValues.js +40 -4
- package/dist/theme/themeAdaptations.d.ts +7 -3
- package/dist/theme/themeAdaptations.d.ts.map +1 -1
- package/dist/theme/themeAdaptations.js +36 -7
- package/dist/theme/themeMotion.d.ts +96 -0
- package/dist/theme/themeMotion.d.ts.map +1 -0
- package/dist/theme/themeMotion.js +177 -0
- package/dist/theme/tokenReferences.d.ts +31 -0
- package/dist/theme/tokenReferences.d.ts.map +1 -0
- package/dist/theme/tokenReferences.js +132 -0
- package/dist/theme/tokens.d.ts +1 -0
- package/dist/theme/tokens.d.ts.map +1 -1
- package/dist/theme/tokens.js +9 -129
- package/dist/theme/tokens.stylex.d.ts +12 -0
- package/dist/theme/tokens.stylex.d.ts.map +1 -1
- package/dist/theme/tokens.stylex.js +12 -0
- package/dist/utils/focusReturn.d.ts +17 -1
- package/dist/utils/focusReturn.d.ts.map +1 -1
- package/dist/utils/focusReturn.js +11 -4
- package/dist/utils/getNodeText.d.ts +8 -0
- package/dist/utils/getNodeText.d.ts.map +1 -0
- package/dist/utils/getNodeText.js +37 -0
- package/dist/utils/interactionModality.d.ts +11 -2
- package/dist/utils/interactionModality.d.ts.map +1 -1
- package/dist/utils/interactionModality.js +58 -5
- package/dist/utils/interactionOverlay.stylex.d.ts.map +1 -1
- package/dist/utils/interactionOverlay.stylex.js +17 -8
- package/dist/utils/plainDate.d.ts +2 -0
- package/dist/utils/plainDate.d.ts.map +1 -1
- package/dist/utils/plainDate.js +46 -3
- package/dist/utils/presentation.d.ts +50 -0
- package/dist/utils/presentation.d.ts.map +1 -0
- package/dist/utils/presentation.js +125 -0
- package/dist/utils/pressFeedback.d.ts.map +1 -1
- package/dist/utils/pressFeedback.js +20 -1
- package/dist/utils/primaryClick.d.ts +11 -0
- package/dist/utils/primaryClick.d.ts.map +1 -0
- package/dist/utils/primaryClick.js +25 -0
- package/dist/utils/timeFormat.d.ts +47 -0
- package/dist/utils/timeFormat.d.ts.map +1 -0
- package/dist/utils/timeFormat.js +240 -0
- package/dist/utils/touchPresentation.d.ts.map +1 -1
- package/dist/utils/touchPresentation.js +5 -3
- package/dist/utils/touchPresentation.stylex.d.ts +15 -0
- package/dist/utils/touchPresentation.stylex.d.ts.map +1 -0
- package/dist/utils/touchPresentation.stylex.js +33 -0
- package/dist/utils/unicodeDigits.d.ts +20 -0
- package/dist/utils/unicodeDigits.d.ts.map +1 -0
- package/dist/utils/unicodeDigits.js +70 -0
- package/locales/en.json +23 -3
- package/locales/pseudo.json +15 -0
- package/package.json +5 -4
- package/src/AlertDialog/AlertDialog.tsx +6 -5
- package/src/AlertDialog/__tests__/AlertDialog.a11y.chromium.spec.ts +18 -6
- package/src/Avatar/Avatar.spec.md +22 -22
- package/src/Avatar/Avatar.tsx +18 -1
- package/src/Avatar/__tests__/AvatarForcedColors.a11y.chromium.spec.ts +126 -0
- package/src/Badge/Badge.doc.mjs +1 -1
- package/src/Badge/Badge.tsx +19 -2
- package/src/Badge/__tests__/BadgeLargerText.a11y.chromium.spec.ts +260 -0
- package/src/BottomSheet/BottomSheet.test.tsx +46 -0
- package/src/BottomSheet/BottomSheet.tsx +1 -1
- package/src/BottomSheet/BottomSheetPanel.tsx +2 -1
- package/src/BottomSheet/useSheetGestures.test.ts +52 -0
- package/src/BottomSheet/useSheetGestures.ts +37 -6
- package/src/Breadcrumbs/__tests__/Breadcrumbs.a11y.chromium.spec.ts +18 -6
- package/src/Button/Button.tsx +4 -4
- package/src/Calendar/Calendar.spec.md +87 -48
- package/src/Calendar/__tests__/Calendar.a11y.chromium.spec.ts +21 -7
- package/src/Carousel/__tests__/Carousel.a11y.chromium.spec.ts +21 -7
- package/src/CheckboxInput/CheckboxInput.tsx +6 -5
- package/src/ClickableCard/ClickableCard.test.tsx +96 -0
- package/src/ClickableCard/ClickableCard.tsx +48 -4
- package/src/Collapsible/Collapsible.spec.md +2 -0
- package/src/Collapsible/CollapsibleGroup.spec.md +57 -29
- package/src/Collapsible/__tests__/Collapsible.a11y.chromium.spec.ts +18 -7
- package/src/Collapsible/__tests__/CollapsibleGroupAccordion.a11y.chromium.spec.ts +18 -6
- package/src/ComplexSelector/ComplexSelector.test.tsx +37 -0
- package/src/ContextMenu/ContextMenu.test.tsx +35 -0
- package/src/DateInput/DateInput.test.tsx +115 -3
- package/src/DateInput/DateInput.tsx +29 -14
- package/src/DateInput/DateInputTouch.test.tsx +76 -0
- package/src/DateInput/MonthScroller.tsx +106 -20
- package/src/DateInput/MonthScrollerSettle.test.tsx +165 -0
- package/src/DateInput/TouchDateField.tsx +14 -3
- package/src/DateInput/__tests__/MonthScrollerSteps.interaction.browser.spec.ts +312 -0
- package/src/DateRangeInput/DateRangeInput.test.tsx +46 -3
- package/src/DateRangeInput/DateRangeInput.tsx +16 -12
- package/src/DateTimeInput/DateTimeInput.doc.mjs +10 -8
- package/src/DateTimeInput/DateTimeInput.spec.md +75 -51
- package/src/DateTimeInput/DateTimeInput.test.tsx +81 -0
- package/src/DateTimeInput/DateTimeInput.tsx +46 -24
- package/src/DateTimeInput/NativeTimeSegment.tsx +4 -6
- package/src/DateTimeInput/TouchDateTimeField.tsx +3 -7
- package/src/Dialog/Dialog.test.tsx +216 -2
- package/src/Dialog/Dialog.tsx +42 -9
- package/src/Dialog/DialogHeader.tsx +5 -4
- package/src/Dialog/__tests__/DialogSecondDocument.a11y.browser.spec.ts +202 -0
- package/src/Divider/Divider.tsx +2 -1
- package/src/Drawer/Drawer.spec.md +111 -80
- package/src/Drawer/Drawer.test.tsx +35 -0
- package/src/Drawer/Drawer.tsx +13 -2
- package/src/Drawer/DrawerHeader.spec.md +181 -0
- package/src/Drawer/__tests__/DrawerNonModalPageScroll.a11y.chromium.spec.ts +267 -0
- package/src/DropdownMenu/DropdownMenu.test.tsx +41 -0
- package/src/DropdownMenu/DropdownMenu.tsx +10 -3
- package/src/DropdownMenu/DropdownMenuSubMenu.test.tsx +34 -0
- package/src/DropdownMenu/MenuBottomSheetActionList.tsx +6 -5
- package/src/DropdownMenu/__tests__/Menu.a11y.chromium.spec.ts +19 -7
- package/src/DropdownMenu/__tests__/MenuButton.a11y.chromium.spec.ts +21 -9
- package/src/DropdownMenu/lazyMenuBottomSheet.test.ts +60 -0
- package/src/DropdownMenu/lazyMenuBottomSheet.ts +36 -7
- package/src/DropdownMenu/renderDropdownItems.tsx +24 -19
- package/src/EmptyState/EmptyState.tsx +2 -1
- package/src/Field/FieldLabel.tsx +5 -4
- package/src/Field/InputClearButton.tsx +3 -3
- package/src/Field/PanelSearchInput.tsx +2 -2
- package/src/FieldStatus/FieldStatus.tsx +2 -2
- package/src/FileInput/FileInput.test.tsx +21 -0
- package/src/FileInput/FileInput.tsx +4 -1
- package/src/FormLayout/FormLayout.spec.md +7 -8
- package/src/Heading/Heading.tsx +1 -1
- package/src/Icon/Icon.tsx +1 -1
- package/src/Indicator/CheckIndicator.spec.md +47 -42
- package/src/Indicator/CheckIndicator.tsx +2 -1
- package/src/Indicator/CheckboxIndicator.spec.md +40 -37
- package/src/Indicator/CheckboxIndicator.tsx +3 -1
- package/src/Indicator/Indicator.spec.md +188 -0
- package/src/Indicator/RadioIndicator.tsx +3 -1
- package/src/Item/Item.tsx +2 -1
- package/src/Item/ItemSwipeLayer.tsx +1 -1
- package/src/Kbd/Kbd.spec.md +81 -38
- package/src/Layer/__tests__/LayerBrowserHide.a11y.chromium.spec.ts +126 -0
- package/src/Layer/popoverOperations.test.ts +205 -0
- package/src/Layer/popoverOperations.ts +172 -0
- package/src/Layer/useLayer.test.tsx +348 -1
- package/src/Layer/useLayer.tsx +118 -71
- package/src/Layout/Layout.spec.md +127 -131
- package/src/Layout/Layout.tsx +1 -1
- package/src/Layout/LayoutContent.spec.md +205 -0
- package/src/Layout/LayoutContent.tsx +1 -1
- package/src/Layout/LayoutFooter.spec.md +190 -0
- package/src/Layout/LayoutFooter.tsx +1 -1
- package/src/Layout/LayoutHeader.spec.md +193 -0
- package/src/Layout/LayoutHeader.tsx +1 -1
- package/src/Layout/LayoutPanel.spec.md +194 -0
- package/src/Layout/__tests__/Layout.a11y.chromium.spec.ts +18 -6
- package/src/Lightbox/Lightbox.test.tsx +18 -0
- package/src/Lightbox/Lightbox.tsx +5 -0
- package/src/Lightbox/LightboxViewport.a11y.browser.spec.ts +8 -4
- package/src/Link/__tests__/Link.a11y.chromium.spec.ts +18 -8
- package/src/List/List.tsx +1 -1
- package/src/List/ListItem.spec.md +205 -0
- package/src/List/ListItem.tsx +3 -3
- package/src/MetadataList/MetadataList.spec.md +187 -0
- package/src/MetadataList/MetadataListItem.spec.md +165 -0
- package/src/MobileNav/MobileNav.test.tsx +15 -0
- package/src/MobileNav/MobileNav.tsx +9 -1
- package/src/MobileNav/__tests__/MobileNavHeader.a11y.chromium.spec.ts +89 -0
- package/src/MoreMenu/MoreMenu.test.tsx +11 -0
- package/src/MultiSelector/CreateRow.a11y.chromium.spec.ts +7 -8
- package/src/MultiSelector/MultiSelector.spec.md +7 -4
- package/src/MultiSelector/MultiSelector.test.tsx +270 -1
- package/src/MultiSelector/MultiSelector.tsx +25 -10
- package/src/MultiSelector/__tests__/MultiSelector.listbox.a11y.test.tsx +8 -1
- package/src/NavMenu/NavHeadingMenu.spec.md +1 -5
- package/src/NumberInput/NumberInput.doc.mjs +13 -7
- package/src/NumberInput/NumberInput.spec.md +59 -37
- package/src/NumberInput/NumberInput.tsx +13 -3
- package/src/NumberInput/__tests__/NumberInput.coarse-pointer.a11y.chromium.spec.ts +102 -0
- package/src/NumberInput/numberParser.ts +7 -45
- package/src/ProgressBar/ProgressBar.test.tsx +108 -32
- package/src/ProgressBar/ProgressBar.tsx +58 -10
- package/src/ProgressBar/__tests__/ProgressBar.a11y.chromium.spec.ts +21 -9
- package/src/ProgressBar/__tests__/ProgressBarMotion.a11y.chromium.spec.ts +831 -0
- package/src/Resizable/__tests__/ResizeHandle.a11y.chromium.spec.ts +21 -14
- package/src/Resizable/useResizable.test.ts +43 -0
- package/src/Resizable/useResizable.ts +32 -9
- package/src/SegmentedControl/SegmentedControl.test.tsx +84 -0
- package/src/SegmentedControl/SegmentedControlItem.tsx +11 -5
- package/src/Selector/Selector.test.tsx +90 -1
- package/src/Selector/__tests__/Selector.combobox.a11y.test.tsx +8 -1
- package/src/Selector/__tests__/Selector.listbox.a11y.test.tsx +8 -1
- package/src/Selector/useSelectorPresentation.ts +31 -3
- package/src/SideNav/SideNav.test.tsx +27 -0
- package/src/SideNav/__tests__/SideNavRailMotion.interaction.browser.spec.ts +176 -51
- package/src/Skeleton/Skeleton.tsx +3 -1
- package/src/Slider/Slider.test.tsx +31 -0
- package/src/Slider/Slider.tsx +8 -3
- package/src/Slider/__tests__/Slider.a11y.chromium.spec.ts +19 -7
- package/src/Spinner/Spinner.tsx +5 -3
- package/src/Stack/Stack.tsx +1 -1
- package/src/Stack/StackItem.tsx +1 -1
- package/src/TabList/TabList.test.tsx +58 -4
- package/src/TabList/TabList.tsx +16 -1
- package/src/TabList/__tests__/TabPressPointer.interaction.browser.spec.ts +163 -0
- package/src/Table/__tests__/Table.a11y.chromium.spec.ts +18 -6
- package/src/Table/plugins/columnResize/useTableColumnResize.test.tsx +22 -0
- package/src/Table/plugins/columnResize/useTableColumnResize.tsx +8 -4
- package/src/Text/Text.tsx +1 -1
- package/src/Text/useTruncation.test.tsx +128 -1
- package/src/Text/useTruncation.ts +23 -5
- package/src/TextArea/TextArea.spec.md +10 -13
- package/src/TextArea/TextArea.test.tsx +60 -0
- package/src/TextArea/TextArea.tsx +15 -1
- package/src/TextInput/TextInput.test.tsx +21 -0
- package/src/TextInput/TextInput.tsx +3 -1
- package/src/TimeInput/NativeTimeInput.test.tsx +17 -0
- package/src/TimeInput/Presentation.test.tsx +21 -0
- package/src/TimeInput/TimeInput.doc.mjs +8 -8
- package/src/TimeInput/TimeInput.spec.md +45 -48
- package/src/TimeInput/TimeInput.test.tsx +76 -0
- package/src/TimeInput/TimeInput.tsx +36 -18
- package/src/TimeInput/TouchTimeField.tsx +4 -6
- package/src/Timestamp/Timestamp.spec.md +6 -12
- package/src/Timestamp/Timestamp.test.tsx +30 -5
- package/src/Timestamp/Timestamp.tsx +6 -2
- package/src/Timestamp/TimestampHoverCard.tsx +4 -4
- package/src/Toast/Toast.spec.md +14 -20
- package/src/Toast/Toast.tsx +104 -36
- package/src/Toast/ToastViewport.test.tsx +719 -1
- package/src/Toast/ToastViewport.tsx +100 -50
- package/src/Tokenizer/Tokenizer.spec.md +5 -3
- package/src/Tokenizer/Tokenizer.test.tsx +68 -0
- package/src/Tokenizer/Tokenizer.tsx +14 -2
- package/src/Tokenizer/TokenizerBottomSheet.test.tsx +15 -4
- package/src/Tokenizer/TokenizerBottomSheet.tsx +3 -3
- package/src/Toolbar/Toolbar.spec.md +5 -6
- package/src/Toolbar/Toolbar.test.tsx +30 -0
- package/src/Toolbar/Toolbar.tsx +2 -1
- package/src/Toolbar/__tests__/Toolbar.a11y.chromium.spec.ts +18 -6
- package/src/Tooltip/__tests__/Tooltip.a11y.chromium.spec.ts +18 -7
- package/src/TopNav/TopNav.test.tsx +27 -0
- package/src/TopNav/TopNavMegaMenu.test.tsx +21 -0
- package/src/TopNav/TopNavMegaMenu.tsx +4 -1
- package/src/TopNav/TopNavMenu.test.tsx +17 -0
- package/src/TopNav/TopNavMenu.tsx +1 -0
- package/src/Typeahead/Typeahead.test.tsx +40 -0
- package/src/Typeahead/Typeahead.tsx +12 -1
- package/src/__tests__/pressState.ts +8 -4
- package/src/gestures/GestureArena.ts +35 -0
- package/src/gestures/GestureHost.ts +2065 -0
- package/src/gestures/GestureRecognizer.ts +18 -0
- package/src/gestures/LongPressGestureRecognizer.ts +4 -0
- package/src/gestures/PanGestureRecognizer.ts +4 -0
- package/src/gestures/PinchGestureRecognizer.ts +148 -0
- package/src/gestures/ScreenEdgePanGestureRecognizer.ts +256 -0
- package/src/gestures/SwipeGestureRecognizer.ts +197 -0
- package/src/gestures/TapGestureRecognizer.ts +4 -0
- package/src/gestures/VelocityTracker.ts +10 -3
- package/src/gestures/__tests__/GestureHost.test.ts +2087 -0
- package/src/gestures/__tests__/PanGestureRecognizer.test.ts +4 -4
- package/src/gestures/__tests__/PinchGestureRecognizer.test.ts +74 -0
- package/src/gestures/__tests__/ScreenEdgePanGestureRecognizer.test.ts +498 -0
- package/src/gestures/__tests__/SwipeGestureRecognizer.test.ts +108 -0
- package/src/gestures/__tests__/arena.test.ts +5 -0
- package/src/gestures/__tests__/arenaPinchSwipeEdge.test.ts +193 -0
- package/src/gestures/__tests__/browser/build-fixture.mjs +46 -0
- package/src/gestures/__tests__/browser/fixture.ts +307 -0
- package/src/gestures/__tests__/browser/gestures.interaction.browser.spec.ts +1137 -0
- package/src/gestures/__tests__/browserNegotiation.test.ts +293 -0
- package/src/gestures/__tests__/haptics.test.ts +187 -0
- package/src/gestures/__tests__/harness.ts +21 -2
- package/src/gestures/__tests__/recognizerKind.test.ts +78 -0
- package/src/gestures/__tests__/touchActionRule.test.ts +54 -0
- package/src/gestures/haptics.ts +203 -0
- package/src/gestures/nativeScroll.ts +277 -0
- package/src/gestures/touchAction.ts +191 -0
- package/src/gestures/touchActionRule.ts +157 -0
- package/src/gestures/types.ts +30 -9
- package/src/hooks/index.ts +5 -1
- package/src/hooks/modalAnnounce.ts +129 -72
- package/src/hooks/useAdaptivePresentation.ts +4 -1
- package/src/hooks/useAnnounce.doc.mjs +35 -3
- package/src/hooks/useAnnounce.replace.test.tsx +516 -0
- package/src/hooks/useAnnounce.ts +209 -102
- package/src/hooks/useAnnounceRenderedText.adopters.test.tsx +10 -1
- package/src/hooks/useAnnounceRenderedText.ts +17 -3
- package/src/hooks/useBusyAnnouncement.ts +7 -10
- package/src/hooks/useMenuHover.ts +15 -1
- package/src/i18n/generated-locales/en.generated.ts +6 -0
- package/src/i18n/generated-locales/pseudo.generated.ts +6 -0
- package/src/motion/README.md +11 -1
- package/src/motion/animator.test.ts +151 -3
- package/src/motion/animator.ts +149 -33
- package/src/theme/Theme.doc.mjs +1 -1
- package/src/theme/defineTheme.ts +117 -8
- package/src/theme/derivedVarRegistry.test.ts +4 -0
- package/src/theme/domainTokens/domainTokenDefaults.ts +19 -0
- package/src/theme/domainTokens/index.ts +6 -10
- package/src/theme/expandMotionScale.test.ts +1 -1
- package/src/theme/index.ts +5 -0
- package/src/theme/motionKnobs.ts +82 -0
- package/src/theme/resolveThemeValues.ts +68 -6
- package/src/theme/themeAdaptations.ts +52 -12
- package/src/theme/themeMotion.test.ts +620 -0
- package/src/theme/themeMotion.ts +221 -0
- package/src/theme/tokenReferences.ts +149 -0
- package/src/theme/tokens.stylex.ts +10 -0
- package/src/theme/tokens.ts +15 -158
- package/src/utils/focusReturn.ts +17 -6
- package/src/utils/getNodeText.test.ts +24 -0
- package/src/utils/getNodeText.ts +37 -0
- package/src/utils/interactionModality.test.ts +45 -1
- package/src/utils/interactionModality.ts +64 -5
- package/src/utils/interactionOverlay.stylex.ts +12 -3
- package/src/utils/plainDate.test.ts +90 -0
- package/src/utils/plainDate.ts +58 -3
- package/src/utils/presentation.test.ts +380 -0
- package/src/utils/presentation.ts +180 -0
- package/src/utils/pressFeedback.test.ts +138 -0
- package/src/utils/pressFeedback.ts +34 -11
- package/src/utils/pressPaintPriority.test.ts +80 -0
- package/src/utils/primaryClick.test.ts +38 -0
- package/src/utils/primaryClick.ts +28 -0
- package/src/utils/timeFormat.test.ts +204 -0
- package/src/utils/timeFormat.ts +314 -0
- package/src/utils/touchPresentation.stylex.ts +43 -0
- package/src/utils/touchPresentation.test.ts +140 -1
- package/src/utils/touchPresentation.ts +5 -3
- package/src/utils/unicodeDigits.test.ts +44 -0
- package/src/utils/unicodeDigits.ts +71 -0
|
@@ -0,0 +1,2087 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file GestureHost.test.ts
|
|
5
|
+
* @input A GestureHost on the jsdom document, real DOM listeners, and
|
|
6
|
+
* dispatched touch, pointer, click, contextmenu and scroll events with
|
|
7
|
+
* explicit timestamps
|
|
8
|
+
* @output The DOM integration: hit-path collection (shadow DOM included),
|
|
9
|
+
* touch-action negotiation, cancelling touchmove only while claimed,
|
|
10
|
+
* click suppression and replay, browser takeover, the mouse path,
|
|
11
|
+
* context menus, focus loss
|
|
12
|
+
* @position jsdom integration tests of packages/core/src/gestures
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
|
|
16
|
+
import {UIKIT_GESTURES} from '../../motion/uikit/gestures';
|
|
17
|
+
import {GestureHost} from '../GestureHost';
|
|
18
|
+
import {LongPressGestureRecognizer} from '../LongPressGestureRecognizer';
|
|
19
|
+
import {PanGestureRecognizer} from '../PanGestureRecognizer';
|
|
20
|
+
import {ScreenEdgePanGestureRecognizer} from '../ScreenEdgePanGestureRecognizer';
|
|
21
|
+
import {TapGestureRecognizer} from '../TapGestureRecognizer';
|
|
22
|
+
import {ManualClock, RecordingConsumer} from './harness';
|
|
23
|
+
import type {GestureTouch} from '../types';
|
|
24
|
+
|
|
25
|
+
interface Contact {
|
|
26
|
+
id: number;
|
|
27
|
+
x: number;
|
|
28
|
+
y: number;
|
|
29
|
+
target?: EventTarget;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function touch(
|
|
33
|
+
type: 'touchstart' | 'touchmove' | 'touchend' | 'touchcancel',
|
|
34
|
+
target: EventTarget,
|
|
35
|
+
changed: Contact[],
|
|
36
|
+
all: Contact[],
|
|
37
|
+
time: number,
|
|
38
|
+
cancelable = true,
|
|
39
|
+
): Event {
|
|
40
|
+
const event = new Event(type, {bubbles: true, cancelable, composed: true});
|
|
41
|
+
const toTouch = (contact: Contact) => ({
|
|
42
|
+
identifier: contact.id,
|
|
43
|
+
clientX: contact.x,
|
|
44
|
+
clientY: contact.y,
|
|
45
|
+
target: contact.target ?? target,
|
|
46
|
+
});
|
|
47
|
+
Object.defineProperty(event, 'changedTouches', {value: changed.map(toTouch)});
|
|
48
|
+
Object.defineProperty(event, 'touches', {value: all.map(toTouch)});
|
|
49
|
+
Object.defineProperty(event, 'timeStamp', {value: time});
|
|
50
|
+
target.dispatchEvent(event);
|
|
51
|
+
return event;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function pointer(
|
|
55
|
+
type: string,
|
|
56
|
+
target: EventTarget,
|
|
57
|
+
init: {
|
|
58
|
+
id?: number;
|
|
59
|
+
pointerType: string;
|
|
60
|
+
x: number;
|
|
61
|
+
y: number;
|
|
62
|
+
buttons?: number;
|
|
63
|
+
time: number;
|
|
64
|
+
},
|
|
65
|
+
): Event {
|
|
66
|
+
const event = new PointerEvent(type, {
|
|
67
|
+
bubbles: true,
|
|
68
|
+
cancelable: true,
|
|
69
|
+
composed: true,
|
|
70
|
+
pointerId: init.id ?? 1,
|
|
71
|
+
pointerType: init.pointerType,
|
|
72
|
+
clientX: init.x,
|
|
73
|
+
clientY: init.y,
|
|
74
|
+
button: 0,
|
|
75
|
+
buttons: init.buttons ?? 0,
|
|
76
|
+
});
|
|
77
|
+
Object.defineProperty(event, 'timeStamp', {value: init.time});
|
|
78
|
+
target.dispatchEvent(event);
|
|
79
|
+
return event;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
let host: GestureHost;
|
|
83
|
+
let clock: ManualClock;
|
|
84
|
+
let detachers: (() => void)[];
|
|
85
|
+
|
|
86
|
+
/** jsdom computes no layout: give a scroller its metrics. */
|
|
87
|
+
function scrollable(
|
|
88
|
+
element: HTMLElement,
|
|
89
|
+
metrics: {scrollTop?: number; scrollHeight?: number; clientHeight?: number},
|
|
90
|
+
): void {
|
|
91
|
+
for (const [key, value] of Object.entries(metrics)) {
|
|
92
|
+
Object.defineProperty(element, key, {configurable: true, value});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* jsdom drops `touch-action` from styles: elements under test carry it in
|
|
98
|
+
* `data-touch-action`, and computed style reports it from there.
|
|
99
|
+
*/
|
|
100
|
+
function touchActionFromData(): () => void {
|
|
101
|
+
const original = window.getComputedStyle.bind(window);
|
|
102
|
+
const spy = vi
|
|
103
|
+
.spyOn(window, 'getComputedStyle')
|
|
104
|
+
.mockImplementation((element: Element, pseudo?: string | null) => {
|
|
105
|
+
const style = original(element, pseudo);
|
|
106
|
+
// The author's rule outranks the host's zero-specificity rule, which
|
|
107
|
+
// applies where the author sets nothing.
|
|
108
|
+
const touchAction =
|
|
109
|
+
element.getAttribute('data-touch-action') ??
|
|
110
|
+
element.getAttribute('data-astryx-touch-action');
|
|
111
|
+
if (touchAction == null) {
|
|
112
|
+
return style;
|
|
113
|
+
}
|
|
114
|
+
return new Proxy(style, {
|
|
115
|
+
get(target, key): string | number | object | null | undefined {
|
|
116
|
+
if (key === 'touchAction') {
|
|
117
|
+
return touchAction;
|
|
118
|
+
}
|
|
119
|
+
if (key === 'getPropertyValue') {
|
|
120
|
+
return (name: string) =>
|
|
121
|
+
name === 'touch-action'
|
|
122
|
+
? touchAction
|
|
123
|
+
: target.getPropertyValue(name);
|
|
124
|
+
}
|
|
125
|
+
const value = Reflect.get(target, key) as
|
|
126
|
+
string | number | object | null | undefined;
|
|
127
|
+
return typeof value === 'function' ? value.bind(target) : value;
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
return () => spy.mockRestore();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** The host's touch-action rules in a document or shadow root. */
|
|
135
|
+
function hostRules(root: Document | ShadowRoot): string {
|
|
136
|
+
const adopted = (root.adoptedStyleSheets ?? []).flatMap(sheet =>
|
|
137
|
+
[...sheet.cssRules].map(rule => rule.cssText),
|
|
138
|
+
);
|
|
139
|
+
const styles = [...root.querySelectorAll('style')].map(
|
|
140
|
+
style => style.textContent ?? '',
|
|
141
|
+
);
|
|
142
|
+
return [...adopted, ...styles].join('\n');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function mount(html: string): Record<string, HTMLElement> {
|
|
146
|
+
document.body.innerHTML = html;
|
|
147
|
+
const elements: Record<string, HTMLElement> = {};
|
|
148
|
+
for (const element of document.querySelectorAll<HTMLElement>('[id]')) {
|
|
149
|
+
elements[element.id] = element;
|
|
150
|
+
}
|
|
151
|
+
return elements;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
beforeEach(() => {
|
|
155
|
+
clock = new ManualClock();
|
|
156
|
+
host = new GestureHost(document, {clock, isPrimaryPointerCoarse: () => true});
|
|
157
|
+
detachers = [];
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
afterEach(() => {
|
|
161
|
+
for (const detach of detachers) {
|
|
162
|
+
detach();
|
|
163
|
+
}
|
|
164
|
+
document.body.innerHTML = '';
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
describe('GestureHost', () => {
|
|
168
|
+
it("measures a screen-edge pan's view, so an edge-zone strip takes the axis at UIKit's 15 px, not 10 (spec:AST-094 FR18)", () => {
|
|
169
|
+
const {strip} = mount('<div id="strip"></div>');
|
|
170
|
+
strip.getBoundingClientRect = () =>
|
|
171
|
+
DOMRect.fromRect({x: 0, y: 0, width: 40, height: 800});
|
|
172
|
+
const pan = new ScreenEdgePanGestureRecognizer({
|
|
173
|
+
name: 'edge',
|
|
174
|
+
edges: ['leading'],
|
|
175
|
+
});
|
|
176
|
+
detachers.push(host.attach(strip, pan));
|
|
177
|
+
const down = {id: 1, x: 5, y: 300};
|
|
178
|
+
touch('touchstart', strip, [down], [down], 0);
|
|
179
|
+
const at14 = {...down, x: 19};
|
|
180
|
+
touch('touchmove', strip, [at14], [at14], 16);
|
|
181
|
+
expect(pan.state).toBe('possible');
|
|
182
|
+
const at15 = {...down, x: 20};
|
|
183
|
+
touch('touchmove', strip, [at15], [at15], 32);
|
|
184
|
+
expect(pan.state).toBe('began');
|
|
185
|
+
touch('touchend', strip, [at15], [], 48);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("re-derives a screen-edge pan's touch-action when its box or the viewport resizes, with no listener of its own (spec:AST-094 FR19, FR24, DEC-22)", () => {
|
|
189
|
+
// jsdom has no ResizeObserver: a stand-in the test drives.
|
|
190
|
+
const observed = new Set<Element>();
|
|
191
|
+
const callbacks: (() => void)[] = [];
|
|
192
|
+
class StandInResizeObserver {
|
|
193
|
+
constructor(callback: () => void) {
|
|
194
|
+
callbacks.push(callback);
|
|
195
|
+
}
|
|
196
|
+
observe(target: Element): void {
|
|
197
|
+
observed.add(target);
|
|
198
|
+
}
|
|
199
|
+
unobserve(target: Element): void {
|
|
200
|
+
observed.delete(target);
|
|
201
|
+
}
|
|
202
|
+
disconnect(): void {
|
|
203
|
+
observed.clear();
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
const original = Object.getOwnPropertyDescriptor(window, 'ResizeObserver');
|
|
207
|
+
const width = Object.getOwnPropertyDescriptor(window, 'innerWidth');
|
|
208
|
+
Object.defineProperty(window, 'ResizeObserver', {
|
|
209
|
+
configurable: true,
|
|
210
|
+
value: StandInResizeObserver,
|
|
211
|
+
});
|
|
212
|
+
const listen = vi.spyOn(window, 'addEventListener');
|
|
213
|
+
try {
|
|
214
|
+
const {strip, trail} = mount(
|
|
215
|
+
'<div id="strip"></div><div id="trail"></div>',
|
|
216
|
+
);
|
|
217
|
+
let stripWidth = 40;
|
|
218
|
+
strip.getBoundingClientRect = () =>
|
|
219
|
+
DOMRect.fromRect({x: 0, y: 0, width: stripWidth, height: 800});
|
|
220
|
+
trail.getBoundingClientRect = () =>
|
|
221
|
+
DOMRect.fromRect({x: 1004, y: 0, width: 20, height: 800});
|
|
222
|
+
Object.defineProperty(window, 'innerWidth', {
|
|
223
|
+
configurable: true,
|
|
224
|
+
value: 1024,
|
|
225
|
+
});
|
|
226
|
+
const leading = new ScreenEdgePanGestureRecognizer({name: 'edge'});
|
|
227
|
+
const detachStrip = host.attach(strip, leading);
|
|
228
|
+
// A leading pan in LTR watches the left side: the viewport cannot move
|
|
229
|
+
// its band, so only its own box is watched.
|
|
230
|
+
expect(observed).toEqual(new Set([strip]));
|
|
231
|
+
const trailing = new ScreenEdgePanGestureRecognizer({
|
|
232
|
+
name: 'back',
|
|
233
|
+
edges: ['trailing'],
|
|
234
|
+
});
|
|
235
|
+
const detachTrail = host.attach(trail, trailing);
|
|
236
|
+
// The trailing pan's band moves with the viewport: the root is watched.
|
|
237
|
+
expect(observed).toEqual(
|
|
238
|
+
new Set([strip, trail, document.documentElement]),
|
|
239
|
+
);
|
|
240
|
+
expect(strip.getAttribute('data-astryx-touch-action')).toBe(
|
|
241
|
+
'pan-y pinch-zoom',
|
|
242
|
+
);
|
|
243
|
+
expect(trail.getAttribute('data-astryx-touch-action')).toBe(
|
|
244
|
+
'pan-y pinch-zoom',
|
|
245
|
+
);
|
|
246
|
+
// The strip grows past the band: its claim goes.
|
|
247
|
+
stripWidth = 390;
|
|
248
|
+
callbacks.forEach(callback => callback());
|
|
249
|
+
expect(strip.hasAttribute('data-astryx-touch-action')).toBe(false);
|
|
250
|
+
// The viewport widens, so the root resizes: the trailing strip no
|
|
251
|
+
// longer sits at its edge.
|
|
252
|
+
Object.defineProperty(window, 'innerWidth', {
|
|
253
|
+
configurable: true,
|
|
254
|
+
value: 1280,
|
|
255
|
+
});
|
|
256
|
+
callbacks.forEach(callback => callback());
|
|
257
|
+
expect(trail.hasAttribute('data-astryx-touch-action')).toBe(false);
|
|
258
|
+
// A disabled pan is not watched (FR24).
|
|
259
|
+
trailing.setEnabled(false);
|
|
260
|
+
expect(observed).toEqual(new Set([strip]));
|
|
261
|
+
// Nothing is watched once the last edge pan lets go, and no listener
|
|
262
|
+
// was ever added for a resize.
|
|
263
|
+
detachStrip();
|
|
264
|
+
detachTrail();
|
|
265
|
+
expect(observed.size).toBe(0);
|
|
266
|
+
expect(listen).not.toHaveBeenCalledWith(
|
|
267
|
+
'resize',
|
|
268
|
+
expect.anything(),
|
|
269
|
+
expect.anything(),
|
|
270
|
+
);
|
|
271
|
+
} finally {
|
|
272
|
+
listen.mockRestore();
|
|
273
|
+
if (original == null) {
|
|
274
|
+
delete (window as {ResizeObserver?: unknown}).ResizeObserver;
|
|
275
|
+
} else {
|
|
276
|
+
Object.defineProperty(window, 'ResizeObserver', original);
|
|
277
|
+
}
|
|
278
|
+
if (width != null) {
|
|
279
|
+
Object.defineProperty(window, 'innerWidth', width);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it('tells a recognizer tracking a finger about a second finger on plain page content, and leaves that finger to the page (spec:AST-094 FR28)', () => {
|
|
285
|
+
const {box} = mount('<div id="box"></div><p id="page">text</p>');
|
|
286
|
+
const page = document.getElementById('page')!;
|
|
287
|
+
const elsewhere: number[] = [];
|
|
288
|
+
class Recorder extends LongPressGestureRecognizer {
|
|
289
|
+
protected override touchBeganElsewhere(
|
|
290
|
+
touches: ReadonlyArray<GestureTouch>,
|
|
291
|
+
): void {
|
|
292
|
+
elsewhere.push(...touches.map(t => t.id));
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
const recorder = new Recorder({name: 'recorder'});
|
|
296
|
+
detachers.push(host.attach(box, recorder));
|
|
297
|
+
const first = {id: 1, x: 10, y: 10};
|
|
298
|
+
touch('touchstart', box, [first], [first], 0);
|
|
299
|
+
const second = {id: 2, x: 200, y: 300};
|
|
300
|
+
const start = touch('touchstart', page, [second], [first, second], 20);
|
|
301
|
+
expect(elsewhere).toHaveLength(1);
|
|
302
|
+
expect(start.defaultPrevented).toBe(false);
|
|
303
|
+
const moved = {...second, y: 340};
|
|
304
|
+
const move = touch('touchmove', page, [moved], [first, moved], 36);
|
|
305
|
+
expect(move.defaultPrevented).toBe(false);
|
|
306
|
+
const end = touch('touchend', page, [moved], [first], 50);
|
|
307
|
+
expect(end.defaultPrevented).toBe(false);
|
|
308
|
+
touch('touchend', box, [first], [], 60);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it('leaves a finger on plain page content alone when no finger is being tracked', () => {
|
|
312
|
+
const {box} = mount('<div id="box"></div><p id="page">text</p>');
|
|
313
|
+
const page = document.getElementById('page')!;
|
|
314
|
+
const elsewhere: number[] = [];
|
|
315
|
+
class Recorder extends LongPressGestureRecognizer {
|
|
316
|
+
protected override touchBeganElsewhere(
|
|
317
|
+
touches: ReadonlyArray<GestureTouch>,
|
|
318
|
+
): void {
|
|
319
|
+
elsewhere.push(...touches.map(t => t.id));
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
detachers.push(host.attach(box, new Recorder({name: 'recorder'})));
|
|
323
|
+
const finger = {id: 1, x: 200, y: 300};
|
|
324
|
+
touch('touchstart', page, [finger], [finger], 0);
|
|
325
|
+
touch('touchend', page, [finger], [], 30);
|
|
326
|
+
expect(elsewhere).toEqual([]);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it("does not read a recognizer's own scroll write as the browser taking the finger (spec:AST-094 FR30)", async () => {
|
|
330
|
+
const {list} = mount('<div id="list"><div id="row"></div></div>');
|
|
331
|
+
scrollable(list, {scrollHeight: 1000, clientHeight: 200});
|
|
332
|
+
const row = document.getElementById('row')!;
|
|
333
|
+
// Tolerant enough to keep tracking a finger that has travelled the
|
|
334
|
+
// press slop: only such a finger's touch can be the browser's pan.
|
|
335
|
+
const press = new LongPressGestureRecognizer({
|
|
336
|
+
name: 'press',
|
|
337
|
+
allowableMovement: 100,
|
|
338
|
+
});
|
|
339
|
+
detachers.push(host.attach(row, press));
|
|
340
|
+
const finger = {id: 1, x: 10, y: 10};
|
|
341
|
+
touch('touchstart', row, [finger], [finger], 0);
|
|
342
|
+
const moved = {...finger, y: 40};
|
|
343
|
+
touch('touchmove', row, [moved], [moved], 16);
|
|
344
|
+
host.ownScroll(list);
|
|
345
|
+
list.dispatchEvent(new Event('scroll'));
|
|
346
|
+
expect(press.numberOfTouches).toBe(1);
|
|
347
|
+
// After the next frame, a scroll nobody announced is the browser's again.
|
|
348
|
+
await new Promise(resolve =>
|
|
349
|
+
requestAnimationFrame(() => requestAnimationFrame(resolve)),
|
|
350
|
+
);
|
|
351
|
+
await new Promise(resolve => requestAnimationFrame(resolve));
|
|
352
|
+
list.dispatchEvent(new Event('scroll'));
|
|
353
|
+
expect(press.numberOfTouches).toBe(0);
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
it('watches the pointer kind only while something is attached (spec:AST-094 FR24)', () => {
|
|
357
|
+
const {box} = mount('<div id="box"></div>');
|
|
358
|
+
const add = vi.fn();
|
|
359
|
+
const remove = vi.fn();
|
|
360
|
+
const matchMedia = window.matchMedia;
|
|
361
|
+
window.matchMedia = ((query: string) => ({
|
|
362
|
+
matches: false,
|
|
363
|
+
media: query,
|
|
364
|
+
addEventListener: add,
|
|
365
|
+
removeEventListener: remove,
|
|
366
|
+
})) as unknown as typeof window.matchMedia;
|
|
367
|
+
try {
|
|
368
|
+
const host2 = new GestureHost(document, {clock});
|
|
369
|
+
expect(add).not.toHaveBeenCalled();
|
|
370
|
+
const detach = host2.attach(box, new PanGestureRecognizer());
|
|
371
|
+
expect(add).toHaveBeenCalledTimes(1);
|
|
372
|
+
detach();
|
|
373
|
+
expect(remove).toHaveBeenCalledWith('change', add.mock.calls[0][1]);
|
|
374
|
+
} finally {
|
|
375
|
+
window.matchMedia = matchMedia;
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
it('sets touch-action for every pointer while a recognizer is enabled (spec:AST-094 FR18, FR43)', () => {
|
|
380
|
+
const {box} = mount('<div id="box"></div>');
|
|
381
|
+
const mark = () => box.getAttribute('data-astryx-touch-action');
|
|
382
|
+
let coarse = false;
|
|
383
|
+
const host2 = new GestureHost(document, {
|
|
384
|
+
clock,
|
|
385
|
+
isPrimaryPointerCoarse: () => coarse,
|
|
386
|
+
});
|
|
387
|
+
const pan = new PanGestureRecognizer({direction: 'horizontal'});
|
|
388
|
+
const detach = host2.attach(box, pan);
|
|
389
|
+
// A touchscreen laptop's finger gets it as a phone's does.
|
|
390
|
+
expect(mark()).toBe('pan-y pinch-zoom');
|
|
391
|
+
pan.setEnabled(false);
|
|
392
|
+
expect(mark()).toBeNull();
|
|
393
|
+
pan.setEnabled(true);
|
|
394
|
+
expect(mark()).toBe('pan-y pinch-zoom');
|
|
395
|
+
coarse = true;
|
|
396
|
+
host2.update(box);
|
|
397
|
+
expect(mark()).toBe('pan-y pinch-zoom');
|
|
398
|
+
detach();
|
|
399
|
+
expect(mark()).toBeNull();
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
it('registers no document listener for a disabled consumer until it is enabled (spec:AST-094 FR24)', () => {
|
|
403
|
+
const {box} = mount('<div id="box"></div>');
|
|
404
|
+
const added = vi.spyOn(document, 'addEventListener');
|
|
405
|
+
const removed = vi.spyOn(document, 'removeEventListener');
|
|
406
|
+
added.mockClear();
|
|
407
|
+
removed.mockClear();
|
|
408
|
+
const types = (spy: typeof added) =>
|
|
409
|
+
spy.mock.calls
|
|
410
|
+
.map(call => call[0])
|
|
411
|
+
.filter(t => /^(touch|pointer)/.test(t));
|
|
412
|
+
const coarse = new GestureHost(document, {
|
|
413
|
+
clock,
|
|
414
|
+
isPrimaryPointerCoarse: () => true,
|
|
415
|
+
});
|
|
416
|
+
let enabled = false;
|
|
417
|
+
const consumer = new RecordingConsumer();
|
|
418
|
+
Object.assign(consumer, {isEnabled: () => enabled});
|
|
419
|
+
const detach = coarse.attachConsumer(box, consumer);
|
|
420
|
+
expect(types(added)).toEqual([]);
|
|
421
|
+
enabled = true;
|
|
422
|
+
coarse.update(box);
|
|
423
|
+
expect(types(added)).toContain('touchstart');
|
|
424
|
+
enabled = false;
|
|
425
|
+
coarse.update(box);
|
|
426
|
+
expect(types(removed)).toContain('touchstart');
|
|
427
|
+
detach();
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
it('registers no page listener while idle where the primary pointer is fine, or while nothing enabled is attached (spec:AST-094 FR24, FR43)', () => {
|
|
431
|
+
const {box} = mount('<div id="box"></div>');
|
|
432
|
+
const added = vi.spyOn(document, 'addEventListener');
|
|
433
|
+
const removed = vi.spyOn(document, 'removeEventListener');
|
|
434
|
+
added.mockClear();
|
|
435
|
+
removed.mockClear();
|
|
436
|
+
const types = (spy: typeof added) =>
|
|
437
|
+
spy.mock.calls
|
|
438
|
+
.map(call => call[0])
|
|
439
|
+
.filter(t => /^(touch|pointer)/.test(t));
|
|
440
|
+
// A fine primary pointer: the page has no listener; the element keeps
|
|
441
|
+
// one passive pointerdown for its press and, as a claimer, its blocking
|
|
442
|
+
// touchmove (FR19, a listener FR24 counts apart).
|
|
443
|
+
const fine = new GestureHost(document, {
|
|
444
|
+
clock,
|
|
445
|
+
isPrimaryPointerCoarse: () => false,
|
|
446
|
+
});
|
|
447
|
+
const boxAdded = vi.spyOn(box, 'addEventListener');
|
|
448
|
+
const detachFine = fine.attach(box, new PanGestureRecognizer());
|
|
449
|
+
expect(types(added)).toEqual([]);
|
|
450
|
+
expect(boxAdded.mock.calls.map(call => [call[0], call[2]])).toEqual([
|
|
451
|
+
['touchmove', {passive: false}],
|
|
452
|
+
['pointerdown', {capture: true, passive: true}],
|
|
453
|
+
]);
|
|
454
|
+
detachFine();
|
|
455
|
+
boxAdded.mockClear();
|
|
456
|
+
// A coarse one, with only a disabled recognizer: nothing at all.
|
|
457
|
+
const coarse = new GestureHost(document, {
|
|
458
|
+
clock,
|
|
459
|
+
isPrimaryPointerCoarse: () => true,
|
|
460
|
+
});
|
|
461
|
+
const pan = new PanGestureRecognizer({isEnabled: false});
|
|
462
|
+
const detach = coarse.attach(box, pan);
|
|
463
|
+
expect(types(added)).toEqual([]);
|
|
464
|
+
expect(boxAdded).not.toHaveBeenCalled();
|
|
465
|
+
// Enabling it registers the listeners; disabling removes them.
|
|
466
|
+
pan.setEnabled(true);
|
|
467
|
+
expect(types(added)).toContain('touchstart');
|
|
468
|
+
expect(boxAdded).toHaveBeenCalledWith('touchmove', expect.any(Function), {
|
|
469
|
+
passive: false,
|
|
470
|
+
});
|
|
471
|
+
pan.setEnabled(false);
|
|
472
|
+
expect(types(removed)).toContain('touchstart');
|
|
473
|
+
detach();
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
it('sets touch-action on attach and restores it on detach; the long-press styles wait for a held press', () => {
|
|
477
|
+
// The author's value is read in the next frame (FR18): fake the frame.
|
|
478
|
+
vi.useFakeTimers({toFake: ['setTimeout', 'requestAnimationFrame']});
|
|
479
|
+
detachers.push(() => vi.useRealTimers());
|
|
480
|
+
const {box} = mount('<div id="box" style="color: red"></div>');
|
|
481
|
+
const set = vi.spyOn(box.style, 'setProperty');
|
|
482
|
+
const remove = vi.spyOn(box.style, 'removeProperty');
|
|
483
|
+
const detachPan = host.attach(
|
|
484
|
+
box,
|
|
485
|
+
new PanGestureRecognizer({direction: 'horizontal'}),
|
|
486
|
+
);
|
|
487
|
+
const detachPress = host.attach(box, new LongPressGestureRecognizer());
|
|
488
|
+
vi.advanceTimersByTime(16);
|
|
489
|
+
expect(set).toHaveBeenCalledWith('touch-action', 'pan-y pinch-zoom');
|
|
490
|
+
expect(set).not.toHaveBeenCalledWith('-webkit-touch-callout', 'none');
|
|
491
|
+
detachPress();
|
|
492
|
+
detachPan();
|
|
493
|
+
expect(remove).toHaveBeenCalledWith('touch-action');
|
|
494
|
+
});
|
|
495
|
+
it("writes a touch-action no wider than the caller's own", () => {
|
|
496
|
+
vi.useFakeTimers({toFake: ['setTimeout', 'requestAnimationFrame']});
|
|
497
|
+
const restore = touchActionFromData();
|
|
498
|
+
try {
|
|
499
|
+
const {trigger, strip} = mount(
|
|
500
|
+
'<div id="trigger" data-touch-action="none"></div><div id="strip" data-touch-action="pan-y"></div>',
|
|
501
|
+
);
|
|
502
|
+
const set = vi.spyOn(trigger.style, 'setProperty');
|
|
503
|
+
const setStrip = vi.spyOn(strip.style, 'setProperty');
|
|
504
|
+
detachers.push(
|
|
505
|
+
host.attach(trigger, new LongPressGestureRecognizer({name: 'hold'})),
|
|
506
|
+
host.attach(
|
|
507
|
+
strip,
|
|
508
|
+
new PanGestureRecognizer({name: 'h', direction: 'horizontal'}),
|
|
509
|
+
),
|
|
510
|
+
);
|
|
511
|
+
vi.advanceTimersByTime(16);
|
|
512
|
+
// manipulation (the hold's) with none, and pan-y pinch-zoom (the
|
|
513
|
+
// pan's) with pan-y, allow the caller's own: its rule stays in force.
|
|
514
|
+
expect(window.getComputedStyle(trigger).touchAction).toBe('none');
|
|
515
|
+
expect(window.getComputedStyle(strip).touchAction).toBe('pan-y');
|
|
516
|
+
expect(set).not.toHaveBeenCalledWith('touch-action', expect.anything());
|
|
517
|
+
expect(setStrip).not.toHaveBeenCalledWith(
|
|
518
|
+
'touch-action',
|
|
519
|
+
expect.anything(),
|
|
520
|
+
);
|
|
521
|
+
} finally {
|
|
522
|
+
restore();
|
|
523
|
+
vi.useRealTimers();
|
|
524
|
+
}
|
|
525
|
+
});
|
|
526
|
+
it('marks an element with its derived touch-action as it attaches, under a rule any author rule outranks, with no inline write and no style read (spec:AST-094 FR18, DEC-8)', () => {
|
|
527
|
+
const restore = touchActionFromData();
|
|
528
|
+
try {
|
|
529
|
+
const {box} = mount('<div id="box"></div>');
|
|
530
|
+
const set = vi.spyOn(box.style, 'setProperty');
|
|
531
|
+
const read = vi.mocked(window.getComputedStyle);
|
|
532
|
+
read.mockClear();
|
|
533
|
+
detachers.push(
|
|
534
|
+
host.attach(box, new PanGestureRecognizer({direction: 'horizontal'})),
|
|
535
|
+
);
|
|
536
|
+
expect(box.getAttribute('data-astryx-touch-action')).toBe(
|
|
537
|
+
'pan-y pinch-zoom',
|
|
538
|
+
);
|
|
539
|
+
expect(hostRules(document)).toContain(
|
|
540
|
+
':where([data-astryx-touch-action="pan-y pinch-zoom"])',
|
|
541
|
+
);
|
|
542
|
+
expect(set).not.toHaveBeenCalledWith('touch-action', expect.anything());
|
|
543
|
+
expect(read).not.toHaveBeenCalled();
|
|
544
|
+
} finally {
|
|
545
|
+
restore();
|
|
546
|
+
}
|
|
547
|
+
});
|
|
548
|
+
it('leaves no touch-action rule in a document or shadow root once its last marked element lets go (spec:AST-094 FR24)', () => {
|
|
549
|
+
const restore = touchActionFromData();
|
|
550
|
+
try {
|
|
551
|
+
const {box, other, outer} = mount(
|
|
552
|
+
'<div id="box"></div><div id="other"></div><div id="outer"></div>',
|
|
553
|
+
);
|
|
554
|
+
const shadow = outer.attachShadow({mode: 'open'});
|
|
555
|
+
shadow.innerHTML = '<div id="inner"></div>';
|
|
556
|
+
const inner = shadow.getElementById('inner') as HTMLElement;
|
|
557
|
+
const releaseBox = host.attach(
|
|
558
|
+
box,
|
|
559
|
+
new PanGestureRecognizer({direction: 'horizontal'}),
|
|
560
|
+
);
|
|
561
|
+
const releaseOther = host.attach(
|
|
562
|
+
other,
|
|
563
|
+
new PanGestureRecognizer({direction: 'vertical'}),
|
|
564
|
+
);
|
|
565
|
+
const releaseInner = host.attach(
|
|
566
|
+
inner,
|
|
567
|
+
new PanGestureRecognizer({direction: 'horizontal'}),
|
|
568
|
+
);
|
|
569
|
+
expect(hostRules(document)).toContain('data-astryx-touch-action');
|
|
570
|
+
expect(hostRules(shadow)).toContain('data-astryx-touch-action');
|
|
571
|
+
// One element still marked keeps its root's rules.
|
|
572
|
+
releaseBox();
|
|
573
|
+
expect(hostRules(document)).toContain('data-astryx-touch-action');
|
|
574
|
+
releaseOther();
|
|
575
|
+
expect(hostRules(document)).not.toContain('data-astryx-touch-action');
|
|
576
|
+
releaseInner();
|
|
577
|
+
expect(hostRules(shadow)).not.toContain('data-astryx-touch-action');
|
|
578
|
+
// A later attach brings its rule back.
|
|
579
|
+
detachers.push(
|
|
580
|
+
host.attach(box, new PanGestureRecognizer({direction: 'horizontal'})),
|
|
581
|
+
);
|
|
582
|
+
expect(hostRules(document)).toContain(
|
|
583
|
+
':where([data-astryx-touch-action="pan-y pinch-zoom"])',
|
|
584
|
+
);
|
|
585
|
+
} finally {
|
|
586
|
+
restore();
|
|
587
|
+
}
|
|
588
|
+
});
|
|
589
|
+
it("keeps an author rule in force from the first frame, and writes the intersection inline only where it is narrower than the author's (spec:AST-094 FR18)", () => {
|
|
590
|
+
vi.useFakeTimers({toFake: ['setTimeout', 'requestAnimationFrame']});
|
|
591
|
+
const restore = touchActionFromData();
|
|
592
|
+
try {
|
|
593
|
+
const {both, strip} = mount(
|
|
594
|
+
'<div id="both" data-touch-action="pan-x pan-y"></div><div id="strip" data-touch-action="pan-y"></div>',
|
|
595
|
+
);
|
|
596
|
+
const setBoth = vi.spyOn(both.style, 'setProperty');
|
|
597
|
+
const setStrip = vi.spyOn(strip.style, 'setProperty');
|
|
598
|
+
detachers.push(
|
|
599
|
+
host.attach(both, new PanGestureRecognizer({direction: 'horizontal'})),
|
|
600
|
+
host.attach(strip, new PanGestureRecognizer({direction: 'horizontal'})),
|
|
601
|
+
);
|
|
602
|
+
// Before the host reads anything, the author's value stands.
|
|
603
|
+
expect(both.getAttribute('data-astryx-touch-action')).toBe(
|
|
604
|
+
'pan-y pinch-zoom',
|
|
605
|
+
);
|
|
606
|
+
expect(window.getComputedStyle(both).touchAction).toBe('pan-x pan-y');
|
|
607
|
+
expect(setBoth).not.toHaveBeenCalledWith(
|
|
608
|
+
'touch-action',
|
|
609
|
+
expect.anything(),
|
|
610
|
+
);
|
|
611
|
+
vi.advanceTimersByTime(16);
|
|
612
|
+
// pan-y pinch-zoom with pan-x pan-y allows pan-y: narrower than the
|
|
613
|
+
// author's, so it is written.
|
|
614
|
+
expect(setBoth).toHaveBeenLastCalledWith('touch-action', 'pan-y');
|
|
615
|
+
// With pan-y it is the author's own value: nothing to write.
|
|
616
|
+
expect(setStrip).not.toHaveBeenCalledWith(
|
|
617
|
+
'touch-action',
|
|
618
|
+
expect.anything(),
|
|
619
|
+
);
|
|
620
|
+
} finally {
|
|
621
|
+
restore();
|
|
622
|
+
vi.useRealTimers();
|
|
623
|
+
}
|
|
624
|
+
});
|
|
625
|
+
for (const {keyword, resolved, make, written} of [
|
|
626
|
+
{
|
|
627
|
+
keyword: 'initial',
|
|
628
|
+
resolved: 'auto',
|
|
629
|
+
make: () => new TapGestureRecognizer({name: 'tap'}),
|
|
630
|
+
written: 'manipulation',
|
|
631
|
+
},
|
|
632
|
+
{
|
|
633
|
+
keyword: 'inherit',
|
|
634
|
+
resolved: 'pan-y',
|
|
635
|
+
make: () =>
|
|
636
|
+
new PanGestureRecognizer({name: 'h', direction: 'horizontal'}),
|
|
637
|
+
written: null,
|
|
638
|
+
},
|
|
639
|
+
]) {
|
|
640
|
+
it(`reads a caller's inline \`touch-action: ${keyword}\` as what it resolves to, never intersecting the keyword itself (spec:AST-094 FR18, DEC-8)`, () => {
|
|
641
|
+
vi.useFakeTimers({toFake: ['setTimeout', 'requestAnimationFrame']});
|
|
642
|
+
const restore = touchActionFromData();
|
|
643
|
+
try {
|
|
644
|
+
// jsdom keeps no touch-action: the inline keyword stands in through
|
|
645
|
+
// getPropertyValue, and what it resolves to through computed style.
|
|
646
|
+
const {box} = mount(
|
|
647
|
+
`<div id="box" data-touch-action="${resolved}"></div>`,
|
|
648
|
+
);
|
|
649
|
+
vi.spyOn(box.style, 'getPropertyValue').mockImplementation(name =>
|
|
650
|
+
name === 'touch-action' ? keyword : '',
|
|
651
|
+
);
|
|
652
|
+
const set = vi.spyOn(box.style, 'setProperty');
|
|
653
|
+
detachers.push(host.attach(box, make()));
|
|
654
|
+
// A keyword names no touch-action of its own: nothing is written
|
|
655
|
+
// from it at once, and never none.
|
|
656
|
+
expect(set).not.toHaveBeenCalledWith('touch-action', expect.anything());
|
|
657
|
+
vi.advanceTimersByTime(16);
|
|
658
|
+
expect(set).not.toHaveBeenCalledWith('touch-action', 'none');
|
|
659
|
+
if (written == null) {
|
|
660
|
+
// What the derived value allows of it is its own value: nothing
|
|
661
|
+
// to narrow.
|
|
662
|
+
expect(set).not.toHaveBeenCalledWith(
|
|
663
|
+
'touch-action',
|
|
664
|
+
expect.anything(),
|
|
665
|
+
);
|
|
666
|
+
} else {
|
|
667
|
+
expect(set).toHaveBeenLastCalledWith('touch-action', written);
|
|
668
|
+
}
|
|
669
|
+
} finally {
|
|
670
|
+
restore();
|
|
671
|
+
vi.useRealTimers();
|
|
672
|
+
}
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
it('removes the mark when the last recognizer detaches', () => {
|
|
676
|
+
const {box} = mount('<div id="box"></div>');
|
|
677
|
+
const detach = host.attach(
|
|
678
|
+
box,
|
|
679
|
+
new PanGestureRecognizer({direction: 'horizontal'}),
|
|
680
|
+
);
|
|
681
|
+
expect(box.hasAttribute('data-astryx-touch-action')).toBe(true);
|
|
682
|
+
detach();
|
|
683
|
+
expect(box.hasAttribute('data-astryx-touch-action')).toBe(false);
|
|
684
|
+
});
|
|
685
|
+
it('adds the rule to the shadow root an element is in', () => {
|
|
686
|
+
const {box} = mount('<div id="box"></div>');
|
|
687
|
+
const shadow = box.attachShadow({mode: 'open'});
|
|
688
|
+
const inner = document.createElement('div');
|
|
689
|
+
shadow.append(inner);
|
|
690
|
+
detachers.push(host.attach(inner, new LongPressGestureRecognizer()));
|
|
691
|
+
expect(inner.getAttribute('data-astryx-touch-action')).toBe('manipulation');
|
|
692
|
+
expect(hostRules(shadow)).toContain(
|
|
693
|
+
':where([data-astryx-touch-action="manipulation"])',
|
|
694
|
+
);
|
|
695
|
+
});
|
|
696
|
+
it("is never wider than the author's touch-action, at any frame (spec:AST-094 FR18, DEC-8)", () => {
|
|
697
|
+
vi.useFakeTimers({toFake: ['setTimeout', 'requestAnimationFrame']});
|
|
698
|
+
const restore = touchActionFromData();
|
|
699
|
+
try {
|
|
700
|
+
const {trigger} = mount(
|
|
701
|
+
'<div id="trigger" data-touch-action="none"></div>',
|
|
702
|
+
);
|
|
703
|
+
const set = vi.spyOn(trigger.style, 'setProperty');
|
|
704
|
+
detachers.push(
|
|
705
|
+
host.attach(trigger, new LongPressGestureRecognizer({name: 'hold'})),
|
|
706
|
+
);
|
|
707
|
+
// The author's none outranks the host's manipulation from the attach
|
|
708
|
+
// on, and the read finds nothing to narrow.
|
|
709
|
+
expect(window.getComputedStyle(trigger).touchAction).toBe('none');
|
|
710
|
+
vi.advanceTimersByTime(16);
|
|
711
|
+
expect(window.getComputedStyle(trigger).touchAction).toBe('none');
|
|
712
|
+
expect(set).not.toHaveBeenCalledWith('touch-action', expect.anything());
|
|
713
|
+
} finally {
|
|
714
|
+
restore();
|
|
715
|
+
vi.useRealTimers();
|
|
716
|
+
}
|
|
717
|
+
});
|
|
718
|
+
it('gives an element attached while on screen its touch-action in the attach itself', () => {
|
|
719
|
+
const restore = touchActionFromData();
|
|
720
|
+
try {
|
|
721
|
+
const {box} = mount('<div id="box"></div>');
|
|
722
|
+
detachers.push(host.attach(box, new TapGestureRecognizer({name: 'tap'})));
|
|
723
|
+
expect(window.getComputedStyle(box).touchAction).toBe('manipulation');
|
|
724
|
+
} finally {
|
|
725
|
+
restore();
|
|
726
|
+
}
|
|
727
|
+
});
|
|
728
|
+
it("reads no computed style while attaching: a caller's inline touch-action applies at once, a stylesheet's is read in the next frame", () => {
|
|
729
|
+
vi.useFakeTimers({toFake: ['setTimeout', 'requestAnimationFrame']});
|
|
730
|
+
const restore = touchActionFromData();
|
|
731
|
+
try {
|
|
732
|
+
const {trigger, strip} = mount(
|
|
733
|
+
'<div id="trigger" data-touch-action="none"></div><div id="strip"></div>',
|
|
734
|
+
);
|
|
735
|
+
// jsdom keeps no touch-action in a style: this stands in for the
|
|
736
|
+
// caller's inline `touch-action: pan-y`.
|
|
737
|
+
vi.spyOn(strip.style, 'getPropertyValue').mockImplementation(name =>
|
|
738
|
+
name === 'touch-action' ? 'pan-y' : '',
|
|
739
|
+
);
|
|
740
|
+
const set = vi.spyOn(trigger.style, 'setProperty');
|
|
741
|
+
const setStrip = vi.spyOn(strip.style, 'setProperty');
|
|
742
|
+
const read = vi.mocked(window.getComputedStyle);
|
|
743
|
+
read.mockClear();
|
|
744
|
+
detachers.push(
|
|
745
|
+
host.attach(trigger, new LongPressGestureRecognizer({name: 'hold'})),
|
|
746
|
+
host.attach(
|
|
747
|
+
strip,
|
|
748
|
+
new PanGestureRecognizer({name: 'h', direction: 'horizontal'}),
|
|
749
|
+
),
|
|
750
|
+
);
|
|
751
|
+
// Attaching forces no style recalc over whatever just mounted.
|
|
752
|
+
expect(read).not.toHaveBeenCalled();
|
|
753
|
+
// The caller's inline value: pan-y pinch-zoom ∩ pan-y, at once.
|
|
754
|
+
expect(setStrip).toHaveBeenLastCalledWith('touch-action', 'pan-y');
|
|
755
|
+
// A stylesheet's value: read once in the next frame, where none is
|
|
756
|
+
// already no wider than both allow.
|
|
757
|
+
vi.advanceTimersByTime(16);
|
|
758
|
+
expect(read).toHaveBeenCalledTimes(1);
|
|
759
|
+
expect(set).not.toHaveBeenCalledWith('touch-action', expect.anything());
|
|
760
|
+
} finally {
|
|
761
|
+
restore();
|
|
762
|
+
vi.useRealTimers();
|
|
763
|
+
}
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
it('turns selection and the callout off for one held press, only where the hold does something (spec:AST-094 FR41)', () => {
|
|
767
|
+
const {hold, plain} = mount(
|
|
768
|
+
'<div><div id="hold"></div><div id="plain"></div></div>',
|
|
769
|
+
);
|
|
770
|
+
detachers.push(host.attach(hold, new LongPressGestureRecognizer()));
|
|
771
|
+
detachers.push(host.attach(plain, new TapGestureRecognizer()));
|
|
772
|
+
// jsdom drops the prefixed properties, so the writes are observed.
|
|
773
|
+
const set = vi.spyOn(hold.style, 'setProperty');
|
|
774
|
+
const remove = vi.spyOn(hold.style, 'removeProperty');
|
|
775
|
+
const plainSet = vi.spyOn(plain.style, 'setProperty');
|
|
776
|
+
const finger = {id: 1, x: 10, y: 10};
|
|
777
|
+
touch('touchstart', hold, [finger], [finger], 0);
|
|
778
|
+
expect(set).toHaveBeenCalledWith('-webkit-touch-callout', 'none');
|
|
779
|
+
expect(set).toHaveBeenCalledWith('user-select', 'none');
|
|
780
|
+
expect(remove).not.toHaveBeenCalledWith('user-select');
|
|
781
|
+
touch('touchend', hold, [finger], [], 100);
|
|
782
|
+
expect(remove).toHaveBeenCalledWith('-webkit-touch-callout');
|
|
783
|
+
expect(remove).toHaveBeenCalledWith('user-select');
|
|
784
|
+
const other = {id: 2, x: 10, y: 40};
|
|
785
|
+
touch('touchstart', plain, [other], [other], 200);
|
|
786
|
+
expect(plainSet).not.toHaveBeenCalledWith('user-select', 'none');
|
|
787
|
+
touch('touchend', plain, [other], [], 260);
|
|
788
|
+
});
|
|
789
|
+
|
|
790
|
+
it('leaves selection and the callout alone on an element attached with manageStyle: false, whose styles the caller keeps', () => {
|
|
791
|
+
const {hold} = mount('<div id="hold"></div>');
|
|
792
|
+
detachers.push(
|
|
793
|
+
host.attach(hold, new LongPressGestureRecognizer(), {manageStyle: false}),
|
|
794
|
+
);
|
|
795
|
+
const set = vi.spyOn(hold.style, 'setProperty');
|
|
796
|
+
const remove = vi.spyOn(hold.style, 'removeProperty');
|
|
797
|
+
const finger = {id: 1, x: 10, y: 10};
|
|
798
|
+
touch('touchstart', hold, [finger], [finger], 0);
|
|
799
|
+
touch('touchend', hold, [finger], [], 100);
|
|
800
|
+
expect(set).not.toHaveBeenCalled();
|
|
801
|
+
expect(remove).not.toHaveBeenCalled();
|
|
802
|
+
});
|
|
803
|
+
|
|
804
|
+
it('cancels touchmove only once a recognizer owns the touch, and swallows the click', () => {
|
|
805
|
+
const {box, child} = mount('<div id="box"><span id="child">x</span></div>');
|
|
806
|
+
const pan = new PanGestureRecognizer({name: 'pan'});
|
|
807
|
+
const states: string[] = [];
|
|
808
|
+
pan.addListener(r => states.push(r.state));
|
|
809
|
+
detachers.push(host.attach(box, pan));
|
|
810
|
+
const finger = {id: 7, x: 10, y: 10};
|
|
811
|
+
touch('touchstart', child, [finger], [finger], 0);
|
|
812
|
+
const early = touch(
|
|
813
|
+
'touchmove',
|
|
814
|
+
child,
|
|
815
|
+
[{...finger, x: 15}],
|
|
816
|
+
[{...finger, x: 15}],
|
|
817
|
+
16,
|
|
818
|
+
);
|
|
819
|
+
expect(early.defaultPrevented).toBe(false);
|
|
820
|
+
expect(pan.state).toBe('possible');
|
|
821
|
+
const claimed = touch(
|
|
822
|
+
'touchmove',
|
|
823
|
+
child,
|
|
824
|
+
[{...finger, x: 30}],
|
|
825
|
+
[{...finger, x: 30}],
|
|
826
|
+
32,
|
|
827
|
+
);
|
|
828
|
+
expect(claimed.defaultPrevented).toBe(true);
|
|
829
|
+
expect(states).toEqual(['began']);
|
|
830
|
+
const end = touch('touchend', child, [{...finger, x: 30}], [], 48);
|
|
831
|
+
expect(end.defaultPrevented).toBe(true);
|
|
832
|
+
expect(states).toEqual(['began', 'ended']);
|
|
833
|
+
});
|
|
834
|
+
|
|
835
|
+
it('leaves the touchend (and the click) alone when nothing claimed the touch', () => {
|
|
836
|
+
const {box} = mount('<div id="box"></div>');
|
|
837
|
+
const pan = new PanGestureRecognizer({name: 'pan'});
|
|
838
|
+
detachers.push(host.attach(box, pan));
|
|
839
|
+
const finger = {id: 1, x: 10, y: 10};
|
|
840
|
+
touch('touchstart', box, [finger], [finger], 0);
|
|
841
|
+
touch('touchmove', box, [{...finger, x: 13}], [{...finger, x: 13}], 16);
|
|
842
|
+
const end = touch('touchend', box, [{...finger, x: 13}], [], 40);
|
|
843
|
+
expect(end.defaultPrevented).toBe(false);
|
|
844
|
+
});
|
|
845
|
+
|
|
846
|
+
it('leaves a second finger no recognizer owns to the page while the claimed one is held (spec:AST-094 FR19)', () => {
|
|
847
|
+
const {box} = mount('<div id="box"></div>');
|
|
848
|
+
const pan = new PanGestureRecognizer({maximumNumberOfTouches: 1});
|
|
849
|
+
detachers.push(host.attach(box, pan));
|
|
850
|
+
const first = {id: 1, x: 10, y: 10};
|
|
851
|
+
touch('touchstart', box, [first], [first], 0);
|
|
852
|
+
const moved = {...first, x: 40};
|
|
853
|
+
touch('touchmove', box, [moved], [moved], 16);
|
|
854
|
+
const second = {id: 2, x: 200, y: 200};
|
|
855
|
+
touch('touchstart', box, [second], [moved, second], 20);
|
|
856
|
+
const pinchLike = touch(
|
|
857
|
+
'touchmove',
|
|
858
|
+
box,
|
|
859
|
+
[{...second, x: 260}],
|
|
860
|
+
[moved, {...second, x: 260}],
|
|
861
|
+
32,
|
|
862
|
+
);
|
|
863
|
+
expect(pinchLike.defaultPrevented).toBe(false);
|
|
864
|
+
expect(pan.state).toBe('began');
|
|
865
|
+
expect(pan.numberOfTouches).toBe(1);
|
|
866
|
+
// The claimed finger's own moves stay cancelled.
|
|
867
|
+
const own = touch(
|
|
868
|
+
'touchmove',
|
|
869
|
+
box,
|
|
870
|
+
[{...moved, x: 60}],
|
|
871
|
+
[
|
|
872
|
+
{...moved, x: 60},
|
|
873
|
+
{...second, x: 260},
|
|
874
|
+
],
|
|
875
|
+
48,
|
|
876
|
+
);
|
|
877
|
+
expect(own.defaultPrevented).toBe(true);
|
|
878
|
+
});
|
|
879
|
+
|
|
880
|
+
it('reads pointercancel on a finger as the browser taking it', () => {
|
|
881
|
+
const {box} = mount('<div id="box"></div>');
|
|
882
|
+
const pan = new PanGestureRecognizer({name: 'pan'});
|
|
883
|
+
const view = new RecordingConsumer();
|
|
884
|
+
const events: string[] = [];
|
|
885
|
+
detachers.push(host.attach(box, pan), host.attachConsumer(box, view));
|
|
886
|
+
pan.addTransitionListener((_r, _from, to) => events.push(to));
|
|
887
|
+
const finger = {id: 3, x: 10, y: 10};
|
|
888
|
+
touch('touchstart', box, [finger], [finger], 0);
|
|
889
|
+
pointer('pointercancel', box, {
|
|
890
|
+
id: 99,
|
|
891
|
+
pointerType: 'touch',
|
|
892
|
+
x: 10,
|
|
893
|
+
y: 14,
|
|
894
|
+
time: 20,
|
|
895
|
+
});
|
|
896
|
+
expect(events).toEqual(['failed', 'possible']);
|
|
897
|
+
// The view's touch is cancelled, with no release.
|
|
898
|
+
expect(view.events.map(event => event.split(':')[0])).toEqual([
|
|
899
|
+
'began',
|
|
900
|
+
'cancelled',
|
|
901
|
+
]);
|
|
902
|
+
});
|
|
903
|
+
|
|
904
|
+
it("pairs a finger's pointer with its touch: pointermoves inside the browser's touchmove slop count", () => {
|
|
905
|
+
const {box} = mount('<div id="box"></div>');
|
|
906
|
+
const pan = new PanGestureRecognizer({name: 'pan'});
|
|
907
|
+
const states: string[] = [];
|
|
908
|
+
pan.addListener(r => states.push(`${r.state}:${r.translation().x}`));
|
|
909
|
+
detachers.push(host.attach(box, pan));
|
|
910
|
+
// Chromium's order: pointerdown, then touchstart, at the same spot.
|
|
911
|
+
pointer('pointerdown', box, {
|
|
912
|
+
id: 21,
|
|
913
|
+
pointerType: 'touch',
|
|
914
|
+
x: 10,
|
|
915
|
+
y: 10,
|
|
916
|
+
buttons: 1,
|
|
917
|
+
time: 0,
|
|
918
|
+
});
|
|
919
|
+
const finger = {id: 0, x: 10, y: 10};
|
|
920
|
+
touch('touchstart', box, [finger], [finger], 0);
|
|
921
|
+
// No touchmove yet (inside the slop), but the pointer moved 12 px.
|
|
922
|
+
pointer('pointermove', box, {
|
|
923
|
+
id: 21,
|
|
924
|
+
pointerType: 'touch',
|
|
925
|
+
x: 16,
|
|
926
|
+
y: 10,
|
|
927
|
+
buttons: 1,
|
|
928
|
+
time: 16,
|
|
929
|
+
});
|
|
930
|
+
pointer('pointermove', box, {
|
|
931
|
+
id: 21,
|
|
932
|
+
pointerType: 'touch',
|
|
933
|
+
x: 22,
|
|
934
|
+
y: 10,
|
|
935
|
+
buttons: 1,
|
|
936
|
+
time: 32,
|
|
937
|
+
});
|
|
938
|
+
// Translation 2: the 10 px hysteresis is left out, as in UIKit.
|
|
939
|
+
expect(states).toEqual(['began:2']);
|
|
940
|
+
// The first touchmove (the same input as a pointermove) is cancelled
|
|
941
|
+
// and not read twice.
|
|
942
|
+
pointer('pointermove', box, {
|
|
943
|
+
id: 21,
|
|
944
|
+
pointerType: 'touch',
|
|
945
|
+
x: 26,
|
|
946
|
+
y: 10,
|
|
947
|
+
buttons: 1,
|
|
948
|
+
time: 48,
|
|
949
|
+
});
|
|
950
|
+
const first = touch(
|
|
951
|
+
'touchmove',
|
|
952
|
+
box,
|
|
953
|
+
[{...finger, x: 26}],
|
|
954
|
+
[{...finger, x: 26}],
|
|
955
|
+
48,
|
|
956
|
+
);
|
|
957
|
+
expect(first.defaultPrevented).toBe(true);
|
|
958
|
+
expect(states).toEqual(['began:2', 'changed:6']);
|
|
959
|
+
// pointercancel names the finger exactly, whatever its coordinates say.
|
|
960
|
+
pointer('pointercancel', box, {
|
|
961
|
+
id: 21,
|
|
962
|
+
pointerType: 'touch',
|
|
963
|
+
x: 0,
|
|
964
|
+
y: 0,
|
|
965
|
+
time: 60,
|
|
966
|
+
});
|
|
967
|
+
expect(host.arena.getTouch(1)?.takenByBrowser).toBe(true);
|
|
968
|
+
});
|
|
969
|
+
|
|
970
|
+
it("gives a finger's target a passive touchmove listener unless a recognizer on that touch can claim while it moves (spec:AST-094 FR19)", () => {
|
|
971
|
+
const {taps, pan} = mount(
|
|
972
|
+
'<div id="taps"><span id="tapChild">t</span></div><div id="pan"><span id="panChild">p</span></div>',
|
|
973
|
+
);
|
|
974
|
+
detachers.push(host.attach(taps, new TapGestureRecognizer({name: 'tap'})));
|
|
975
|
+
detachers.push(host.attach(pan, new PanGestureRecognizer({name: 'pan'})));
|
|
976
|
+
const moveListenerFlags = (element: Element) => {
|
|
977
|
+
const added = vi.spyOn(element, 'addEventListener');
|
|
978
|
+
return () =>
|
|
979
|
+
added.mock.calls
|
|
980
|
+
.filter(([type]) => type === 'touchmove')
|
|
981
|
+
.map(([, , options]) => (options as AddEventListenerOptions).passive);
|
|
982
|
+
};
|
|
983
|
+
const tapChild = taps.querySelector('#tapChild')!;
|
|
984
|
+
const panChild = pan.querySelector('#panChild')!;
|
|
985
|
+
const onTapChild = moveListenerFlags(tapChild);
|
|
986
|
+
const onPanChild = moveListenerFlags(panChild);
|
|
987
|
+
const tapFinger = {id: 1, x: 10, y: 10};
|
|
988
|
+
touch('touchstart', tapChild, [tapFinger], [tapFinger], 0);
|
|
989
|
+
const panFinger = {id: 2, x: 10, y: 10};
|
|
990
|
+
touch('touchstart', panChild, [panFinger], [tapFinger, panFinger], 0);
|
|
991
|
+
expect(onTapChild()).toEqual([true]);
|
|
992
|
+
expect(onPanChild()).toEqual([false]);
|
|
993
|
+
touch('touchend', tapChild, [tapFinger], [panFinger], 20);
|
|
994
|
+
touch('touchend', panChild, [panFinger], [], 20);
|
|
995
|
+
});
|
|
996
|
+
|
|
997
|
+
it('gives the blocking touchmove listener only to elements whose recognizers claim while moving', () => {
|
|
998
|
+
const {taps, pan, press, both, row} = mount(
|
|
999
|
+
'<div id="taps"></div><div id="pan"></div><div id="press"></div><div id="both"></div><div id="row"></div>',
|
|
1000
|
+
);
|
|
1001
|
+
const blocking = (element: HTMLElement) => {
|
|
1002
|
+
const added = vi.spyOn(element, 'addEventListener');
|
|
1003
|
+
const removed = vi.spyOn(element, 'removeEventListener');
|
|
1004
|
+
return {
|
|
1005
|
+
added: () =>
|
|
1006
|
+
added.mock.calls.filter(([type, , options]) => {
|
|
1007
|
+
const flags = options as AddEventListenerOptions | undefined;
|
|
1008
|
+
return type === 'touchmove' && flags?.passive === false;
|
|
1009
|
+
}).length,
|
|
1010
|
+
removed: () =>
|
|
1011
|
+
removed.mock.calls.filter(([type]) => type === 'touchmove').length,
|
|
1012
|
+
};
|
|
1013
|
+
};
|
|
1014
|
+
// Taps claim only at the lift: a scroll starting on them never waits.
|
|
1015
|
+
const onTaps = blocking(taps);
|
|
1016
|
+
detachers.push(host.attach(taps, new TapGestureRecognizer({name: 'tap'})));
|
|
1017
|
+
detachers.push(
|
|
1018
|
+
host.attach(
|
|
1019
|
+
taps,
|
|
1020
|
+
new TapGestureRecognizer({name: 'double', numberOfTapsRequired: 2}),
|
|
1021
|
+
),
|
|
1022
|
+
);
|
|
1023
|
+
expect(onTaps.added()).toBe(0);
|
|
1024
|
+
// A press consumer adds none either.
|
|
1025
|
+
const onRow = blocking(row);
|
|
1026
|
+
detachers.push(host.attachConsumer(row, new RecordingConsumer()));
|
|
1027
|
+
expect(onRow.added()).toBe(0);
|
|
1028
|
+
// A pan and a long press can take a moving touch: they get one.
|
|
1029
|
+
const onPan = blocking(pan);
|
|
1030
|
+
detachers.push(host.attach(pan, new PanGestureRecognizer({name: 'pan'})));
|
|
1031
|
+
expect(onPan.added()).toBe(1);
|
|
1032
|
+
const onPress = blocking(press);
|
|
1033
|
+
detachers.push(
|
|
1034
|
+
host.attach(press, new LongPressGestureRecognizer({name: 'press'})),
|
|
1035
|
+
);
|
|
1036
|
+
expect(onPress.added()).toBe(1);
|
|
1037
|
+
// A tap beside a pan: the listener comes with the pan and leaves with it.
|
|
1038
|
+
const onBoth = blocking(both);
|
|
1039
|
+
detachers.push(host.attach(both, new TapGestureRecognizer({name: 'tap2'})));
|
|
1040
|
+
expect(onBoth.added()).toBe(0);
|
|
1041
|
+
const detachPan = host.attach(
|
|
1042
|
+
both,
|
|
1043
|
+
new PanGestureRecognizer({name: 'pan2', direction: 'horizontal'}),
|
|
1044
|
+
);
|
|
1045
|
+
expect(onBoth.added()).toBe(1);
|
|
1046
|
+
detachPan();
|
|
1047
|
+
expect(onBoth.removed()).toBe(1);
|
|
1048
|
+
});
|
|
1049
|
+
|
|
1050
|
+
it("leaves a one-way pan's touch to the page until the pan begins (spec:AST-094 FR19)", () => {
|
|
1051
|
+
const {card} = mount('<div id="card"></div>');
|
|
1052
|
+
const pan = new PanGestureRecognizer({name: 'pan', direction: 'up'});
|
|
1053
|
+
detachers.push(host.attach(card, pan));
|
|
1054
|
+
const finger = {id: 1, x: 50, y: 100};
|
|
1055
|
+
touch('touchstart', card, [finger], [finger], 0);
|
|
1056
|
+
const at = (y: number, time: number) =>
|
|
1057
|
+
touch('touchmove', card, [{...finger, y}], [{...finger, y}], time);
|
|
1058
|
+
expect(at(97, 16).defaultPrevented).toBe(false);
|
|
1059
|
+
expect(at(95, 32).defaultPrevented).toBe(false);
|
|
1060
|
+
expect(pan.state).toBe('possible');
|
|
1061
|
+
expect(at(88, 48).defaultPrevented).toBe(true);
|
|
1062
|
+
expect(pan.state).toBe('began');
|
|
1063
|
+
touch('touchend', card, [{...finger, y: 88}], [], 64);
|
|
1064
|
+
});
|
|
1065
|
+
|
|
1066
|
+
it('leaves a scroll-edge pan to claim at its begin', () => {
|
|
1067
|
+
const {sheet} = mount('<div id="sheet"></div>');
|
|
1068
|
+
const pan = new PanGestureRecognizer({
|
|
1069
|
+
name: 'sheet',
|
|
1070
|
+
direction: 'down',
|
|
1071
|
+
scrollEdge: true,
|
|
1072
|
+
});
|
|
1073
|
+
detachers.push(host.attach(sheet, pan));
|
|
1074
|
+
const finger = {id: 1, x: 50, y: 100};
|
|
1075
|
+
touch('touchstart', sheet, [finger], [finger], 0);
|
|
1076
|
+
const at = (y: number, time: number) =>
|
|
1077
|
+
touch('touchmove', sheet, [{...finger, y}], [{...finger, y}], time);
|
|
1078
|
+
expect(at(107, 16).defaultPrevented).toBe(false);
|
|
1079
|
+
expect(at(112, 32).defaultPrevented).toBe(true);
|
|
1080
|
+
expect(pan.state).toBe('began');
|
|
1081
|
+
touch('touchend', sheet, [{...finger, y: 112}], [], 48);
|
|
1082
|
+
});
|
|
1083
|
+
|
|
1084
|
+
it('names the pointer a finger, a pen or a mouse is driven by', () => {
|
|
1085
|
+
const {box} = mount('<div id="box"></div>');
|
|
1086
|
+
const press = new LongPressGestureRecognizer({name: 'press'});
|
|
1087
|
+
detachers.push(host.attach(box, press));
|
|
1088
|
+
// A finger: its pointerdown pairs with its touchstart.
|
|
1089
|
+
pointer('pointerdown', box, {
|
|
1090
|
+
id: 31,
|
|
1091
|
+
pointerType: 'touch',
|
|
1092
|
+
x: 10,
|
|
1093
|
+
y: 10,
|
|
1094
|
+
buttons: 1,
|
|
1095
|
+
time: 0,
|
|
1096
|
+
});
|
|
1097
|
+
const finger = {id: 0, x: 10, y: 10};
|
|
1098
|
+
touch('touchstart', box, [finger], [finger], 0);
|
|
1099
|
+
const [held] = press.trackedTouches;
|
|
1100
|
+
expect(host.pointerIdOf(held)).toBe(31);
|
|
1101
|
+
expect(host.pointerIdOf(held.id)).toBe(31);
|
|
1102
|
+
touch('touchend', box, [finger], [], 40);
|
|
1103
|
+
// A pen is its own pointer.
|
|
1104
|
+
pointer('pointerdown', box, {
|
|
1105
|
+
id: 7,
|
|
1106
|
+
pointerType: 'pen',
|
|
1107
|
+
x: 10,
|
|
1108
|
+
y: 10,
|
|
1109
|
+
buttons: 1,
|
|
1110
|
+
time: 100,
|
|
1111
|
+
});
|
|
1112
|
+
const [pen] = press.trackedTouches;
|
|
1113
|
+
expect(host.pointerIdOf(pen)).toBe(7);
|
|
1114
|
+
pointer('pointerup', box, {
|
|
1115
|
+
id: 7,
|
|
1116
|
+
pointerType: 'pen',
|
|
1117
|
+
x: 10,
|
|
1118
|
+
y: 10,
|
|
1119
|
+
time: 140,
|
|
1120
|
+
});
|
|
1121
|
+
expect(host.pointerIdOf(pen)).toBeNull();
|
|
1122
|
+
});
|
|
1123
|
+
|
|
1124
|
+
it('reads a touchmove that can no longer be cancelled as the browser taking it, where it could pan', () => {
|
|
1125
|
+
const {list, box} = mount(
|
|
1126
|
+
'<div id="list" style="overflow-y: auto"><div id="box"></div></div>',
|
|
1127
|
+
);
|
|
1128
|
+
scrollable(list, {scrollTop: 0, scrollHeight: 1000, clientHeight: 400});
|
|
1129
|
+
const tap = new TapGestureRecognizer({name: 'tap'});
|
|
1130
|
+
const events: string[] = [];
|
|
1131
|
+
tap.addTransitionListener((_r, _from, to) => events.push(to));
|
|
1132
|
+
detachers.push(host.attach(box, tap));
|
|
1133
|
+
const finger = {id: 1, x: 10, y: 30};
|
|
1134
|
+
touch('touchstart', box, [finger], [finger], 0);
|
|
1135
|
+
touch(
|
|
1136
|
+
'touchmove',
|
|
1137
|
+
box,
|
|
1138
|
+
[{...finger, y: 14}],
|
|
1139
|
+
[{...finger, y: 14}],
|
|
1140
|
+
16,
|
|
1141
|
+
false,
|
|
1142
|
+
);
|
|
1143
|
+
expect(events).toEqual(['failed', 'possible']);
|
|
1144
|
+
});
|
|
1145
|
+
|
|
1146
|
+
it('keeps a horizontal drag under pan-y when Chromium stops asking after one unconsumed move', () => {
|
|
1147
|
+
const restore = touchActionFromData();
|
|
1148
|
+
try {
|
|
1149
|
+
const {list, row} = mount(
|
|
1150
|
+
'<div id="list" style="overflow-y: auto"><div id="row" data-touch-action="pan-y pinch-zoom"></div></div>',
|
|
1151
|
+
);
|
|
1152
|
+
scrollable(list, {scrollTop: 0, scrollHeight: 1000, clientHeight: 400});
|
|
1153
|
+
const consumer = new RecordingConsumer();
|
|
1154
|
+
detachers.push(host.attachConsumer(row, consumer));
|
|
1155
|
+
const finger = {id: 1, x: 100, y: 30};
|
|
1156
|
+
touch('touchstart', row, [finger], [finger], 0);
|
|
1157
|
+
// The first move goes unconsumed; Chromium sends the rest uncancelable
|
|
1158
|
+
// although pan-y keeps it from panning sideways.
|
|
1159
|
+
touch('touchmove', row, [{...finger, x: 120}], [{...finger, x: 120}], 16);
|
|
1160
|
+
touch(
|
|
1161
|
+
'touchmove',
|
|
1162
|
+
row,
|
|
1163
|
+
[{...finger, x: 140, y: 33}],
|
|
1164
|
+
[{...finger, x: 140, y: 33}],
|
|
1165
|
+
32,
|
|
1166
|
+
false,
|
|
1167
|
+
);
|
|
1168
|
+
touch(
|
|
1169
|
+
'touchmove',
|
|
1170
|
+
row,
|
|
1171
|
+
[{...finger, x: 160, y: 35}],
|
|
1172
|
+
[{...finger, x: 160, y: 35}],
|
|
1173
|
+
48,
|
|
1174
|
+
false,
|
|
1175
|
+
);
|
|
1176
|
+
expect(consumer.events).toEqual([
|
|
1177
|
+
'began:1',
|
|
1178
|
+
'moved:1',
|
|
1179
|
+
'moved:1',
|
|
1180
|
+
'moved:1',
|
|
1181
|
+
]);
|
|
1182
|
+
} finally {
|
|
1183
|
+
restore();
|
|
1184
|
+
}
|
|
1185
|
+
});
|
|
1186
|
+
|
|
1187
|
+
it("judges only the first uncancelable move: a sideways drag that turns vertical stays the page's", () => {
|
|
1188
|
+
const restore = touchActionFromData();
|
|
1189
|
+
try {
|
|
1190
|
+
const {list, row} = mount(
|
|
1191
|
+
'<div id="list" style="overflow-y: auto"><div id="row" data-touch-action="pan-y pinch-zoom"></div></div>',
|
|
1192
|
+
);
|
|
1193
|
+
scrollable(list, {scrollTop: 0, scrollHeight: 1000, clientHeight: 400});
|
|
1194
|
+
const consumer = new RecordingConsumer();
|
|
1195
|
+
detachers.push(host.attachConsumer(row, consumer));
|
|
1196
|
+
const finger = {id: 1, x: 100, y: 100};
|
|
1197
|
+
touch('touchstart', row, [finger], [finger], 0);
|
|
1198
|
+
touch('touchmove', row, [{...finger, x: 120}], [{...finger, x: 120}], 16);
|
|
1199
|
+
touch(
|
|
1200
|
+
'touchmove',
|
|
1201
|
+
row,
|
|
1202
|
+
[{...finger, x: 130, y: 98}],
|
|
1203
|
+
[{...finger, x: 130, y: 98}],
|
|
1204
|
+
32,
|
|
1205
|
+
false,
|
|
1206
|
+
);
|
|
1207
|
+
// Chromium dropped the scroll when it began sideways; the finger
|
|
1208
|
+
// turning upward later does not make it a pan.
|
|
1209
|
+
touch(
|
|
1210
|
+
'touchmove',
|
|
1211
|
+
row,
|
|
1212
|
+
[{...finger, x: 132, y: 40}],
|
|
1213
|
+
[{...finger, x: 132, y: 40}],
|
|
1214
|
+
48,
|
|
1215
|
+
false,
|
|
1216
|
+
);
|
|
1217
|
+
expect(
|
|
1218
|
+
consumer.events.filter(event => event.startsWith('cancelled')),
|
|
1219
|
+
).toEqual([]);
|
|
1220
|
+
expect(consumer.events.at(-1)).toBe('moved:1');
|
|
1221
|
+
} finally {
|
|
1222
|
+
restore();
|
|
1223
|
+
}
|
|
1224
|
+
});
|
|
1225
|
+
|
|
1226
|
+
it('reads a scroll above an unclaimed touch that has travelled as the browser taking it (spec:AST-094 FR20)', () => {
|
|
1227
|
+
const {list, row} = mount('<div id="list"><div id="row"></div></div>');
|
|
1228
|
+
const tap = new TapGestureRecognizer({name: 'tap'});
|
|
1229
|
+
detachers.push(host.attach(row, tap));
|
|
1230
|
+
const finger = {id: 1, x: 10, y: 10};
|
|
1231
|
+
touch('touchstart', row, [finger], [finger], 0);
|
|
1232
|
+
const moved = {...finger, y: 20};
|
|
1233
|
+
touch('touchmove', row, [moved], [moved], 16);
|
|
1234
|
+
list.dispatchEvent(new Event('scroll'));
|
|
1235
|
+
expect(tap.state).toBe('possible');
|
|
1236
|
+
expect(tap.numberOfTouches).toBe(0);
|
|
1237
|
+
});
|
|
1238
|
+
|
|
1239
|
+
it('lets a scroll take an unclaimed touch from 10 px of travel, straight or diagonal, and not before (spec:AST-094 FR20)', () => {
|
|
1240
|
+
const takenAfter = (dx: number, dy: number) => {
|
|
1241
|
+
const {list, row} = mount('<div id="list"><div id="row"></div></div>');
|
|
1242
|
+
const consumer = new RecordingConsumer();
|
|
1243
|
+
const detach = host.attachConsumer(row, consumer);
|
|
1244
|
+
const finger = {id: 1, x: 10, y: 10};
|
|
1245
|
+
touch('touchstart', row, [finger], [finger], 0);
|
|
1246
|
+
const moved = {...finger, x: 10 + dx, y: 10 + dy};
|
|
1247
|
+
touch('touchmove', row, [moved], [moved], 16);
|
|
1248
|
+
list.dispatchEvent(new Event('scroll'));
|
|
1249
|
+
const taken = consumer.events.some(event =>
|
|
1250
|
+
event.startsWith('cancelled'),
|
|
1251
|
+
);
|
|
1252
|
+
touch('touchend', row, [moved], [], 32);
|
|
1253
|
+
detach();
|
|
1254
|
+
return taken;
|
|
1255
|
+
};
|
|
1256
|
+
// A still finger: the scroll is the page's own (anchoring, a carousel).
|
|
1257
|
+
expect(takenAfter(0, 0)).toBe(false);
|
|
1258
|
+
expect(takenAfter(0, 9)).toBe(false);
|
|
1259
|
+
expect(takenAfter(0, 10)).toBe(true);
|
|
1260
|
+
// 9.9 px and 11.3 px from where it landed.
|
|
1261
|
+
expect(takenAfter(7, 7)).toBe(false);
|
|
1262
|
+
expect(takenAfter(8, 8)).toBe(true);
|
|
1263
|
+
});
|
|
1264
|
+
|
|
1265
|
+
it('drives mouse input through Pointer Events', () => {
|
|
1266
|
+
const {box} = mount('<div id="box"></div>');
|
|
1267
|
+
const pan = new PanGestureRecognizer({name: 'pan'});
|
|
1268
|
+
let released = {x: 0, y: 0};
|
|
1269
|
+
pan.addListener(r => {
|
|
1270
|
+
if (r.state === 'ended') {
|
|
1271
|
+
released = r.velocity();
|
|
1272
|
+
}
|
|
1273
|
+
});
|
|
1274
|
+
detachers.push(host.attach(box, pan));
|
|
1275
|
+
pointer('pointerdown', box, {
|
|
1276
|
+
pointerType: 'mouse',
|
|
1277
|
+
x: 0,
|
|
1278
|
+
y: 0,
|
|
1279
|
+
buttons: 1,
|
|
1280
|
+
time: 0,
|
|
1281
|
+
});
|
|
1282
|
+
for (let t = 8; t <= 96; t += 8) {
|
|
1283
|
+
pointer('pointermove', box, {
|
|
1284
|
+
pointerType: 'mouse',
|
|
1285
|
+
x: t * 2,
|
|
1286
|
+
y: 0,
|
|
1287
|
+
buttons: 1,
|
|
1288
|
+
time: t,
|
|
1289
|
+
});
|
|
1290
|
+
}
|
|
1291
|
+
pointer('pointerup', box, {pointerType: 'mouse', x: 192, y: 0, time: 104});
|
|
1292
|
+
expect(pan.state).toBe('possible');
|
|
1293
|
+
expect(released.x).toBeCloseTo(2000, 0);
|
|
1294
|
+
});
|
|
1295
|
+
|
|
1296
|
+
it('cancels the context menu while a long press owns the touch', () => {
|
|
1297
|
+
const {box} = mount('<div id="box"></div>');
|
|
1298
|
+
const press = new LongPressGestureRecognizer({name: 'press'});
|
|
1299
|
+
detachers.push(host.attach(box, press));
|
|
1300
|
+
const finger = {id: 1, x: 10, y: 10};
|
|
1301
|
+
touch('touchstart', box, [finger], [finger], 0);
|
|
1302
|
+
const before = new MouseEvent('contextmenu', {
|
|
1303
|
+
bubbles: true,
|
|
1304
|
+
cancelable: true,
|
|
1305
|
+
});
|
|
1306
|
+
box.dispatchEvent(before);
|
|
1307
|
+
expect(before.defaultPrevented).toBe(false);
|
|
1308
|
+
host.arena.advanceTime(500);
|
|
1309
|
+
expect(press.state).toBe('began');
|
|
1310
|
+
const during = new MouseEvent('contextmenu', {
|
|
1311
|
+
bubbles: true,
|
|
1312
|
+
cancelable: true,
|
|
1313
|
+
});
|
|
1314
|
+
box.dispatchEvent(during);
|
|
1315
|
+
expect(during.defaultPrevented).toBe(true);
|
|
1316
|
+
});
|
|
1317
|
+
|
|
1318
|
+
it('holds the click of a tap a double tap may claim, and replays it when the double tap fails', () => {
|
|
1319
|
+
const {row, button} = mount(
|
|
1320
|
+
'<div id="row"><button id="button">Go</button></div>',
|
|
1321
|
+
);
|
|
1322
|
+
const double = new TapGestureRecognizer({
|
|
1323
|
+
name: 'double',
|
|
1324
|
+
numberOfTapsRequired: 2,
|
|
1325
|
+
});
|
|
1326
|
+
detachers.push(host.attach(row, double));
|
|
1327
|
+
const clicks: {target: EventTarget | null; detail: number}[] = [];
|
|
1328
|
+
button.addEventListener('click', event =>
|
|
1329
|
+
clicks.push({target: event.target, detail: event.detail}),
|
|
1330
|
+
);
|
|
1331
|
+
const finger = {id: 1, x: 10, y: 10};
|
|
1332
|
+
touch('touchstart', button, [finger], [finger], 0);
|
|
1333
|
+
const end = touch('touchend', button, [finger], [], 60);
|
|
1334
|
+
expect(end.defaultPrevented).toBe(true);
|
|
1335
|
+
expect(clicks).toEqual([]);
|
|
1336
|
+
const interval = UIKIT_GESTURES.tap.maximumIntervalBetweenTaps.value;
|
|
1337
|
+
host.arena.advanceTime(60 + interval);
|
|
1338
|
+
expect(clicks).toEqual([{target: button, detail: 1}]);
|
|
1339
|
+
});
|
|
1340
|
+
|
|
1341
|
+
it('collects recognizers across shadow roots', () => {
|
|
1342
|
+
const {outer} = mount('<div id="outer"></div>');
|
|
1343
|
+
const shadow = outer.attachShadow({mode: 'open'});
|
|
1344
|
+
shadow.innerHTML = '<span id="inner">x</span>';
|
|
1345
|
+
const inner = shadow.getElementById('inner') as HTMLElement;
|
|
1346
|
+
const tap = new TapGestureRecognizer({name: 'tap'});
|
|
1347
|
+
const actions: string[] = [];
|
|
1348
|
+
tap.addListener(r => actions.push(r.state));
|
|
1349
|
+
detachers.push(host.attach(outer, tap));
|
|
1350
|
+
const finger = {id: 1, x: 5, y: 5, target: inner};
|
|
1351
|
+
touch('touchstart', inner, [finger], [finger], 0);
|
|
1352
|
+
touch('touchend', inner, [finger], [], 50);
|
|
1353
|
+
expect(actions).toEqual(['ended']);
|
|
1354
|
+
});
|
|
1355
|
+
|
|
1356
|
+
it('cancels every touch in flight when the window loses focus', () => {
|
|
1357
|
+
const {box} = mount('<div id="box"></div>');
|
|
1358
|
+
const pan = new PanGestureRecognizer({name: 'pan'});
|
|
1359
|
+
const states: string[] = [];
|
|
1360
|
+
pan.addListener(r => states.push(r.state));
|
|
1361
|
+
detachers.push(host.attach(box, pan));
|
|
1362
|
+
const finger = {id: 1, x: 10, y: 10};
|
|
1363
|
+
touch('touchstart', box, [finger], [finger], 0);
|
|
1364
|
+
touch('touchmove', box, [{...finger, x: 40}], [{...finger, x: 40}], 16);
|
|
1365
|
+
window.dispatchEvent(new Event('blur'));
|
|
1366
|
+
expect(states).toEqual(['began', 'cancelled']);
|
|
1367
|
+
});
|
|
1368
|
+
|
|
1369
|
+
it('finishes a gesture whose target left the document mid-gesture', () => {
|
|
1370
|
+
const {box, child} = mount('<div id="box"><span id="child">x</span></div>');
|
|
1371
|
+
const pan = new PanGestureRecognizer({name: 'pan'});
|
|
1372
|
+
const states: string[] = [];
|
|
1373
|
+
pan.addListener(r => states.push(r.state));
|
|
1374
|
+
detachers.push(host.attach(box, pan));
|
|
1375
|
+
const finger = {id: 1, x: 10, y: 10};
|
|
1376
|
+
touch('touchstart', child, [finger], [finger], 0);
|
|
1377
|
+
touch('touchmove', child, [{...finger, x: 40}], [{...finger, x: 40}], 16);
|
|
1378
|
+
// A re-render replaces the span: its events no longer reach the document.
|
|
1379
|
+
child.remove();
|
|
1380
|
+
const move = touch(
|
|
1381
|
+
'touchmove',
|
|
1382
|
+
child,
|
|
1383
|
+
[{...finger, x: 60}],
|
|
1384
|
+
[{...finger, x: 60}],
|
|
1385
|
+
32,
|
|
1386
|
+
);
|
|
1387
|
+
expect(move.defaultPrevented).toBe(true);
|
|
1388
|
+
touch('touchend', child, [{...finger, x: 60}], [], 48);
|
|
1389
|
+
// The move and the lift after the removal still reached the pan.
|
|
1390
|
+
expect(states).toEqual(['began', 'changed', 'ended']);
|
|
1391
|
+
expect(host.arena.touches).toEqual([]);
|
|
1392
|
+
});
|
|
1393
|
+
|
|
1394
|
+
it('cancels a finger the events say is no longer down', () => {
|
|
1395
|
+
const {box} = mount('<div id="box"></div>');
|
|
1396
|
+
const pan = new PanGestureRecognizer({name: 'pan'});
|
|
1397
|
+
const states: string[] = [];
|
|
1398
|
+
pan.addListener(r => states.push(r.state));
|
|
1399
|
+
detachers.push(host.attach(box, pan));
|
|
1400
|
+
const first = {id: 1, x: 10, y: 10};
|
|
1401
|
+
touch('touchstart', box, [first], [first], 0);
|
|
1402
|
+
touch('touchmove', box, [{...first, x: 40}], [{...first, x: 40}], 16);
|
|
1403
|
+
// Its touchend went to a node outside the document; the next event for
|
|
1404
|
+
// another finger no longer lists it.
|
|
1405
|
+
const other = {id: 2, x: 300, y: 300};
|
|
1406
|
+
touch('touchmove', document.body, [other], [other], 32);
|
|
1407
|
+
expect(states).toEqual(['began', 'cancelled']);
|
|
1408
|
+
});
|
|
1409
|
+
|
|
1410
|
+
it('treats a reused finger number as a new touch', () => {
|
|
1411
|
+
const {box} = mount('<div id="box"></div>');
|
|
1412
|
+
const tap = new TapGestureRecognizer({name: 'tap'});
|
|
1413
|
+
const states: string[] = [];
|
|
1414
|
+
tap.addListener(r => states.push(r.state));
|
|
1415
|
+
detachers.push(host.attach(box, tap));
|
|
1416
|
+
const finger = {id: 4, x: 10, y: 10};
|
|
1417
|
+
touch('touchstart', box, [finger], [finger], 0);
|
|
1418
|
+
// No touchend reached us; the same identifier lands again.
|
|
1419
|
+
touch('touchstart', box, [finger], [finger], 500);
|
|
1420
|
+
touch('touchend', box, [finger], [], 560);
|
|
1421
|
+
expect(states).toEqual(['ended']);
|
|
1422
|
+
});
|
|
1423
|
+
|
|
1424
|
+
it('cancels every finger on a touchcancel that names none', () => {
|
|
1425
|
+
const {box} = mount('<div id="box"></div>');
|
|
1426
|
+
const pan = new PanGestureRecognizer({name: 'pan'});
|
|
1427
|
+
const states: string[] = [];
|
|
1428
|
+
pan.addListener(r => states.push(r.state));
|
|
1429
|
+
detachers.push(host.attach(box, pan));
|
|
1430
|
+
const finger = {id: 1, x: 10, y: 10};
|
|
1431
|
+
touch('touchstart', box, [finger], [finger], 0);
|
|
1432
|
+
touch('touchmove', box, [{...finger, x: 40}], [{...finger, x: 40}], 16);
|
|
1433
|
+
box.dispatchEvent(new Event('touchcancel', {bubbles: true}));
|
|
1434
|
+
expect(states).toEqual(['began', 'cancelled']);
|
|
1435
|
+
});
|
|
1436
|
+
|
|
1437
|
+
it('reads epoch-stamped events on the arena clock', () => {
|
|
1438
|
+
const {box} = mount('<div id="box"></div>');
|
|
1439
|
+
const pan = new PanGestureRecognizer({name: 'pan'});
|
|
1440
|
+
let released = {x: 0, y: 0};
|
|
1441
|
+
pan.addListener(r => {
|
|
1442
|
+
if (r.state === 'ended') {
|
|
1443
|
+
released = r.velocity();
|
|
1444
|
+
}
|
|
1445
|
+
});
|
|
1446
|
+
detachers.push(host.attach(box, pan));
|
|
1447
|
+
const epoch = performance.timeOrigin + 5000;
|
|
1448
|
+
const finger = {id: 1, x: 0, y: 10};
|
|
1449
|
+
touch('touchstart', box, [finger], [finger], epoch);
|
|
1450
|
+
for (let t = 8; t <= 96; t += 8) {
|
|
1451
|
+
const at = {...finger, x: t};
|
|
1452
|
+
touch('touchmove', box, [at], [at], epoch + t);
|
|
1453
|
+
}
|
|
1454
|
+
touch('touchend', box, [{...finger, x: 96}], [], epoch + 100);
|
|
1455
|
+
expect(released.x).toBeCloseTo(1000, 0);
|
|
1456
|
+
expect(host.arena.now()).toBeLessThan(1e9);
|
|
1457
|
+
});
|
|
1458
|
+
|
|
1459
|
+
it('ignores touches that land outside every attached element', () => {
|
|
1460
|
+
const {box, elsewhere} = mount(
|
|
1461
|
+
'<div id="box"></div><div id="elsewhere"></div>',
|
|
1462
|
+
);
|
|
1463
|
+
const pan = new PanGestureRecognizer({name: 'pan'});
|
|
1464
|
+
detachers.push(host.attach(box, pan));
|
|
1465
|
+
const finger = {id: 1, x: 10, y: 10};
|
|
1466
|
+
touch('touchstart', elsewhere, [finger], [finger], 0);
|
|
1467
|
+
const move = touch(
|
|
1468
|
+
'touchmove',
|
|
1469
|
+
elsewhere,
|
|
1470
|
+
[{...finger, x: 60}],
|
|
1471
|
+
[{...finger, x: 60}],
|
|
1472
|
+
16,
|
|
1473
|
+
);
|
|
1474
|
+
expect(move.defaultPrevented).toBe(false);
|
|
1475
|
+
expect(host.arena.touches).toEqual([]);
|
|
1476
|
+
});
|
|
1477
|
+
});
|
|
1478
|
+
|
|
1479
|
+
describe('every pointer where the primary pointer is fine (spec:AST-094 FR19, FR24, FR43)', () => {
|
|
1480
|
+
/** The page listeners live now, by type: what was added and not removed. */
|
|
1481
|
+
function pageListeners(): () => string[] {
|
|
1482
|
+
const counts = new Map<string, number>();
|
|
1483
|
+
for (const target of [document, window] as EventTarget[]) {
|
|
1484
|
+
const add = target.addEventListener.bind(target);
|
|
1485
|
+
const remove = target.removeEventListener.bind(target);
|
|
1486
|
+
vi.spyOn(target, 'addEventListener').mockImplementation(
|
|
1487
|
+
(type, listener, options) => {
|
|
1488
|
+
counts.set(type, (counts.get(type) ?? 0) + 1);
|
|
1489
|
+
add(type, listener, options);
|
|
1490
|
+
},
|
|
1491
|
+
);
|
|
1492
|
+
vi.spyOn(target, 'removeEventListener').mockImplementation(
|
|
1493
|
+
(type, listener, options) => {
|
|
1494
|
+
counts.set(type, (counts.get(type) ?? 0) - 1);
|
|
1495
|
+
remove(type, listener, options);
|
|
1496
|
+
},
|
|
1497
|
+
);
|
|
1498
|
+
}
|
|
1499
|
+
return () =>
|
|
1500
|
+
[...counts]
|
|
1501
|
+
.filter(
|
|
1502
|
+
([type, count]) =>
|
|
1503
|
+
count > 0 &&
|
|
1504
|
+
/^(touch|pointer|click|blur|scroll|contextmenu|visibilitychange)/.test(
|
|
1505
|
+
type,
|
|
1506
|
+
),
|
|
1507
|
+
)
|
|
1508
|
+
.map(([type]) => type)
|
|
1509
|
+
.sort();
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
function fineHost(): GestureHost {
|
|
1513
|
+
return new GestureHost(document, {
|
|
1514
|
+
clock,
|
|
1515
|
+
isPrimaryPointerCoarse: () => false,
|
|
1516
|
+
});
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
/** A button press with the button and modifier given. */
|
|
1520
|
+
function press(
|
|
1521
|
+
target: EventTarget,
|
|
1522
|
+
init: {pointerType: string; button: number; ctrlKey?: boolean},
|
|
1523
|
+
): void {
|
|
1524
|
+
const event = new PointerEvent('pointerdown', {
|
|
1525
|
+
bubbles: true,
|
|
1526
|
+
cancelable: true,
|
|
1527
|
+
composed: true,
|
|
1528
|
+
pointerId: 9,
|
|
1529
|
+
pointerType: init.pointerType,
|
|
1530
|
+
clientX: 10,
|
|
1531
|
+
clientY: 10,
|
|
1532
|
+
button: init.button,
|
|
1533
|
+
buttons: init.button === 0 ? 1 : 2,
|
|
1534
|
+
ctrlKey: init.ctrlKey ?? false,
|
|
1535
|
+
});
|
|
1536
|
+
Object.defineProperty(event, 'timeStamp', {value: 0});
|
|
1537
|
+
target.dispatchEvent(event);
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
afterEach(() => {
|
|
1541
|
+
vi.restoreAllMocks();
|
|
1542
|
+
vi.useRealTimers();
|
|
1543
|
+
});
|
|
1544
|
+
|
|
1545
|
+
it('keeps one passive pointerdown listener on each element that uses a gesture and none on the page while idle; a disabled one keeps none', () => {
|
|
1546
|
+
const {box, row, off} = mount(
|
|
1547
|
+
'<div id="box"></div><div id="row"></div><div id="off"></div>',
|
|
1548
|
+
);
|
|
1549
|
+
const live = pageListeners();
|
|
1550
|
+
const downs = (element: HTMLElement) => {
|
|
1551
|
+
const spy = vi.spyOn(element, 'addEventListener');
|
|
1552
|
+
return () => spy.mock.calls.filter(call => call[0] === 'pointerdown');
|
|
1553
|
+
};
|
|
1554
|
+
const boxDowns = downs(box);
|
|
1555
|
+
const rowDowns = downs(row);
|
|
1556
|
+
const offAdded = vi.spyOn(off, 'addEventListener');
|
|
1557
|
+
const fine = fineHost();
|
|
1558
|
+
detachers.push(
|
|
1559
|
+
fine.attach(box, new PanGestureRecognizer({direction: 'horizontal'})),
|
|
1560
|
+
fine.attach(row, new TapGestureRecognizer()),
|
|
1561
|
+
fine.attach(off, new TapGestureRecognizer({isEnabled: false})),
|
|
1562
|
+
);
|
|
1563
|
+
expect(live()).toEqual([]);
|
|
1564
|
+
expect(boxDowns()).toEqual([
|
|
1565
|
+
['pointerdown', expect.any(Function), {capture: true, passive: true}],
|
|
1566
|
+
]);
|
|
1567
|
+
expect(rowDowns()).toHaveLength(1);
|
|
1568
|
+
expect(offAdded).not.toHaveBeenCalled();
|
|
1569
|
+
});
|
|
1570
|
+
|
|
1571
|
+
it("installs the page listeners for a mouse's primary-button press, serves it to every recognizer on its path, and removes them as the press ends", () => {
|
|
1572
|
+
vi.useFakeTimers({toFake: ['setTimeout', 'clearTimeout', 'performance']});
|
|
1573
|
+
const {outer, box} = mount('<div id="outer"><div id="box"></div></div>');
|
|
1574
|
+
const live = pageListeners();
|
|
1575
|
+
const fine = fineHost();
|
|
1576
|
+
const pan = new PanGestureRecognizer({
|
|
1577
|
+
name: 'pan',
|
|
1578
|
+
direction: 'horizontal',
|
|
1579
|
+
});
|
|
1580
|
+
const outerTap = new TapGestureRecognizer({name: 'outer'});
|
|
1581
|
+
const states: string[] = [];
|
|
1582
|
+
const outerTransitions: string[] = [];
|
|
1583
|
+
pan.addListener(r => states.push(r.state));
|
|
1584
|
+
outerTap.addTransitionListener((_current, _from, to) =>
|
|
1585
|
+
outerTransitions.push(to),
|
|
1586
|
+
);
|
|
1587
|
+
detachers.push(fine.attach(box, pan), fine.attach(outer, outerTap));
|
|
1588
|
+
pointer('pointerdown', box, {
|
|
1589
|
+
pointerType: 'mouse',
|
|
1590
|
+
x: 10,
|
|
1591
|
+
y: 10,
|
|
1592
|
+
buttons: 1,
|
|
1593
|
+
time: 0,
|
|
1594
|
+
});
|
|
1595
|
+
expect(live()).toEqual(
|
|
1596
|
+
expect.arrayContaining(['click', 'pointermove', 'pointerup']),
|
|
1597
|
+
);
|
|
1598
|
+
pointer('pointermove', box, {
|
|
1599
|
+
pointerType: 'mouse',
|
|
1600
|
+
x: 40,
|
|
1601
|
+
y: 10,
|
|
1602
|
+
buttons: 1,
|
|
1603
|
+
time: 16,
|
|
1604
|
+
});
|
|
1605
|
+
pointer('pointerup', box, {pointerType: 'mouse', x: 40, y: 10, time: 32});
|
|
1606
|
+
expect(states[0]).toBe('began');
|
|
1607
|
+
expect(states[states.length - 1]).toBe('ended');
|
|
1608
|
+
expect(outerTransitions).toContain('failed');
|
|
1609
|
+
// The click the claimed lift owes is still swallowed (FR21): its
|
|
1610
|
+
// listener alone stays, for the swallower's window.
|
|
1611
|
+
expect(live()).toEqual(['click']);
|
|
1612
|
+
vi.advanceTimersByTime(1001);
|
|
1613
|
+
expect(live()).toEqual([]);
|
|
1614
|
+
});
|
|
1615
|
+
|
|
1616
|
+
it('removes every page listener at once after a press nothing claimed', () => {
|
|
1617
|
+
const {box} = mount('<div id="box"></div>');
|
|
1618
|
+
const live = pageListeners();
|
|
1619
|
+
const fine = fineHost();
|
|
1620
|
+
detachers.push(fine.attach(box, new PanGestureRecognizer()));
|
|
1621
|
+
pointer('pointerdown', box, {
|
|
1622
|
+
pointerType: 'mouse',
|
|
1623
|
+
x: 10,
|
|
1624
|
+
y: 10,
|
|
1625
|
+
buttons: 1,
|
|
1626
|
+
time: 0,
|
|
1627
|
+
});
|
|
1628
|
+
pointer('pointermove', box, {
|
|
1629
|
+
pointerType: 'mouse',
|
|
1630
|
+
x: 13,
|
|
1631
|
+
y: 10,
|
|
1632
|
+
buttons: 1,
|
|
1633
|
+
time: 16,
|
|
1634
|
+
});
|
|
1635
|
+
pointer('pointerup', box, {pointerType: 'mouse', x: 13, y: 10, time: 40});
|
|
1636
|
+
expect(live()).toEqual([]);
|
|
1637
|
+
});
|
|
1638
|
+
|
|
1639
|
+
it("starts no press for a right click, a pen's barrel button or a ctrl-click on macOS (spec:AST-094 FR43)", () => {
|
|
1640
|
+
const {box} = mount('<div id="box"></div>');
|
|
1641
|
+
const live = pageListeners();
|
|
1642
|
+
const fine = fineHost();
|
|
1643
|
+
const tap = new TapGestureRecognizer({name: 'tap'});
|
|
1644
|
+
const transitions: string[] = [];
|
|
1645
|
+
tap.addTransitionListener((_current, _from, to) => transitions.push(to));
|
|
1646
|
+
detachers.push(fine.attach(box, tap));
|
|
1647
|
+
press(box, {pointerType: 'mouse', button: 2});
|
|
1648
|
+
press(box, {pointerType: 'pen', button: 2});
|
|
1649
|
+
vi.spyOn(navigator, 'platform', 'get').mockReturnValue('MacIntel');
|
|
1650
|
+
press(box, {pointerType: 'mouse', button: 0, ctrlKey: true});
|
|
1651
|
+
expect(live()).toEqual([]);
|
|
1652
|
+
expect(transitions).toEqual([]);
|
|
1653
|
+
expect(host.arena.touches).toEqual([]);
|
|
1654
|
+
});
|
|
1655
|
+
|
|
1656
|
+
it('serves a pen press, and keeps no pointer listener after it', () => {
|
|
1657
|
+
const {box} = mount('<div id="box"></div>');
|
|
1658
|
+
const live = pageListeners();
|
|
1659
|
+
const fine = fineHost();
|
|
1660
|
+
const pan = new PanGestureRecognizer({direction: 'horizontal'});
|
|
1661
|
+
const states: string[] = [];
|
|
1662
|
+
pan.addListener(r => states.push(r.state));
|
|
1663
|
+
detachers.push(fine.attach(box, pan));
|
|
1664
|
+
pointer('pointerdown', box, {
|
|
1665
|
+
id: 3,
|
|
1666
|
+
pointerType: 'pen',
|
|
1667
|
+
x: 10,
|
|
1668
|
+
y: 10,
|
|
1669
|
+
buttons: 1,
|
|
1670
|
+
time: 0,
|
|
1671
|
+
});
|
|
1672
|
+
pointer('pointermove', box, {
|
|
1673
|
+
id: 3,
|
|
1674
|
+
pointerType: 'pen',
|
|
1675
|
+
x: 40,
|
|
1676
|
+
y: 10,
|
|
1677
|
+
buttons: 1,
|
|
1678
|
+
time: 16,
|
|
1679
|
+
});
|
|
1680
|
+
pointer('pointerup', box, {
|
|
1681
|
+
id: 3,
|
|
1682
|
+
pointerType: 'pen',
|
|
1683
|
+
x: 40,
|
|
1684
|
+
y: 10,
|
|
1685
|
+
time: 32,
|
|
1686
|
+
});
|
|
1687
|
+
expect(states[0]).toBe('began');
|
|
1688
|
+
expect(states[states.length - 1]).toBe('ended');
|
|
1689
|
+
expect(live().filter(type => type !== 'click')).toEqual([]);
|
|
1690
|
+
});
|
|
1691
|
+
|
|
1692
|
+
it("serves a touchscreen laptop's finger: its pointerdown brings the page listeners, its touches are read, and a claim cancels its touchmove and its lift", () => {
|
|
1693
|
+
const {box} = mount('<div id="box"></div>');
|
|
1694
|
+
const live = pageListeners();
|
|
1695
|
+
const fine = fineHost();
|
|
1696
|
+
const pan = new PanGestureRecognizer({direction: 'horizontal'});
|
|
1697
|
+
const states: string[] = [];
|
|
1698
|
+
pan.addListener(r => states.push(r.state));
|
|
1699
|
+
detachers.push(fine.attach(box, pan));
|
|
1700
|
+
const finger = {id: 1, x: 10, y: 10};
|
|
1701
|
+
const moved = {id: 1, x: 40, y: 10};
|
|
1702
|
+
pointer('pointerdown', box, {
|
|
1703
|
+
id: 5,
|
|
1704
|
+
pointerType: 'touch',
|
|
1705
|
+
x: 10,
|
|
1706
|
+
y: 10,
|
|
1707
|
+
buttons: 1,
|
|
1708
|
+
time: 0,
|
|
1709
|
+
});
|
|
1710
|
+
expect(live()).toEqual(expect.arrayContaining(['touchstart', 'touchend']));
|
|
1711
|
+
touch('touchstart', box, [finger], [finger], 0);
|
|
1712
|
+
const move = touch('touchmove', box, [moved], [moved], 16);
|
|
1713
|
+
expect(states).toContain('began');
|
|
1714
|
+
expect(move.defaultPrevented).toBe(true);
|
|
1715
|
+
pointer('pointerup', box, {
|
|
1716
|
+
id: 5,
|
|
1717
|
+
pointerType: 'touch',
|
|
1718
|
+
x: 40,
|
|
1719
|
+
y: 10,
|
|
1720
|
+
time: 32,
|
|
1721
|
+
});
|
|
1722
|
+
const lift = touch('touchend', box, [moved], [], 32);
|
|
1723
|
+
expect(lift.defaultPrevented).toBe(true);
|
|
1724
|
+
expect(states[states.length - 1]).toBe('ended');
|
|
1725
|
+
expect(live().filter(type => type !== 'click')).toEqual([]);
|
|
1726
|
+
});
|
|
1727
|
+
|
|
1728
|
+
it('serves an action group like every attachment: its press reaches every recognizer on its path (family:action-groups FR5)', () => {
|
|
1729
|
+
const {outer, group, one} = mount(
|
|
1730
|
+
'<div id="outer"><div id="group"><button id="one"></button></div></div>',
|
|
1731
|
+
);
|
|
1732
|
+
const fine = fineHost();
|
|
1733
|
+
const outerPan = new PanGestureRecognizer({direction: 'horizontal'});
|
|
1734
|
+
const outerStates: string[] = [];
|
|
1735
|
+
outerPan.addListener(r => outerStates.push(r.state));
|
|
1736
|
+
detachers.push(
|
|
1737
|
+
fine.attach(group, new TapGestureRecognizer(), {actionGroup: true}),
|
|
1738
|
+
fine.attach(outer, outerPan),
|
|
1739
|
+
);
|
|
1740
|
+
pointer('pointerdown', one, {
|
|
1741
|
+
pointerType: 'mouse',
|
|
1742
|
+
x: 10,
|
|
1743
|
+
y: 10,
|
|
1744
|
+
buttons: 1,
|
|
1745
|
+
time: 0,
|
|
1746
|
+
});
|
|
1747
|
+
pointer('pointermove', one, {
|
|
1748
|
+
pointerType: 'mouse',
|
|
1749
|
+
x: 40,
|
|
1750
|
+
y: 10,
|
|
1751
|
+
buttons: 1,
|
|
1752
|
+
time: 16,
|
|
1753
|
+
});
|
|
1754
|
+
pointer('pointerup', one, {pointerType: 'mouse', x: 40, y: 10, time: 32});
|
|
1755
|
+
expect(outerStates).toContain('began');
|
|
1756
|
+
});
|
|
1757
|
+
|
|
1758
|
+
it("re-syncs when only some of an element's recognizers detach: one left disabled keeps no pointerdown, page listener or ResizeObserver (spec:AST-094 FR24)", () => {
|
|
1759
|
+
const {box} = mount('<div id="box"></div>');
|
|
1760
|
+
const live = pageListeners();
|
|
1761
|
+
// Where the primary pointer is fine: the element's one pointerdown goes
|
|
1762
|
+
// with its last enabled recognizer.
|
|
1763
|
+
const boxRemoved = vi.spyOn(box, 'removeEventListener');
|
|
1764
|
+
const fine = fineHost();
|
|
1765
|
+
const detachPan = fine.attach(box, new PanGestureRecognizer());
|
|
1766
|
+
detachers.push(
|
|
1767
|
+
fine.attach(box, new TapGestureRecognizer({isEnabled: false})),
|
|
1768
|
+
);
|
|
1769
|
+
detachPan();
|
|
1770
|
+
expect(
|
|
1771
|
+
boxRemoved.mock.calls.filter(call => call[0] === 'pointerdown'),
|
|
1772
|
+
).toHaveLength(1);
|
|
1773
|
+
// A press consumer's detach re-syncs as a recognizer's does.
|
|
1774
|
+
const {pad} = mount('<div id="box"></div><div id="pad"></div>');
|
|
1775
|
+
const padRemoved = vi.spyOn(pad, 'removeEventListener');
|
|
1776
|
+
const detachConsumer = fine.attachConsumer(pad, new RecordingConsumer());
|
|
1777
|
+
detachers.push(
|
|
1778
|
+
fine.attach(pad, new TapGestureRecognizer({isEnabled: false})),
|
|
1779
|
+
);
|
|
1780
|
+
detachConsumer();
|
|
1781
|
+
expect(
|
|
1782
|
+
padRemoved.mock.calls.filter(call => call[0] === 'pointerdown'),
|
|
1783
|
+
).toHaveLength(1);
|
|
1784
|
+
// Where it is coarse: the page's listeners go too.
|
|
1785
|
+
const coarse = new GestureHost(document, {
|
|
1786
|
+
clock,
|
|
1787
|
+
isPrimaryPointerCoarse: () => true,
|
|
1788
|
+
});
|
|
1789
|
+
const {row} = mount('<div id="row"></div>');
|
|
1790
|
+
const detachRowPan = coarse.attach(row, new PanGestureRecognizer());
|
|
1791
|
+
detachers.push(
|
|
1792
|
+
coarse.attach(row, new TapGestureRecognizer({isEnabled: false})),
|
|
1793
|
+
);
|
|
1794
|
+
expect(live()).toContain('touchstart');
|
|
1795
|
+
detachRowPan();
|
|
1796
|
+
expect(live()).toEqual([]);
|
|
1797
|
+
});
|
|
1798
|
+
|
|
1799
|
+
it("watches the viewport once a touch-down finds a leading pan's direction switched in place to the right edge, and stops when it switches back (spec:AST-094 FR18, FR24, DEC-22)", () => {
|
|
1800
|
+
const observed = new Set<Element>();
|
|
1801
|
+
class StandInResizeObserver {
|
|
1802
|
+
observe(target: Element): void {
|
|
1803
|
+
observed.add(target);
|
|
1804
|
+
}
|
|
1805
|
+
unobserve(target: Element): void {
|
|
1806
|
+
observed.delete(target);
|
|
1807
|
+
}
|
|
1808
|
+
disconnect(): void {
|
|
1809
|
+
observed.clear();
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
const original = Object.getOwnPropertyDescriptor(window, 'ResizeObserver');
|
|
1813
|
+
Object.defineProperty(window, 'ResizeObserver', {
|
|
1814
|
+
configurable: true,
|
|
1815
|
+
value: StandInResizeObserver,
|
|
1816
|
+
});
|
|
1817
|
+
try {
|
|
1818
|
+
const {strip} = mount('<div id="strip" style="direction: ltr"></div>');
|
|
1819
|
+
detachers.push(
|
|
1820
|
+
host.attach(
|
|
1821
|
+
strip,
|
|
1822
|
+
new ScreenEdgePanGestureRecognizer({
|
|
1823
|
+
name: 'edge',
|
|
1824
|
+
edges: ['leading'],
|
|
1825
|
+
}),
|
|
1826
|
+
),
|
|
1827
|
+
);
|
|
1828
|
+
// Under LTR the leading edge is the left one: its band stays put.
|
|
1829
|
+
expect(observed.has(document.documentElement)).toBe(false);
|
|
1830
|
+
// The page turns RTL in place; the next touch-down measures again, and
|
|
1831
|
+
// the leading edge is the right one, whose band moves with the viewport.
|
|
1832
|
+
strip.style.direction = 'rtl';
|
|
1833
|
+
const first = {id: 1, x: 4, y: 300};
|
|
1834
|
+
touch('touchstart', strip, [first], [first], 0);
|
|
1835
|
+
touch('touchend', strip, [first], [], 40);
|
|
1836
|
+
expect(observed.has(document.documentElement)).toBe(true);
|
|
1837
|
+
// Back to LTR in place: the root is watched for nothing, and goes.
|
|
1838
|
+
strip.style.direction = 'ltr';
|
|
1839
|
+
const second = {id: 2, x: 4, y: 300};
|
|
1840
|
+
touch('touchstart', strip, [second], [second], 200);
|
|
1841
|
+
touch('touchend', strip, [second], [], 240);
|
|
1842
|
+
expect(observed.has(document.documentElement)).toBe(false);
|
|
1843
|
+
} finally {
|
|
1844
|
+
if (original == null) {
|
|
1845
|
+
delete (window as {ResizeObserver?: unknown}).ResizeObserver;
|
|
1846
|
+
} else {
|
|
1847
|
+
Object.defineProperty(window, 'ResizeObserver', original);
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
});
|
|
1851
|
+
|
|
1852
|
+
it("stops watching a screen-edge pan's box when it detaches while a disabled recognizer stays on the element (spec:AST-094 FR24)", () => {
|
|
1853
|
+
const observed = new Set<Element>();
|
|
1854
|
+
class StandInResizeObserver {
|
|
1855
|
+
observe(target: Element): void {
|
|
1856
|
+
observed.add(target);
|
|
1857
|
+
}
|
|
1858
|
+
unobserve(target: Element): void {
|
|
1859
|
+
observed.delete(target);
|
|
1860
|
+
}
|
|
1861
|
+
disconnect(): void {
|
|
1862
|
+
observed.clear();
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
const original = Object.getOwnPropertyDescriptor(window, 'ResizeObserver');
|
|
1866
|
+
Object.defineProperty(window, 'ResizeObserver', {
|
|
1867
|
+
configurable: true,
|
|
1868
|
+
value: StandInResizeObserver,
|
|
1869
|
+
});
|
|
1870
|
+
try {
|
|
1871
|
+
const {strip} = mount('<div id="strip"></div>');
|
|
1872
|
+
const fine = fineHost();
|
|
1873
|
+
const detachEdge = fine.attach(
|
|
1874
|
+
strip,
|
|
1875
|
+
new ScreenEdgePanGestureRecognizer({name: 'edge'}),
|
|
1876
|
+
);
|
|
1877
|
+
detachers.push(
|
|
1878
|
+
fine.attach(strip, new TapGestureRecognizer({isEnabled: false})),
|
|
1879
|
+
);
|
|
1880
|
+
expect(observed).toEqual(new Set([strip]));
|
|
1881
|
+
detachEdge();
|
|
1882
|
+
expect(observed.size).toBe(0);
|
|
1883
|
+
} finally {
|
|
1884
|
+
if (original == null) {
|
|
1885
|
+
delete (window as {ResizeObserver?: unknown}).ResizeObserver;
|
|
1886
|
+
} else {
|
|
1887
|
+
Object.defineProperty(window, 'ResizeObserver', original);
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
});
|
|
1891
|
+
|
|
1892
|
+
it('removes the element listener on detach and leaves nothing on the page', () => {
|
|
1893
|
+
const {box} = mount('<div id="box"></div>');
|
|
1894
|
+
const live = pageListeners();
|
|
1895
|
+
const boxRemoved = vi.spyOn(box, 'removeEventListener');
|
|
1896
|
+
const fine = fineHost();
|
|
1897
|
+
const detach = fine.attach(box, new TapGestureRecognizer());
|
|
1898
|
+
detach();
|
|
1899
|
+
expect(boxRemoved).toHaveBeenCalledWith(
|
|
1900
|
+
'pointerdown',
|
|
1901
|
+
expect.any(Function),
|
|
1902
|
+
{capture: true, passive: true},
|
|
1903
|
+
);
|
|
1904
|
+
expect(live()).toEqual([]);
|
|
1905
|
+
});
|
|
1906
|
+
|
|
1907
|
+
it('gives a coarse primary pointer its page listeners and no element pointerdown', () => {
|
|
1908
|
+
const {box} = mount('<div id="box"></div>');
|
|
1909
|
+
const live = pageListeners();
|
|
1910
|
+
const boxAdded = vi.spyOn(box, 'addEventListener');
|
|
1911
|
+
const coarse = new GestureHost(document, {
|
|
1912
|
+
clock,
|
|
1913
|
+
isPrimaryPointerCoarse: () => true,
|
|
1914
|
+
});
|
|
1915
|
+
detachers.push(coarse.attach(box, new TapGestureRecognizer()));
|
|
1916
|
+
expect(live()).toEqual(
|
|
1917
|
+
expect.arrayContaining(['pointerdown', 'touchstart']),
|
|
1918
|
+
);
|
|
1919
|
+
expect(
|
|
1920
|
+
boxAdded.mock.calls.filter(call => call[0] === 'pointerdown'),
|
|
1921
|
+
).toEqual([]);
|
|
1922
|
+
});
|
|
1923
|
+
});
|
|
1924
|
+
|
|
1925
|
+
describe("inputMode 'pointer-events'", () => {
|
|
1926
|
+
/** A clock whose timers the test fires. */
|
|
1927
|
+
class TimerClock {
|
|
1928
|
+
time = 0;
|
|
1929
|
+
private readonly timers = new Map<number, () => void>();
|
|
1930
|
+
private next = 1;
|
|
1931
|
+
now(): number {
|
|
1932
|
+
return this.time;
|
|
1933
|
+
}
|
|
1934
|
+
setTimeout(callback: () => void): number {
|
|
1935
|
+
const id = this.next++;
|
|
1936
|
+
this.timers.set(id, callback);
|
|
1937
|
+
return id;
|
|
1938
|
+
}
|
|
1939
|
+
clearTimeout(id: number): void {
|
|
1940
|
+
this.timers.delete(id);
|
|
1941
|
+
}
|
|
1942
|
+
/** Runs every timer, and the ones they set, at `time`. */
|
|
1943
|
+
fireAll(time: number): void {
|
|
1944
|
+
this.time = time;
|
|
1945
|
+
while (this.timers.size > 0) {
|
|
1946
|
+
const due = [...this.timers.values()];
|
|
1947
|
+
this.timers.clear();
|
|
1948
|
+
for (const callback of due) {
|
|
1949
|
+
callback();
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
}
|
|
1954
|
+
|
|
1955
|
+
it("tracks a finger once, from its pointer events, so a still finger's long press begins", () => {
|
|
1956
|
+
const {box} = mount('<div id="box"></div>');
|
|
1957
|
+
const timers = new TimerClock();
|
|
1958
|
+
const pointerHost = new GestureHost(document, {
|
|
1959
|
+
clock: timers,
|
|
1960
|
+
inputMode: 'pointer-events',
|
|
1961
|
+
isPrimaryPointerCoarse: () => true,
|
|
1962
|
+
});
|
|
1963
|
+
const press = new LongPressGestureRecognizer();
|
|
1964
|
+
const states: string[] = [];
|
|
1965
|
+
press.addListener(r => states.push(r.state));
|
|
1966
|
+
detachers.push(pointerHost.attach(box, press));
|
|
1967
|
+
// A browser with Touch Events sends both for one finger.
|
|
1968
|
+
pointer('pointerdown', box, {
|
|
1969
|
+
pointerType: 'touch',
|
|
1970
|
+
x: 10,
|
|
1971
|
+
y: 10,
|
|
1972
|
+
buttons: 1,
|
|
1973
|
+
time: 0,
|
|
1974
|
+
});
|
|
1975
|
+
const finger = {id: 7, x: 10, y: 10};
|
|
1976
|
+
touch('touchstart', box, [finger], [finger], 0);
|
|
1977
|
+
expect(press.numberOfTouches).toBe(1);
|
|
1978
|
+
timers.fireAll(800);
|
|
1979
|
+
expect(states).toContain('began');
|
|
1980
|
+
pointer('pointerup', box, {pointerType: 'touch', x: 10, y: 10, time: 820});
|
|
1981
|
+
touch('touchend', box, [finger], [], 820);
|
|
1982
|
+
expect(states).toContain('ended');
|
|
1983
|
+
});
|
|
1984
|
+
|
|
1985
|
+
it("cancels a claimed finger's touchmove, matching its touches to its pointer", () => {
|
|
1986
|
+
const {box} = mount('<div id="box"></div>');
|
|
1987
|
+
const pointerHost = new GestureHost(document, {
|
|
1988
|
+
clock,
|
|
1989
|
+
inputMode: 'pointer-events',
|
|
1990
|
+
isPrimaryPointerCoarse: () => true,
|
|
1991
|
+
});
|
|
1992
|
+
const pan = new PanGestureRecognizer({direction: 'horizontal'});
|
|
1993
|
+
detachers.push(pointerHost.attach(box, pan));
|
|
1994
|
+
pointer('pointerdown', box, {
|
|
1995
|
+
pointerType: 'touch',
|
|
1996
|
+
x: 10,
|
|
1997
|
+
y: 10,
|
|
1998
|
+
buttons: 1,
|
|
1999
|
+
time: 0,
|
|
2000
|
+
});
|
|
2001
|
+
const finger = {id: 7, x: 10, y: 10};
|
|
2002
|
+
touch('touchstart', box, [finger], [finger], 0);
|
|
2003
|
+
// Before the pan claims, the browser keeps the move.
|
|
2004
|
+
const early = {...finger, x: 14};
|
|
2005
|
+
pointer('pointermove', box, {
|
|
2006
|
+
pointerType: 'touch',
|
|
2007
|
+
x: 14,
|
|
2008
|
+
y: 10,
|
|
2009
|
+
buttons: 1,
|
|
2010
|
+
time: 8,
|
|
2011
|
+
});
|
|
2012
|
+
expect(touch('touchmove', box, [early], [early], 8).defaultPrevented).toBe(
|
|
2013
|
+
false,
|
|
2014
|
+
);
|
|
2015
|
+
// Past its hysteresis the pan owns the finger: its touchmoves are cancelled.
|
|
2016
|
+
const claimed = {...finger, x: 40};
|
|
2017
|
+
pointer('pointermove', box, {
|
|
2018
|
+
pointerType: 'touch',
|
|
2019
|
+
x: 40,
|
|
2020
|
+
y: 10,
|
|
2021
|
+
buttons: 1,
|
|
2022
|
+
time: 32,
|
|
2023
|
+
});
|
|
2024
|
+
expect(pan.state).toBe('began');
|
|
2025
|
+
expect(
|
|
2026
|
+
touch('touchmove', box, [claimed], [claimed], 32).defaultPrevented,
|
|
2027
|
+
).toBe(true);
|
|
2028
|
+
});
|
|
2029
|
+
|
|
2030
|
+
it("cancels a claimed finger's touchend, and leaves an unclaimed one's alone", () => {
|
|
2031
|
+
const {box, other} = mount('<div id="box"></div><div id="other"></div>');
|
|
2032
|
+
const pointerHost = new GestureHost(document, {
|
|
2033
|
+
clock,
|
|
2034
|
+
inputMode: 'pointer-events',
|
|
2035
|
+
isPrimaryPointerCoarse: () => true,
|
|
2036
|
+
});
|
|
2037
|
+
const pan = new PanGestureRecognizer({direction: 'horizontal'});
|
|
2038
|
+
detachers.push(
|
|
2039
|
+
pointerHost.attach(box, pan),
|
|
2040
|
+
pointerHost.attach(other, new TapGestureRecognizer()),
|
|
2041
|
+
);
|
|
2042
|
+
// A pan that claims, then lifts: its touchend is cancelled (FR21).
|
|
2043
|
+
pointer('pointerdown', box, {
|
|
2044
|
+
pointerType: 'touch',
|
|
2045
|
+
x: 10,
|
|
2046
|
+
y: 10,
|
|
2047
|
+
buttons: 1,
|
|
2048
|
+
time: 0,
|
|
2049
|
+
});
|
|
2050
|
+
const finger = {id: 7, x: 10, y: 10};
|
|
2051
|
+
touch('touchstart', box, [finger], [finger], 0);
|
|
2052
|
+
pointer('pointermove', box, {
|
|
2053
|
+
pointerType: 'touch',
|
|
2054
|
+
x: 40,
|
|
2055
|
+
y: 10,
|
|
2056
|
+
buttons: 1,
|
|
2057
|
+
time: 32,
|
|
2058
|
+
});
|
|
2059
|
+
expect(pan.state).toBe('began');
|
|
2060
|
+
pointer('pointerup', box, {pointerType: 'touch', x: 40, y: 10, time: 48});
|
|
2061
|
+
const lifted = {...finger, x: 40};
|
|
2062
|
+
expect(touch('touchend', box, [lifted], [], 48).defaultPrevented).toBe(
|
|
2063
|
+
true,
|
|
2064
|
+
);
|
|
2065
|
+
// A tap nobody claimed before its lift keeps its touchend.
|
|
2066
|
+
pointer('pointerdown', other, {
|
|
2067
|
+
id: 2,
|
|
2068
|
+
pointerType: 'touch',
|
|
2069
|
+
x: 10,
|
|
2070
|
+
y: 80,
|
|
2071
|
+
buttons: 1,
|
|
2072
|
+
time: 400,
|
|
2073
|
+
});
|
|
2074
|
+
const second = {id: 8, x: 10, y: 80};
|
|
2075
|
+
touch('touchstart', other, [second], [second], 400);
|
|
2076
|
+
pointer('pointerup', other, {
|
|
2077
|
+
id: 2,
|
|
2078
|
+
pointerType: 'touch',
|
|
2079
|
+
x: 10,
|
|
2080
|
+
y: 80,
|
|
2081
|
+
time: 460,
|
|
2082
|
+
});
|
|
2083
|
+
expect(touch('touchend', other, [second], [], 460).defaultPrevented).toBe(
|
|
2084
|
+
false,
|
|
2085
|
+
);
|
|
2086
|
+
});
|
|
2087
|
+
});
|