@astryxdesign/core 0.7.5 → 0.7.6-canary.bb18c81
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/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 +4 -1
- package/dist/DateInput/MonthScroller.d.ts.map +1 -1
- package/dist/DateInput/MonthScroller.js +101 -15
- package/dist/DateRangeInput/DateRangeInput.d.ts.map +1 -1
- package/dist/DateRangeInput/DateRangeInput.js +1 -1
- package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
- package/dist/DateTimeInput/DateTimeInput.js +4 -1
- 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.map +1 -1
- package/dist/Drawer/Drawer.js +5 -0
- 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/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/Timestamp/Timestamp.d.ts.map +1 -1
- package/dist/Timestamp/Timestamp.js +2 -2
- package/dist/Timestamp/TimestampHoverCard.js +4 -4
- 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 +14 -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/presentation.d.ts +50 -0
- package/dist/utils/presentation.d.ts.map +1 -0
- package/dist/utils/presentation.js +125 -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/locales/en.json +13 -1
- 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/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/ContextMenu/ContextMenu.test.tsx +35 -0
- package/src/DateInput/DateInput.test.tsx +53 -2
- package/src/DateInput/DateInput.tsx +15 -11
- package/src/DateInput/MonthScroller.tsx +106 -20
- package/src/DateInput/MonthScrollerSettle.test.tsx +165 -0
- 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.spec.md +71 -48
- package/src/DateTimeInput/DateTimeInput.test.tsx +61 -0
- package/src/DateTimeInput/DateTimeInput.tsx +15 -11
- 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 +22 -0
- package/src/Drawer/Drawer.tsx +5 -0
- package/src/Drawer/DrawerHeader.spec.md +181 -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.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/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/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 +52 -1
- 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/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 +7 -10
- package/src/Toast/ToastViewport.test.tsx +575 -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/presentation.test.ts +380 -0
- package/src/utils/presentation.ts +180 -0
- package/src/utils/pressPaintPriority.test.ts +80 -0
- package/src/utils/touchPresentation.stylex.ts +43 -0
- package/src/utils/touchPresentation.test.ts +140 -1
- package/src/utils/touchPresentation.ts +5 -3
|
@@ -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} from '@testing-library/react';
|
|
14
15
|
import {__resetMenuPressForTest} from '../hooks/useMenuPress';
|
|
15
16
|
import userEvent from '@testing-library/user-event';
|
|
16
17
|
import {
|
|
@@ -1237,6 +1238,56 @@ describe('TabList overflow (scroll)', () => {
|
|
|
1237
1238
|
expect(scrollBy).not.toHaveBeenCalled();
|
|
1238
1239
|
});
|
|
1239
1240
|
|
|
1241
|
+
it('measures a pressed tab after the press, in the next frame, and still shows it whole', async () => {
|
|
1242
|
+
function Controlled() {
|
|
1243
|
+
const [value, setValue] = useState('a');
|
|
1244
|
+
return (
|
|
1245
|
+
<TabList value={value} onChange={setValue}>
|
|
1246
|
+
<Tab value="a" label="Alpha" />
|
|
1247
|
+
<Tab value="b" label="Beta" />
|
|
1248
|
+
<Tab value="c" label="Gamma" />
|
|
1249
|
+
</TabList>
|
|
1250
|
+
);
|
|
1251
|
+
}
|
|
1252
|
+
const {container} = render(<Controlled />);
|
|
1253
|
+
const strip = container.querySelector<HTMLElement>(STRIP);
|
|
1254
|
+
const tab = container.querySelector<HTMLElement>('[data-tab-value="c"]');
|
|
1255
|
+
if (!strip || !tab) {
|
|
1256
|
+
throw new Error('no tab strip');
|
|
1257
|
+
}
|
|
1258
|
+
const scrollBy = vi.fn();
|
|
1259
|
+
strip.scrollBy = scrollBy;
|
|
1260
|
+
const stripRect = vi.fn(
|
|
1261
|
+
() => ({left: 0, right: 300, width: 300}) as DOMRect,
|
|
1262
|
+
);
|
|
1263
|
+
strip.getBoundingClientRect = stripRect;
|
|
1264
|
+
tab.getBoundingClientRect = () =>
|
|
1265
|
+
({left: 240, right: 320, width: 80}) as DOMRect;
|
|
1266
|
+
|
|
1267
|
+
// Pressing a tab focuses it first, and focusing it reveals it.
|
|
1268
|
+
fireEvent.focus(tab);
|
|
1269
|
+
expect(scrollBy).toHaveBeenCalledTimes(1);
|
|
1270
|
+
expect(scrollBy.mock.calls[0][0].left).toBe(20);
|
|
1271
|
+
const readsBeforePress = stripRect.mock.calls.length;
|
|
1272
|
+
|
|
1273
|
+
// The selection itself reads no geometry inside the press...
|
|
1274
|
+
fireEvent.click(tab);
|
|
1275
|
+
expect(tab).toHaveAttribute('aria-current', 'true');
|
|
1276
|
+
expect(stripRect.mock.calls.length).toBe(readsBeforePress);
|
|
1277
|
+
expect(scrollBy).toHaveBeenCalledTimes(1);
|
|
1278
|
+
|
|
1279
|
+
// ...and the next frame measures the selected tab, now wider, so it still
|
|
1280
|
+
// ends fully inside the strip.
|
|
1281
|
+
tab.getBoundingClientRect = () =>
|
|
1282
|
+
({left: 220, right: 306, width: 86}) as DOMRect;
|
|
1283
|
+
await act(
|
|
1284
|
+
async () =>
|
|
1285
|
+
new Promise<void>(resolve => requestAnimationFrame(() => resolve())),
|
|
1286
|
+
);
|
|
1287
|
+
expect(scrollBy).toHaveBeenCalledTimes(2);
|
|
1288
|
+
expect(scrollBy.mock.calls[1][0].left).toBe(6);
|
|
1289
|
+
});
|
|
1290
|
+
|
|
1240
1291
|
it('reveals on a selection change, not on an unrelated overflow change', () => {
|
|
1241
1292
|
const {strip, rerender} = renderStrip({value: 'c', overflow: 'visible'});
|
|
1242
1293
|
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
|
+
});
|
|
@@ -100,15 +100,27 @@ async function runState(
|
|
|
100
100
|
});
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
103
|
+
// Checking coverage runs every state again, which on a loaded runner nears
|
|
104
|
+
// the per-test timeout as one test. It runs as one serial part per state,
|
|
105
|
+
// sharing their results, then one check over all of them.
|
|
106
|
+
test.describe('every Table expectation is exercised by at least one Table state', () => {
|
|
107
|
+
test.describe.configure({mode: 'serial'});
|
|
107
108
|
const results: BindingResult[] = [];
|
|
109
|
+
|
|
108
110
|
for (const state of TABLE_A11Y_STATES) {
|
|
109
|
-
|
|
111
|
+
test(`runs ${state.id}`, async ({page}) => {
|
|
112
|
+
const cdp = await page.context().newCDPSession(page);
|
|
113
|
+
results.push(await runState(page, cdp, state));
|
|
114
|
+
});
|
|
110
115
|
}
|
|
111
|
-
|
|
116
|
+
|
|
117
|
+
test('no expectation is left unexercised', () => {
|
|
118
|
+
// Every part ran in this worker; this test alone proves nothing.
|
|
119
|
+
expect(results.map(result => result.state).sort()).toEqual(
|
|
120
|
+
TABLE_A11Y_STATES.map(state => state.id).sort(),
|
|
121
|
+
);
|
|
122
|
+
expect(neverExercised(results)).toEqual([]);
|
|
123
|
+
});
|
|
112
124
|
});
|
|
113
125
|
|
|
114
126
|
for (const state of TABLE_A11Y_STATES) {
|
|
@@ -574,6 +574,28 @@ describe('useTableColumnResize', () => {
|
|
|
574
574
|
expect(handle).toHaveAttribute('aria-label', 'Resize column Name');
|
|
575
575
|
});
|
|
576
576
|
|
|
577
|
+
it("names a node header's handle by the header's visible text (AR1)", () => {
|
|
578
|
+
function Glyph() {
|
|
579
|
+
return <svg aria-hidden />;
|
|
580
|
+
}
|
|
581
|
+
render(
|
|
582
|
+
<ResizeTable
|
|
583
|
+
columns={[
|
|
584
|
+
{key: 'name', header: <span>Amount (EUR)</span>},
|
|
585
|
+
{key: 'role', header: <Glyph />},
|
|
586
|
+
{key: 'id', header: 'ID'},
|
|
587
|
+
]}
|
|
588
|
+
/>,
|
|
589
|
+
);
|
|
590
|
+
const [amount, role] = getResizeHandles();
|
|
591
|
+
expect(amount).toHaveAttribute(
|
|
592
|
+
'aria-label',
|
|
593
|
+
'Resize column Amount (EUR)',
|
|
594
|
+
);
|
|
595
|
+
// A header with no text it renders through children falls back to the key.
|
|
596
|
+
expect(role).toHaveAttribute('aria-label', 'Resize column role');
|
|
597
|
+
});
|
|
598
|
+
|
|
577
599
|
it('handle has aria-valuemin', () => {
|
|
578
600
|
render(<ResizeTable minWidth={80} />);
|
|
579
601
|
const handle = getResizeHandles()[0];
|
|
@@ -29,6 +29,8 @@ import {
|
|
|
29
29
|
DEFAULT_MIN_COLUMN_WIDTH,
|
|
30
30
|
} from '../../columnUtils';
|
|
31
31
|
import {observeResize} from '../../../utils/sharedResizeObserver';
|
|
32
|
+
import {getNodeText} from '../../../utils/getNodeText';
|
|
33
|
+
import {useTranslator} from '../../../i18n';
|
|
32
34
|
|
|
33
35
|
// =============================================================================
|
|
34
36
|
// Config Type
|
|
@@ -384,6 +386,7 @@ function ResizeHandle({
|
|
|
384
386
|
isDraggingRef,
|
|
385
387
|
tableRef,
|
|
386
388
|
}: ResizeHandleProps) {
|
|
389
|
+
const t = useTranslator();
|
|
387
390
|
const setTableDragging = useCallback(
|
|
388
391
|
(dragging: boolean) => {
|
|
389
392
|
const table = tableRef.current;
|
|
@@ -743,10 +746,11 @@ function ResizeHandle({
|
|
|
743
746
|
],
|
|
744
747
|
);
|
|
745
748
|
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
749
|
+
// Named by the column's visible text, also when its header is a node; the
|
|
750
|
+
// key is the fallback only for a header with no reachable text (AR1).
|
|
751
|
+
const ariaLabel = t('@astryx.table.columnResize.resizeColumn', {
|
|
752
|
+
label: getNodeText(columnHeader) || columnKey,
|
|
753
|
+
});
|
|
750
754
|
|
|
751
755
|
return (
|
|
752
756
|
<div
|
package/src/Text/Text.tsx
CHANGED
|
@@ -48,7 +48,7 @@ import {
|
|
|
48
48
|
} from './text.stylex';
|
|
49
49
|
import {useTruncation} from './useTruncation';
|
|
50
50
|
import type {LayerPlacement} from '../Layer';
|
|
51
|
-
import {mergeProps} from '../utils';
|
|
51
|
+
import {mergeProps} from '../utils/mergeProps';
|
|
52
52
|
import {useMergedRefs} from '../hooks/useMergedRefs';
|
|
53
53
|
import type {BaseProps} from '../BaseProps';
|
|
54
54
|
import {themeProps} from '../utils/themeProps';
|
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
* Tests for useTruncation
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import {render, screen, waitFor} from '@testing-library/react';
|
|
8
|
+
import {act, render, screen, waitFor} from '@testing-library/react';
|
|
9
9
|
import {afterEach, describe, it, expect, vi} from 'vitest';
|
|
10
|
+
import {useCallback} from 'react';
|
|
10
11
|
import {useTruncation} from './useTruncation';
|
|
11
12
|
|
|
12
13
|
function Probe({text}: {text: string}) {
|
|
@@ -37,6 +38,132 @@ describe('useTruncation', () => {
|
|
|
37
38
|
);
|
|
38
39
|
});
|
|
39
40
|
|
|
41
|
+
describe('first measurement', () => {
|
|
42
|
+
afterEach(() => {
|
|
43
|
+
vi.unstubAllGlobals();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Measuring inside React's commit forces a style and layout pass per text
|
|
47
|
+
// on a page that mounts many; after the commit, every text reads together.
|
|
48
|
+
it('measures after the commit, not inside it', async () => {
|
|
49
|
+
const observed: Element[] = [];
|
|
50
|
+
vi.stubGlobal(
|
|
51
|
+
'ResizeObserver',
|
|
52
|
+
class {
|
|
53
|
+
observe(target: Element) {
|
|
54
|
+
observed.push(target);
|
|
55
|
+
}
|
|
56
|
+
unobserve() {}
|
|
57
|
+
disconnect() {}
|
|
58
|
+
},
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
const {unmount} = render(<Probe text="a status line" />);
|
|
62
|
+
|
|
63
|
+
// Observed, but not measured during the commit.
|
|
64
|
+
expect(observed).toHaveLength(1);
|
|
65
|
+
expect(screen.getByTestId('full-text')).toHaveTextContent('');
|
|
66
|
+
|
|
67
|
+
await act(async () => {});
|
|
68
|
+
|
|
69
|
+
expect(screen.getByTestId('full-text')).toHaveTextContent(
|
|
70
|
+
'a status line',
|
|
71
|
+
);
|
|
72
|
+
unmount();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// FR4/PR1: with measuring turned off before the first measurement runs,
|
|
76
|
+
// nothing is measured and the hook reports no truncation.
|
|
77
|
+
it('skips the first measurement when measuring turns off in the same task', async () => {
|
|
78
|
+
vi.stubGlobal(
|
|
79
|
+
'ResizeObserver',
|
|
80
|
+
class {
|
|
81
|
+
observe() {}
|
|
82
|
+
unobserve() {}
|
|
83
|
+
disconnect() {}
|
|
84
|
+
},
|
|
85
|
+
);
|
|
86
|
+
const proto = window.HTMLElement.prototype;
|
|
87
|
+
const scrollWidth = Object.getOwnPropertyDescriptor(proto, 'scrollWidth');
|
|
88
|
+
const offsetWidth = Object.getOwnPropertyDescriptor(proto, 'offsetWidth');
|
|
89
|
+
Object.defineProperty(proto, 'scrollWidth', {
|
|
90
|
+
configurable: true,
|
|
91
|
+
get: () => 400,
|
|
92
|
+
});
|
|
93
|
+
Object.defineProperty(proto, 'offsetWidth', {
|
|
94
|
+
configurable: true,
|
|
95
|
+
get: () => 100,
|
|
96
|
+
});
|
|
97
|
+
function Unclamping({maxLines}: {maxLines: number}) {
|
|
98
|
+
const truncation = useTruncation({maxLines});
|
|
99
|
+
return (
|
|
100
|
+
<>
|
|
101
|
+
<span ref={truncation.ref}>overflowing text</span>
|
|
102
|
+
<output data-testid="state">
|
|
103
|
+
{String(truncation.isTruncated)}|{truncation.fullText}
|
|
104
|
+
</output>
|
|
105
|
+
</>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
// Measuring turns off in the same task, before the first measurement.
|
|
110
|
+
const {rerender, unmount} = render(<Unclamping maxLines={1} />);
|
|
111
|
+
rerender(<Unclamping maxLines={0} />);
|
|
112
|
+
await act(async () => {});
|
|
113
|
+
expect(screen.getByTestId('state')).toHaveTextContent(/^false\|$/);
|
|
114
|
+
unmount();
|
|
115
|
+
} finally {
|
|
116
|
+
if (scrollWidth) {
|
|
117
|
+
Object.defineProperty(proto, 'scrollWidth', scrollWidth);
|
|
118
|
+
}
|
|
119
|
+
if (offsetWidth) {
|
|
120
|
+
Object.defineProperty(proto, 'offsetWidth', offsetWidth);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// The browser sends no second first-notification for an element the
|
|
126
|
+
// shared observer already watches, so the first measurement cannot wait
|
|
127
|
+
// for one.
|
|
128
|
+
it('measures an element another subscriber already observes', async () => {
|
|
129
|
+
vi.stubGlobal(
|
|
130
|
+
'ResizeObserver',
|
|
131
|
+
class {
|
|
132
|
+
observe() {}
|
|
133
|
+
unobserve() {}
|
|
134
|
+
disconnect() {}
|
|
135
|
+
},
|
|
136
|
+
);
|
|
137
|
+
const {observeResize} = await import('../utils/sharedResizeObserver');
|
|
138
|
+
const watched = document.createElement('span');
|
|
139
|
+
function Shared({text}: {text: string}) {
|
|
140
|
+
const truncation = useTruncation({maxLines: 1});
|
|
141
|
+
const {ref} = truncation;
|
|
142
|
+
const attach = useCallback(
|
|
143
|
+
(element: HTMLElement | null) => {
|
|
144
|
+
if (element && element !== watched) {
|
|
145
|
+
observeResize(element, () => {});
|
|
146
|
+
}
|
|
147
|
+
ref(element);
|
|
148
|
+
},
|
|
149
|
+
[ref],
|
|
150
|
+
);
|
|
151
|
+
return (
|
|
152
|
+
<>
|
|
153
|
+
<span ref={attach}>{text}</span>
|
|
154
|
+
<output data-testid="full-text">{truncation.fullText}</output>
|
|
155
|
+
</>
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
const {unmount} = render(<Shared text="already watched" />);
|
|
159
|
+
await act(async () => {});
|
|
160
|
+
expect(screen.getByTestId('full-text')).toHaveTextContent(
|
|
161
|
+
'already watched',
|
|
162
|
+
);
|
|
163
|
+
unmount();
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
40
167
|
describe('content observation', () => {
|
|
41
168
|
afterEach(() => {
|
|
42
169
|
vi.unstubAllGlobals();
|
|
@@ -123,12 +123,30 @@ export function useTruncation(
|
|
|
123
123
|
elementRef.current = element;
|
|
124
124
|
|
|
125
125
|
if (element && maxLines > 0) {
|
|
126
|
-
// Observe via shared singleton (if available).
|
|
127
|
-
//
|
|
128
|
-
//
|
|
126
|
+
// Observe via shared singleton (if available). The first measurement
|
|
127
|
+
// runs after the commit, in a microtask, instead of in the call
|
|
128
|
+
// observeResize makes on registration: that call runs inside React's
|
|
129
|
+
// commit, after other components' DOM writes, and forced a style and
|
|
130
|
+
// layout pass per text on a page that mounts many. Every text
|
|
131
|
+
// registered in one commit then reads layout once, together, and a
|
|
132
|
+
// text whose element another subscriber already observes is measured
|
|
133
|
+
// too. The truncated state only decides whether to compose the
|
|
134
|
+
// tooltip, so nothing paints differently.
|
|
129
135
|
if (typeof ResizeObserver !== 'undefined') {
|
|
130
|
-
|
|
131
|
-
|
|
136
|
+
let registered = false;
|
|
137
|
+
const unobserve = observeResize(element, () => {
|
|
138
|
+
if (registered) {
|
|
139
|
+
checkTruncation(element);
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
registered = true;
|
|
143
|
+
unobserveRef.current = unobserve;
|
|
144
|
+
// Skipped when this registration has ended by then (measuring
|
|
145
|
+
// turned off, or the element replaced, in the same task).
|
|
146
|
+
queueMicrotask(() => {
|
|
147
|
+
if (unobserveRef.current === unobserve) {
|
|
148
|
+
checkTruncation(element);
|
|
149
|
+
}
|
|
132
150
|
});
|
|
133
151
|
} else {
|
|
134
152
|
checkTruncation(element);
|
|
@@ -52,15 +52,15 @@ system_specs: [spec:AST-085, spec:AST-086]
|
|
|
52
52
|
|
|
53
53
|
## Contract at a glance
|
|
54
54
|
|
|
55
|
-
| Area | Contract
|
|
56
|
-
| ----------------------- |
|
|
57
|
-
| Public contract | None changes. TextArea is a controlled multi-line field with Field props, `rows`, size, status, start icon, `maxLength` counter, `onChange`, `changeAction`, `isLoading`, disabled and read-only states, and the input-family part props.
|
|
58
|
-
| Behavior | Typing calls `onChange`, then an optional `changeAction` with an optimistic value and one busy Spinner. `maxLength` adds a visual character counter that warns but never blocks typing.
|
|
59
|
-
| End-user impact | People write longer text with a live count that is announced only when they near or pass the limit. Keyboard and screen-reader users can reach a disabled field and its reason. An error without a message has no text.
|
|
60
|
-
| Builder impact | None.
|
|
61
|
-
| Compatibility/readiness | Records shipped behavior; no API, default, or DOM change. Attached status on a tall surface, dropped grouped container props, text under the counter,
|
|
62
|
-
| Review checks | Reject a counter that truncates input or counts code units, an announcement on every keystroke, text that runs under the icons or counter, a disabled field that leaves the tab order or submits, or root styling props.
|
|
63
|
-
| Governing rules | `family:input-fields` FR4–FR6, FR8–FR10, AV3; `spec:AST-085` FR1–FR3, FR6; `spec:AST-086` FR8; `architecture:component-theming-surface`.
|
|
55
|
+
| Area | Contract |
|
|
56
|
+
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
57
|
+
| Public contract | None changes. TextArea is a controlled multi-line field with Field props, `rows`, size, status, start icon, `maxLength` counter, `onChange`, `changeAction`, `isLoading`, disabled and read-only states, and the input-family part props. |
|
|
58
|
+
| Behavior | Typing calls `onChange`, then an optional `changeAction` with an optimistic value and one busy Spinner. `maxLength` adds a visual character counter that warns but never blocks typing. |
|
|
59
|
+
| End-user impact | People write longer text with a live count that is announced only when they near or pass the limit. Keyboard and screen-reader users can reach a disabled field and its reason. An error without a message has no text. |
|
|
60
|
+
| Builder impact | None. |
|
|
61
|
+
| Compatibility/readiness | Records shipped behavior; no API, default, or DOM change. Attached status on a tall surface, dropped grouped container props, text under the counter, an error without text, and a color-only `detached` status are listed under Current-state impact. |
|
|
62
|
+
| Review checks | Reject a counter that truncates input or counts code units, an announcement on every keystroke, text that runs under the icons or counter, a disabled field that leaves the tab order or submits, or root styling props. |
|
|
63
|
+
| Governing rules | `family:input-fields` FR4–FR6, FR8–FR10, AV3; `spec:AST-085` FR1–FR3, FR6; `spec:AST-086` FR8; `architecture:component-theming-surface`. |
|
|
64
64
|
|
|
65
65
|
This table is a review projection; the body below is authoritative.
|
|
66
66
|
|
|
@@ -128,7 +128,7 @@ Consumer migration instructions belong in consumer docs and release notes.
|
|
|
128
128
|
| FR2 | The input container carries `text-area` with `data-size`, `data-status`, `data-disabled`, and `data-readonly`; the textarea carries `text-area-control`; the counter carries `text-area-counter`. Placeholder text is part of the textarea; the over-limit glyph is part of the counter. | `architecture:component-theming-surface`; current source and tests |
|
|
129
129
|
| FR3 | The textarea shows `rows` lines (default 3), resizes only vertically, and uses body text, floored at 16 CSS px only on the iOS touch path. `lg` adds block padding; `sm` and `md` share the default inset. | `family:input-fields` FR9, AV3; current source and tests |
|
|
130
130
|
| FR4 | Editing calls `onChange(value, event)`; unless that call prevents default, `changeAction(value, event)` then runs in a transition and the field shows the proposed value while the Action is pending, and `value` once it settles. While `isLoading` or while the shown value differs from `value`, the textarea sets `aria-busy`, a decorative Spinner shows, and the busy start is announced once. | `family:input-fields` FR5, FR6; `spec:AST-086` FR8; current tests |
|
|
131
|
-
| FR5 | With `maxLength`, a counter showing the character count and the limit renders at the container's bottom end and describes the textarea. It counts user-perceived characters, so a multi-code-point emoji counts once. It never sets the native `maxLength` and never blocks typing. Over the limit it turns the error color, adds a decorative warning icon, and sets `aria-invalid`. The counter text MUST come from one catalog message (`architecture:internationalization` INV1a)
|
|
131
|
+
| FR5 | With `maxLength`, a counter showing the character count and the limit renders at the container's bottom end and describes the textarea. It counts user-perceived characters, so a multi-code-point emoji counts once. It never sets the native `maxLength` and never blocks typing. Over the limit it turns the error color, adds a decorative warning icon, and sets `aria-invalid`. The counter text MUST come from one catalog message (`architecture:internationalization` INV1a), with both numbers in the locale's number format. | Current tests; `architecture:internationalization` INV1a |
|
|
132
132
|
| FR6 | Counter announcements happen when an edit lands in a different zone than the previous edit: entering the last 20% of the limit announces the remaining characters politely; exceeding the limit announces the overage assertively; moving back under announces nothing. Zone tracking starts unset, so the first edit after mount announces the near or over zone it lands in, even when the mounted value was already there. | Current tests |
|
|
133
133
|
| FR7 | The start icon renders at the top start, and the Spinner and on-field status icon at the top end; the textarea reserves inline padding for each one that renders (more when both end items show) and bottom padding for the counter, so text never runs under them; the counter's bottom padding clears text only at the end of the scroll range. Implementation gap: with a value longer than `rows`, the last visible line paints under the counter unless the field is scrolled to the end. A detached status reserves no end space because it renders no on-field icon. | `family:input-fields` FR2; current source and tests |
|
|
134
134
|
| FR8 | A status paints the status border; `error` sets `aria-invalid`. The on-field status icon shows for `attached` and `tooltip` placement; a `detached` status shows its glyph only in the message box, which renders only with a message. The message renders through Field in the chosen placement. Every status MUST show a cue other than color (WCAG 2.2 1.4.1), and an error MUST be identified in text (WCAG 2.2 3.3.1). Implementation gap: TextArea offers `attached` placement, as its default and explicitly, although `family:input-fields` FR8 and AV6 allow `attached` only on a fixed-height control; an error status may have no message; and a `detached` status without a message shows only the colored border. | `family:input-fields` FR8; current tests |
|
|
@@ -276,9 +276,6 @@ above match the current Field, FieldStatus, Icon, Spinner, and Tooltip targets.
|
|
|
276
276
|
FR8 and AV6 allow `attached` only on a fixed-height control.
|
|
277
277
|
- **Grouped container props (FR12).** Inside InputGroup, TextArea still renders
|
|
278
278
|
its Field box but drops `container*` props, warning that no Field box renders.
|
|
279
|
-
- **Counter text (FR5).** The counter's count-and-limit text is assembled in
|
|
280
|
-
code rather than from one catalog message, against
|
|
281
|
-
`architecture:internationalization` INV1a.
|
|
282
279
|
- **Text under the counter (FR7).** With a value longer than `rows`, the last
|
|
283
280
|
visible line paints under the counter unless the field is scrolled to the
|
|
284
281
|
end.
|