@astryxdesign/core 0.7.7-canary.c0929c8 → 0.7.7-canary.c19ca12
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 +4 -1
- package/dist/AlertDialog/AlertDialog.d.ts.map +1 -1
- package/dist/AlertDialog/AlertDialog.js +36 -6
- package/dist/AppShell/useCloseMobileDrawerOnActivate.d.ts +13 -0
- package/dist/AppShell/useCloseMobileDrawerOnActivate.d.ts.map +1 -0
- package/dist/AppShell/useCloseMobileDrawerOnActivate.js +45 -0
- package/dist/BottomSheet/BottomSheet.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheet.js +79 -9
- package/dist/BottomSheet/BottomSheetPanel.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheetPanel.js +23 -4
- package/dist/BottomSheet/bottomSheetStack.d.ts.map +1 -1
- package/dist/BottomSheet/bottomSheetStack.js +5 -4
- package/dist/BottomSheet/useSheetGestures.d.ts.map +1 -1
- package/dist/BottomSheet/useSheetGestures.js +3 -1
- package/dist/Breadcrumbs/BreadcrumbItem.d.ts +2 -1
- package/dist/Breadcrumbs/BreadcrumbItem.d.ts.map +1 -1
- package/dist/Breadcrumbs/BreadcrumbItem.js +7 -3
- package/dist/Button/Button.d.ts.map +1 -1
- package/dist/Button/Button.js +47 -3
- package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
- package/dist/CheckboxInput/CheckboxInput.js +13 -2
- package/dist/CheckboxList/CheckboxList.d.ts.map +1 -1
- package/dist/CheckboxList/CheckboxList.js +7 -1
- package/dist/CheckboxList/CheckboxListContext.d.ts +6 -0
- package/dist/CheckboxList/CheckboxListContext.d.ts.map +1 -1
- package/dist/CheckboxList/CheckboxListItem.d.ts.map +1 -1
- package/dist/CheckboxList/CheckboxListItem.js +10 -2
- package/dist/ComplexSelector/ComplexSelector.d.ts +4 -1
- package/dist/ComplexSelector/ComplexSelector.d.ts.map +1 -1
- package/dist/ComplexSelector/ComplexSelector.js +14 -4
- package/dist/ContextMenu/ContextMenu.js +2 -2
- package/dist/DateInput/DateInput.d.ts.map +1 -1
- package/dist/DateInput/DateInput.js +5 -2
- package/dist/DateInput/MonthScroller.d.ts.map +1 -1
- package/dist/DateInput/MonthScroller.js +13 -27
- package/dist/DateInput/Wheel.d.ts.map +1 -1
- package/dist/DateInput/Wheel.js +22 -41
- package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
- package/dist/DateTimeInput/DateTimeInput.js +5 -2
- package/dist/Dialog/Dialog.d.ts.map +1 -1
- package/dist/Dialog/Dialog.js +23 -45
- package/dist/Drawer/Drawer.d.ts.map +1 -1
- package/dist/Drawer/Drawer.js +15 -11
- package/dist/Drawer/useDrawerDialogPresence.d.ts.map +1 -1
- package/dist/Drawer/useDrawerDialogPresence.js +5 -1
- package/dist/Field/Field.js +2 -2
- package/dist/Field/PanelSearchInput.d.ts +9 -1
- package/dist/Field/PanelSearchInput.d.ts.map +1 -1
- package/dist/Field/PanelSearchInput.js +3 -1
- package/dist/HoverCard/HoverCard.d.ts +4 -3
- package/dist/HoverCard/HoverCard.d.ts.map +1 -1
- package/dist/HoverCard/HoverCard.js +27 -2
- package/dist/HoverCard/useHoverCard.d.ts +5 -4
- package/dist/HoverCard/useHoverCard.d.ts.map +1 -1
- package/dist/HoverCard/useHoverCard.js +17 -31
- package/dist/Icon/Icon.d.ts +5 -1
- package/dist/Icon/Icon.d.ts.map +1 -1
- package/dist/Icon/Icon.js +5 -2
- package/dist/InfoTip/InfoTip.d.ts +63 -0
- package/dist/InfoTip/InfoTip.d.ts.map +1 -0
- package/dist/InfoTip/InfoTip.js +126 -0
- package/dist/InfoTip/index.d.ts +10 -0
- package/dist/InfoTip/index.d.ts.map +1 -0
- package/dist/InfoTip/index.js +12 -0
- package/dist/Layer/layerAnimations.stylex.d.ts +7 -8
- package/dist/Layer/layerAnimations.stylex.d.ts.map +1 -1
- package/dist/Layer/layerAnimations.stylex.js +25 -20
- package/dist/Layer/useBackdropPress.d.ts +27 -0
- package/dist/Layer/useBackdropPress.d.ts.map +1 -0
- package/dist/Layer/useBackdropPress.js +53 -0
- package/dist/Layer/usePressPin.d.ts +20 -10
- package/dist/Layer/usePressPin.d.ts.map +1 -1
- package/dist/Layer/usePressPin.js +66 -18
- package/dist/Lightbox/Lightbox.d.ts.map +1 -1
- package/dist/Lightbox/Lightbox.js +18 -0
- package/dist/MobileNav/MobileNav.js +0 -1
- package/dist/MultiSelector/hooks.d.ts.map +1 -1
- package/dist/MultiSelector/hooks.js +2 -4
- package/dist/Outline/Outline.d.ts +2 -4
- package/dist/Outline/Outline.d.ts.map +1 -1
- package/dist/Outline/Outline.js +4 -52
- package/dist/Outline/useOutlineFromMarkdown.d.ts.map +1 -1
- package/dist/Outline/useOutlineFromMarkdown.js +1 -0
- package/dist/Outline/useScrollSpy.js +3 -3
- package/dist/PowerSearch/PowerSearch.d.ts +36 -3
- package/dist/PowerSearch/PowerSearch.d.ts.map +1 -1
- package/dist/PowerSearch/PowerSearch.js +571 -302
- package/dist/PowerSearch/PowerSearchBottomSheet.d.ts +100 -0
- package/dist/PowerSearch/PowerSearchBottomSheet.d.ts.map +1 -0
- package/dist/PowerSearch/PowerSearchBottomSheet.js +1913 -0
- package/dist/PowerSearch/PowerSearchEditPopover.d.ts +32 -1
- package/dist/PowerSearch/PowerSearchEditPopover.d.ts.map +1 -1
- package/dist/PowerSearch/PowerSearchEditPopover.js +17 -6
- package/dist/PowerSearch/PowerSearchValueEditor.d.ts +38 -3
- package/dist/PowerSearch/PowerSearchValueEditor.d.ts.map +1 -1
- package/dist/PowerSearch/PowerSearchValueEditor.js +170 -61
- package/dist/PowerSearch/formatFilterValue.d.ts +7 -0
- package/dist/PowerSearch/formatFilterValue.d.ts.map +1 -1
- package/dist/PowerSearch/formatFilterValue.js +11 -1
- package/dist/PowerSearch/formatTokenValue.d.ts +28 -0
- package/dist/PowerSearch/formatTokenValue.d.ts.map +1 -0
- package/dist/PowerSearch/formatTokenValue.js +160 -0
- package/dist/PowerSearch/powerSearchSheetUtils.d.ts +65 -0
- package/dist/PowerSearch/powerSearchSheetUtils.d.ts.map +1 -0
- package/dist/PowerSearch/powerSearchSheetUtils.js +168 -0
- package/dist/RadioList/RadioList.d.ts.map +1 -1
- package/dist/RadioList/RadioList.js +8 -2
- package/dist/RadioList/RadioListItem.d.ts +1 -1
- package/dist/RadioList/RadioListItem.d.ts.map +1 -1
- package/dist/RadioList/RadioListItem.js +5 -3
- package/dist/Resizable/ResizeHandle.d.ts.map +1 -1
- package/dist/Resizable/ResizeHandle.js +118 -260
- package/dist/Resizable/resizeGesture.d.ts +91 -0
- package/dist/Resizable/resizeGesture.d.ts.map +1 -0
- package/dist/Resizable/resizeGesture.js +251 -0
- package/dist/SegmentedControl/SegmentedControl.d.ts +3 -1
- package/dist/SegmentedControl/SegmentedControl.d.ts.map +1 -1
- package/dist/SegmentedControl/SegmentedControl.js +115 -8
- package/dist/SegmentedControl/SegmentedControlContext.d.ts +6 -0
- package/dist/SegmentedControl/SegmentedControlContext.d.ts.map +1 -1
- package/dist/SegmentedControl/SegmentedControlItem.d.ts +1 -0
- package/dist/SegmentedControl/SegmentedControlItem.d.ts.map +1 -1
- package/dist/SegmentedControl/SegmentedControlItem.js +8 -1
- package/dist/SegmentedControl/useSegmentedSelectionMotion.d.ts +357 -0
- package/dist/SegmentedControl/useSegmentedSelectionMotion.d.ts.map +1 -0
- package/dist/SegmentedControl/useSegmentedSelectionMotion.js +1349 -0
- package/dist/SegmentedControl/useSegmentedTouchTracking.d.ts +70 -0
- package/dist/SegmentedControl/useSegmentedTouchTracking.d.ts.map +1 -0
- package/dist/SegmentedControl/useSegmentedTouchTracking.js +264 -0
- package/dist/Selector/Selector.d.ts.map +1 -1
- package/dist/Selector/Selector.js +96 -67
- package/dist/Selector/optionShape.d.ts +24 -0
- package/dist/Selector/optionShape.d.ts.map +1 -0
- package/dist/Selector/optionShape.js +48 -0
- package/dist/Selector/selectableOptionEntries.d.ts +20 -0
- package/dist/Selector/selectableOptionEntries.d.ts.map +1 -0
- package/dist/Selector/selectableOptionEntries.js +38 -0
- package/dist/Selector/utils.d.ts +2 -17
- package/dist/Selector/utils.d.ts.map +1 -1
- package/dist/Selector/utils.js +3 -51
- package/dist/SideNav/SideNavItem.d.ts.map +1 -1
- package/dist/SideNav/SideNavItem.js +3 -9
- package/dist/Slider/sliderTouchController.js +2 -2
- package/dist/Spinner/Spinner.d.ts +1 -1
- package/dist/Spinner/Spinner.d.ts.map +1 -1
- package/dist/Spinner/Spinner.js +60 -58
- package/dist/Switch/switchTouch.d.ts +0 -7
- package/dist/Switch/switchTouch.d.ts.map +1 -1
- package/dist/Switch/switchTouch.js +2 -10
- package/dist/Switch/useSwitchTouch.d.ts +2 -11
- package/dist/Switch/useSwitchTouch.d.ts.map +1 -1
- package/dist/Switch/useSwitchTouch.js +35 -93
- package/dist/Table/BaseTable.d.ts.map +1 -1
- package/dist/Table/BaseTable.js +29 -8
- package/dist/TextInput/TextInput.d.ts +2 -2
- package/dist/TextInput/TextInput.d.ts.map +1 -1
- package/dist/TextInput/TextInput.js +10 -7
- package/dist/Toast/Toast.d.ts +11 -1
- package/dist/Toast/Toast.d.ts.map +1 -1
- package/dist/Toast/Toast.js +30 -15
- package/dist/Toast/ToastViewport.d.ts.map +1 -1
- package/dist/Toast/ToastViewport.js +94 -34
- package/dist/Toast/types.d.ts +9 -3
- package/dist/Toast/types.d.ts.map +1 -1
- package/dist/Token/Token.d.ts +2 -2
- package/dist/Token/Token.d.ts.map +1 -1
- package/dist/Token/Token.js +13 -4
- package/dist/Token/TokenNamingContext.d.ts +14 -0
- package/dist/Token/TokenNamingContext.d.ts.map +1 -0
- package/dist/Token/TokenNamingContext.js +23 -0
- package/dist/Tokenizer/Tokenizer.d.ts +46 -59
- package/dist/Tokenizer/Tokenizer.d.ts.map +1 -1
- package/dist/Tokenizer/Tokenizer.js +40 -57
- package/dist/Tokenizer/TokenizerBottomSheet.d.ts +25 -28
- package/dist/Tokenizer/TokenizerBottomSheet.d.ts.map +1 -1
- package/dist/Tokenizer/TokenizerBottomSheet.js +157 -54
- package/dist/Tokenizer/TokenizerItemRemovableContext.d.ts +3 -0
- package/dist/Tokenizer/TokenizerItemRemovableContext.d.ts.map +1 -0
- package/dist/Tokenizer/TokenizerItemRemovableContext.js +19 -0
- package/dist/Tooltip/Tooltip.d.ts +25 -30
- package/dist/Tooltip/Tooltip.d.ts.map +1 -1
- package/dist/Tooltip/Tooltip.js +24 -5
- package/dist/Tooltip/index.d.ts +1 -1
- package/dist/Tooltip/index.d.ts.map +1 -1
- package/dist/Tooltip/useTooltip.d.ts +45 -28
- package/dist/Tooltip/useTooltip.d.ts.map +1 -1
- package/dist/Tooltip/useTooltip.js +52 -59
- package/dist/TopNav/TopNavItem.d.ts.map +1 -1
- package/dist/TopNav/TopNavItem.js +4 -7
- package/dist/TopNav/TopNavMegaMenuItem.d.ts.map +1 -1
- package/dist/TopNav/TopNavMegaMenuItem.js +4 -7
- package/dist/TopNav/TopNavMenu.d.ts.map +1 -1
- package/dist/TopNav/TopNavMenu.js +4 -7
- package/dist/Typeahead/TypeaheadItem.d.ts +2 -1
- package/dist/Typeahead/TypeaheadItem.d.ts.map +1 -1
- package/dist/animator/createAnimator.d.ts +98 -0
- package/dist/animator/createAnimator.d.ts.map +1 -0
- package/dist/animator/createAnimator.js +259 -0
- package/dist/astryx.css +96 -7
- package/dist/gestures/GestureHost.d.ts +27 -1
- package/dist/gestures/GestureHost.d.ts.map +1 -1
- package/dist/gestures/GestureHost.js +157 -21
- package/dist/gestures/PressTracker.d.ts.map +1 -1
- package/dist/gestures/PressTracker.js +4 -3
- package/dist/hooks/actionGroupTracker.d.ts +7 -4
- package/dist/hooks/actionGroupTracker.d.ts.map +1 -1
- package/dist/hooks/actionGroupTracker.js +19 -56
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +1 -0
- package/dist/hooks/pressGroupContext.d.ts +6 -0
- package/dist/hooks/pressGroupContext.d.ts.map +1 -0
- package/dist/hooks/pressGroupContext.js +20 -0
- package/dist/hooks/useActionGroupTracking.d.ts +53 -0
- package/dist/hooks/useActionGroupTracking.d.ts.map +1 -0
- package/dist/hooks/useActionGroupTracking.js +174 -0
- package/dist/hooks/useAnimator.d.ts +41 -0
- package/dist/hooks/useAnimator.d.ts.map +1 -0
- package/dist/hooks/useAnimator.js +70 -0
- package/dist/hooks/useAttachment.d.ts +1 -1
- package/dist/hooks/useAttachment.js +1 -1
- package/dist/hooks/useHighlightedOptionScroll.d.ts +11 -1
- package/dist/hooks/useHighlightedOptionScroll.d.ts.map +1 -1
- package/dist/hooks/useHighlightedOptionScroll.js +112 -11
- package/dist/hooks/useInputStatusIcon.d.ts +1 -1
- package/dist/hooks/useInputStatusIcon.d.ts.map +1 -1
- package/dist/hooks/useInputStatusIcon.js +2 -2
- package/dist/hooks/useLongPress.d.ts +6 -2
- package/dist/hooks/useLongPress.d.ts.map +1 -1
- package/dist/hooks/useLongPress.js +95 -15
- package/dist/hooks/useMediaQuery.d.ts +2 -1
- package/dist/hooks/useMediaQuery.d.ts.map +1 -1
- package/dist/hooks/useMediaQuery.js +7 -4
- package/dist/hooks/useMenuPress.d.ts.map +1 -1
- package/dist/hooks/useMenuPress.js +4 -2
- package/dist/i18n/generated-english/InfoTip/InfoTip.generated.d.ts +8 -0
- package/dist/i18n/generated-english/InfoTip/InfoTip.generated.d.ts.map +1 -0
- package/dist/i18n/generated-english/InfoTip/InfoTip.generated.js +12 -0
- package/dist/i18n/generated-english/PowerSearch/PowerSearch.generated.d.ts.map +1 -1
- package/dist/i18n/generated-english/PowerSearch/PowerSearch.generated.js +1 -0
- package/dist/i18n/generated-english/PowerSearch/PowerSearchBottomSheet.generated.d.ts +8 -0
- package/dist/i18n/generated-english/PowerSearch/PowerSearchBottomSheet.generated.d.ts.map +1 -0
- package/dist/i18n/generated-english/PowerSearch/PowerSearchBottomSheet.generated.js +42 -0
- package/dist/i18n/generated-english/PowerSearch/PowerSearchValueEditor.generated.d.ts.map +1 -1
- package/dist/i18n/generated-english/PowerSearch/PowerSearchValueEditor.generated.js +6 -0
- package/dist/i18n/generated-english/PowerSearch/formatTokenValue.generated.d.ts +8 -0
- package/dist/i18n/generated-english/PowerSearch/formatTokenValue.generated.d.ts.map +1 -0
- package/dist/i18n/generated-english/PowerSearch/formatTokenValue.generated.js +14 -0
- package/dist/i18n/generated-english/Tokenizer/TokenizerBottomSheet.generated.d.ts.map +1 -1
- package/dist/i18n/generated-english/Tokenizer/TokenizerBottomSheet.generated.js +7 -1
- package/dist/i18n/generated-locales/en.generated.d.ts.map +1 -1
- package/dist/i18n/generated-locales/en.generated.js +33 -1
- package/dist/i18n/generated-locales/pseudo.generated.d.ts.map +1 -1
- package/dist/i18n/generated-locales/pseudo.generated.js +33 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/internal/SheetConfirmation.d.ts +11 -0
- package/dist/internal/SheetConfirmation.d.ts.map +1 -0
- package/dist/internal/SheetConfirmation.js +92 -0
- package/dist/internal/indicatorPress.stylex.d.ts +20 -0
- package/dist/internal/indicatorPress.stylex.d.ts.map +1 -0
- package/dist/internal/indicatorPress.stylex.js +26 -0
- package/dist/internal/sheetDraft.d.ts +35 -0
- package/dist/internal/sheetDraft.d.ts.map +1 -0
- package/dist/internal/sheetDraft.js +65 -0
- package/dist/internal/touch-sheet.d.ts +21 -0
- package/dist/internal/touch-sheet.d.ts.map +1 -0
- package/dist/internal/touch-sheet.js +19 -0
- package/dist/internal/useDismissSearchOnTouchDrag.d.ts +24 -0
- package/dist/internal/useDismissSearchOnTouchDrag.d.ts.map +1 -0
- package/dist/internal/useDismissSearchOnTouchDrag.js +56 -0
- package/dist/motion/animator.d.ts +14 -14
- package/dist/motion/animator.d.ts.map +1 -1
- package/dist/motion/animator.js +41 -18
- package/dist/motion/reducedMotion.d.ts +24 -0
- package/dist/motion/reducedMotion.d.ts.map +1 -1
- package/dist/motion/reducedMotion.js +78 -1
- package/dist/motion/roles.d.ts +7 -1
- package/dist/motion/roles.d.ts.map +1 -1
- package/dist/motion/roles.js +20 -1
- package/dist/motion/uikit/alert.d.ts +41 -0
- package/dist/motion/uikit/alert.d.ts.map +1 -0
- package/dist/motion/uikit/alert.js +43 -0
- package/dist/motion/uikit/index.d.ts +6 -5
- package/dist/motion/uikit/index.d.ts.map +1 -1
- package/dist/motion/uikit/index.js +6 -5
- package/dist/motion/uikit/segmented.d.ts +55 -0
- package/dist/motion/uikit/segmented.d.ts.map +1 -0
- package/dist/motion/uikit/segmented.js +77 -0
- package/dist/motion/uikit/springs.d.ts +0 -18
- package/dist/motion/uikit/springs.d.ts.map +1 -1
- package/dist/motion/uikit/springs.js +5 -21
- package/dist/privateAttributes/pressGroupItem.stylex.d.ts +15 -0
- package/dist/privateAttributes/pressGroupItem.stylex.d.ts.map +1 -0
- package/dist/privateAttributes/pressGroupItem.stylex.js +23 -0
- package/dist/privateAttributes/segmentTitle.stylex.d.ts +5 -0
- package/dist/privateAttributes/segmentTitle.stylex.d.ts.map +1 -0
- package/dist/privateAttributes/segmentTitle.stylex.js +22 -0
- package/dist/utils/browserClickWait.d.ts +35 -0
- package/dist/utils/browserClickWait.d.ts.map +1 -0
- package/dist/utils/browserClickWait.js +123 -0
- package/dist/utils/clickGuard.d.ts +14 -1
- package/dist/utils/clickGuard.d.ts.map +1 -1
- package/dist/utils/clickGuard.js +46 -1
- package/dist/utils/dateParser.d.ts.map +1 -1
- package/dist/utils/dateParser.js +12 -0
- package/dist/utils/focusReturn.d.ts +15 -0
- package/dist/utils/focusReturn.d.ts.map +1 -1
- package/dist/utils/focusReturn.js +28 -0
- package/dist/utils/interactionOverlay.stylex.d.ts +3 -2
- package/dist/utils/interactionOverlay.stylex.d.ts.map +1 -1
- package/dist/utils/interactionOverlay.stylex.js +3 -2
- package/dist/utils/pressFeedback.d.ts.map +1 -1
- package/dist/utils/pressFeedback.js +19 -6
- package/dist/utils/triggerFocus.d.ts +36 -0
- package/dist/utils/triggerFocus.d.ts.map +1 -0
- package/dist/utils/triggerFocus.js +65 -0
- package/locales/en.json +128 -0
- package/locales/pseudo.json +96 -0
- package/package.json +18 -8
- package/src/AlertDialog/AlertDialog.doc.mjs +2 -2
- package/src/AlertDialog/AlertDialog.tsx +37 -5
- package/src/AppShell/useCloseMobileDrawerOnActivate.ts +52 -0
- package/src/BottomSheet/BottomSheet.tsx +95 -10
- package/src/BottomSheet/BottomSheetPanel.tsx +40 -5
- package/src/BottomSheet/bottomSheetStack.ts +8 -4
- package/src/BottomSheet/useSheetGestures.ts +3 -1
- package/src/Breadcrumbs/BreadcrumbItem.tsx +13 -3
- package/src/Button/Button.tsx +63 -5
- package/src/CheckboxInput/CheckboxInput.doc.mjs +18 -0
- package/src/CheckboxInput/CheckboxInput.tsx +36 -9
- package/src/CheckboxList/CheckboxList.doc.mjs +4 -2
- package/src/CheckboxList/CheckboxList.tsx +6 -0
- package/src/CheckboxList/CheckboxListContext.tsx +6 -0
- package/src/CheckboxList/CheckboxListItem.tsx +11 -2
- package/src/ComplexSelector/ComplexSelector.tsx +20 -5
- package/src/ContextMenu/ContextMenu.tsx +2 -2
- package/src/DateInput/DateInput.tsx +8 -2
- package/src/DateInput/MonthScroller.tsx +18 -22
- package/src/DateInput/Wheel.tsx +30 -32
- package/src/DateTimeInput/DateTimeInput.tsx +8 -2
- package/src/Dialog/Dialog.tsx +54 -45
- package/src/Drawer/Drawer.tsx +28 -11
- package/src/Drawer/useDrawerDialogPresence.ts +5 -1
- package/src/Field/Field.tsx +2 -2
- package/src/Field/PanelSearchInput.tsx +11 -0
- package/src/HoverCard/HoverCard.doc.mjs +4 -2
- package/src/HoverCard/HoverCard.tsx +35 -4
- package/src/HoverCard/useHoverCard.doc.mjs +178 -36
- package/src/HoverCard/useHoverCard.tsx +46 -52
- package/src/Icon/Icon.doc.mjs +49 -8
- package/src/Icon/Icon.tsx +15 -7
- package/src/InfoTip/InfoTip.doc.mjs +192 -0
- package/src/InfoTip/InfoTip.tsx +186 -0
- package/src/InfoTip/index.ts +14 -0
- package/src/Layer/layerAnimations.stylex.ts +24 -16
- package/src/Layer/useBackdropPress.ts +81 -0
- package/src/Layer/usePressPin.ts +86 -26
- package/src/Lightbox/Lightbox.tsx +18 -0
- package/src/MobileNav/MobileNav.tsx +1 -1
- package/src/MultiSelector/hooks.ts +2 -4
- package/src/Outline/Outline.doc.mjs +177 -51
- package/src/Outline/Outline.tsx +5 -57
- package/src/Outline/useOutlineFromMarkdown.doc.mjs +157 -0
- package/src/Outline/useOutlineFromMarkdown.ts +1 -0
- package/src/Outline/useScrollSpy.ts +3 -3
- package/src/PowerSearch/PowerSearch.doc.mjs +55 -6
- package/src/PowerSearch/PowerSearch.tsx +798 -324
- package/src/PowerSearch/PowerSearchBottomSheet.tsx +2515 -0
- package/src/PowerSearch/PowerSearchEditPopover.tsx +28 -6
- package/src/PowerSearch/PowerSearchValueEditor.tsx +203 -38
- package/src/PowerSearch/formatFilterValue.ts +18 -1
- package/src/PowerSearch/formatTokenValue.ts +215 -0
- package/src/PowerSearch/powerSearchSheetUtils.ts +224 -0
- package/src/RadioList/RadioList.doc.mjs +4 -2
- package/src/RadioList/RadioList.tsx +7 -2
- package/src/RadioList/RadioListItem.tsx +5 -3
- package/src/Resizable/ResizeHandle.tsx +133 -328
- package/src/Resizable/resizeGesture.ts +368 -0
- package/src/SegmentedControl/SegmentedControl.doc.mjs +3 -3
- package/src/SegmentedControl/SegmentedControl.tsx +179 -5
- package/src/SegmentedControl/SegmentedControlContext.ts +6 -0
- package/src/SegmentedControl/SegmentedControlItem.tsx +12 -1
- package/src/SegmentedControl/useSegmentedSelectionMotion.ts +1640 -0
- package/src/SegmentedControl/useSegmentedTouchTracking.ts +308 -0
- package/src/Selector/Selector.tsx +136 -89
- package/src/Selector/optionShape.ts +64 -0
- package/src/Selector/selectableOptionEntries.ts +39 -0
- package/src/Selector/utils.ts +9 -68
- package/src/SideNav/SideNavItem.tsx +3 -7
- package/src/Slider/sliderTouchController.ts +2 -2
- package/src/Spinner/Spinner.tsx +77 -63
- package/src/Switch/switchTouch.ts +2 -10
- package/src/Switch/useSwitchTouch.ts +38 -104
- package/src/Table/BaseTable.tsx +46 -12
- package/src/TextInput/TextInput.tsx +11 -10
- package/src/Toast/Toast.doc.mjs +4 -3
- package/src/Toast/Toast.tsx +57 -32
- package/src/Toast/ToastViewport.tsx +121 -35
- package/src/Toast/types.ts +9 -3
- package/src/Token/Token.tsx +15 -4
- package/src/Token/TokenNamingContext.ts +28 -0
- package/src/Tokenizer/Tokenizer.doc.mjs +16 -0
- package/src/Tokenizer/Tokenizer.tsx +89 -85
- package/src/Tokenizer/TokenizerBottomSheet.tsx +219 -93
- package/src/Tokenizer/TokenizerItemRemovableContext.ts +24 -0
- package/src/Tooltip/Tooltip.doc.mjs +123 -54
- package/src/Tooltip/Tooltip.tsx +36 -39
- package/src/Tooltip/index.ts +0 -1
- package/src/Tooltip/useTooltip.doc.mjs +171 -33
- package/src/Tooltip/useTooltip.tsx +98 -86
- package/src/TopNav/TopNavItem.tsx +6 -5
- package/src/TopNav/TopNavMegaMenuItem.tsx +6 -5
- package/src/TopNav/TopNavMenu.tsx +6 -5
- package/src/Typeahead/TypeaheadItem.doc.mjs +6 -3
- package/src/Typeahead/TypeaheadItem.tsx +2 -1
- package/src/animator/createAnimator.doc.mjs +153 -0
- package/src/animator/createAnimator.ts +324 -0
- package/src/gestures/GestureHost.ts +198 -20
- package/src/gestures/PressTracker.ts +4 -3
- package/src/gestures/README.md +7 -1
- package/src/hooks/actionGroupTracker.ts +32 -61
- package/src/hooks/index.ts +2 -0
- package/src/hooks/pressGroupContext.ts +21 -0
- package/src/hooks/useActionGroupTracking.ts +224 -0
- package/src/hooks/useAnimator.doc.mjs +109 -0
- package/src/hooks/useAnimator.ts +67 -0
- package/src/hooks/useAttachment.ts +1 -1
- package/src/hooks/useHighlightedOptionScroll.ts +123 -15
- package/src/hooks/useInputStatusIcon.tsx +3 -2
- package/src/hooks/useLongPress.doc.mjs +2 -2
- package/src/hooks/useLongPress.ts +136 -15
- package/src/hooks/useMediaQuery.doc.mjs +2 -2
- package/src/hooks/useMediaQuery.ts +13 -4
- package/src/hooks/useMenuPress.ts +8 -2
- package/src/i18n/generated-english/InfoTip/InfoTip.generated.ts +13 -0
- package/src/i18n/generated-english/PowerSearch/PowerSearch.generated.ts +1 -0
- package/src/i18n/generated-english/PowerSearch/PowerSearchBottomSheet.generated.ts +43 -0
- package/src/i18n/generated-english/PowerSearch/PowerSearchValueEditor.generated.ts +6 -0
- package/src/i18n/generated-english/PowerSearch/formatTokenValue.generated.ts +15 -0
- package/src/i18n/generated-english/Tokenizer/TokenizerBottomSheet.generated.ts +7 -1
- package/src/i18n/generated-locales/en.generated.ts +43 -0
- package/src/i18n/generated-locales/pseudo.generated.ts +44 -0
- package/src/index.ts +14 -0
- package/src/internal/SheetConfirmation.tsx +101 -0
- package/src/internal/indicatorPress.stylex.ts +24 -0
- package/src/internal/sheetDraft.ts +80 -0
- package/src/internal/touch-sheet.ts +21 -0
- package/src/internal/useDismissSearchOnTouchDrag.ts +69 -0
- package/src/motion/MEASUREMENTS.md +7 -0
- package/src/motion/README.md +9 -5
- package/src/motion/animator.ts +61 -29
- package/src/motion/reducedMotion.ts +79 -1
- package/src/motion/roles.ts +20 -1
- package/src/motion/uikit/alert.ts +43 -0
- package/src/motion/uikit/index.ts +5 -5
- package/src/motion/uikit/segmented.ts +74 -0
- package/src/motion/uikit/springs.ts +5 -21
- package/src/privateAttributes/pressGroupItem.stylex.ts +23 -0
- package/src/privateAttributes/segmentTitle.stylex.ts +22 -0
- package/src/utils/browserClickWait.ts +133 -0
- package/src/utils/clickGuard.ts +64 -1
- package/src/utils/dateParser.ts +16 -0
- package/src/utils/focusReturn.ts +30 -0
- package/src/utils/interactionOverlay.stylex.ts +3 -2
- package/src/utils/pressFeedback.ts +19 -6
- package/src/utils/triggerFocus.ts +80 -0
- package/dist/internal/tokenizer-presentation.d.ts +0 -21
- package/dist/internal/tokenizer-presentation.d.ts.map +0 -1
- package/dist/internal/tokenizer-presentation.js +0 -21
- package/src/internal/tokenizer-presentation.ts +0 -25
|
@@ -0,0 +1,1640 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
'use client';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @file useSegmentedSelectionMotion.ts
|
|
7
|
+
* @input The radiogroup, the selected value, the presentation and tracking
|
|
8
|
+
* flags; the animator, the UIKit table and the touch tracking
|
|
9
|
+
* @output Exports `useSegmentedSelectionMotion`, `SegmentedSelectionMotion`,
|
|
10
|
+
* the layer custom properties and the segment and title selectors
|
|
11
|
+
* @position SegmentedControl's touch layer
|
|
12
|
+
*
|
|
13
|
+
* SYNC: When modified, update:
|
|
14
|
+
* - /packages/core/src/SegmentedControl/SegmentedControl.tsx (the pseudo-elements)
|
|
15
|
+
* - /packages/core/src/SegmentedControl/SegmentedControlItem.tsx (the title marker)
|
|
16
|
+
* - /packages/core/src/SegmentedControl/useSegmentedTouchTracking.ts
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
useCallback,
|
|
21
|
+
useEffect,
|
|
22
|
+
useRef,
|
|
23
|
+
useState,
|
|
24
|
+
type RefCallback,
|
|
25
|
+
type RefObject,
|
|
26
|
+
} from 'react';
|
|
27
|
+
import {
|
|
28
|
+
createMotionAnimator,
|
|
29
|
+
type AnimateOptions,
|
|
30
|
+
type Animator,
|
|
31
|
+
type MotionCurve,
|
|
32
|
+
type MotionHandle,
|
|
33
|
+
type MotionValues,
|
|
34
|
+
type SpringTransition,
|
|
35
|
+
} from '../motion/animator';
|
|
36
|
+
import {
|
|
37
|
+
registerSegmentedTable,
|
|
38
|
+
segmentedControl,
|
|
39
|
+
} from '../motion/uikit/segmented';
|
|
40
|
+
import {registerSpringsTable, springs} from '../motion/uikit/springs';
|
|
41
|
+
import {useIsomorphicLayoutEffect} from '../hooks/useIsomorphicLayoutEffect';
|
|
42
|
+
import {observeResize} from '../utils/sharedResizeObserver';
|
|
43
|
+
import {titleOfSegment} from '../privateAttributes/segmentTitle.stylex';
|
|
44
|
+
import {cubicBezier} from '../motion/bezier';
|
|
45
|
+
import {parseCssEasing, parseMotionRoleValue} from '../motion/roles';
|
|
46
|
+
import {useSegmentedTouchTracking} from './useSegmentedTouchTracking';
|
|
47
|
+
|
|
48
|
+
/** The segments, in DOM order. */
|
|
49
|
+
export const SEGMENT_SELECTOR = '[role="radio"][data-value]';
|
|
50
|
+
/**
|
|
51
|
+
* The selected face moves to another segment only when the lens's center is
|
|
52
|
+
* this far past the boundary. The distance is a quarter of that segment's
|
|
53
|
+
* width, at least 8 px.
|
|
54
|
+
*/
|
|
55
|
+
const FACE_HYSTERESIS_RATIO = 0.25;
|
|
56
|
+
const FACE_HYSTERESIS_MIN_PX = 8;
|
|
57
|
+
|
|
58
|
+
/** The selected surface's paint, copied into a layer's custom properties. */
|
|
59
|
+
export const SURFACE_PROPERTIES: ReadonlyArray<string> = [
|
|
60
|
+
'background-color',
|
|
61
|
+
'background-image',
|
|
62
|
+
'background-position',
|
|
63
|
+
'background-size',
|
|
64
|
+
'background-repeat',
|
|
65
|
+
'background-origin',
|
|
66
|
+
'background-clip',
|
|
67
|
+
'box-shadow',
|
|
68
|
+
'border-top-left-radius',
|
|
69
|
+
'border-top-right-radius',
|
|
70
|
+
'border-bottom-right-radius',
|
|
71
|
+
'border-bottom-left-radius',
|
|
72
|
+
'border-top-width',
|
|
73
|
+
'border-right-width',
|
|
74
|
+
'border-bottom-width',
|
|
75
|
+
'border-left-width',
|
|
76
|
+
'border-top-style',
|
|
77
|
+
'border-right-style',
|
|
78
|
+
'border-bottom-style',
|
|
79
|
+
'border-left-style',
|
|
80
|
+
'border-top-color',
|
|
81
|
+
'border-right-color',
|
|
82
|
+
'border-bottom-color',
|
|
83
|
+
'border-left-color',
|
|
84
|
+
'backdrop-filter',
|
|
85
|
+
'-webkit-backdrop-filter',
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The radiogroup's two pseudo-element layers: the lens (::before) and the
|
|
90
|
+
* ::after, which under Reduced Motion shows the surface that fades out where
|
|
91
|
+
* it is.
|
|
92
|
+
*/
|
|
93
|
+
export type Layer = 'lens' | 'after';
|
|
94
|
+
|
|
95
|
+
/** Box and visibility of a layer, besides its paint. */
|
|
96
|
+
export const LAYER_GEOMETRY: ReadonlyArray<string> = [
|
|
97
|
+
'left',
|
|
98
|
+
'top',
|
|
99
|
+
'width',
|
|
100
|
+
'height',
|
|
101
|
+
'visibility',
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
/** The custom property on the radiogroup that sets `property` of `layer`. */
|
|
105
|
+
export function layerVariable(layer: Layer, property: string): string {
|
|
106
|
+
return `--_segmented-${layer}-${property.replace(/^-+/, '')}`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* What the destination segment shows while the lens carries its surface.
|
|
111
|
+
* Written inline with priority so a theme's rule for the selected segment
|
|
112
|
+
* cannot paint a second surface; borders keep their width so nothing moves.
|
|
113
|
+
*/
|
|
114
|
+
const HIDDEN_SURFACE: ReadonlyArray<readonly [string, string]> = [
|
|
115
|
+
['background-color', 'transparent'],
|
|
116
|
+
['background-image', 'none'],
|
|
117
|
+
['box-shadow', 'none'],
|
|
118
|
+
['border-top-color', 'transparent'],
|
|
119
|
+
['border-right-color', 'transparent'],
|
|
120
|
+
['border-bottom-color', 'transparent'],
|
|
121
|
+
['border-left-color', 'transparent'],
|
|
122
|
+
['backdrop-filter', 'none'],
|
|
123
|
+
['-webkit-backdrop-filter', 'none'],
|
|
124
|
+
];
|
|
125
|
+
|
|
126
|
+
/** A title's face: what a dragging finger moves between titles. */
|
|
127
|
+
interface TitleFace {
|
|
128
|
+
readonly color: string;
|
|
129
|
+
readonly fontWeight: string;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** A box in the track's padding-box coordinates, in CSS px. */
|
|
133
|
+
interface Box {
|
|
134
|
+
readonly x: number;
|
|
135
|
+
readonly y: number;
|
|
136
|
+
readonly width: number;
|
|
137
|
+
readonly height: number;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
interface Carried {
|
|
141
|
+
/** The segment the lens is bound for: it lands there at rest. */
|
|
142
|
+
readonly segment: HTMLElement;
|
|
143
|
+
/**
|
|
144
|
+
* The lens's layout box: the segment's, so it lands at translate 0 from
|
|
145
|
+
* the segment and scale 1, the segment's own size.
|
|
146
|
+
*/
|
|
147
|
+
readonly box: Box;
|
|
148
|
+
/**
|
|
149
|
+
* The segment whose surface the lens shows, hidden there meanwhile: the
|
|
150
|
+
* selected one. A touch moves the lens before the new value comes back,
|
|
151
|
+
* so until then this is still the old selection.
|
|
152
|
+
*/
|
|
153
|
+
hidden: HTMLElement;
|
|
154
|
+
/** The track's layout size when the box was measured. */
|
|
155
|
+
readonly trackSize: {readonly width: number; readonly height: number};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** A finger's touch, from landing to letting go. */
|
|
159
|
+
interface Touch {
|
|
160
|
+
/** The track's geometry when the finger landed (read once). */
|
|
161
|
+
readonly toTrackX: (clientX: number) => number;
|
|
162
|
+
readonly boxes: ReadonlyMap<HTMLElement, Box>;
|
|
163
|
+
/** The segment the finger landed on, if any (the tracker's begin target). */
|
|
164
|
+
readonly origin: HTMLElement | null;
|
|
165
|
+
/** Whether it landed on the selected segment (a drag carries the lens). */
|
|
166
|
+
readonly carriesLens: boolean;
|
|
167
|
+
/** A finger pressed: under Reduced Motion its release cross-fades. */
|
|
168
|
+
readonly isFinger: boolean;
|
|
169
|
+
/** The selected title's face and an unselected one, read at landing. */
|
|
170
|
+
readonly faces: {
|
|
171
|
+
readonly selected: TitleFace;
|
|
172
|
+
readonly unselected: TitleFace;
|
|
173
|
+
} | null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* The motion a release starts after its commit's click runs. It aims at the
|
|
178
|
+
* destination's box when the new value shows, because the titles' new weights
|
|
179
|
+
* move the segments. Until the value comes back, it aims at the box measured
|
|
180
|
+
* at landing.
|
|
181
|
+
*/
|
|
182
|
+
interface Pending {
|
|
183
|
+
readonly destination: HTMLElement;
|
|
184
|
+
/** The destination's box when the finger landed. */
|
|
185
|
+
readonly box: Box;
|
|
186
|
+
/** The held lens settles there; otherwise it slides from `from`. */
|
|
187
|
+
readonly isHeld: boolean;
|
|
188
|
+
/** The selected segment's box: where a tap's slide starts. */
|
|
189
|
+
readonly from: Box | null;
|
|
190
|
+
/** A finger released it: under Reduced Motion it cross-fades. */
|
|
191
|
+
readonly touchRelease: boolean;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function faceOf(element: HTMLElement): TitleFace {
|
|
195
|
+
const style = getComputedStyle(element);
|
|
196
|
+
return {color: style.color, fontWeight: style.fontWeight};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* A hidden segment's own inline declarations (its `style`) for the properties
|
|
201
|
+
* `hideSurface` overrides: value and priority, restored by `showSurface`.
|
|
202
|
+
*/
|
|
203
|
+
const ownSurface = new WeakMap<
|
|
204
|
+
HTMLElement,
|
|
205
|
+
ReadonlyArray<readonly [property: string, value: string, priority: string]>
|
|
206
|
+
>();
|
|
207
|
+
|
|
208
|
+
function hideSurface(segment: HTMLElement): void {
|
|
209
|
+
if (!ownSurface.has(segment)) {
|
|
210
|
+
ownSurface.set(
|
|
211
|
+
segment,
|
|
212
|
+
HIDDEN_SURFACE.map(([property]) => [
|
|
213
|
+
property,
|
|
214
|
+
segment.style.getPropertyValue(property),
|
|
215
|
+
segment.style.getPropertyPriority(property),
|
|
216
|
+
]),
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
for (const [property, value] of HIDDEN_SURFACE) {
|
|
220
|
+
segment.style.setProperty(property, value, 'important');
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Gives a hidden segment its surface back, its own inline values included. */
|
|
225
|
+
function showSurface(segment: HTMLElement): void {
|
|
226
|
+
const own = ownSurface.get(segment);
|
|
227
|
+
if (own == null) {
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
ownSurface.delete(segment);
|
|
231
|
+
for (const [property, value, priority] of own) {
|
|
232
|
+
if (value === '') {
|
|
233
|
+
segment.style.removeProperty(property);
|
|
234
|
+
} else {
|
|
235
|
+
segment.style.setProperty(property, value, priority);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Paints `layer` with `segment`'s computed surface. All values are read
|
|
242
|
+
* before any is written. A write to the track's style makes the next
|
|
243
|
+
* computed-style read recompute. Interleaved reads and writes restyle the
|
|
244
|
+
* control once per property.
|
|
245
|
+
*/
|
|
246
|
+
function paintLayer(
|
|
247
|
+
track: HTMLElement,
|
|
248
|
+
layer: Layer,
|
|
249
|
+
segment: HTMLElement,
|
|
250
|
+
): void {
|
|
251
|
+
const style = getComputedStyle(segment);
|
|
252
|
+
const values = SURFACE_PROPERTIES.map(property =>
|
|
253
|
+
style.getPropertyValue(property),
|
|
254
|
+
);
|
|
255
|
+
SURFACE_PROPERTIES.forEach((property, index) => {
|
|
256
|
+
track.style.setProperty(layerVariable(layer, property), values[index]);
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** Copies the lens's paint onto the ::after. */
|
|
261
|
+
function copyLensPaint(track: HTMLElement): void {
|
|
262
|
+
for (const property of SURFACE_PROPERTIES) {
|
|
263
|
+
track.style.setProperty(
|
|
264
|
+
layerVariable('after', property),
|
|
265
|
+
track.style.getPropertyValue(layerVariable('lens', property)),
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** Lays `layer` out on `box` and shows it. */
|
|
271
|
+
function placeLayer(track: HTMLElement, layer: Layer, box: Box): void {
|
|
272
|
+
track.style.setProperty(layerVariable(layer, 'left'), `${box.x}px`);
|
|
273
|
+
track.style.setProperty(layerVariable(layer, 'top'), `${box.y}px`);
|
|
274
|
+
track.style.setProperty(layerVariable(layer, 'width'), `${box.width}px`);
|
|
275
|
+
track.style.setProperty(layerVariable(layer, 'height'), `${box.height}px`);
|
|
276
|
+
track.style.setProperty(layerVariable(layer, 'visibility'), 'visible');
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** Hides `layer` and drops its custom properties. */
|
|
280
|
+
function clearLayer(track: HTMLElement, layer: Layer): void {
|
|
281
|
+
for (const property of [...LAYER_GEOMETRY, ...SURFACE_PROPERTIES]) {
|
|
282
|
+
track.style.removeProperty(layerVariable(layer, property));
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* The scale an ancestor transform applies to the track's client rects. The
|
|
288
|
+
* lens lays out untransformed. `offsetWidth` rounds to whole pixels, so a
|
|
289
|
+
* fractional track width reads as a scale slightly off 1. Only a real
|
|
290
|
+
* transform moves it further than that rounding.
|
|
291
|
+
*/
|
|
292
|
+
function scaleOf(track: HTMLElement, rect: DOMRect): number {
|
|
293
|
+
if (track.offsetWidth <= 0 || rect.width <= 0) {
|
|
294
|
+
return 1;
|
|
295
|
+
}
|
|
296
|
+
return Math.abs(rect.width - track.offsetWidth) <= 1
|
|
297
|
+
? 1
|
|
298
|
+
: rect.width / track.offsetWidth;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** Boxes relative to the track's padding box. */
|
|
302
|
+
function boxesIn(track: HTMLElement): (element: HTMLElement) => Box {
|
|
303
|
+
const trackRect = track.getBoundingClientRect();
|
|
304
|
+
const scale = scaleOf(track, trackRect);
|
|
305
|
+
return element => {
|
|
306
|
+
const rect = element.getBoundingClientRect();
|
|
307
|
+
return {
|
|
308
|
+
x: (rect.left - trackRect.left) / scale - track.clientLeft,
|
|
309
|
+
y: (rect.top - trackRect.top) / scale - track.clientTop,
|
|
310
|
+
width: rect.width / scale,
|
|
311
|
+
height: rect.height / scale,
|
|
312
|
+
};
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** Maps a client x to the track's padding-box x, read once per touch. */
|
|
317
|
+
function trackXIn(track: HTMLElement): (clientX: number) => number {
|
|
318
|
+
const rect = track.getBoundingClientRect();
|
|
319
|
+
const scale = scaleOf(track, rect);
|
|
320
|
+
return clientX => (clientX - rect.left) / scale - track.clientLeft;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function segmentsOf(track: HTMLElement): HTMLElement[] {
|
|
324
|
+
return Array.from(track.querySelectorAll<HTMLElement>(SEGMENT_SELECTOR));
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function isLaidOut(box: Box): boolean {
|
|
328
|
+
return box.width > 0 && box.height > 0;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function center(box: Box): number {
|
|
332
|
+
return box.x + box.width / 2;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** Whether two boxes are the same to a quarter of a pixel. */
|
|
336
|
+
function isSameBox(a: Box, b: Box): boolean {
|
|
337
|
+
return (
|
|
338
|
+
Math.abs(a.x - b.x) < 0.25 &&
|
|
339
|
+
Math.abs(a.y - b.y) < 0.25 &&
|
|
340
|
+
Math.abs(a.width - b.width) < 0.25 &&
|
|
341
|
+
Math.abs(a.height - b.height) < 0.25
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function isEnabledSegment(segment: HTMLElement): boolean {
|
|
346
|
+
return (
|
|
347
|
+
!segment.hasAttribute('disabled') &&
|
|
348
|
+
segment.getAttribute('aria-disabled') !== 'true'
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const titleOf = titleOfSegment;
|
|
353
|
+
|
|
354
|
+
/** The theme's timing: a duration in seconds and its easing. */
|
|
355
|
+
interface ThemeTiming {
|
|
356
|
+
readonly seconds: number;
|
|
357
|
+
readonly ease: (progress: number) => number;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* The timing of `segment`'s own transition, which reads the theme's duration
|
|
362
|
+
* and easing tokens. The values stay readable while a carry turns the
|
|
363
|
+
* transition's property off. Null when the theme gives no duration.
|
|
364
|
+
*/
|
|
365
|
+
function themeTimingOf(segment: HTMLElement): ThemeTiming | null {
|
|
366
|
+
const view = segment.ownerDocument.defaultView;
|
|
367
|
+
if (view == null) {
|
|
368
|
+
return null;
|
|
369
|
+
}
|
|
370
|
+
const style = view.getComputedStyle(segment);
|
|
371
|
+
const ms = parseMotionRoleValue(
|
|
372
|
+
'duration',
|
|
373
|
+
style.transitionDuration.split(',')[0] ?? '',
|
|
374
|
+
);
|
|
375
|
+
const points = parseCssEasing(
|
|
376
|
+
style.transitionTimingFunction.split(/,(?![^(]*\))/)[0] ?? '',
|
|
377
|
+
);
|
|
378
|
+
if (ms == null || ms <= 0 || points == null) {
|
|
379
|
+
return null;
|
|
380
|
+
}
|
|
381
|
+
return {seconds: ms / 1000, ease: cubicBezier(points)};
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/** A theme that gives the segments no transition: the lens lands at once. */
|
|
385
|
+
const INSTANT: ThemeTiming = {seconds: 0.001, ease: progress => progress};
|
|
386
|
+
|
|
387
|
+
/** A curve from `from` to `to` on `timing`. */
|
|
388
|
+
function timedCurve(
|
|
389
|
+
from: number,
|
|
390
|
+
to: number,
|
|
391
|
+
timing: ThemeTiming,
|
|
392
|
+
): MotionCurve {
|
|
393
|
+
const {seconds, ease} = timing;
|
|
394
|
+
const at = (t: number) =>
|
|
395
|
+
from + (to - from) * ease(Math.min(1, Math.max(0, t / seconds)));
|
|
396
|
+
const step = Math.min(seconds / 100, 0.001);
|
|
397
|
+
return {
|
|
398
|
+
duration: seconds,
|
|
399
|
+
at,
|
|
400
|
+
velocity: t => {
|
|
401
|
+
const before = Math.max(0, t - step);
|
|
402
|
+
const after = Math.min(seconds, t + step);
|
|
403
|
+
return (at(after) - at(before)) / (after - before);
|
|
404
|
+
},
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/** What the control reads of its selection motion. */
|
|
409
|
+
export interface SegmentedSelection {
|
|
410
|
+
/**
|
|
411
|
+
* A press is down on the control (any pointer's), or its tap's click has
|
|
412
|
+
* not run yet. The press commits through that click, so a focus move it
|
|
413
|
+
* causes selects nothing.
|
|
414
|
+
*/
|
|
415
|
+
readonly isPressing: () => boolean;
|
|
416
|
+
/** A key went down: a focus move it causes is the keyboard's, and selects. */
|
|
417
|
+
readonly keyPressed: () => void;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
export interface SegmentedSelectionMotionOptions {
|
|
421
|
+
/** The animator's clock (ms, document timeline); `performance.now()` by default. */
|
|
422
|
+
readonly now?: () => number;
|
|
423
|
+
/**
|
|
424
|
+
* The animator for the radiogroup's ::after (the surface that fades out
|
|
425
|
+
* under Reduced Motion); one of its own by default.
|
|
426
|
+
*/
|
|
427
|
+
readonly afterAnimator?: Animator;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function defaultNow(): number {
|
|
431
|
+
return typeof performance === 'undefined' ? Date.now() : performance.now();
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* The selection motion of one SegmentedControl, as UIKit's segmented control
|
|
436
|
+
* moves it. It lives outside React's render cycle. Nothing here sets state,
|
|
437
|
+
* so a slide never re-renders anything.
|
|
438
|
+
*
|
|
439
|
+
* At rest, the selected segment paints its own surface, so themes style it
|
|
440
|
+
* as before. For a slide, the lens takes over. The lens is the radiogroup's
|
|
441
|
+
* ::before, painted from custom properties copied from the segment's
|
|
442
|
+
* computed surface. The segment hides its own surface inline, so a theme
|
|
443
|
+
* rule cannot keep it painted. The lens moves with `translate` and `scale`
|
|
444
|
+
* on the compositor animator. Nothing lays out or paints per frame, and the
|
|
445
|
+
* slide keeps moving while JavaScript is busy. The lens lays out on the
|
|
446
|
+
* destination's box. It lands at translate 0 and scale 1 and hands the
|
|
447
|
+
* surface back pixel for pixel. Its position and its size run on separate
|
|
448
|
+
* springs, as UIKit's do. A change mid-flight starts from the presented
|
|
449
|
+
* geometry and its velocities. The titles change with the selection, at
|
|
450
|
+
* once.
|
|
451
|
+
*
|
|
452
|
+
* Under a finger:
|
|
453
|
+
* - A theme role owns a pressed thumb's lift, so a press on the selected
|
|
454
|
+
* segment lifts nothing here.
|
|
455
|
+
* - A drag from the selected segment carries the lens at its resting size.
|
|
456
|
+
* Its center chases the finger, at most `dragOvershoot` past the end
|
|
457
|
+
* segments.
|
|
458
|
+
* - The selected title's color follows the lens's center. It moves to a
|
|
459
|
+
* neighbor only once the center is a quarter of that segment past the
|
|
460
|
+
* boundary. Every title keeps its weight until the lift.
|
|
461
|
+
* - The lift commits the segment nearest the finger, through that segment's
|
|
462
|
+
* click. A lift outside the control commits nothing, and the lens settles
|
|
463
|
+
* back.
|
|
464
|
+
* - A press on another segment leaves the lens in place until the lift.
|
|
465
|
+
* Meanwhile that segment shows the shared pressed overlay.
|
|
466
|
+
* - A click that the item cancels sends the lens back at once. Otherwise the
|
|
467
|
+
* lens waits for the new value, which a parent can adopt later. It goes
|
|
468
|
+
* back only if the slide settles without the new value.
|
|
469
|
+
* - A cancelled touch puts the lens back.
|
|
470
|
+
*
|
|
471
|
+
* Under a mouse or a pen, the same press commits the same segment, but the
|
|
472
|
+
* theme owns the timing. A click, a key or a new value slides the lens on the
|
|
473
|
+
* theme's duration and easing, as the segments' own transition does. A drag
|
|
474
|
+
* from the selected segment carries the lens on the pointer itself.
|
|
475
|
+
*
|
|
476
|
+
* Under Reduced Motion, a held lens still follows the pointer: that is direct
|
|
477
|
+
* movement, not a transition. Nothing else chases, travels or scales. The lens
|
|
478
|
+
* fades in at the new segment while ::after, painted with the old surface,
|
|
479
|
+
* fades out in place. With a fine pointer under Reduce Motion, a change shows
|
|
480
|
+
* through the theme's transition.
|
|
481
|
+
*/
|
|
482
|
+
export class SegmentedSelectionMotion {
|
|
483
|
+
private track: HTMLElement | null = null;
|
|
484
|
+
private carried: Carried | null = null;
|
|
485
|
+
/**
|
|
486
|
+
* Segments whose surface the lens carried until a change turned it
|
|
487
|
+
* elsewhere. They keep their surface hidden until the lens lands. A tap's
|
|
488
|
+
* press paint can still fade on such a segment. That paint would cover the
|
|
489
|
+
* lens, which sits under the segments' backgrounds, while the lens crosses
|
|
490
|
+
* it.
|
|
491
|
+
*/
|
|
492
|
+
private readonly abandoned = new Set<HTMLElement>();
|
|
493
|
+
/** The segment showing the selected face under a dragging finger. */
|
|
494
|
+
private facedSegment: HTMLElement | null = null;
|
|
495
|
+
/** The frame request that keeps the face on the lens while it is carried. */
|
|
496
|
+
private lensFrame: number | null = null;
|
|
497
|
+
/** Titles wearing the other face's color during a drag. */
|
|
498
|
+
private readonly faceOverrides = new Set<HTMLElement>();
|
|
499
|
+
/** The motions the hand-back waits for. */
|
|
500
|
+
private readonly running = new Set<MotionHandle>();
|
|
501
|
+
/** A finger holds the lens: a drag from the selected segment carries it. */
|
|
502
|
+
private isHeld = false;
|
|
503
|
+
/**
|
|
504
|
+
* The touch presentation applies (a coarse primary pointer), so every
|
|
505
|
+
* change slides on UIKit's springs. When it is off, a change slides on the
|
|
506
|
+
* theme's duration and easing, as the segments' transition does. A drag from
|
|
507
|
+
* the selected segment carries the lens on the pointer itself.
|
|
508
|
+
*/
|
|
509
|
+
private isPresenting = true;
|
|
510
|
+
/**
|
|
511
|
+
* Segments whose own transitions are off while a fine pointer's carry is
|
|
512
|
+
* on screen, with their own inline values to give back.
|
|
513
|
+
*/
|
|
514
|
+
private readonly stilled = new Map<HTMLElement, readonly [string, string]>();
|
|
515
|
+
/** The frame request that gives the stilled segments their transitions back. */
|
|
516
|
+
private thawFrame: number | null = null;
|
|
517
|
+
private touch: Touch | null = null;
|
|
518
|
+
/**
|
|
519
|
+
* The value a touch already moved the lens to: the selection change that
|
|
520
|
+
* follows (the controlled value coming back) re-aims the lens.
|
|
521
|
+
*/
|
|
522
|
+
private committed: string | null = null;
|
|
523
|
+
/**
|
|
524
|
+
* The commit's click ran without a veto: the lens waits for the value
|
|
525
|
+
* until the slide settles.
|
|
526
|
+
*/
|
|
527
|
+
private isAwaitingValue = false;
|
|
528
|
+
/** The motion a release starts once its commit's click has run. */
|
|
529
|
+
private pending: Pending | null = null;
|
|
530
|
+
/** The last release was a finger's (decline and re-aim follow it). */
|
|
531
|
+
private releasedByFinger = true;
|
|
532
|
+
/** The ::after shows the leaving surface (Reduced Motion). */
|
|
533
|
+
private isLeavingShown = false;
|
|
534
|
+
/** Bumped by every leaving fade; a finished fade clears only its own. */
|
|
535
|
+
private leaving = 0;
|
|
536
|
+
|
|
537
|
+
private readonly now: () => number;
|
|
538
|
+
/** The ::after: the surface fading out under Reduced Motion. */
|
|
539
|
+
private readonly after: Animator;
|
|
540
|
+
|
|
541
|
+
constructor(
|
|
542
|
+
private readonly animator: Animator = createMotionAnimator(null, {
|
|
543
|
+
pseudoElement: '::before',
|
|
544
|
+
}),
|
|
545
|
+
options: SegmentedSelectionMotionOptions = {},
|
|
546
|
+
) {
|
|
547
|
+
// The chase plays the shared `springs.fingerTracking` role, and the slide
|
|
548
|
+
// plays the segmented role. Both register before the first animation,
|
|
549
|
+
// because nothing registers on import. A second call does nothing.
|
|
550
|
+
registerSpringsTable();
|
|
551
|
+
registerSegmentedTable();
|
|
552
|
+
this.now = options.now ?? defaultNow;
|
|
553
|
+
this.after =
|
|
554
|
+
options.afterAnimator ??
|
|
555
|
+
createMotionAnimator(null, {
|
|
556
|
+
pseudoElement: '::after',
|
|
557
|
+
initial: {opacity: 0},
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/** Attaches the radiogroup whose pseudo-elements are the lens (null detaches). */
|
|
562
|
+
attachTrack(element: HTMLElement | null): void {
|
|
563
|
+
if (element !== this.track) {
|
|
564
|
+
this.settle();
|
|
565
|
+
}
|
|
566
|
+
this.track = element;
|
|
567
|
+
this.animator.attach(element);
|
|
568
|
+
this.after.attach(element);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/** True while the lens carries the selected surface. */
|
|
572
|
+
get isSliding(): boolean {
|
|
573
|
+
return this.carried != null;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/** A press (any pointer's) is down on the control, from landing to letting go. */
|
|
577
|
+
get isPressed(): boolean {
|
|
578
|
+
return this.touch != null;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* Whether the touch presentation applies. Off, under a fine primary
|
|
583
|
+
* pointer, a change slides on the theme's timing, and a drag from the
|
|
584
|
+
* selected segment still carries the lens. A change of presentation hands
|
|
585
|
+
* anything in flight back.
|
|
586
|
+
*/
|
|
587
|
+
present(isPresenting: boolean): void {
|
|
588
|
+
if (this.isPresenting === isPresenting) {
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
this.isPresenting = isPresenting;
|
|
592
|
+
this.settle();
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/** The segment that commits `value`, if the track has one. */
|
|
596
|
+
segmentFor(track: HTMLElement, value: string): HTMLElement | null {
|
|
597
|
+
return (
|
|
598
|
+
segmentsOf(track).find(segment => segment.dataset.value === value) ?? null
|
|
599
|
+
);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/** The selection moved from `from` to `to`; the DOM already shows `to`. */
|
|
603
|
+
select(track: HTMLElement, from: string, to: string): void {
|
|
604
|
+
if (this.track !== track) {
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
const origin = this.segmentFor(track, from);
|
|
608
|
+
const destination = this.segmentFor(track, to);
|
|
609
|
+
const isTouchCommit = this.committed === to;
|
|
610
|
+
// The commit's own styles take over from a drag's borrowed faces.
|
|
611
|
+
this.clearFaces();
|
|
612
|
+
this.committed = null;
|
|
613
|
+
this.isAwaitingValue = false;
|
|
614
|
+
if (isTouchCommit) {
|
|
615
|
+
if (this.pending?.destination === destination && destination != null) {
|
|
616
|
+
// Adopted during the click: the lens moves now, on the box the
|
|
617
|
+
// destination has with the new value shown.
|
|
618
|
+
this.startPending(destination, boxesIn(track)(destination));
|
|
619
|
+
}
|
|
620
|
+
// The new selection now paints the surface the lens carries, and its
|
|
621
|
+
// titles' weights may have moved the segments since the lens set off.
|
|
622
|
+
const carried = this.carried;
|
|
623
|
+
if (carried != null && carried.segment === destination) {
|
|
624
|
+
if (carried.hidden !== destination) {
|
|
625
|
+
showSurface(carried.hidden);
|
|
626
|
+
paintLayer(track, 'lens', destination);
|
|
627
|
+
hideSurface(destination);
|
|
628
|
+
carried.hidden = destination;
|
|
629
|
+
}
|
|
630
|
+
this.reaim(track, destination);
|
|
631
|
+
}
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
if (!this.slidesChanges()) {
|
|
635
|
+
// A fine pointer under Reduce Motion: the theme's transition shows the
|
|
636
|
+
// change.
|
|
637
|
+
this.settle();
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
if (destination == null) {
|
|
641
|
+
// Nothing is selected any more: no surface to carry.
|
|
642
|
+
this.settle();
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
if (this.carried == null && origin == null) {
|
|
646
|
+
// Nothing was selected: there is no surface to slide from.
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
const boxOf = boxesIn(track);
|
|
650
|
+
const to_ = boxOf(destination);
|
|
651
|
+
if (!isLaidOut(to_)) {
|
|
652
|
+
this.settle();
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
const from_ = this.carried == null ? boxOf(origin as HTMLElement) : null;
|
|
656
|
+
if (this.animator.reducedMotion() !== 'off') {
|
|
657
|
+
this.crossFadeTo(destination, to_, from_, destination, false);
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
const at = this.carry(destination, to_, from_, destination);
|
|
661
|
+
// A programmatic or keyboard change: the whole box slides.
|
|
662
|
+
this.settleLens(destination, {at});
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* Whether a change of value slides the lens. A fine pointer under Reduce
|
|
667
|
+
* Motion leaves the change to the theme's transition.
|
|
668
|
+
*/
|
|
669
|
+
private slidesChanges(): boolean {
|
|
670
|
+
return this.isPresenting || this.animator.reducedMotion() === 'off';
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/**
|
|
674
|
+
* A press landed on `origin`, the segment the action-group tracker resolved
|
|
675
|
+
* for it (null: none). It reads the track's geometry and title faces once.
|
|
676
|
+
* Under Reduced Motion a finger's release cross-fades.
|
|
677
|
+
*/
|
|
678
|
+
begin(track: HTMLElement, origin: HTMLElement | null, isFinger = true): void {
|
|
679
|
+
const segments = segmentsOf(track);
|
|
680
|
+
const boxOf = boxesIn(track);
|
|
681
|
+
const boxes = new Map(segments.map(segment => [segment, boxOf(segment)]));
|
|
682
|
+
const toTrackX = trackXIn(track);
|
|
683
|
+
const selected =
|
|
684
|
+
segments.find(
|
|
685
|
+
segment => segment.getAttribute('aria-checked') === 'true',
|
|
686
|
+
) ?? null;
|
|
687
|
+
const carriesLens = origin != null && origin === selected;
|
|
688
|
+
const selectedTitle = selected != null ? titleOf(selected) : null;
|
|
689
|
+
// An enabled segment's unselected face: a disabled title has its own.
|
|
690
|
+
const other = segments.find(
|
|
691
|
+
segment => segment !== selected && isEnabledSegment(segment),
|
|
692
|
+
);
|
|
693
|
+
const otherTitle = other != null ? titleOf(other) : null;
|
|
694
|
+
const faces =
|
|
695
|
+
selectedTitle != null && otherTitle != null
|
|
696
|
+
? {selected: faceOf(selectedTitle), unselected: faceOf(otherTitle)}
|
|
697
|
+
: null;
|
|
698
|
+
this.touch = {
|
|
699
|
+
toTrackX,
|
|
700
|
+
boxes,
|
|
701
|
+
origin,
|
|
702
|
+
carriesLens,
|
|
703
|
+
isFinger,
|
|
704
|
+
faces,
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
/**
|
|
709
|
+
* The finger moved to `clientX`. A drag from the selected segment carries
|
|
710
|
+
* the lens at its resting size. A touch elsewhere leaves the lens alone,
|
|
711
|
+
* and that segment shows the shared pressed overlay.
|
|
712
|
+
*/
|
|
713
|
+
drag(clientX: number): void {
|
|
714
|
+
const touch = this.touch;
|
|
715
|
+
if (touch == null || !touch.carriesLens) {
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
this.hold();
|
|
719
|
+
if (!this.isHeld) {
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
this.chase(clientX);
|
|
723
|
+
this.followLens();
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
/**
|
|
727
|
+
* The first move of a drag from the selected segment binds the lens there,
|
|
728
|
+
* at its resting size. Under Reduced Motion too: the lens follows the hand,
|
|
729
|
+
* and only the release cross-fades.
|
|
730
|
+
*/
|
|
731
|
+
private hold(): void {
|
|
732
|
+
const touch = this.touch;
|
|
733
|
+
if (touch == null || touch.origin == null || this.isHeld) {
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
const box = touch.boxes.get(touch.origin) as Box;
|
|
737
|
+
if (!isLaidOut(box)) {
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
this.carry(touch.origin, box, null, touch.origin);
|
|
741
|
+
this.isHeld = true;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
/**
|
|
745
|
+
* The selected face follows the held lens: it goes to the segment under the
|
|
746
|
+
* lens's presented center. Hysteresis around each boundary keeps a finger
|
|
747
|
+
* that rests on a boundary from flipping it. This runs every frame while
|
|
748
|
+
* the lens is held, so the face keeps up as the lens settles under a still
|
|
749
|
+
* finger.
|
|
750
|
+
*/
|
|
751
|
+
private followLens(): void {
|
|
752
|
+
const touch = this.touch;
|
|
753
|
+
const carried = this.carried;
|
|
754
|
+
if (
|
|
755
|
+
touch == null ||
|
|
756
|
+
!touch.carriesLens ||
|
|
757
|
+
!this.isHeld ||
|
|
758
|
+
carried == null ||
|
|
759
|
+
touch.origin == null
|
|
760
|
+
) {
|
|
761
|
+
this.lensFrame = null;
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
const lensCenter = this.presented(carried.box, this.now()).center;
|
|
765
|
+
const spans = [...touch.boxes.entries()]
|
|
766
|
+
.filter(([segment]) => isEnabledSegment(segment))
|
|
767
|
+
.sort((a, b) => a[1].x - b[1].x);
|
|
768
|
+
const current = this.facedSegment ?? touch.origin;
|
|
769
|
+
const at = spans.findIndex(([segment]) => segment === current);
|
|
770
|
+
if (at >= 0) {
|
|
771
|
+
const edge = (i: number) =>
|
|
772
|
+
(spans[i][1].x + spans[i][1].width + spans[i + 1][1].x) / 2;
|
|
773
|
+
let next = at;
|
|
774
|
+
while (next + 1 < spans.length && lensCenter > edge(next)) {
|
|
775
|
+
next += 1;
|
|
776
|
+
}
|
|
777
|
+
while (next > 0 && lensCenter < edge(next - 1)) {
|
|
778
|
+
next -= 1;
|
|
779
|
+
}
|
|
780
|
+
if (next !== at) {
|
|
781
|
+
const boundary = next > at ? edge(at) : edge(at - 1);
|
|
782
|
+
const hysteresis = Math.max(
|
|
783
|
+
FACE_HYSTERESIS_MIN_PX,
|
|
784
|
+
FACE_HYSTERESIS_RATIO * spans[next][1].width,
|
|
785
|
+
);
|
|
786
|
+
if (Math.abs(lensCenter - boundary) >= hysteresis) {
|
|
787
|
+
const segment = spans[next][0];
|
|
788
|
+
this.showFaceAt(segment === touch.origin ? null : segment);
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
const view = this.track?.ownerDocument.defaultView;
|
|
793
|
+
if (this.lensFrame == null && view != null) {
|
|
794
|
+
this.lensFrame = view.requestAnimationFrame(() => {
|
|
795
|
+
this.lensFrame = null;
|
|
796
|
+
this.followLens();
|
|
797
|
+
});
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
/** The held lens's center chases the finger at `clientX`. */
|
|
802
|
+
private chase(clientX: number): void {
|
|
803
|
+
const touch = this.touch;
|
|
804
|
+
const carried = this.carried;
|
|
805
|
+
if (touch == null || carried == null) {
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
const centers = [...touch.boxes.values()].map(center);
|
|
809
|
+
const reach = segmentedControl.dragOvershoot;
|
|
810
|
+
const target = Math.min(
|
|
811
|
+
Math.max(touch.toTrackX(clientX), Math.min(...centers) - reach),
|
|
812
|
+
Math.max(...centers) + reach,
|
|
813
|
+
);
|
|
814
|
+
if (!this.isPresenting || this.animator.reducedMotion() !== 'off') {
|
|
815
|
+
// A mouse or a pen holds the lens directly, as a finger does under
|
|
816
|
+
// Reduce Motion. Each move writes the pointer's position.
|
|
817
|
+
this.animator.set({x: target - center(carried.box)});
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
// Start each chase at the call. A chase from rest waits for the browser
|
|
821
|
+
// to start it. Android Chrome sends the next move first, so the lens
|
|
822
|
+
// would stick.
|
|
823
|
+
const at = this.now();
|
|
824
|
+
this.springLens(
|
|
825
|
+
{x: target - center(carried.box)},
|
|
826
|
+
this.springFrom({x: target - center(carried.box)}, 'tracking'),
|
|
827
|
+
{at},
|
|
828
|
+
);
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
/**
|
|
832
|
+
* The finger let go on `target`, the segment the tracker resolved for it
|
|
833
|
+
* (null: none, or a disabled one). Moves the lens there and returns its
|
|
834
|
+
* value when it is not the selected one (the tracker's lift commits it
|
|
835
|
+
* through its click), else null.
|
|
836
|
+
*/
|
|
837
|
+
release(target: HTMLElement | null): string | null {
|
|
838
|
+
const touch = this.touch;
|
|
839
|
+
this.touch = null;
|
|
840
|
+
if (touch == null) {
|
|
841
|
+
return null;
|
|
842
|
+
}
|
|
843
|
+
const selected =
|
|
844
|
+
[...touch.boxes.keys()].find(
|
|
845
|
+
segment => segment.getAttribute('aria-checked') === 'true',
|
|
846
|
+
) ?? null;
|
|
847
|
+
// A disabled segment never commits: the lens settles back on the
|
|
848
|
+
// selection.
|
|
849
|
+
const destination =
|
|
850
|
+
(target != null && isEnabledSegment(target) ? target : null) ?? selected;
|
|
851
|
+
if (destination == null) {
|
|
852
|
+
this.clearFaces();
|
|
853
|
+
if (this.isHeld) {
|
|
854
|
+
this.isHeld = false;
|
|
855
|
+
this.settle();
|
|
856
|
+
}
|
|
857
|
+
return null;
|
|
858
|
+
}
|
|
859
|
+
const box = touch.boxes.get(destination) as Box;
|
|
860
|
+
const value =
|
|
861
|
+
destination !== selected ? (destination.dataset.value ?? null) : null;
|
|
862
|
+
// A drag's borrowed face stays only if the lift commits (the commit
|
|
863
|
+
// takes it over).
|
|
864
|
+
if (value == null) {
|
|
865
|
+
this.clearFaces();
|
|
866
|
+
}
|
|
867
|
+
const isHeld = touch.carriesLens && this.isHeld;
|
|
868
|
+
this.committed = value;
|
|
869
|
+
this.isAwaitingValue = false;
|
|
870
|
+
this.releasedByFinger = touch.isFinger;
|
|
871
|
+
if (value == null) {
|
|
872
|
+
if (isHeld) {
|
|
873
|
+
// The held lens settles back on the selection.
|
|
874
|
+
this.moveLensTo(
|
|
875
|
+
destination,
|
|
876
|
+
box,
|
|
877
|
+
selected ?? destination,
|
|
878
|
+
touch.isFinger,
|
|
879
|
+
);
|
|
880
|
+
}
|
|
881
|
+
return null;
|
|
882
|
+
}
|
|
883
|
+
// The commit follows at once, through the segment's click; the lens
|
|
884
|
+
// moves once it has run, on the box the destination has then. A held
|
|
885
|
+
// lens settles there; a tap (or a drag that started elsewhere) slides
|
|
886
|
+
// there from the selected segment.
|
|
887
|
+
// A fine pointer under Reduce Motion moves only a held lens.
|
|
888
|
+
if (isHeld || (selected != null && this.slidesChanges())) {
|
|
889
|
+
this.pending = {
|
|
890
|
+
destination,
|
|
891
|
+
box,
|
|
892
|
+
isHeld,
|
|
893
|
+
from:
|
|
894
|
+
!isHeld && selected != null
|
|
895
|
+
? (touch.boxes.get(selected) as Box)
|
|
896
|
+
: null,
|
|
897
|
+
touchRelease: touch.isFinger,
|
|
898
|
+
};
|
|
899
|
+
}
|
|
900
|
+
return value;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
/**
|
|
904
|
+
* The commit's click for `value` has run. A click that the item's
|
|
905
|
+
* `onClick` cancels sends the lens back at once. Otherwise the lens waits
|
|
906
|
+
* for the value, which a parent can adopt later.
|
|
907
|
+
*/
|
|
908
|
+
clicked(track: HTMLElement, value: string, isCancelled: boolean): void {
|
|
909
|
+
if (this.committed !== value) {
|
|
910
|
+
// Adopted during the click, or superseded.
|
|
911
|
+
return;
|
|
912
|
+
}
|
|
913
|
+
if (isCancelled) {
|
|
914
|
+
this.pending = null;
|
|
915
|
+
this.decline(track, value);
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
918
|
+
this.isAwaitingValue = true;
|
|
919
|
+
// React can render the click's change only after the event that
|
|
920
|
+
// dispatched it ends: a click inside a touch handler is batched with it.
|
|
921
|
+
// Wait one turn. If the value is adopted by then, the lens already moves
|
|
922
|
+
// on the new box. Otherwise it aims at the box measured at landing and
|
|
923
|
+
// re-aims when the value comes back.
|
|
924
|
+
queueMicrotask(() => {
|
|
925
|
+
if (this.committed === value && this.pending != null) {
|
|
926
|
+
this.startPending(this.selectedIn(track));
|
|
927
|
+
}
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
/**
|
|
932
|
+
* The value the finger let go on was not adopted. The item's `onClick`
|
|
933
|
+
* cancelled it, or a controlled parent kept its value. The lens goes back
|
|
934
|
+
* to the selected segment. The next change to that value slides as any
|
|
935
|
+
* change does.
|
|
936
|
+
*/
|
|
937
|
+
decline(track: HTMLElement, value: string): void {
|
|
938
|
+
if (this.committed !== value) {
|
|
939
|
+
return;
|
|
940
|
+
}
|
|
941
|
+
this.committed = null;
|
|
942
|
+
this.isAwaitingValue = false;
|
|
943
|
+
this.pending = null;
|
|
944
|
+
this.clearFaces();
|
|
945
|
+
const selected =
|
|
946
|
+
segmentsOf(track).find(
|
|
947
|
+
segment => segment.getAttribute('aria-checked') === 'true',
|
|
948
|
+
) ?? null;
|
|
949
|
+
if (selected == null || this.carried == null) {
|
|
950
|
+
this.settle();
|
|
951
|
+
return;
|
|
952
|
+
}
|
|
953
|
+
const box = boxesIn(track)(selected);
|
|
954
|
+
if (!isLaidOut(box)) {
|
|
955
|
+
this.settle();
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
958
|
+
this.moveLensTo(selected, box, selected, this.releasedByFinger);
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
/** The touch was taken away (the page scrolled): the lens goes back. */
|
|
962
|
+
cancel(): void {
|
|
963
|
+
const touch = this.touch;
|
|
964
|
+
this.touch = null;
|
|
965
|
+
this.clearFaces();
|
|
966
|
+
if (
|
|
967
|
+
touch == null ||
|
|
968
|
+
!touch.carriesLens ||
|
|
969
|
+
touch.origin == null ||
|
|
970
|
+
!this.isHeld
|
|
971
|
+
) {
|
|
972
|
+
// Nothing was held: nothing to put back.
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
975
|
+
const box = touch.boxes.get(touch.origin) as Box;
|
|
976
|
+
this.moveLensTo(touch.origin, box, touch.origin, touch.isFinger);
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
/** Hands the surface back to its segment now and hides the lens. */
|
|
980
|
+
settle(): void {
|
|
981
|
+
this.endLeaving();
|
|
982
|
+
this.isHeld = false;
|
|
983
|
+
this.pending = null;
|
|
984
|
+
const carried = this.carried;
|
|
985
|
+
if (carried != null) {
|
|
986
|
+
this.carried = null;
|
|
987
|
+
this.running.clear();
|
|
988
|
+
this.animator.stop();
|
|
989
|
+
this.after.stop();
|
|
990
|
+
showSurface(carried.hidden);
|
|
991
|
+
for (const segment of this.abandoned) {
|
|
992
|
+
showSurface(segment);
|
|
993
|
+
}
|
|
994
|
+
this.abandoned.clear();
|
|
995
|
+
if (this.track != null) {
|
|
996
|
+
clearLayer(this.track, 'lens');
|
|
997
|
+
clearLayer(this.track, 'after');
|
|
998
|
+
}
|
|
999
|
+
this.isLeavingShown = false;
|
|
1000
|
+
}
|
|
1001
|
+
// Once the surfaces are back on screen, so nothing fades in.
|
|
1002
|
+
this.thawTransitions();
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
/**
|
|
1006
|
+
* The control resized. The lens aims at boxes measured when its motion
|
|
1007
|
+
* began, so it hands the surface back rather than land somewhere stale.
|
|
1008
|
+
* Two cases keep the motion. In the first, the track keeps its measured
|
|
1009
|
+
* size: a new title weight resized a content-sized control before the box
|
|
1010
|
+
* was measured. In the second, a finger holds the lens.
|
|
1011
|
+
*/
|
|
1012
|
+
resized(track: HTMLElement): void {
|
|
1013
|
+
const carried = this.carried;
|
|
1014
|
+
// A finger that holds the lens reflows the titles itself, because the
|
|
1015
|
+
// selected face moves to the title under it. The lens follows the finger,
|
|
1016
|
+
// and the commit measures the destination again.
|
|
1017
|
+
if (carried == null || this.touch != null) {
|
|
1018
|
+
return;
|
|
1019
|
+
}
|
|
1020
|
+
if (
|
|
1021
|
+
Math.abs(track.offsetWidth - carried.trackSize.width) < 0.5 &&
|
|
1022
|
+
Math.abs(track.offsetHeight - carried.trackSize.height) < 0.5
|
|
1023
|
+
) {
|
|
1024
|
+
return;
|
|
1025
|
+
}
|
|
1026
|
+
this.settle();
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
/**
|
|
1030
|
+
* Ends everything in flight (unmount). The controller stays usable, so an
|
|
1031
|
+
* effect cleanup followed by a setup (StrictMode's extra mount) loses
|
|
1032
|
+
* nothing.
|
|
1033
|
+
*/
|
|
1034
|
+
stop(): void {
|
|
1035
|
+
this.touch = null;
|
|
1036
|
+
if (this.lensFrame != null) {
|
|
1037
|
+
this.track?.ownerDocument.defaultView?.cancelAnimationFrame(
|
|
1038
|
+
this.lensFrame,
|
|
1039
|
+
);
|
|
1040
|
+
this.lensFrame = null;
|
|
1041
|
+
}
|
|
1042
|
+
this.committed = null;
|
|
1043
|
+
this.isAwaitingValue = false;
|
|
1044
|
+
this.clearFaces();
|
|
1045
|
+
this.settle();
|
|
1046
|
+
this.cancelThaw();
|
|
1047
|
+
this.restoreTransitions();
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
/**
|
|
1051
|
+
* A fine pointer's carry. The segments' own transitions go off while the
|
|
1052
|
+
* lens is on screen, so the borrowed surface does not fade out and back in.
|
|
1053
|
+
* Under a finger, the touch presentation already turns them off.
|
|
1054
|
+
*/
|
|
1055
|
+
private stillTransitions(): void {
|
|
1056
|
+
const track = this.track;
|
|
1057
|
+
if (track == null) {
|
|
1058
|
+
return;
|
|
1059
|
+
}
|
|
1060
|
+
this.cancelThaw();
|
|
1061
|
+
for (const segment of segmentsOf(track)) {
|
|
1062
|
+
if (!this.stilled.has(segment)) {
|
|
1063
|
+
this.stilled.set(segment, [
|
|
1064
|
+
segment.style.getPropertyValue('transition-property'),
|
|
1065
|
+
segment.style.getPropertyPriority('transition-property'),
|
|
1066
|
+
]);
|
|
1067
|
+
}
|
|
1068
|
+
segment.style.setProperty('transition-property', 'none', 'important');
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
/**
|
|
1073
|
+
* Gives the stilled segments their transitions back two frames after the
|
|
1074
|
+
* hand-back, once the surfaces it gave back have been on screen.
|
|
1075
|
+
*/
|
|
1076
|
+
private thawTransitions(): void {
|
|
1077
|
+
if (this.stilled.size === 0 || this.thawFrame != null) {
|
|
1078
|
+
return;
|
|
1079
|
+
}
|
|
1080
|
+
const view = this.track?.ownerDocument.defaultView;
|
|
1081
|
+
if (view == null) {
|
|
1082
|
+
this.restoreTransitions();
|
|
1083
|
+
return;
|
|
1084
|
+
}
|
|
1085
|
+
this.thawFrame = view.requestAnimationFrame(() => {
|
|
1086
|
+
this.thawFrame = view.requestAnimationFrame(() => {
|
|
1087
|
+
this.thawFrame = null;
|
|
1088
|
+
this.restoreTransitions();
|
|
1089
|
+
});
|
|
1090
|
+
});
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
private cancelThaw(): void {
|
|
1094
|
+
if (this.thawFrame != null) {
|
|
1095
|
+
this.track?.ownerDocument.defaultView?.cancelAnimationFrame(
|
|
1096
|
+
this.thawFrame,
|
|
1097
|
+
);
|
|
1098
|
+
this.thawFrame = null;
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
private restoreTransitions(): void {
|
|
1103
|
+
for (const [segment, [value, priority]] of this.stilled) {
|
|
1104
|
+
if (value === '') {
|
|
1105
|
+
segment.style.removeProperty('transition-property');
|
|
1106
|
+
} else {
|
|
1107
|
+
segment.style.setProperty('transition-property', value, priority);
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
this.stilled.clear();
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
/** The selected segment, as the DOM shows it now. */
|
|
1114
|
+
private selectedIn(track: HTMLElement): HTMLElement | null {
|
|
1115
|
+
return (
|
|
1116
|
+
segmentsOf(track).find(
|
|
1117
|
+
segment => segment.getAttribute('aria-checked') === 'true',
|
|
1118
|
+
) ?? null
|
|
1119
|
+
);
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
/**
|
|
1123
|
+
* Starts the motion the release left pending: on `box` (the destination's
|
|
1124
|
+
* box now) or the box measured at landing, showing `paint`'s surface (the
|
|
1125
|
+
* selected segment's).
|
|
1126
|
+
*/
|
|
1127
|
+
private startPending(paint: HTMLElement | null, box?: Box): void {
|
|
1128
|
+
const pending = this.pending;
|
|
1129
|
+
this.pending = null;
|
|
1130
|
+
if (pending == null) {
|
|
1131
|
+
return;
|
|
1132
|
+
}
|
|
1133
|
+
const to = box ?? pending.box;
|
|
1134
|
+
const surface = this.carried?.hidden ?? paint ?? pending.destination;
|
|
1135
|
+
if (pending.isHeld) {
|
|
1136
|
+
this.moveLensTo(pending.destination, to, surface, pending.touchRelease);
|
|
1137
|
+
} else if (pending.from != null) {
|
|
1138
|
+
this.slideTo(
|
|
1139
|
+
pending.destination,
|
|
1140
|
+
to,
|
|
1141
|
+
pending.from,
|
|
1142
|
+
surface,
|
|
1143
|
+
pending.touchRelease,
|
|
1144
|
+
);
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
/**
|
|
1149
|
+
* Aims the lens at `destination`'s box as it is now: a value adopted
|
|
1150
|
+
* after its click (mid-flight) moves the segments with its titles' new
|
|
1151
|
+
* weights.
|
|
1152
|
+
*/
|
|
1153
|
+
private reaim(track: HTMLElement, destination: HTMLElement): void {
|
|
1154
|
+
const carried = this.carried;
|
|
1155
|
+
if (carried == null) {
|
|
1156
|
+
return;
|
|
1157
|
+
}
|
|
1158
|
+
const box = boxesIn(track)(destination);
|
|
1159
|
+
if (!isLaidOut(box) || isSameBox(box, carried.box)) {
|
|
1160
|
+
return;
|
|
1161
|
+
}
|
|
1162
|
+
this.moveLensTo(destination, box, carried.hidden, this.releasedByFinger);
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
/**
|
|
1166
|
+
* Binds the lens for `segment`, with `box` as its layout box and `paint`'s
|
|
1167
|
+
* surface, the selected segment's. It keeps what is on screen. With nothing
|
|
1168
|
+
* carried, it starts from `from`, a segment's resting box. Otherwise it
|
|
1169
|
+
* starts from the presented geometry, re-based onto the new box with its
|
|
1170
|
+
* velocities.
|
|
1171
|
+
*/
|
|
1172
|
+
private carry(
|
|
1173
|
+
segment: HTMLElement,
|
|
1174
|
+
box: Box,
|
|
1175
|
+
from: Box | null,
|
|
1176
|
+
paint: HTMLElement,
|
|
1177
|
+
): number | undefined {
|
|
1178
|
+
const track = this.track;
|
|
1179
|
+
if (track == null) {
|
|
1180
|
+
return undefined;
|
|
1181
|
+
}
|
|
1182
|
+
if (!this.isPresenting) {
|
|
1183
|
+
// Before the surface is borrowed: it must not fade out.
|
|
1184
|
+
this.stillTransitions();
|
|
1185
|
+
}
|
|
1186
|
+
const previous = this.carried;
|
|
1187
|
+
let start: MotionValues;
|
|
1188
|
+
// Motion already on screen continues from this instant; a start from
|
|
1189
|
+
// rest waits for the browser to commit it (the animator's rule).
|
|
1190
|
+
let at: number | undefined;
|
|
1191
|
+
this.velocity = {};
|
|
1192
|
+
if (previous != null) {
|
|
1193
|
+
at = this.now();
|
|
1194
|
+
const presented = this.presented(previous.box, at);
|
|
1195
|
+
start = {
|
|
1196
|
+
x: presented.center - center(box),
|
|
1197
|
+
scaleX: presented.width / box.width,
|
|
1198
|
+
scaleY: presented.height / box.height,
|
|
1199
|
+
};
|
|
1200
|
+
this.velocity = {
|
|
1201
|
+
x: this.animator.getVelocity('x', at),
|
|
1202
|
+
scaleX:
|
|
1203
|
+
(this.animator.getVelocity('scaleX', at) * previous.box.width) /
|
|
1204
|
+
box.width,
|
|
1205
|
+
scaleY:
|
|
1206
|
+
(this.animator.getVelocity('scaleY', at) * previous.box.height) /
|
|
1207
|
+
box.height,
|
|
1208
|
+
};
|
|
1209
|
+
} else {
|
|
1210
|
+
const origin = from ?? box;
|
|
1211
|
+
start = {
|
|
1212
|
+
x: center(origin) - center(box),
|
|
1213
|
+
scaleX: origin.width / box.width,
|
|
1214
|
+
scaleY: origin.height / box.height,
|
|
1215
|
+
};
|
|
1216
|
+
}
|
|
1217
|
+
if (previous?.hidden !== paint) {
|
|
1218
|
+
if (previous != null) {
|
|
1219
|
+
this.abandoned.add(previous.hidden);
|
|
1220
|
+
}
|
|
1221
|
+
// A segment abandoned mid-flight still has its surface hidden: read
|
|
1222
|
+
// its paint with it shown.
|
|
1223
|
+
if (this.abandoned.has(paint)) {
|
|
1224
|
+
showSurface(paint);
|
|
1225
|
+
}
|
|
1226
|
+
paintLayer(track, 'lens', paint);
|
|
1227
|
+
hideSurface(paint);
|
|
1228
|
+
this.abandoned.delete(paint);
|
|
1229
|
+
}
|
|
1230
|
+
placeLayer(track, 'lens', box);
|
|
1231
|
+
this.carried = {
|
|
1232
|
+
segment,
|
|
1233
|
+
box,
|
|
1234
|
+
hidden: paint,
|
|
1235
|
+
trackSize: {width: track.offsetWidth, height: track.offsetHeight},
|
|
1236
|
+
};
|
|
1237
|
+
// Opaque, whatever a Reduce Motion fade left.
|
|
1238
|
+
this.animator.set({...start, opacity: 1});
|
|
1239
|
+
return at;
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
/** Velocities captured by the last `carry`, in the new box's units. */
|
|
1243
|
+
private velocity: Partial<Record<'x' | 'scaleX' | 'scaleY', number>> = {};
|
|
1244
|
+
|
|
1245
|
+
/** A spring transition that keeps the captured velocities of `values`. */
|
|
1246
|
+
private springFrom(
|
|
1247
|
+
values: MotionValues,
|
|
1248
|
+
kind: 'selection' | 'tracking',
|
|
1249
|
+
): SpringTransition {
|
|
1250
|
+
const spring =
|
|
1251
|
+
kind === 'selection'
|
|
1252
|
+
? segmentedControl.selectionSpring
|
|
1253
|
+
: springs.fingerTracking;
|
|
1254
|
+
const velocity: Record<string, number> = {};
|
|
1255
|
+
for (const key of Object.keys(values) as (keyof typeof this.velocity)[]) {
|
|
1256
|
+
const v = this.velocity[key];
|
|
1257
|
+
if (v != null && v !== 0) {
|
|
1258
|
+
velocity[key] = v;
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
// A captured velocity is spent by the first call that drives its channel.
|
|
1262
|
+
this.velocity = Object.fromEntries(
|
|
1263
|
+
Object.entries(this.velocity).filter(([key]) => !(key in values)),
|
|
1264
|
+
);
|
|
1265
|
+
return Object.keys(velocity).length > 0 ? {spring, velocity} : {spring};
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
/**
|
|
1269
|
+
* The lens settles at its resting place on `segment`. Under a finger it
|
|
1270
|
+
* springs on the slide spring. Under a fine pointer it takes the theme's
|
|
1271
|
+
* duration and easing, as the segments' own transition does.
|
|
1272
|
+
*/
|
|
1273
|
+
private settleLens(segment: HTMLElement, options: AnimateOptions): void {
|
|
1274
|
+
const rest = {x: 0, scaleX: 1, scaleY: 1} as const;
|
|
1275
|
+
if (this.isPresenting) {
|
|
1276
|
+
this.springLens(rest, this.springFrom(rest, 'selection'), options);
|
|
1277
|
+
return;
|
|
1278
|
+
}
|
|
1279
|
+
this.velocity = {};
|
|
1280
|
+
const timing = themeTimingOf(segment) ?? INSTANT;
|
|
1281
|
+
const at = options.at;
|
|
1282
|
+
this.trackHandle(
|
|
1283
|
+
this.animator.play(
|
|
1284
|
+
{
|
|
1285
|
+
x: timedCurve(this.animator.get('x', at), 0, timing),
|
|
1286
|
+
scaleX: timedCurve(this.animator.get('scaleX', at), 1, timing),
|
|
1287
|
+
scaleY: timedCurve(this.animator.get('scaleY', at), 1, timing),
|
|
1288
|
+
},
|
|
1289
|
+
options,
|
|
1290
|
+
),
|
|
1291
|
+
);
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
/** Springs the lens toward `values`. */
|
|
1295
|
+
private springLens(
|
|
1296
|
+
values: MotionValues,
|
|
1297
|
+
transition: SpringTransition,
|
|
1298
|
+
options?: AnimateOptions,
|
|
1299
|
+
): void {
|
|
1300
|
+
this.trackHandle(this.animator.animate(values, transition, options));
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
/** The lens's presented center and size, from the animator. */
|
|
1304
|
+
private presented(
|
|
1305
|
+
box: Box,
|
|
1306
|
+
at: number,
|
|
1307
|
+
): {center: number; width: number; height: number} {
|
|
1308
|
+
return {
|
|
1309
|
+
center: center(box) + this.animator.get('x', at),
|
|
1310
|
+
width: box.width * this.animator.get('scaleX', at),
|
|
1311
|
+
height: box.height * this.animator.get('scaleY', at),
|
|
1312
|
+
};
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
/**
|
|
1316
|
+
* The lens settles on `segment`, at its resting size. Under Reduced Motion
|
|
1317
|
+
* a finger's release (`touchRelease`) cross-fades on any primary pointer. A
|
|
1318
|
+
* mouse's or a pen's lands at once under a fine one.
|
|
1319
|
+
*/
|
|
1320
|
+
private moveLensTo(
|
|
1321
|
+
segment: HTMLElement,
|
|
1322
|
+
box: Box,
|
|
1323
|
+
paint: HTMLElement,
|
|
1324
|
+
touchRelease: boolean,
|
|
1325
|
+
): void {
|
|
1326
|
+
this.isHeld = false;
|
|
1327
|
+
if (!isLaidOut(box)) {
|
|
1328
|
+
this.settle();
|
|
1329
|
+
return;
|
|
1330
|
+
}
|
|
1331
|
+
if (this.animator.reducedMotion({touchRelease}) !== 'off') {
|
|
1332
|
+
this.crossFadeTo(
|
|
1333
|
+
segment,
|
|
1334
|
+
box,
|
|
1335
|
+
null,
|
|
1336
|
+
this.carried?.hidden ?? paint,
|
|
1337
|
+
touchRelease,
|
|
1338
|
+
);
|
|
1339
|
+
return;
|
|
1340
|
+
}
|
|
1341
|
+
const at = this.carry(segment, box, null, this.carried?.hidden ?? paint);
|
|
1342
|
+
this.settleLens(segment, {at});
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
/**
|
|
1346
|
+
* A tap's slide: from `from` to `segment` on the slide spring. Under Reduced
|
|
1347
|
+
* Motion there is no travel, and a finger's tap (`touchRelease`) fades.
|
|
1348
|
+
*/
|
|
1349
|
+
private slideTo(
|
|
1350
|
+
segment: HTMLElement,
|
|
1351
|
+
box: Box,
|
|
1352
|
+
from: Box,
|
|
1353
|
+
paint: HTMLElement,
|
|
1354
|
+
touchRelease: boolean,
|
|
1355
|
+
): void {
|
|
1356
|
+
if (!isLaidOut(box)) {
|
|
1357
|
+
this.settle();
|
|
1358
|
+
return;
|
|
1359
|
+
}
|
|
1360
|
+
if (this.animator.reducedMotion({touchRelease}) !== 'off') {
|
|
1361
|
+
this.crossFadeTo(
|
|
1362
|
+
segment,
|
|
1363
|
+
box,
|
|
1364
|
+
this.carried == null ? from : null,
|
|
1365
|
+
this.carried?.hidden ?? paint,
|
|
1366
|
+
touchRelease,
|
|
1367
|
+
);
|
|
1368
|
+
return;
|
|
1369
|
+
}
|
|
1370
|
+
const at = this.carry(
|
|
1371
|
+
segment,
|
|
1372
|
+
box,
|
|
1373
|
+
this.carried == null ? from : null,
|
|
1374
|
+
this.carried?.hidden ?? paint,
|
|
1375
|
+
);
|
|
1376
|
+
this.settleLens(segment, {at});
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
/**
|
|
1380
|
+
* Under Reduced Motion, the lens lands on `segment` and fades in there, as
|
|
1381
|
+
* an animator entrance. The surface on screen fades out in place on the
|
|
1382
|
+
* same clock. That surface is the lens when it was carried, else `from`'s
|
|
1383
|
+
* resting surface. The animator decides whether anything fades. On a fine
|
|
1384
|
+
* pointer, both change at once unless a touch released the change
|
|
1385
|
+
* (`touchRelease`).
|
|
1386
|
+
*/
|
|
1387
|
+
private crossFadeTo(
|
|
1388
|
+
segment: HTMLElement,
|
|
1389
|
+
box: Box,
|
|
1390
|
+
from: Box | null,
|
|
1391
|
+
paint: HTMLElement,
|
|
1392
|
+
touchRelease: boolean,
|
|
1393
|
+
): void {
|
|
1394
|
+
const track = this.track;
|
|
1395
|
+
if (track == null) {
|
|
1396
|
+
return;
|
|
1397
|
+
}
|
|
1398
|
+
// What is on screen before the lens is rebound.
|
|
1399
|
+
let leaving: Box | null = null;
|
|
1400
|
+
let opacity = 1;
|
|
1401
|
+
const previous = this.carried;
|
|
1402
|
+
if (previous != null) {
|
|
1403
|
+
const now = this.now();
|
|
1404
|
+
const presented = this.presented(previous.box, now);
|
|
1405
|
+
leaving = {
|
|
1406
|
+
x: presented.center - presented.width / 2,
|
|
1407
|
+
y: previous.box.y,
|
|
1408
|
+
width: presented.width,
|
|
1409
|
+
height: previous.box.height,
|
|
1410
|
+
};
|
|
1411
|
+
opacity = this.animator.get('opacity', now);
|
|
1412
|
+
} else if (from != null) {
|
|
1413
|
+
leaving = from;
|
|
1414
|
+
}
|
|
1415
|
+
this.isHeld = false;
|
|
1416
|
+
this.endLeaving();
|
|
1417
|
+
this.after.stop();
|
|
1418
|
+
clearLayer(track, 'after');
|
|
1419
|
+
this.isLeavingShown = false;
|
|
1420
|
+
const fades =
|
|
1421
|
+
leaving != null &&
|
|
1422
|
+
opacity > 0 &&
|
|
1423
|
+
this.animator.reducedMotion({touchRelease}) === 'crossfade';
|
|
1424
|
+
if (fades) {
|
|
1425
|
+
this.leave(track, previous != null, paint, leaving as Box, opacity, box);
|
|
1426
|
+
}
|
|
1427
|
+
const at = this.carry(segment, box, from, paint);
|
|
1428
|
+
this.velocity = {};
|
|
1429
|
+
this.trackHandle(
|
|
1430
|
+
this.animator.animate(
|
|
1431
|
+
{x: 0, scaleX: 1, scaleY: 1},
|
|
1432
|
+
{spring: segmentedControl.selectionSpring},
|
|
1433
|
+
{at, crossfadeAs: 'entrance', touchRelease},
|
|
1434
|
+
),
|
|
1435
|
+
);
|
|
1436
|
+
if (fades) {
|
|
1437
|
+
this.fadeLeaving(box, leaving as Box, at, touchRelease);
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
/**
|
|
1442
|
+
* Paints the ::after with the surface on screen (the lens's paint, or
|
|
1443
|
+
* `paint`'s resting surface) where it is (`from`, at `opacity`).
|
|
1444
|
+
*/
|
|
1445
|
+
private leave(
|
|
1446
|
+
track: HTMLElement,
|
|
1447
|
+
fromLens: boolean,
|
|
1448
|
+
paint: HTMLElement,
|
|
1449
|
+
from: Box,
|
|
1450
|
+
opacity: number,
|
|
1451
|
+
box: Box,
|
|
1452
|
+
): void {
|
|
1453
|
+
if (fromLens) {
|
|
1454
|
+
copyLensPaint(track);
|
|
1455
|
+
} else {
|
|
1456
|
+
paintLayer(track, 'after', paint);
|
|
1457
|
+
}
|
|
1458
|
+
placeLayer(track, 'after', {...from, y: box.y, height: box.height});
|
|
1459
|
+
this.isLeavingShown = true;
|
|
1460
|
+
this.after.set({x: 0, scaleX: 1, scaleY: 1, opacity});
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
/**
|
|
1464
|
+
* The ::after fades out in place while the lens fades in on `box`. As a
|
|
1465
|
+
* leaving travel, it fades on the animator's Reduce Motion clock. It lands
|
|
1466
|
+
* only once it is invisible.
|
|
1467
|
+
*/
|
|
1468
|
+
private fadeLeaving(
|
|
1469
|
+
box: Box,
|
|
1470
|
+
from: Box,
|
|
1471
|
+
at: number | undefined,
|
|
1472
|
+
touchRelease: boolean,
|
|
1473
|
+
): void {
|
|
1474
|
+
const id = ++this.leaving;
|
|
1475
|
+
const handle = this.after.animate(
|
|
1476
|
+
{x: center(box) - center(from), opacity: 0},
|
|
1477
|
+
{spring: segmentedControl.selectionSpring},
|
|
1478
|
+
{at, touchRelease},
|
|
1479
|
+
);
|
|
1480
|
+
void handle.finished.then(() => {
|
|
1481
|
+
if (this.leaving === id) {
|
|
1482
|
+
this.endLeaving();
|
|
1483
|
+
}
|
|
1484
|
+
});
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
private endLeaving(): void {
|
|
1488
|
+
if (!this.isLeavingShown) {
|
|
1489
|
+
return;
|
|
1490
|
+
}
|
|
1491
|
+
this.isLeavingShown = false;
|
|
1492
|
+
this.leaving++;
|
|
1493
|
+
this.after.stop();
|
|
1494
|
+
if (this.track != null) {
|
|
1495
|
+
clearLayer(this.track, 'after');
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
/**
|
|
1500
|
+
* Waits for `handle`; the surface goes back when nothing is left. A
|
|
1501
|
+
* touch's value still waiting to come back when the motion settles goes
|
|
1502
|
+
* back instead: the settled slide bounds the wait.
|
|
1503
|
+
*/
|
|
1504
|
+
private trackHandle(handle: MotionHandle): void {
|
|
1505
|
+
this.running.add(handle);
|
|
1506
|
+
void handle.finished.then(() => {
|
|
1507
|
+
this.running.delete(handle);
|
|
1508
|
+
if (this.running.size > 0 || this.isHeld || this.touch != null) {
|
|
1509
|
+
return;
|
|
1510
|
+
}
|
|
1511
|
+
const committed = this.committed;
|
|
1512
|
+
if (this.isAwaitingValue && committed != null && this.track != null) {
|
|
1513
|
+
this.decline(this.track, committed);
|
|
1514
|
+
return;
|
|
1515
|
+
}
|
|
1516
|
+
if (this.committed == null) {
|
|
1517
|
+
this.settle();
|
|
1518
|
+
}
|
|
1519
|
+
});
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
/**
|
|
1523
|
+
* Gives `segment`'s title the selected face and the selected segment's
|
|
1524
|
+
* title an unselected one (none for null or the selected segment), at once.
|
|
1525
|
+
*/
|
|
1526
|
+
private showFaceAt(segment: HTMLElement | null): void {
|
|
1527
|
+
const touch = this.touch;
|
|
1528
|
+
const selected = touch?.origin ?? null;
|
|
1529
|
+
if (segment === this.facedSegment) {
|
|
1530
|
+
return;
|
|
1531
|
+
}
|
|
1532
|
+
this.clearFaces();
|
|
1533
|
+
if (
|
|
1534
|
+
touch?.faces == null ||
|
|
1535
|
+
selected == null ||
|
|
1536
|
+
segment == null ||
|
|
1537
|
+
segment === selected
|
|
1538
|
+
) {
|
|
1539
|
+
return;
|
|
1540
|
+
}
|
|
1541
|
+
const title = titleOf(segment);
|
|
1542
|
+
const selectedTitle = titleOf(selected);
|
|
1543
|
+
if (title == null || selectedTitle == null) {
|
|
1544
|
+
return;
|
|
1545
|
+
}
|
|
1546
|
+
this.wearFace(title, touch.faces.selected);
|
|
1547
|
+
this.wearFace(selectedTitle, touch.faces.unselected);
|
|
1548
|
+
this.facedSegment = segment;
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
/**
|
|
1552
|
+
* Mid-drag, the face is the color alone. A weight swap changes the titles'
|
|
1553
|
+
* widths, which moves the segments under the finger and shakes the
|
|
1554
|
+
* boundary. The commit's own styles give the new weight at the lift.
|
|
1555
|
+
*/
|
|
1556
|
+
private wearFace(title: HTMLElement, face: TitleFace): void {
|
|
1557
|
+
title.style.color = face.color;
|
|
1558
|
+
this.faceOverrides.add(title);
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
/** Every title back to its own face. */
|
|
1562
|
+
private clearFaces(): void {
|
|
1563
|
+
for (const title of this.faceOverrides) {
|
|
1564
|
+
title.style.removeProperty('color');
|
|
1565
|
+
}
|
|
1566
|
+
this.faceOverrides.clear();
|
|
1567
|
+
this.facedSegment = null;
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
/**
|
|
1572
|
+
* Wires `SegmentedSelectionMotion` to a SegmentedControl. While enabled,
|
|
1573
|
+
* every change of `value` slides the selection in the commit that shows the
|
|
1574
|
+
* new value, so no frame shows it unanimated. A press drives the lens while
|
|
1575
|
+
* `isTracking`.
|
|
1576
|
+
*/
|
|
1577
|
+
export function useSegmentedSelectionMotion(
|
|
1578
|
+
trackRef: RefObject<HTMLElement | null>,
|
|
1579
|
+
value: string,
|
|
1580
|
+
isPresenting: boolean,
|
|
1581
|
+
isTracking: boolean,
|
|
1582
|
+
): {
|
|
1583
|
+
readonly selection: SegmentedSelection;
|
|
1584
|
+
/** The radiogroup's callback ref: it attaches the animators to the node. */
|
|
1585
|
+
readonly attachTrack: RefCallback<HTMLElement>;
|
|
1586
|
+
} {
|
|
1587
|
+
const [motion] = useState(() => new SegmentedSelectionMotion());
|
|
1588
|
+
const previousValueRef = useRef(value);
|
|
1589
|
+
// The lens is the radiogroup's own pseudo-elements. The animators attach to
|
|
1590
|
+
// it while the touch presentation applies or while a press can drag it,
|
|
1591
|
+
// which any pointer can.
|
|
1592
|
+
const isEnabled = isPresenting || isTracking;
|
|
1593
|
+
|
|
1594
|
+
// Syncs the lens animators with the pointer presentation: a flip hands any
|
|
1595
|
+
// motion in flight back.
|
|
1596
|
+
useIsomorphicLayoutEffect(() => {
|
|
1597
|
+
motion.present(isPresenting);
|
|
1598
|
+
}, [isPresenting, motion]);
|
|
1599
|
+
|
|
1600
|
+
useIsomorphicLayoutEffect(() => {
|
|
1601
|
+
const from = previousValueRef.current;
|
|
1602
|
+
previousValueRef.current = value;
|
|
1603
|
+
const track = trackRef.current;
|
|
1604
|
+
if (!isEnabled || track == null || from === value) {
|
|
1605
|
+
return;
|
|
1606
|
+
}
|
|
1607
|
+
motion.select(track, from, value);
|
|
1608
|
+
}, [isEnabled, motion, trackRef, value]);
|
|
1609
|
+
|
|
1610
|
+
// Stops the animators that drive the radiogroup's pseudo-elements.
|
|
1611
|
+
useEffect(() => () => motion.stop(), [motion]);
|
|
1612
|
+
|
|
1613
|
+
// Every pointer's press is tracked. The lift commits the segment nearest
|
|
1614
|
+
// the pointer, through that segment's click.
|
|
1615
|
+
const clickWait = useSegmentedTouchTracking(trackRef, motion, isTracking);
|
|
1616
|
+
|
|
1617
|
+
// The animators attach to the radiogroup while enabled. The lens aims at
|
|
1618
|
+
// boxes measured when its motion began, so a resize hands the surface back.
|
|
1619
|
+
const attachTrack = useCallback(
|
|
1620
|
+
(track: HTMLElement | null) => {
|
|
1621
|
+
if (track == null || !isEnabled) {
|
|
1622
|
+
return undefined;
|
|
1623
|
+
}
|
|
1624
|
+
motion.attachTrack(track);
|
|
1625
|
+
const unobserve = observeResize(track, () => motion.resized(track));
|
|
1626
|
+
return () => {
|
|
1627
|
+
unobserve();
|
|
1628
|
+
motion.settle();
|
|
1629
|
+
motion.attachTrack(null);
|
|
1630
|
+
};
|
|
1631
|
+
},
|
|
1632
|
+
[isEnabled, motion],
|
|
1633
|
+
);
|
|
1634
|
+
|
|
1635
|
+
const [selection] = useState<SegmentedSelection>(() => ({
|
|
1636
|
+
isPressing: () => motion.isPressed || clickWait.isAwaiting(),
|
|
1637
|
+
keyPressed: clickWait.end,
|
|
1638
|
+
}));
|
|
1639
|
+
return {selection, attachTrack};
|
|
1640
|
+
}
|