@astryxdesign/core 0.7.5 → 0.7.6-canary.e482998
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/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 +4 -1
- package/dist/DropdownMenu/MenuBottomSheetActionList.d.ts.map +1 -1
- package/dist/DropdownMenu/MenuBottomSheetActionList.js +6 -5
- package/dist/DropdownMenu/lazyMenuBottomSheet.d.ts +10 -1
- package/dist/DropdownMenu/lazyMenuBottomSheet.d.ts.map +1 -1
- package/dist/DropdownMenu/lazyMenuBottomSheet.js +16 -2
- 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/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/MultiSelector.d.ts +1 -1
- package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
- package/dist/MultiSelector/MultiSelector.js +5 -2
- 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/Slider/Slider.d.ts.map +1 -1
- package/dist/Slider/Slider.js +7 -1
- package/dist/Spinner/Spinner.js +2 -2
- 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/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 +15 -1
- package/dist/gestures/PinchGestureRecognizer.d.ts +30 -0
- package/dist/gestures/PinchGestureRecognizer.d.ts.map +1 -0
- package/dist/gestures/PinchGestureRecognizer.js +125 -0
- package/dist/gestures/ScreenEdgePanGestureRecognizer.d.ts +70 -0
- package/dist/gestures/ScreenEdgePanGestureRecognizer.d.ts.map +1 -0
- package/dist/gestures/ScreenEdgePanGestureRecognizer.js +220 -0
- package/dist/gestures/SwipeGestureRecognizer.d.ts +36 -0
- package/dist/gestures/SwipeGestureRecognizer.d.ts.map +1 -0
- package/dist/gestures/SwipeGestureRecognizer.js +145 -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/types.d.ts +11 -7
- package/dist/gestures/types.d.ts.map +1 -1
- package/dist/gestures/types.js +22 -2
- package/dist/hooks/useAdaptivePresentation.d.ts.map +1 -1
- package/dist/hooks/useAdaptivePresentation.js +4 -1
- 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 +3 -0
- package/dist/i18n/generated-locales/pseudo.generated.d.ts.map +1 -1
- package/dist/i18n/generated-locales/pseudo.generated.js +3 -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.js +1 -1
- 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/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/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/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 +15 -3
- package/locales/pseudo.json +9 -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/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 +7 -1
- 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 +53 -0
- package/src/DropdownMenu/lazyMenuBottomSheet.ts +23 -6
- 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/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/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/MultiSelector.spec.md +7 -4
- package/src/MultiSelector/MultiSelector.test.tsx +118 -1
- package/src/MultiSelector/MultiSelector.tsx +6 -2
- 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/Slider/Slider.test.tsx +12 -0
- package/src/Slider/Slider.tsx +4 -1
- package/src/Slider/__tests__/Slider.a11y.chromium.spec.ts +19 -7
- package/src/Spinner/Spinner.tsx +2 -2
- 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/Typeahead/Typeahead.test.tsx +40 -0
- package/src/Typeahead/Typeahead.tsx +12 -1
- package/src/gestures/PinchGestureRecognizer.ts +144 -0
- package/src/gestures/ScreenEdgePanGestureRecognizer.ts +252 -0
- package/src/gestures/SwipeGestureRecognizer.ts +193 -0
- package/src/gestures/VelocityTracker.ts +10 -3
- 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__/arenaPinchSwipeEdge.test.ts +188 -0
- package/src/gestures/__tests__/harness.ts +21 -2
- package/src/gestures/types.ts +23 -9
- package/src/hooks/useAdaptivePresentation.ts +4 -1
- package/src/hooks/useMenuHover.ts +15 -1
- package/src/i18n/generated-locales/en.generated.ts +4 -0
- package/src/i18n/generated-locales/pseudo.generated.ts +4 -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/defineTheme.ts +1 -1
- 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/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/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/pressPaintPriority.test.ts +80 -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
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
* rest, the rail ends icons-only or with its labels, a second press mid-motion
|
|
13
13
|
* stays continuous, an expand whose old wrap is not copied keeps the edges
|
|
14
14
|
* together, and Reduce Motion cross-fades under a finger (DR2) and changes at
|
|
15
|
-
* once under a mouse (DR3)
|
|
15
|
+
* once under a mouse (DR3), also on a loaded machine: those two cases read
|
|
16
|
+
* the motion by its own start and end, never at a fixed time
|
|
16
17
|
* @position Testing; the components' unit cases are
|
|
17
18
|
* SideNav/SideNavRailMotion.test.tsx
|
|
18
19
|
*
|
|
@@ -216,30 +217,145 @@ function toggleButton(page: Page) {
|
|
|
216
217
|
return page.getByRole('button', {name: /^(Collapse|Expand) sidebar$/});
|
|
217
218
|
}
|
|
218
219
|
|
|
219
|
-
/**
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
220
|
+
/**
|
|
221
|
+
* A loaded machine, made repeatable: Chromium's CPU throttle slows the page's
|
|
222
|
+
* main thread four times, or its animation clock runs every animation at a
|
|
223
|
+
* quarter of its speed, as when frames come late. Neither changes what the
|
|
224
|
+
* motion is; both move when it starts and ends in real time.
|
|
225
|
+
*/
|
|
226
|
+
const LOADS = ['idle', 'a slow CPU', 'slow frames'] as const;
|
|
227
|
+
type Load = (typeof LOADS)[number];
|
|
228
|
+
|
|
229
|
+
async function applyLoad(page: Page, load: Load): Promise<void> {
|
|
230
|
+
if (load === 'idle') {
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
const cdp = await page.context().newCDPSession(page);
|
|
234
|
+
if (load === 'a slow CPU') {
|
|
235
|
+
await cdp.send('Emulation.setCPUThrottlingRate', {rate: 4});
|
|
236
|
+
} else {
|
|
237
|
+
await cdp.send('Animation.enable');
|
|
238
|
+
await cdp.send('Animation.setPlaybackRate', {playbackRate: 0.25});
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** The test-name suffix for a load: nothing when idle. */
|
|
243
|
+
function underLoad(load: Load): string {
|
|
244
|
+
return load === 'idle' ? '' : `, with ${load}`;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** One animation of the rail's motion: what it animates and when it ends. */
|
|
248
|
+
interface MotionPart {
|
|
249
|
+
readonly properties: ReadonlyArray<string>;
|
|
250
|
+
/**
|
|
251
|
+
* When it ends on its own clock, in ms: its delay, every iteration and its
|
|
252
|
+
* end delay, so a delayed or repeated fade counts in full.
|
|
253
|
+
*/
|
|
254
|
+
readonly endTime: number;
|
|
255
|
+
/** Whether it animates an element holding the page's main content. */
|
|
256
|
+
readonly holdsPage: boolean;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** The record `recordMotion` keeps on the page, for `settledMotion`. */
|
|
260
|
+
interface MotionRecord {
|
|
261
|
+
readonly seen: Map<Animation, MotionPart>;
|
|
262
|
+
readonly collect: () => void;
|
|
263
|
+
sampling: boolean;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Starts recording the motion a press of the toggle causes: every animation
|
|
268
|
+
* running on any frame from now on, described when first seen. Animations on
|
|
269
|
+
* the toggle element itself are its press feedback (its highlight's release
|
|
270
|
+
* fade runs on the button), not the rail's, so they are left out; everything
|
|
271
|
+
* inside it, its chevron included, stays in. `settledMotion` ends the record.
|
|
272
|
+
*/
|
|
273
|
+
async function recordMotion(page: Page): Promise<void> {
|
|
274
|
+
await toggleButton(page).evaluate(toggle => {
|
|
275
|
+
const seen = new Map<Animation, MotionPart>();
|
|
276
|
+
const collect = () => {
|
|
277
|
+
for (const animation of document.getAnimations()) {
|
|
278
|
+
const effect = animation.effect;
|
|
279
|
+
if (
|
|
280
|
+
seen.has(animation) ||
|
|
281
|
+
!(effect instanceof KeyframeEffect) ||
|
|
282
|
+
effect.target === toggle
|
|
283
|
+
) {
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
const properties = new Set<string>();
|
|
287
|
+
for (const frame of effect.getKeyframes()) {
|
|
288
|
+
for (const key of Object.keys(frame)) {
|
|
289
|
+
if (
|
|
290
|
+
!['offset', 'computedOffset', 'easing', 'composite'].includes(key)
|
|
291
|
+
) {
|
|
292
|
+
properties.add(key);
|
|
293
|
+
}
|
|
231
294
|
}
|
|
232
295
|
}
|
|
296
|
+
seen.set(animation, {
|
|
297
|
+
properties: [...properties],
|
|
298
|
+
endTime: Number(effect.getComputedTiming().endTime),
|
|
299
|
+
holdsPage:
|
|
300
|
+
effect.target instanceof Element &&
|
|
301
|
+
effect.target.contains(document.querySelector('[role="main"]')),
|
|
302
|
+
});
|
|
233
303
|
}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
304
|
+
};
|
|
305
|
+
const record: MotionRecord = {seen, collect, sampling: true};
|
|
306
|
+
(window as unknown as {railMotion: MotionRecord}).railMotion = record;
|
|
307
|
+
const sample = () => {
|
|
308
|
+
if (record.sampling) {
|
|
309
|
+
collect();
|
|
310
|
+
requestAnimationFrame(sample);
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
requestAnimationFrame(sample);
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Ends the record once the motion has ended on its own clock: every recorded
|
|
319
|
+
* animation has finished or been cancelled, and two more frames have passed
|
|
320
|
+
* without a new one, so what its end commits has landed. One that never ends
|
|
321
|
+
* (endless iterations) is not waited for; it is returned with an `endTime`
|
|
322
|
+
* of Infinity, for the cases to fail on. It never waits a fixed time: a loaded
|
|
323
|
+
* machine changes how long this takes, not what it reads.
|
|
324
|
+
*/
|
|
325
|
+
async function settledMotion(page: Page): Promise<ReadonlyArray<MotionPart>> {
|
|
326
|
+
return page.evaluate(async () => {
|
|
327
|
+
const record = (window as unknown as {railMotion: MotionRecord}).railMotion;
|
|
328
|
+
const isRunning = (animation: Animation) =>
|
|
329
|
+
animation.playState === 'running' &&
|
|
330
|
+
Number.isFinite(Number(animation.effect?.getComputedTiming().endTime));
|
|
331
|
+
const twoFrames = async () =>
|
|
332
|
+
new Promise<void>(resolve => {
|
|
333
|
+
requestAnimationFrame(() => requestAnimationFrame(() => resolve()));
|
|
334
|
+
});
|
|
335
|
+
for (;;) {
|
|
336
|
+
record.collect();
|
|
337
|
+
const running = [...record.seen.keys()].filter(isRunning);
|
|
338
|
+
if (running.length > 0) {
|
|
339
|
+
await Promise.all(
|
|
340
|
+
running.map(async animation =>
|
|
341
|
+
animation.finished.catch(() => undefined),
|
|
342
|
+
),
|
|
343
|
+
);
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
const seenBefore = record.seen.size;
|
|
347
|
+
await twoFrames();
|
|
348
|
+
record.collect();
|
|
349
|
+
if (
|
|
350
|
+
record.seen.size === seenBefore &&
|
|
351
|
+
![...record.seen.keys()].some(isRunning)
|
|
352
|
+
) {
|
|
353
|
+
break;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
record.sampling = false;
|
|
357
|
+
return [...record.seen.values()];
|
|
358
|
+
});
|
|
243
359
|
}
|
|
244
360
|
|
|
245
361
|
test.describe('component:SideNav FR6 and component:AppShell FR17 under a finger', () => {
|
|
@@ -341,25 +457,29 @@ test.describe('component:SideNav FR6 and component:AppShell FR17 under a finger'
|
|
|
341
457
|
});
|
|
342
458
|
});
|
|
343
459
|
|
|
344
|
-
|
|
345
|
-
page
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
expect(
|
|
354
|
-
|
|
355
|
-
expect(
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
460
|
+
for (const load of LOADS) {
|
|
461
|
+
test(`under Reduce Motion a tap only cross-fades, in 200 ms or less, with the page surface still${underLoad(load)} (design:reduced-motion DR2)`, async ({
|
|
462
|
+
page,
|
|
463
|
+
}) => {
|
|
464
|
+
await page.emulateMedia({reducedMotion: 'reduce'});
|
|
465
|
+
await openStory(page, PHONE_STORY);
|
|
466
|
+
await applyLoad(page, load);
|
|
467
|
+
await recordMotion(page);
|
|
468
|
+
await toggleButton(page).tap();
|
|
469
|
+
await expect(toggleButton(page)).toHaveAccessibleName('Expand sidebar');
|
|
470
|
+
const motion = await settledMotion(page);
|
|
471
|
+
expect(motion.length).toBeGreaterThan(0);
|
|
472
|
+
for (const {properties, endTime, holdsPage} of motion) {
|
|
473
|
+
expect(properties).toEqual(['opacity']);
|
|
474
|
+
expect(endTime).toBeLessThanOrEqual(200);
|
|
475
|
+
expect(holdsPage).toBe(false);
|
|
476
|
+
}
|
|
477
|
+
expect(await railAtRest(page)).toMatchObject({
|
|
478
|
+
labelsShown: false,
|
|
479
|
+
copies: 0,
|
|
480
|
+
});
|
|
361
481
|
});
|
|
362
|
-
}
|
|
482
|
+
}
|
|
363
483
|
});
|
|
364
484
|
|
|
365
485
|
test.describe('component:SideNav FR6 and component:AppShell FR17 under a mouse', () => {
|
|
@@ -402,16 +522,21 @@ test.describe('component:SideNav FR6 and component:AppShell FR17 under a mouse',
|
|
|
402
522
|
});
|
|
403
523
|
});
|
|
404
524
|
|
|
405
|
-
|
|
406
|
-
page
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
525
|
+
for (const load of LOADS) {
|
|
526
|
+
test(`under Reduce Motion a click changes the rail and the page at once${underLoad(load)} (design:reduced-motion DR3)`, async ({
|
|
527
|
+
page,
|
|
528
|
+
}) => {
|
|
529
|
+
await page.emulateMedia({reducedMotion: 'reduce'});
|
|
530
|
+
await openStory(page, DESKTOP_STORY);
|
|
531
|
+
await applyLoad(page, load);
|
|
532
|
+
await recordMotion(page);
|
|
533
|
+
await toggleButton(page).click();
|
|
534
|
+
await expect(toggleButton(page)).toHaveAccessibleName('Expand sidebar');
|
|
535
|
+
expect(await settledMotion(page)).toEqual([]);
|
|
536
|
+
expect(await railAtRest(page)).toMatchObject({
|
|
537
|
+
labelsShown: false,
|
|
538
|
+
copies: 0,
|
|
539
|
+
});
|
|
415
540
|
});
|
|
416
|
-
}
|
|
541
|
+
}
|
|
417
542
|
});
|
|
@@ -337,6 +337,18 @@ describe('Slider', () => {
|
|
|
337
337
|
expect(slider).not.toHaveAttribute('aria-required');
|
|
338
338
|
});
|
|
339
339
|
|
|
340
|
+
it('conveys required state under FormLayout defaultOptionality="required" unless isOptional (FormLayout FR5)', () => {
|
|
341
|
+
render(
|
|
342
|
+
<FormLayout defaultOptionality="required">
|
|
343
|
+
<Slider label="Volume" value={50} />
|
|
344
|
+
<Slider label="Balance" value={50} isOptional />
|
|
345
|
+
</FormLayout>,
|
|
346
|
+
);
|
|
347
|
+
const [volume, balance] = screen.getAllByRole('slider');
|
|
348
|
+
expect(volume).toHaveAccessibleDescription(/Required/);
|
|
349
|
+
expect(balance).not.toHaveAccessibleDescription(/Required/);
|
|
350
|
+
});
|
|
351
|
+
|
|
340
352
|
it('conveys required state on both thumbs of a range slider', () => {
|
|
341
353
|
render(
|
|
342
354
|
<Slider
|
package/src/Slider/Slider.tsx
CHANGED
|
@@ -58,6 +58,7 @@ import type {SizeValue} from '../utils/types';
|
|
|
58
58
|
import {themeProps} from '../utils/themeProps';
|
|
59
59
|
|
|
60
60
|
import {useMergedRefs} from '../hooks/useMergedRefs';
|
|
61
|
+
import {useResolvedRequired} from '../hooks/useResolvedRequired';
|
|
61
62
|
// =============================================================================
|
|
62
63
|
// Types
|
|
63
64
|
// =============================================================================
|
|
@@ -591,7 +592,9 @@ export function Slider({ref, ...props}: SliderProps) {
|
|
|
591
592
|
// role="slider" in WAI-ARIA 1.2, so the thumb instead points its
|
|
592
593
|
// aria-describedby at a visually hidden "Required" span — mirroring the
|
|
593
594
|
// Field label's visible indicator (where isOptional takes precedence).
|
|
594
|
-
|
|
595
|
+
// A surrounding FormLayout's defaultOptionality="required" makes an unmarked
|
|
596
|
+
// field required too (FormLayout FR5).
|
|
597
|
+
const conveysRequired = useResolvedRequired({isRequired, isOptional});
|
|
595
598
|
|
|
596
599
|
// Build aria-describedby
|
|
597
600
|
const describedByParts: string[] = [];
|
|
@@ -87,16 +87,28 @@ async function runState(
|
|
|
87
87
|
});
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
90
|
+
// Checking coverage runs every state again, which on a loaded runner nears
|
|
91
|
+
// the per-test timeout as one test. It runs as one serial part per state,
|
|
92
|
+
// sharing their results, then one check over all of them.
|
|
93
|
+
test.describe('every slider expectation is exercised by a Slider state', () => {
|
|
94
|
+
test.describe.configure({mode: 'serial'});
|
|
94
95
|
const results: BindingResult[] = [];
|
|
96
|
+
|
|
95
97
|
for (const state of SLIDER_BINDING_STATES) {
|
|
96
|
-
|
|
98
|
+
test(`runs ${state.id}`, async ({page}) => {
|
|
99
|
+
const cdp = await page.context().newCDPSession(page);
|
|
100
|
+
results.push(await runState(page, cdp, state));
|
|
101
|
+
});
|
|
97
102
|
}
|
|
98
|
-
|
|
99
|
-
|
|
103
|
+
|
|
104
|
+
test('no expectation is left unexercised', () => {
|
|
105
|
+
// Every part ran in this worker; this test alone proves nothing.
|
|
106
|
+
expect(results.map(result => result.state).sort()).toEqual(
|
|
107
|
+
SLIDER_BINDING_STATES.map(state => state.id).sort(),
|
|
108
|
+
);
|
|
109
|
+
expect(neverExercised(results)).toEqual([]);
|
|
110
|
+
expect(unmatchedKnownFailures(SLIDER_KNOWN_FAILURES, results)).toEqual([]);
|
|
111
|
+
});
|
|
100
112
|
});
|
|
101
113
|
|
|
102
114
|
for (const state of SLIDER_BINDING_STATES) {
|
package/src/Spinner/Spinner.tsx
CHANGED
|
@@ -21,8 +21,8 @@ import * as stylex from '@stylexjs/stylex';
|
|
|
21
21
|
import {colorVars, durationVars, spacingVars} from '../theme/tokens.stylex';
|
|
22
22
|
import type {BaseProps} from '../BaseProps';
|
|
23
23
|
import {Text} from '../Text/Text';
|
|
24
|
-
import {mergeProps} from '../utils';
|
|
25
|
-
import {useTranslator} from '../i18n';
|
|
24
|
+
import {mergeProps} from '../utils/mergeProps';
|
|
25
|
+
import {useTranslator} from '../i18n/useTranslator';
|
|
26
26
|
import {themeProps} from '../utils/themeProps';
|
|
27
27
|
|
|
28
28
|
// =============================================================================
|
package/src/Stack/Stack.tsx
CHANGED
|
@@ -30,7 +30,7 @@ import {
|
|
|
30
30
|
paddingBlockStartStyles,
|
|
31
31
|
paddingBlockEndStyles,
|
|
32
32
|
} from '../Layout/padding.stylex';
|
|
33
|
-
import {mergeProps} from '../utils';
|
|
33
|
+
import {mergeProps} from '../utils/mergeProps';
|
|
34
34
|
import {themeProps} from '../utils/themeProps';
|
|
35
35
|
|
|
36
36
|
const overflowStyles = stylex.create({
|
package/src/Stack/StackItem.tsx
CHANGED
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
type StackItemCrossAlignSelf,
|
|
20
20
|
type StackItemSize,
|
|
21
21
|
} from './stackItem.stylex';
|
|
22
|
-
import {mergeProps} from '../utils';
|
|
22
|
+
import {mergeProps} from '../utils/mergeProps';
|
|
23
23
|
import {themeProps} from '../utils/themeProps';
|
|
24
24
|
|
|
25
25
|
const overflowStyles = stylex.create({
|
|
@@ -9,8 +9,9 @@
|
|
|
9
9
|
* SYNC: When TabList components change, update tests to match new behavior
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
import {useState} from 'react';
|
|
12
13
|
import {describe, it, expect, vi, beforeAll, afterAll} from 'vitest';
|
|
13
|
-
import {render, screen, fireEvent} from '@testing-library/react';
|
|
14
|
+
import {act, render, screen, fireEvent, waitFor} from '@testing-library/react';
|
|
14
15
|
import {__resetMenuPressForTest} from '../hooks/useMenuPress';
|
|
15
16
|
import userEvent from '@testing-library/user-event';
|
|
16
17
|
import {
|
|
@@ -933,8 +934,10 @@ describe('TabMenu keyboard navigation (roving tabindex)', () => {
|
|
|
933
934
|
hidden: true,
|
|
934
935
|
});
|
|
935
936
|
|
|
936
|
-
|
|
937
|
-
|
|
937
|
+
// Opening moves focus to the first item a frame later (APG menu-button
|
|
938
|
+
// focus-on-open): wait for it, since a key pressed before that frame
|
|
939
|
+
// starts from no item and one pressed after it starts from the first.
|
|
940
|
+
await waitFor(() => expect(analytics).toHaveFocus());
|
|
938
941
|
|
|
939
942
|
fireEvent.keyDown(menu, {key: 'ArrowDown'});
|
|
940
943
|
expect(reports).toHaveFocus();
|
|
@@ -963,7 +966,8 @@ describe('TabMenu keyboard navigation (roving tabindex)', () => {
|
|
|
963
966
|
hidden: true,
|
|
964
967
|
});
|
|
965
968
|
|
|
966
|
-
|
|
969
|
+
// Focus lands on the first item a frame after opening (see above).
|
|
970
|
+
await waitFor(() => expect(analytics).toHaveFocus());
|
|
967
971
|
fireEvent.keyDown(menu, {key: 'ArrowDown'}); // focus reports
|
|
968
972
|
|
|
969
973
|
// The tab stop follows focus, so it is still a single stop after moving.
|
|
@@ -1237,6 +1241,56 @@ describe('TabList overflow (scroll)', () => {
|
|
|
1237
1241
|
expect(scrollBy).not.toHaveBeenCalled();
|
|
1238
1242
|
});
|
|
1239
1243
|
|
|
1244
|
+
it('measures a pressed tab after the press, in the next frame, and still shows it whole', async () => {
|
|
1245
|
+
function Controlled() {
|
|
1246
|
+
const [value, setValue] = useState('a');
|
|
1247
|
+
return (
|
|
1248
|
+
<TabList value={value} onChange={setValue}>
|
|
1249
|
+
<Tab value="a" label="Alpha" />
|
|
1250
|
+
<Tab value="b" label="Beta" />
|
|
1251
|
+
<Tab value="c" label="Gamma" />
|
|
1252
|
+
</TabList>
|
|
1253
|
+
);
|
|
1254
|
+
}
|
|
1255
|
+
const {container} = render(<Controlled />);
|
|
1256
|
+
const strip = container.querySelector<HTMLElement>(STRIP);
|
|
1257
|
+
const tab = container.querySelector<HTMLElement>('[data-tab-value="c"]');
|
|
1258
|
+
if (!strip || !tab) {
|
|
1259
|
+
throw new Error('no tab strip');
|
|
1260
|
+
}
|
|
1261
|
+
const scrollBy = vi.fn();
|
|
1262
|
+
strip.scrollBy = scrollBy;
|
|
1263
|
+
const stripRect = vi.fn(
|
|
1264
|
+
() => ({left: 0, right: 300, width: 300}) as DOMRect,
|
|
1265
|
+
);
|
|
1266
|
+
strip.getBoundingClientRect = stripRect;
|
|
1267
|
+
tab.getBoundingClientRect = () =>
|
|
1268
|
+
({left: 240, right: 320, width: 80}) as DOMRect;
|
|
1269
|
+
|
|
1270
|
+
// Pressing a tab focuses it first, and focusing it reveals it.
|
|
1271
|
+
fireEvent.focus(tab);
|
|
1272
|
+
expect(scrollBy).toHaveBeenCalledTimes(1);
|
|
1273
|
+
expect(scrollBy.mock.calls[0][0].left).toBe(20);
|
|
1274
|
+
const readsBeforePress = stripRect.mock.calls.length;
|
|
1275
|
+
|
|
1276
|
+
// The selection itself reads no geometry inside the press...
|
|
1277
|
+
fireEvent.click(tab);
|
|
1278
|
+
expect(tab).toHaveAttribute('aria-current', 'true');
|
|
1279
|
+
expect(stripRect.mock.calls.length).toBe(readsBeforePress);
|
|
1280
|
+
expect(scrollBy).toHaveBeenCalledTimes(1);
|
|
1281
|
+
|
|
1282
|
+
// ...and the next frame measures the selected tab, now wider, so it still
|
|
1283
|
+
// ends fully inside the strip.
|
|
1284
|
+
tab.getBoundingClientRect = () =>
|
|
1285
|
+
({left: 220, right: 306, width: 86}) as DOMRect;
|
|
1286
|
+
await act(
|
|
1287
|
+
async () =>
|
|
1288
|
+
new Promise<void>(resolve => requestAnimationFrame(() => resolve())),
|
|
1289
|
+
);
|
|
1290
|
+
expect(scrollBy).toHaveBeenCalledTimes(2);
|
|
1291
|
+
expect(scrollBy.mock.calls[1][0].left).toBe(6);
|
|
1292
|
+
});
|
|
1293
|
+
|
|
1240
1294
|
it('reveals on a selection change, not on an unrelated overflow change', () => {
|
|
1241
1295
|
const {strip, rerender} = renderStrip({value: 'c', overflow: 'visible'});
|
|
1242
1296
|
const scrollBy = vi.fn();
|
package/src/TabList/TabList.tsx
CHANGED
|
@@ -515,6 +515,10 @@ export function TabList({
|
|
|
515
515
|
revealSelectedTabRef.current = revealSelectedTab;
|
|
516
516
|
}, [revealSelectedTab]);
|
|
517
517
|
|
|
518
|
+
// The tab whose focus revealed it (see `handleRootFocus`), while it keeps
|
|
519
|
+
// focus.
|
|
520
|
+
const focusRevealedValueRef = useRef<string | null>(null);
|
|
521
|
+
|
|
518
522
|
// The tab you are on has to be visible. Selection can move without focus —
|
|
519
523
|
// on mount, or when the host sets `value` itself — and neither scrolls the
|
|
520
524
|
// strip the way clicking or arrowing to a tab does. The check is what makes
|
|
@@ -525,6 +529,14 @@ export function TabList({
|
|
|
525
529
|
return;
|
|
526
530
|
}
|
|
527
531
|
revealedValueRef.current = value;
|
|
532
|
+
if (value != null && focusRevealedValueRef.current === value) {
|
|
533
|
+
// A press or an arrow focused this tab first, and focusing it revealed
|
|
534
|
+
// it. Measuring now would force the new selection's style and layout
|
|
535
|
+
// inside the press, before it can paint. The next frame lays them out
|
|
536
|
+
// anyway, so finish there: the selected weight can still widen the tab.
|
|
537
|
+
const frame = requestAnimationFrame(() => revealSelectedTabRef.current());
|
|
538
|
+
return () => cancelAnimationFrame(frame);
|
|
539
|
+
}
|
|
528
540
|
revealSelectedTabRef.current();
|
|
529
541
|
}, [value]);
|
|
530
542
|
|
|
@@ -595,7 +607,9 @@ export function TabList({
|
|
|
595
607
|
// The browser scrolls a focused element into view only when it is
|
|
596
608
|
// entirely outside the scrollport, so arrowing onto a half-visible stop
|
|
597
609
|
// leaves it cut off under the fade. Finish the job it started.
|
|
598
|
-
|
|
610
|
+
const stop = e.target.closest<HTMLElement>(TAB_STOP_SELECTOR);
|
|
611
|
+
revealStop(stop);
|
|
612
|
+
focusRevealedValueRef.current = stop?.dataset.tabValue ?? null;
|
|
599
613
|
},
|
|
600
614
|
[onFocusProp, onHintFocus, handleFocus, revealStop],
|
|
601
615
|
);
|
|
@@ -603,6 +617,7 @@ export function TabList({
|
|
|
603
617
|
const handleRootBlur = useCallback(
|
|
604
618
|
(e: React.FocusEvent<HTMLElement>) => {
|
|
605
619
|
onBlurProp?.(e);
|
|
620
|
+
focusRevealedValueRef.current = null;
|
|
606
621
|
if (e.defaultPrevented) {
|
|
607
622
|
return;
|
|
608
623
|
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file TabPressPointer.interaction.browser.spec.ts
|
|
5
|
+
* @input Uses @playwright/test and the interaction harness (a built
|
|
6
|
+
* Storybook)
|
|
7
|
+
* @output Proves a Tab's hover and press layer under a fine and a coarse
|
|
8
|
+
* primary pointer
|
|
9
|
+
* @position Interaction proof for packages/core/src/TabList/Tab.tsx
|
|
10
|
+
*
|
|
11
|
+
* Tab's press layer has a hover branch and a coarse-pointer rule, which
|
|
12
|
+
* StyleX makes exclusive. The outcome on every pointer: a fine pointer
|
|
13
|
+
* paints the hover, then a pressed overlay that differs from it; under a
|
|
14
|
+
* coarse pointer a finger's press paints as an image (`data-astryx-press`),
|
|
15
|
+
* and the colour layer stays transparent while the tab is `:active`.
|
|
16
|
+
* Chromium only: it holds a finger, and forces `:active`, through the
|
|
17
|
+
* DevTools protocol.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import {expect, test, type CDPSession, type Page} from '@playwright/test';
|
|
21
|
+
import {openStory, withStorybook} from '@astryxdesign/interaction-harness';
|
|
22
|
+
|
|
23
|
+
const storybook = withStorybook(test);
|
|
24
|
+
|
|
25
|
+
const TRANSPARENT = 'rgba(0, 0, 0, 0)';
|
|
26
|
+
|
|
27
|
+
/** The "Projects" tab: not the story's current one. */
|
|
28
|
+
function projectsTab(page: Page) {
|
|
29
|
+
return page.locator('button', {hasText: 'Projects'}).first();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The tab's colour layer (its first aria-hidden span) and the pointer. */
|
|
33
|
+
async function tabState(page: Page): Promise<{
|
|
34
|
+
colour: string;
|
|
35
|
+
image: string;
|
|
36
|
+
press: string | null;
|
|
37
|
+
coarse: boolean;
|
|
38
|
+
hover: boolean;
|
|
39
|
+
}> {
|
|
40
|
+
return projectsTab(page).evaluate(element => {
|
|
41
|
+
const layer = element.querySelector('span[aria-hidden="true"]');
|
|
42
|
+
const style = layer == null ? null : getComputedStyle(layer);
|
|
43
|
+
return {
|
|
44
|
+
colour: style?.backgroundColor ?? '',
|
|
45
|
+
image: style?.backgroundImage ?? '',
|
|
46
|
+
press: element.getAttribute('data-astryx-press'),
|
|
47
|
+
coarse: matchMedia('(pointer: coarse)').matches,
|
|
48
|
+
hover: matchMedia('(hover: hover)').matches,
|
|
49
|
+
};
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The layer's colour once its transition has landed (three equal reads). */
|
|
54
|
+
async function settledColour(page: Page): Promise<string> {
|
|
55
|
+
let last = '';
|
|
56
|
+
let same = 0;
|
|
57
|
+
for (let read = 0; read < 60 && same < 3; read++) {
|
|
58
|
+
const colour = (await tabState(page)).colour;
|
|
59
|
+
same = colour === last ? same + 1 : 1;
|
|
60
|
+
last = colour;
|
|
61
|
+
await page.waitForTimeout(50);
|
|
62
|
+
}
|
|
63
|
+
return last;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** The tab's centre, in page coordinates. */
|
|
67
|
+
async function tabCentre(page: Page): Promise<{x: number; y: number}> {
|
|
68
|
+
const box = await projectsTab(page).boundingBox();
|
|
69
|
+
if (box == null) {
|
|
70
|
+
throw new Error('the Projects tab has no box');
|
|
71
|
+
}
|
|
72
|
+
return {x: box.x + box.width / 2, y: box.y + box.height / 2};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** A finger held on the tab's centre; returns the lift. */
|
|
76
|
+
async function holdFinger(
|
|
77
|
+
page: Page,
|
|
78
|
+
cdp: CDPSession,
|
|
79
|
+
): Promise<() => Promise<void>> {
|
|
80
|
+
await cdp.send('Input.dispatchTouchEvent', {
|
|
81
|
+
type: 'touchStart',
|
|
82
|
+
touchPoints: [await tabCentre(page)],
|
|
83
|
+
});
|
|
84
|
+
return async () => {
|
|
85
|
+
await cdp.send('Input.dispatchTouchEvent', {
|
|
86
|
+
type: 'touchEnd',
|
|
87
|
+
touchPoints: [],
|
|
88
|
+
});
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Puts the tab in `:active` (or out of it) through the DevTools protocol. */
|
|
93
|
+
async function forceActive(cdp: CDPSession, isActive: boolean): Promise<void> {
|
|
94
|
+
await cdp.send('DOM.enable');
|
|
95
|
+
await cdp.send('CSS.enable');
|
|
96
|
+
const {root} = await cdp.send('DOM.getDocument', {depth: -1});
|
|
97
|
+
const {nodeIds} = await cdp.send('DOM.querySelectorAll', {
|
|
98
|
+
nodeId: root.nodeId,
|
|
99
|
+
selector: 'button',
|
|
100
|
+
});
|
|
101
|
+
for (const nodeId of nodeIds) {
|
|
102
|
+
const {outerHTML} = await cdp.send('DOM.getOuterHTML', {nodeId});
|
|
103
|
+
if (outerHTML.includes('Projects')) {
|
|
104
|
+
await cdp.send('CSS.forcePseudoState', {
|
|
105
|
+
nodeId,
|
|
106
|
+
forcedPseudoClasses: isActive ? ['active'] : [],
|
|
107
|
+
});
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
throw new Error('the Projects tab has no node');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
test.describe('Tab press layer by pointer', () => {
|
|
115
|
+
test.skip(
|
|
116
|
+
({browserName}) => browserName !== 'chromium',
|
|
117
|
+
'Chromium holds a finger and forces :active through the DevTools protocol',
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
test.describe('fine pointer with hover', () => {
|
|
121
|
+
test.use({isMobile: false, hasTouch: false});
|
|
122
|
+
test('paints the hover, then a pressed overlay that differs from it', async ({
|
|
123
|
+
page,
|
|
124
|
+
}) => {
|
|
125
|
+
await openStory(page, storybook(), 'core-tablist--default');
|
|
126
|
+
await projectsTab(page).hover();
|
|
127
|
+
const state = await tabState(page);
|
|
128
|
+
expect(state.coarse).toBe(false);
|
|
129
|
+
expect(state.hover).toBe(true);
|
|
130
|
+
await expect
|
|
131
|
+
.poll(async () => (await tabState(page)).colour)
|
|
132
|
+
.not.toBe(TRANSPARENT);
|
|
133
|
+
const hovered = await settledColour(page);
|
|
134
|
+
await page.mouse.down();
|
|
135
|
+
// The pressed overlay is its own colour, not the hover paint held on.
|
|
136
|
+
await expect
|
|
137
|
+
.poll(async () => (await tabState(page)).colour)
|
|
138
|
+
.not.toBe(hovered);
|
|
139
|
+
expect(await settledColour(page)).not.toBe(hovered);
|
|
140
|
+
await page.mouse.up();
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("coarse pointer: a finger's press paints as an image, and the colour layer stays transparent while the tab is :active", async ({
|
|
145
|
+
page,
|
|
146
|
+
}) => {
|
|
147
|
+
await openStory(page, storybook(), 'core-tablist--default');
|
|
148
|
+
const cdp = await page.context().newCDPSession(page);
|
|
149
|
+
expect((await tabState(page)).coarse).toBe(true);
|
|
150
|
+
// The finger's press: the press model marks the tab, and the layer
|
|
151
|
+
// paints the pressed overlay as an image.
|
|
152
|
+
const lift = await holdFinger(page, cdp);
|
|
153
|
+
await expect.poll(async () => (await tabState(page)).press).toBe('on');
|
|
154
|
+
expect((await tabState(page)).image).not.toBe('none');
|
|
155
|
+
await lift();
|
|
156
|
+
await expect.poll(async () => (await tabState(page)).press).not.toBe('on');
|
|
157
|
+
// The tab in :active under the coarse pointer: the reset keeps the
|
|
158
|
+
// colour layer transparent, past any transition.
|
|
159
|
+
await forceActive(cdp, true);
|
|
160
|
+
expect(await settledColour(page)).toBe(TRANSPARENT);
|
|
161
|
+
await forceActive(cdp, false);
|
|
162
|
+
});
|
|
163
|
+
});
|