@astryxdesign/core 0.7.6 → 0.7.7-canary.0eb44a2
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/AppShell/AppShell.d.ts.map +1 -1
- package/dist/AppShell/AppShell.js +23 -3
- package/dist/Avatar/Avatar.d.ts.map +1 -1
- package/dist/Avatar/Avatar.js +1 -1
- package/dist/BottomSheet/BottomSheetPanel.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheetPanel.js +16 -9
- package/dist/Breadcrumbs/BreadcrumbItem.d.ts.map +1 -1
- package/dist/Breadcrumbs/BreadcrumbItem.js +2 -1
- package/dist/Button/Button.d.ts.map +1 -1
- package/dist/Button/Button.js +3 -2
- package/dist/Chat/ChatComposerInput.d.ts +9 -0
- package/dist/Chat/ChatComposerInput.d.ts.map +1 -1
- package/dist/Chat/ChatComposerInput.js +2 -0
- package/dist/Chat/ChatMessage.js +2 -2
- package/dist/Chat/chatMessage.vars.stylex.d.ts +8 -8
- package/dist/Chat/chatMessage.vars.stylex.js +8 -8
- package/dist/CommandPalette/CommandPaletteInput.d.ts.map +1 -1
- package/dist/CommandPalette/CommandPaletteInput.js +8 -1
- package/dist/DateInput/MonthScroller.d.ts.map +1 -1
- package/dist/DateInput/MonthScroller.js +72 -2
- package/dist/DateInput/Wheel.d.ts +17 -1
- package/dist/DateInput/Wheel.d.ts.map +1 -1
- package/dist/DateInput/Wheel.js +179 -19
- package/dist/DateRangeInput/DateRangeInput.d.ts.map +1 -1
- package/dist/DateRangeInput/DateRangeInput.js +63 -49
- package/dist/Dialog/Dialog.d.ts.map +1 -1
- package/dist/Dialog/Dialog.js +37 -1
- package/dist/Drawer/Drawer.d.ts.map +1 -1
- package/dist/Drawer/Drawer.js +6 -0
- package/dist/Drawer/useDrawerDialogPresence.d.ts.map +1 -1
- package/dist/Drawer/useDrawerDialogPresence.js +4 -0
- package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenu.js +20 -4
- package/dist/DropdownMenu/useMenuOverflow.d.ts +13 -3
- package/dist/DropdownMenu/useMenuOverflow.d.ts.map +1 -1
- package/dist/DropdownMenu/useMenuOverflow.js +31 -8
- package/dist/Field/PanelSearchInput.d.ts.map +1 -1
- package/dist/Field/PanelSearchInput.js +8 -1
- package/dist/Field/textEntryTraits.d.ts +6 -6
- package/dist/Field/textEntryTraits.js +6 -6
- package/dist/Layer/layerClickBoundary.d.ts +7 -1
- package/dist/Layer/layerClickBoundary.d.ts.map +1 -1
- package/dist/Layer/layerClickBoundary.js +2 -2
- package/dist/Layer/usePressPin.d.ts +95 -0
- package/dist/Layer/usePressPin.d.ts.map +1 -0
- package/dist/Layer/usePressPin.js +365 -0
- package/dist/Layer/useTouchTrigger.d.ts +23 -1
- package/dist/Layer/useTouchTrigger.d.ts.map +1 -1
- package/dist/Layer/useTouchTrigger.js +45 -7
- package/dist/Lightbox/Lightbox.d.ts.map +1 -1
- package/dist/Lightbox/Lightbox.js +2 -1
- package/dist/Link/Link.d.ts.map +1 -1
- package/dist/Link/Link.js +1 -0
- package/dist/Popover/usePopover.d.ts.map +1 -1
- package/dist/Popover/usePopover.js +56 -7
- package/dist/PowerSearch/PowerSearch.d.ts.map +1 -1
- package/dist/PowerSearch/PowerSearch.js +38 -30
- package/dist/ProgressBar/ProgressBar.d.ts +1 -1
- package/dist/ProgressBar/ProgressBar.d.ts.map +1 -1
- package/dist/ProgressBar/ProgressBar.js +52 -14
- package/dist/ProgressBar/ProgressBarTouch.d.ts +29 -0
- package/dist/ProgressBar/ProgressBarTouch.d.ts.map +1 -0
- package/dist/ProgressBar/ProgressBarTouch.js +91 -0
- package/dist/SegmentedControl/SegmentedControlItem.d.ts.map +1 -1
- package/dist/SegmentedControl/SegmentedControlItem.js +2 -1
- package/dist/Selector/Selector.d.ts.map +1 -1
- package/dist/Selector/Selector.js +25 -13
- package/dist/Slider/Slider.d.ts.map +1 -1
- package/dist/Slider/Slider.js +404 -131
- package/dist/Slider/sliderTouchController.d.ts +189 -0
- package/dist/Slider/sliderTouchController.d.ts.map +1 -0
- package/dist/Slider/sliderTouchController.js +1017 -0
- package/dist/Slider/useSliderTouch.d.ts +22 -0
- package/dist/Slider/useSliderTouch.d.ts.map +1 -0
- package/dist/Slider/useSliderTouch.js +56 -0
- package/dist/Switch/Switch.d.ts +1 -1
- package/dist/Switch/Switch.d.ts.map +1 -1
- package/dist/Switch/Switch.js +200 -46
- package/dist/Switch/switchFill.d.ts +31 -0
- package/dist/Switch/switchFill.d.ts.map +1 -0
- package/dist/Switch/switchFill.js +88 -0
- package/dist/Switch/switchTouch.d.ts +81 -0
- package/dist/Switch/switchTouch.d.ts.map +1 -0
- package/dist/Switch/switchTouch.js +133 -0
- package/dist/Switch/useSwitchTouch.d.ts +285 -0
- package/dist/Switch/useSwitchTouch.d.ts.map +1 -0
- package/dist/Switch/useSwitchTouch.js +998 -0
- package/dist/TabList/Tab.d.ts.map +1 -1
- package/dist/TabList/Tab.js +4 -3
- package/dist/Tokenizer/Tokenizer.d.ts.map +1 -1
- package/dist/Tokenizer/Tokenizer.js +12 -5
- package/dist/Tokenizer/TokenizerItemRemovableContext.d.ts +3 -0
- package/dist/Tokenizer/TokenizerItemRemovableContext.d.ts.map +1 -0
- package/dist/Tokenizer/TokenizerItemRemovableContext.js +17 -0
- package/dist/Tooltip/Tooltip.d.ts +30 -6
- package/dist/Tooltip/Tooltip.d.ts.map +1 -1
- package/dist/Tooltip/Tooltip.js +5 -0
- package/dist/Tooltip/index.d.ts +1 -1
- package/dist/Tooltip/index.d.ts.map +1 -1
- package/dist/Tooltip/useTooltip.d.ts +31 -5
- package/dist/Tooltip/useTooltip.d.ts.map +1 -1
- package/dist/Tooltip/useTooltip.js +59 -18
- package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
- package/dist/Typeahead/BaseTypeahead.js +19 -9
- package/dist/Typeahead/Typeahead.d.ts.map +1 -1
- package/dist/Typeahead/Typeahead.js +26 -15
- package/dist/astryx.css +91 -8
- package/dist/gestures/GestureArena.d.ts +21 -2
- package/dist/gestures/GestureArena.d.ts.map +1 -1
- package/dist/gestures/GestureArena.js +23 -3
- package/dist/gestures/GestureHost.d.ts +123 -3
- package/dist/gestures/GestureHost.d.ts.map +1 -1
- package/dist/gestures/GestureHost.js +797 -15
- package/dist/gestures/GestureRecognizer.d.ts +15 -1
- package/dist/gestures/GestureRecognizer.d.ts.map +1 -1
- package/dist/gestures/GestureRecognizer.js +12 -0
- package/dist/gestures/PanGestureRecognizer.d.ts +8 -0
- package/dist/gestures/PanGestureRecognizer.d.ts.map +1 -1
- package/dist/gestures/PinchGestureRecognizer.d.ts +5 -1
- package/dist/gestures/PinchGestureRecognizer.d.ts.map +1 -1
- package/dist/gestures/PinchGestureRecognizer.js +10 -0
- package/dist/gestures/PressTracker.d.ts +6 -0
- package/dist/gestures/PressTracker.d.ts.map +1 -1
- package/dist/gestures/PressTracker.js +3 -0
- package/dist/gestures/ScreenEdgePanGestureRecognizer.d.ts +3 -3
- package/dist/gestures/ScreenEdgePanGestureRecognizer.d.ts.map +1 -1
- package/dist/gestures/ScreenEdgePanGestureRecognizer.js +7 -5
- package/dist/gestures/nativeScroll.d.ts +16 -2
- package/dist/gestures/nativeScroll.d.ts.map +1 -1
- package/dist/gestures/nativeScroll.js +39 -8
- package/dist/gestures/recognizerInternals.d.ts +4 -2
- package/dist/gestures/recognizerInternals.d.ts.map +1 -1
- package/dist/gestures/types.d.ts +17 -0
- package/dist/gestures/types.d.ts.map +1 -1
- package/dist/gestures/types.js +7 -0
- package/dist/gestures/usePressTracker.d.ts.map +1 -1
- package/dist/gestures/usePressTracker.js +1 -0
- package/dist/gestures/wheelSegmenter.d.ts +56 -0
- package/dist/gestures/wheelSegmenter.d.ts.map +1 -0
- package/dist/gestures/wheelSegmenter.js +122 -0
- package/dist/hooks/useAttachment.d.ts +16 -8
- package/dist/hooks/useAttachment.d.ts.map +1 -1
- package/dist/hooks/useAttachment.js +35 -15
- package/dist/hooks/useFocusTrap.d.ts +15 -0
- package/dist/hooks/useFocusTrap.d.ts.map +1 -1
- package/dist/hooks/useFocusTrap.js +13 -2
- package/dist/hooks/usePager.d.ts +261 -0
- package/dist/hooks/usePager.d.ts.map +1 -0
- package/dist/hooks/usePager.js +1169 -0
- package/dist/motion/animator.d.ts.map +1 -1
- package/dist/motion/animator.js +140 -19
- package/dist/motion/paging.d.ts +69 -0
- package/dist/motion/paging.d.ts.map +1 -0
- package/dist/motion/paging.js +148 -0
- package/dist/motion/uikit/index.d.ts +8 -4
- package/dist/motion/uikit/index.d.ts.map +1 -1
- package/dist/motion/uikit/index.js +8 -4
- package/dist/motion/uikit/paging.d.ts +171 -0
- package/dist/motion/uikit/paging.d.ts.map +1 -0
- package/dist/motion/uikit/paging.js +192 -0
- package/dist/motion/uikit/progressBar.d.ts +79 -0
- package/dist/motion/uikit/progressBar.d.ts.map +1 -0
- package/dist/motion/uikit/progressBar.js +102 -0
- package/dist/motion/uikit/scroll.d.ts +0 -19
- package/dist/motion/uikit/scroll.d.ts.map +1 -1
- package/dist/motion/uikit/scroll.js +2 -21
- package/dist/motion/uikit/slider.d.ts +97 -0
- package/dist/motion/uikit/slider.d.ts.map +1 -0
- package/dist/motion/uikit/slider.js +111 -0
- package/dist/motion/uikit/springs.d.ts +59 -30
- package/dist/motion/uikit/springs.d.ts.map +1 -1
- package/dist/motion/uikit/springs.js +64 -34
- package/dist/motion/uikit/switch.d.ts +166 -0
- package/dist/motion/uikit/switch.d.ts.map +1 -0
- package/dist/motion/uikit/switch.js +186 -0
- package/dist/privateAttributes/pressHoldsDrag.stylex.d.ts +18 -0
- package/dist/privateAttributes/pressHoldsDrag.stylex.d.ts.map +1 -0
- package/dist/privateAttributes/pressHoldsDrag.stylex.js +28 -0
- package/dist/utils/pressFeedback.d.ts.map +1 -1
- package/dist/utils/pressFeedback.js +5 -0
- package/dist/utils/selectionLock.d.ts +43 -0
- package/dist/utils/selectionLock.d.ts.map +1 -1
- package/dist/utils/selectionLock.js +394 -6
- package/package.json +5 -5
- package/src/AppShell/AppShell.doc.mjs +10 -0
- package/src/AppShell/AppShell.tsx +48 -2
- package/src/Avatar/Avatar.tsx +3 -1
- package/src/BottomSheet/BottomSheetPanel.tsx +16 -9
- package/src/Breadcrumbs/BreadcrumbItem.tsx +4 -1
- package/src/Button/Button.tsx +3 -2
- package/src/Chat/Chat.doc.mjs +1 -0
- package/src/Chat/ChatComposerInput.doc.mjs +11 -0
- package/src/Chat/ChatComposerInput.tsx +11 -0
- package/src/Chat/ChatMessage.tsx +2 -2
- package/src/Chat/chatMessage.vars.stylex.ts +8 -8
- package/src/CommandPalette/CommandPaletteInput.tsx +6 -0
- package/src/DateInput/MonthScroller.tsx +63 -5
- package/src/DateInput/Wheel.tsx +174 -14
- package/src/DateRangeInput/DateRangeInput.doc.mjs +3 -2
- package/src/DateRangeInput/DateRangeInput.tsx +117 -71
- package/src/Dialog/Dialog.tsx +37 -1
- package/src/Drawer/Drawer.tsx +6 -0
- package/src/Drawer/useDrawerDialogPresence.ts +4 -0
- package/src/DropdownMenu/DropdownMenu.tsx +28 -6
- package/src/DropdownMenu/useMenuOverflow.ts +44 -8
- package/src/Field/PanelSearchInput.tsx +6 -0
- package/src/Field/textEntryTraits.ts +6 -6
- package/src/Layer/layerClickBoundary.ts +2 -2
- package/src/Layer/usePressPin.ts +516 -0
- package/src/Layer/useTouchTrigger.ts +56 -6
- package/src/Lightbox/Lightbox.tsx +2 -1
- package/src/Link/Link.tsx +3 -0
- package/src/Popover/usePopover.tsx +71 -9
- package/src/PowerSearch/PowerSearch.tsx +38 -31
- package/src/ProgressBar/ProgressBar.doc.mjs +7 -5
- package/src/ProgressBar/ProgressBar.tsx +147 -16
- package/src/ProgressBar/ProgressBarTouch.ts +109 -0
- package/src/SegmentedControl/SegmentedControlItem.tsx +2 -1
- package/src/Selector/Selector.tsx +29 -2
- package/src/Slider/Slider.doc.mjs +11 -7
- package/src/Slider/Slider.tsx +539 -132
- package/src/Slider/sliderTouchController.ts +1209 -0
- package/src/Slider/useSliderTouch.ts +78 -0
- package/src/Switch/Switch.doc.mjs +3 -3
- package/src/Switch/Switch.tsx +412 -86
- package/src/Switch/switchFill.ts +106 -0
- package/src/Switch/switchTouch.ts +194 -0
- package/src/Switch/useSwitchTouch.ts +1121 -0
- package/src/TabList/Tab.tsx +5 -3
- package/src/Tokenizer/Tokenizer.tsx +15 -5
- package/src/Tokenizer/TokenizerItemRemovableContext.ts +22 -0
- package/src/Tooltip/Tooltip.doc.mjs +21 -6
- package/src/Tooltip/Tooltip.tsx +42 -5
- package/src/Tooltip/index.ts +1 -0
- package/src/Tooltip/useTooltip.doc.mjs +4 -2
- package/src/Tooltip/useTooltip.tsx +84 -21
- package/src/Typeahead/BaseTypeahead.tsx +16 -8
- package/src/Typeahead/Typeahead.tsx +29 -15
- package/src/gestures/GestureArena.ts +45 -4
- package/src/gestures/GestureHost.ts +958 -15
- package/src/gestures/GestureRecognizer.ts +28 -0
- package/src/gestures/PanGestureRecognizer.ts +8 -0
- package/src/gestures/PinchGestureRecognizer.ts +16 -1
- package/src/gestures/PressTracker.ts +10 -0
- package/src/gestures/README.md +82 -3
- package/src/gestures/ScreenEdgePanGestureRecognizer.ts +7 -5
- package/src/gestures/nativeScroll.ts +49 -9
- package/src/gestures/recognizerInternals.ts +9 -2
- package/src/gestures/types.ts +18 -0
- package/src/gestures/usePressTracker.ts +2 -0
- package/src/gestures/wheelSegmenter.ts +143 -0
- package/src/hooks/useAttachment.ts +56 -14
- package/src/hooks/useFocusTrap.ts +26 -2
- package/src/hooks/usePager.ts +1446 -0
- package/src/motion/MEASUREMENTS.md +36 -27
- package/src/motion/README.md +7 -3
- package/src/motion/animator.ts +193 -25
- package/src/motion/paging.ts +161 -0
- package/src/motion/uikit/index.ts +7 -4
- package/src/motion/uikit/paging.ts +193 -0
- package/src/motion/uikit/progressBar.ts +102 -0
- package/src/motion/uikit/scroll.ts +2 -18
- package/src/motion/uikit/slider.ts +105 -0
- package/src/motion/uikit/springs.ts +53 -32
- package/src/motion/uikit/switch.ts +174 -0
- package/src/privateAttributes/pressHoldsDrag.stylex.ts +28 -0
- package/src/utils/pressFeedback.ts +5 -0
- package/src/utils/selectionLock.ts +501 -6
- package/dist/motion/selectPage.d.ts +0 -26
- package/dist/motion/selectPage.d.ts.map +0 -1
- package/dist/motion/selectPage.js +0 -55
- package/src/motion/selectPage.ts +0 -55
|
@@ -0,0 +1,1121 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
'use client';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @file useSwitchTouch.ts
|
|
7
|
+
* @input Switch's parts (gesture surface, input, thumb and track), its value,
|
|
8
|
+
* whether it is interactive, the track's other state (switchFill.ts) and
|
|
9
|
+
* the thumb geometry (switchTouch.ts)
|
|
10
|
+
* @output Exports useSwitchTouch: Switch's gestures for a mouse, a pen and a
|
|
11
|
+
* finger, and its touch presentation under a coarse primary pointer
|
|
12
|
+
* @position DOM half of Switch's touch presentation, on the gesture system
|
|
13
|
+
* (../gestures) and the compositor animator (../motion/animator)
|
|
14
|
+
*
|
|
15
|
+
* SYNC: When modified, update switchTouch.ts, Switch.tsx
|
|
16
|
+
* and __tests__/Switch.interaction.browser.spec.ts.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import {useMemo, useState, type RefObject} from 'react';
|
|
20
|
+
import {getGestureHost} from '../gestures/GestureHost';
|
|
21
|
+
import {createPressTracker, type ControlEvent} from '../gestures/PressTracker';
|
|
22
|
+
import type {GestureTouch} from '../gestures/types';
|
|
23
|
+
import {isRtlElement} from '../hooks/isRtlElement';
|
|
24
|
+
import {useIsomorphicLayoutEffect} from '../hooks/useIsomorphicLayoutEffect';
|
|
25
|
+
import {
|
|
26
|
+
createAnimator,
|
|
27
|
+
type Animator,
|
|
28
|
+
type MotionCurve,
|
|
29
|
+
} from '../motion/animator';
|
|
30
|
+
import {solveSpring, toSpring, type SpringDescription} from '../motion/spring';
|
|
31
|
+
import {UIKIT_GESTURES} from '../motion/uikit/gestures';
|
|
32
|
+
import {registerSwitchTable, switchControl} from '../motion/uikit/switch';
|
|
33
|
+
import {useTouchPresentation} from '../utils/touchPresentation';
|
|
34
|
+
import {armClickGuard, withoutClickGuard} from '../utils/clickGuard';
|
|
35
|
+
import {
|
|
36
|
+
BROWSER_CLICK_WAIT_MS,
|
|
37
|
+
commitOnRelease,
|
|
38
|
+
dragTarget,
|
|
39
|
+
settleTime,
|
|
40
|
+
startFlip,
|
|
41
|
+
stepFlip,
|
|
42
|
+
switchTouchGeometry,
|
|
43
|
+
type SwitchFlip,
|
|
44
|
+
type SwitchTouchGeometry,
|
|
45
|
+
} from './switchTouch';
|
|
46
|
+
import {
|
|
47
|
+
readFillOf,
|
|
48
|
+
sameTrackState,
|
|
49
|
+
trackStateOf,
|
|
50
|
+
type TrackState,
|
|
51
|
+
} from './switchFill';
|
|
52
|
+
|
|
53
|
+
export interface SwitchTouchParts {
|
|
54
|
+
/** Receives the touches: the input and the track. */
|
|
55
|
+
readonly surface: RefObject<HTMLElement | null>;
|
|
56
|
+
readonly input: RefObject<HTMLInputElement | null>;
|
|
57
|
+
/** Moves: the themed thumb itself. */
|
|
58
|
+
readonly thumb: RefObject<HTMLElement | null>;
|
|
59
|
+
/**
|
|
60
|
+
* The track: its `::after` is the fade layer over its own fill, showing
|
|
61
|
+
* the fill it is leaving or heading to (switchFill.ts).
|
|
62
|
+
*/
|
|
63
|
+
readonly track: RefObject<HTMLElement | null>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface SwitchTouchOptions {
|
|
67
|
+
readonly isOn: boolean;
|
|
68
|
+
/** False while disabled or busy: no touch is tracked. */
|
|
69
|
+
readonly isInteractive: boolean;
|
|
70
|
+
/**
|
|
71
|
+
* Disabled: the touch presentation is off and attaches nothing. A
|
|
72
|
+
* busy switch stays attached, so a change that starts a busy state still
|
|
73
|
+
* plays its release motion.
|
|
74
|
+
*/
|
|
75
|
+
readonly isDisabled: boolean;
|
|
76
|
+
/** The track's box, px (width × height): the thumb geometry's scale. */
|
|
77
|
+
readonly trackSize: readonly [number, number];
|
|
78
|
+
/**
|
|
79
|
+
* The props the track renders with in its other state (its theme target
|
|
80
|
+
* and fill), for the fade layer's colour.
|
|
81
|
+
*/
|
|
82
|
+
readonly otherTrackProps: object;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** The fade layer's colour, set on the track for its ::after (Switch.tsx). */
|
|
86
|
+
const FADE_FILL = '--_switch-fade-fill';
|
|
87
|
+
/** The track's own fill while a change is held (inline, over its CSS). */
|
|
88
|
+
const BASE_FILL = 'background-color';
|
|
89
|
+
const TRAVEL = switchControl.travel;
|
|
90
|
+
const TRACKING = switchControl.tracking;
|
|
91
|
+
/**
|
|
92
|
+
* Horizontal travel at which the press begins if it has not (a pan
|
|
93
|
+
* begins here), px.
|
|
94
|
+
*/
|
|
95
|
+
const PAN_HYSTERESIS = UIKIT_GESTURES.pan.hysteresis.value;
|
|
96
|
+
|
|
97
|
+
function now(): number {
|
|
98
|
+
return typeof performance === 'undefined' ? Date.now() : performance.now();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* The lift spring until `holdSeconds`, then the settle spring from where the
|
|
103
|
+
* lift got to. It is one curve, so the settle starts on the compositor's
|
|
104
|
+
* clock.
|
|
105
|
+
*/
|
|
106
|
+
function liftThenSettle({
|
|
107
|
+
from,
|
|
108
|
+
velocity,
|
|
109
|
+
lifted,
|
|
110
|
+
rest,
|
|
111
|
+
holdSeconds,
|
|
112
|
+
threshold,
|
|
113
|
+
lift,
|
|
114
|
+
settle,
|
|
115
|
+
}: {
|
|
116
|
+
from: number;
|
|
117
|
+
velocity: number;
|
|
118
|
+
lifted: number;
|
|
119
|
+
rest: number;
|
|
120
|
+
holdSeconds: number;
|
|
121
|
+
threshold: number;
|
|
122
|
+
/** The lift and settle springs as the theme sets them. */
|
|
123
|
+
lift: SpringDescription;
|
|
124
|
+
settle: SpringDescription;
|
|
125
|
+
}): MotionCurve {
|
|
126
|
+
const hold = Math.max(0, holdSeconds);
|
|
127
|
+
const first = solveSpring(toSpring(lift), {from, to: lifted, velocity});
|
|
128
|
+
const second = solveSpring(toSpring(settle), {
|
|
129
|
+
from: first.value(hold),
|
|
130
|
+
to: rest,
|
|
131
|
+
velocity: first.velocity(hold),
|
|
132
|
+
});
|
|
133
|
+
return {
|
|
134
|
+
duration: hold + second.settleTime(threshold),
|
|
135
|
+
at: t => (t < hold ? first.value(t) : second.value(t - hold)),
|
|
136
|
+
velocity: t => (t < hold ? first.velocity(t) : second.velocity(t - hold)),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* The switch's presentation: the touch presentation under a coarse primary
|
|
142
|
+
* pointer, or its own under a fine one.
|
|
143
|
+
*/
|
|
144
|
+
type SwitchPresentation = 'touch' | 'pointer';
|
|
145
|
+
|
|
146
|
+
interface Elements {
|
|
147
|
+
surface: HTMLElement;
|
|
148
|
+
input: HTMLInputElement;
|
|
149
|
+
thumb: HTMLElement;
|
|
150
|
+
track: HTMLElement;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
interface Drag {
|
|
154
|
+
/** The thumb's offset when the finger landed. */
|
|
155
|
+
readonly start: number;
|
|
156
|
+
flip: SwitchFlip;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* The touch presentation of one switch: animators, recognizers and the
|
|
161
|
+
* gesture state. No React state: a gesture never renders. UISwitch on iOS
|
|
162
|
+
* 26.2 is the reference; every number is in motion/uikit/switch.ts.
|
|
163
|
+
*
|
|
164
|
+
* - The thumb's resting place is CSS (its inline-start inset). So the steady
|
|
165
|
+
* state needs no script and mirrors under RTL by itself. The animator only
|
|
166
|
+
* moves the thumb by a delta from that place (`translate`). When the value
|
|
167
|
+
* moves the CSS place, the delta is rebased in the same layout effect.
|
|
168
|
+
* Nothing moves on screen, and the velocity is kept.
|
|
169
|
+
* - Touches go to a press tracker, which never blocks a scroll. No
|
|
170
|
+
* recognizer claims the touch. `touch-action: pan-y` (Switch.tsx) keeps
|
|
171
|
+
* horizontal moves for the switch: a drag follows and flips it, and the
|
|
172
|
+
* release commits. A vertical drag is the page's scroll, and the switch
|
|
173
|
+
* yields.
|
|
174
|
+
* - A tap, a hold or a short drag activates through the browser's own click.
|
|
175
|
+
* The browser sends no click for a finger's hold, so the switch clicks its
|
|
176
|
+
* input once the click window passes. A mouse or a pen gets a click when
|
|
177
|
+
* it is released on the input. A drag that flipped the switch gets no
|
|
178
|
+
* click, so the switch clicks its input, and the shared click guard
|
|
179
|
+
* swallows the browser's late click. A finger's press slid off and lifted
|
|
180
|
+
* outside toggles too, as UISwitch's does. Forms, `onChange` and the
|
|
181
|
+
* keyboard stay the native checkbox's.
|
|
182
|
+
* - The drag never renders: each move retargets the thumb's tracking spring
|
|
183
|
+
* and steps the flip rule on the finger's travel.
|
|
184
|
+
* - The thumb lifts while a finger presses, into half of the gap around it,
|
|
185
|
+
* and its shadow fades in. At the release it settles back, no sooner than
|
|
186
|
+
* the minimum lift time. The lift and the settle are one curve, built at
|
|
187
|
+
* the release, so the settle starts on time on the compositor.
|
|
188
|
+
* - The track keeps its own fill in both states. A fade layer over it,
|
|
189
|
+
* painted with the track's other fill, fades the colour on the compositor.
|
|
190
|
+
* When the value changes, the layer swaps to the other fill and to the
|
|
191
|
+
* complement of its opacity. So nothing changes on screen. A change the
|
|
192
|
+
* fade already heads to swaps nothing: the track holds its previous fill
|
|
193
|
+
* inline until the fade lands. WebKit plays a curve built in a held frame
|
|
194
|
+
* only once the long task ends, so a swap there stepped the colour back.
|
|
195
|
+
* The layer is composited from the start (`will-change`, Switch.tsx), or
|
|
196
|
+
* iOS Safari runs its fade on the main thread.
|
|
197
|
+
* - Under reduced motion nothing travels or springs: a change plays the
|
|
198
|
+
* animator's Reduce Motion cross-fade. A dragging finger still moves the
|
|
199
|
+
* thumb: that is direct manipulation.
|
|
200
|
+
* - Under a fine primary pointer the switch keeps its own look. The same
|
|
201
|
+
* tracking, flip rule and release decide the value, with no animator, and
|
|
202
|
+
* the thumb moves when the value does. Only its press listens at rest. Its
|
|
203
|
+
* click listener comes with a press.
|
|
204
|
+
*/
|
|
205
|
+
export class SwitchTouchController {
|
|
206
|
+
private elements: Elements | null = null;
|
|
207
|
+
private geometry: SwitchTouchGeometry;
|
|
208
|
+
private isOn: boolean;
|
|
209
|
+
private isInteractive = true;
|
|
210
|
+
/** The side the visuals show or head to. */
|
|
211
|
+
private isVisuallyOn: boolean;
|
|
212
|
+
/** +1 left-to-right, −1 right-to-left. */
|
|
213
|
+
private direction = 1;
|
|
214
|
+
private thumb: Animator | null = null;
|
|
215
|
+
/** The fade layer: its opacity is how far the visuals are from the value. */
|
|
216
|
+
private fade: Animator | null = null;
|
|
217
|
+
/** The lifted shadow: the thumb's `::after`, faded in with the lift. */
|
|
218
|
+
private shadow: Animator | null = null;
|
|
219
|
+
/** When the thumb lifted (document timeline, ms); null while at rest. */
|
|
220
|
+
private liftTime: number | null = null;
|
|
221
|
+
/** When the running settle leaves the lift (settleTime), ms. */
|
|
222
|
+
private settleAt: number | null = null;
|
|
223
|
+
private otherTrack: TrackState | null = null;
|
|
224
|
+
/**
|
|
225
|
+
* A change landed mid-fade, and the fade already heads to it. The track's
|
|
226
|
+
* own fill stays the previous value's (inline) until the fade lands. So the
|
|
227
|
+
* change's frame builds no curve and repaints nothing. `baseHold` counts
|
|
228
|
+
* holds, so a settled one ignores its late landing.
|
|
229
|
+
*/
|
|
230
|
+
private baseHeld = false;
|
|
231
|
+
private baseHold = 0;
|
|
232
|
+
private detachers: (() => void)[] = [];
|
|
233
|
+
/** When the press began (document timeline, ms); null with no press. */
|
|
234
|
+
private pressTime: number | null = null;
|
|
235
|
+
/** The thumb's offset when the current finger landed. */
|
|
236
|
+
private downOffset = 0;
|
|
237
|
+
private drag: Drag | null = null;
|
|
238
|
+
/** The finger's last travel along x, for a lift that reports none. */
|
|
239
|
+
private lastTravel = 0;
|
|
240
|
+
/**
|
|
241
|
+
* The press and the pan both report every touch event; the first report
|
|
242
|
+
* takes the step and this skips the second (cleared after the event).
|
|
243
|
+
*/
|
|
244
|
+
private isStepping = false;
|
|
245
|
+
/** The current touch already ended in a release. */
|
|
246
|
+
private released = false;
|
|
247
|
+
/**
|
|
248
|
+
* Reduced motion: a release changed the value. The thumb's travel waits for
|
|
249
|
+
* the value to commit, so its one cross-fade lands in the new frame.
|
|
250
|
+
*/
|
|
251
|
+
private pendingTravel = false;
|
|
252
|
+
/** Passive: a finger is down on the switch. */
|
|
253
|
+
private isTracking = false;
|
|
254
|
+
/** Which presentation the switch has: the touch one, or its own. */
|
|
255
|
+
private presentation: SwitchPresentation = 'touch';
|
|
256
|
+
/** The press-time listeners under a fine pointer, while armed. */
|
|
257
|
+
private pressListeners: (() => void) | null = null;
|
|
258
|
+
private pressListenersTimer: ReturnType<typeof setTimeout> | null = null;
|
|
259
|
+
/** Passive: a lift is waiting for the browser's own click. */
|
|
260
|
+
private browserClickTimer: ReturnType<typeof setTimeout> | null = null;
|
|
261
|
+
private reconcileTimer: ReturnType<typeof setTimeout> | null = null;
|
|
262
|
+
|
|
263
|
+
constructor(isOn: boolean, geometry: SwitchTouchGeometry) {
|
|
264
|
+
this.isOn = isOn;
|
|
265
|
+
this.isVisuallyOn = isOn;
|
|
266
|
+
this.geometry = geometry;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
get isAttached(): boolean {
|
|
270
|
+
return this.elements != null;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
attach(elements: Elements, presentation: SwitchPresentation): void {
|
|
274
|
+
this.detach();
|
|
275
|
+
this.elements = elements;
|
|
276
|
+
this.presentation = presentation;
|
|
277
|
+
this.measureGeometry();
|
|
278
|
+
this.direction = isRtlElement(elements.surface) ? -1 : 1;
|
|
279
|
+
this.isVisuallyOn = this.isOn;
|
|
280
|
+
const host = getGestureHost(elements.surface.ownerDocument);
|
|
281
|
+
if (presentation === 'pointer') {
|
|
282
|
+
// The switch's own look: the gestures decide the value and the CSS
|
|
283
|
+
// shows it.
|
|
284
|
+
this.detachers = this.attachPassive(host, elements);
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
registerSwitchTable();
|
|
288
|
+
this.thumb = createAnimator(elements.thumb);
|
|
289
|
+
// The layer rests clear (its class's opacity 0) while the visuals show
|
|
290
|
+
// the value; from here its opacity is the animator's.
|
|
291
|
+
this.fade = createAnimator(elements.track, {
|
|
292
|
+
pseudoElement: '::after',
|
|
293
|
+
initial: {opacity: 0},
|
|
294
|
+
});
|
|
295
|
+
this.fade.set({opacity: 0});
|
|
296
|
+
this.shadow = createAnimator(elements.thumb, {
|
|
297
|
+
pseudoElement: '::after',
|
|
298
|
+
initial: {opacity: 0},
|
|
299
|
+
});
|
|
300
|
+
this.shadow.set({opacity: 0});
|
|
301
|
+
this.paintFade();
|
|
302
|
+
this.detachers = this.attachPassive(host, elements);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Passive: UIControl's tracking, which never blocks a scroll. The press
|
|
307
|
+
* begins with the touch, or with the first horizontal move a pan would
|
|
308
|
+
* begin on. Inside a scroller the touch waits for the content delay. Every
|
|
309
|
+
* move steps the flip rule, and the release commits. A touch the browser
|
|
310
|
+
* takes for its scroll yields with no toggle.
|
|
311
|
+
*/
|
|
312
|
+
private attachPassive(
|
|
313
|
+
host: ReturnType<typeof getGestureHost>,
|
|
314
|
+
elements: Elements,
|
|
315
|
+
): (() => void)[] {
|
|
316
|
+
const {surface} = elements;
|
|
317
|
+
const tracker = createPressTracker({
|
|
318
|
+
view: surface,
|
|
319
|
+
getBounds: () => surface.getBoundingClientRect(),
|
|
320
|
+
isEnabled: () => this.isInteractive,
|
|
321
|
+
isBraking: () => host.isScrollBraking(surface),
|
|
322
|
+
// A held switch opens no menu (UISwitch shows none).
|
|
323
|
+
holdsContextMenu: () => this.pressTime != null,
|
|
324
|
+
onControlEvent: (event, touch) => this.onControlEvent(event, touch),
|
|
325
|
+
onHighlightChange: isHighlighted => {
|
|
326
|
+
if (isHighlighted && this.isTracking && this.pressTime == null) {
|
|
327
|
+
this.pressBegan(now());
|
|
328
|
+
}
|
|
329
|
+
},
|
|
330
|
+
});
|
|
331
|
+
// The lift's own click activates the input natively: the switch then
|
|
332
|
+
// clicks nothing itself.
|
|
333
|
+
const onClick = (event: MouseEvent) => {
|
|
334
|
+
if (event.isTrusted && this.browserClickTimer != null) {
|
|
335
|
+
clearTimeout(this.browserClickTimer);
|
|
336
|
+
this.browserClickTimer = null;
|
|
337
|
+
this.scheduleReconcile();
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
const listen = (): (() => void) => {
|
|
341
|
+
surface.addEventListener('click', onClick, true);
|
|
342
|
+
return () => {
|
|
343
|
+
surface.removeEventListener('click', onClick, true);
|
|
344
|
+
};
|
|
345
|
+
};
|
|
346
|
+
if (this.presentation === 'pointer') {
|
|
347
|
+
// Under a fine pointer only the press the host arms listens at rest.
|
|
348
|
+
// The switch's click listener comes with a press and goes after its
|
|
349
|
+
// click wait.
|
|
350
|
+
this.listenForPress = listen;
|
|
351
|
+
return [
|
|
352
|
+
host.attachConsumer(surface, tracker),
|
|
353
|
+
() => tracker.dispose(),
|
|
354
|
+
() => this.stopPressListeners(),
|
|
355
|
+
];
|
|
356
|
+
}
|
|
357
|
+
return [
|
|
358
|
+
host.attachConsumer(surface, tracker),
|
|
359
|
+
() => tracker.dispose(),
|
|
360
|
+
listen(),
|
|
361
|
+
];
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/** Under a fine pointer: adds the switch's own listeners for a press. */
|
|
365
|
+
private listenForPress: (() => () => void) | null = null;
|
|
366
|
+
|
|
367
|
+
private startPressListeners(): void {
|
|
368
|
+
if (this.pressListenersTimer != null) {
|
|
369
|
+
clearTimeout(this.pressListenersTimer);
|
|
370
|
+
this.pressListenersTimer = null;
|
|
371
|
+
}
|
|
372
|
+
if (this.pressListeners == null && this.listenForPress != null) {
|
|
373
|
+
this.pressListeners = this.listenForPress();
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/** The press's click window is over: its listeners go. */
|
|
378
|
+
private endPressListeners(afterMs: number): void {
|
|
379
|
+
if (this.pressListeners == null) {
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
if (this.pressListenersTimer != null) {
|
|
383
|
+
clearTimeout(this.pressListenersTimer);
|
|
384
|
+
}
|
|
385
|
+
this.pressListenersTimer = setTimeout(() => {
|
|
386
|
+
this.pressListenersTimer = null;
|
|
387
|
+
this.stopPressListeners();
|
|
388
|
+
}, afterMs);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
private stopPressListeners(): void {
|
|
392
|
+
if (this.pressListenersTimer != null) {
|
|
393
|
+
clearTimeout(this.pressListenersTimer);
|
|
394
|
+
this.pressListenersTimer = null;
|
|
395
|
+
}
|
|
396
|
+
this.pressListeners?.();
|
|
397
|
+
this.pressListeners = null;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/** Undo everything: listeners, timers, animations and inline styles. */
|
|
401
|
+
detach(): void {
|
|
402
|
+
for (const detach of this.detachers) {
|
|
403
|
+
detach();
|
|
404
|
+
}
|
|
405
|
+
this.detachers = [];
|
|
406
|
+
this.listenForPress = null;
|
|
407
|
+
this.clearTimers();
|
|
408
|
+
for (const animator of [this.thumb, this.fade, this.shadow]) {
|
|
409
|
+
const element = animator?.element;
|
|
410
|
+
animator?.dispose();
|
|
411
|
+
if (element != null) {
|
|
412
|
+
for (const property of ['translate', 'scale', 'opacity']) {
|
|
413
|
+
element.style.removeProperty(property);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
this.elements?.track.style.removeProperty(FADE_FILL);
|
|
418
|
+
this.elements?.track.style.removeProperty(BASE_FILL);
|
|
419
|
+
this.baseHeld = false;
|
|
420
|
+
this.baseHold++;
|
|
421
|
+
this.thumb = this.fade = this.shadow = null;
|
|
422
|
+
this.liftTime = null;
|
|
423
|
+
this.elements = null;
|
|
424
|
+
this.pressTime = null;
|
|
425
|
+
this.drag = null;
|
|
426
|
+
this.isTracking = false;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
setInteractive(isInteractive: boolean): void {
|
|
430
|
+
this.isInteractive = isInteractive;
|
|
431
|
+
if (!isInteractive && this.pressTime != null) {
|
|
432
|
+
this.abandon(now());
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/** The track's other state, re-rendered with each value. */
|
|
437
|
+
setOtherTrack(otherTrack: TrackState): void {
|
|
438
|
+
const previous = this.otherTrack;
|
|
439
|
+
this.otherTrack = otherTrack;
|
|
440
|
+
// A held fill was read from the previous other state; a re-render that
|
|
441
|
+
// keeps it keeps the hold.
|
|
442
|
+
if (this.baseHeld && !sameTrackState(previous, otherTrack)) {
|
|
443
|
+
this.landBase(now());
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
setGeometry(geometry: SwitchTouchGeometry): void {
|
|
448
|
+
this.geometry = geometry;
|
|
449
|
+
this.measureGeometry();
|
|
450
|
+
this.thumb?.set({x: 0, scaleX: 1, scaleY: 1});
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* The thumb geometry from the rendered boxes. The track's box lets a theme
|
|
455
|
+
* resize the track. The thumb's box is the circle of the current value. It
|
|
456
|
+
* rests by insets from the track's padding box, so padding does not move it.
|
|
457
|
+
*/
|
|
458
|
+
private measureGeometry(): void {
|
|
459
|
+
const track = this.elements?.track;
|
|
460
|
+
const thumb = this.elements?.thumb;
|
|
461
|
+
if (track == null) {
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
// Fractional boxes: the track's border box less its border (it has no
|
|
465
|
+
// transform). The thumb's used size comes from its computed style, which
|
|
466
|
+
// its scale does not change.
|
|
467
|
+
const box = track.getBoundingClientRect();
|
|
468
|
+
const trackWidth = box.width - 2 * track.clientLeft;
|
|
469
|
+
const trackHeight = box.height - 2 * track.clientTop;
|
|
470
|
+
if (trackWidth <= 0 || trackHeight <= 0) {
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
const style = thumb == null ? null : getComputedStyle(thumb);
|
|
474
|
+
const thumbWidth = style == null ? NaN : parseFloat(style.width);
|
|
475
|
+
const thumbHeight = style == null ? NaN : parseFloat(style.height);
|
|
476
|
+
this.geometry = switchTouchGeometry(
|
|
477
|
+
trackWidth,
|
|
478
|
+
trackHeight,
|
|
479
|
+
thumbWidth > 0 && thumbHeight > 0
|
|
480
|
+
? {width: thumbWidth, height: thumbHeight}
|
|
481
|
+
: undefined,
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* The rendered value changed. The CSS has already moved the thumb's
|
|
487
|
+
* resting place. So the thumb's delta is rebased first, and nothing moves on
|
|
488
|
+
* screen. Then the visuals head for the new value.
|
|
489
|
+
*/
|
|
490
|
+
setValue(isOn: boolean): void {
|
|
491
|
+
if (isOn === this.isOn) {
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
const previous = this.isOn;
|
|
495
|
+
this.isOn = isOn;
|
|
496
|
+
const thumb = this.thumb;
|
|
497
|
+
if (thumb == null) {
|
|
498
|
+
this.isVisuallyOn = isOn;
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
const time = now();
|
|
502
|
+
// The thumb's CSS rest moved with the value: shift it back by as much,
|
|
503
|
+
// so nothing moves on screen. A spring keeps its target through the shift
|
|
504
|
+
// and is sent to the new rest below. A reduced-motion dip shifts whole,
|
|
505
|
+
// its end included, and keeps going.
|
|
506
|
+
const springs = thumb.reducedMotion() === 'off';
|
|
507
|
+
thumb.rebase({
|
|
508
|
+
x: this.direction * (this.rest(previous) - this.rest(isOn)),
|
|
509
|
+
});
|
|
510
|
+
// Each state has its own thumb size, the circle a mouse sees. The box the
|
|
511
|
+
// CSS now renders is the new value's. Its scale takes the old size over
|
|
512
|
+
// and grows or shrinks it from there.
|
|
513
|
+
const before = this.geometry;
|
|
514
|
+
this.measureGeometry();
|
|
515
|
+
if (springs) {
|
|
516
|
+
this.rescaleThumb(before, this.geometry, time);
|
|
517
|
+
}
|
|
518
|
+
// The track now shows the new value's fill. The fade layer swaps to the
|
|
519
|
+
// other fill and to the complement of its opacity. Nothing changes on
|
|
520
|
+
// screen, and the fade keeps heading where the visuals head.
|
|
521
|
+
const fade = this.fade;
|
|
522
|
+
if (fade != null) {
|
|
523
|
+
this.landBase(time);
|
|
524
|
+
// Already heading here (a release, a drag that flipped). The running
|
|
525
|
+
// fade carries the change over the previous fill, held as the track's
|
|
526
|
+
// own until it lands. WebKit plays a curve built in a held frame only
|
|
527
|
+
// once the long task ends. A swap here would step the colour back.
|
|
528
|
+
if (this.isVisuallyOn !== isOn || !this.holdBase()) {
|
|
529
|
+
this.swapFade(time);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
const wasDragging = this.drag != null && !this.released;
|
|
533
|
+
if (wasDragging) {
|
|
534
|
+
// The app changed the value mid-gesture: the value wins and the
|
|
535
|
+
// gesture ends without committing.
|
|
536
|
+
this.drag = null;
|
|
537
|
+
this.released = true;
|
|
538
|
+
this.pressEnded();
|
|
539
|
+
}
|
|
540
|
+
// A release already sent the visuals here. Any other change, the app's or
|
|
541
|
+
// one mid-gesture, sends them now, and a spring heads for the new rest.
|
|
542
|
+
if (
|
|
543
|
+
springs ||
|
|
544
|
+
wasDragging ||
|
|
545
|
+
this.pendingTravel ||
|
|
546
|
+
this.isVisuallyOn !== isOn
|
|
547
|
+
) {
|
|
548
|
+
const touchRelease = this.pendingTravel;
|
|
549
|
+
this.pendingTravel = false;
|
|
550
|
+
this.showSide(isOn, time, thumb.getVelocity('x', time), touchRelease);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// --- Touches ---------------------------------------------------------------
|
|
555
|
+
|
|
556
|
+
private touchDown(time: number): void {
|
|
557
|
+
this.landBase(time);
|
|
558
|
+
this.released = false;
|
|
559
|
+
this.pendingTravel = false;
|
|
560
|
+
this.drag = null;
|
|
561
|
+
this.lastTravel = 0;
|
|
562
|
+
this.downOffset = this.offsetAt(time);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/** Passive: a mouse, a pen or a finger on the switch, UIControl's events. */
|
|
566
|
+
private onControlEvent(event: ControlEvent, touch: GestureTouch): void {
|
|
567
|
+
const travel = touch.x - touch.startX;
|
|
568
|
+
// The switch reads the thumb when it handles the event. Its motion has no
|
|
569
|
+
// start of its own. A call that continues motion starts then, from the
|
|
570
|
+
// curve on screen. A call from rest starts at the frame that shows it, as
|
|
571
|
+
// Core Animation starts an animation at its commit. A busy main thread
|
|
572
|
+
// hands the switch an event late and commits its frame late. The motion
|
|
573
|
+
// is delayed, never stepped back or skipped into. The event's own time is
|
|
574
|
+
// the recognizers'.
|
|
575
|
+
const time = now();
|
|
576
|
+
switch (event) {
|
|
577
|
+
case 'touchDown':
|
|
578
|
+
this.isTracking = true;
|
|
579
|
+
this.startPressListeners();
|
|
580
|
+
this.touchDown(time);
|
|
581
|
+
return;
|
|
582
|
+
case 'touchDragInside':
|
|
583
|
+
case 'touchDragOutside':
|
|
584
|
+
if (this.released || !this.isInteractive) {
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
if (this.pressTime == null) {
|
|
588
|
+
if (Math.abs(travel) < PAN_HYSTERESIS) {
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
// A pan would begin here: the switch takes the horizontal drag.
|
|
592
|
+
this.pressBegan(time);
|
|
593
|
+
}
|
|
594
|
+
this.move(time, travel);
|
|
595
|
+
return;
|
|
596
|
+
case 'touchUpInside':
|
|
597
|
+
case 'touchUpOutside':
|
|
598
|
+
this.isTracking = false;
|
|
599
|
+
if (this.released || !this.isInteractive) {
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
// The finger's lift is a step of its own: a flip armed by the last
|
|
603
|
+
// move happens here (a fast swipe flips on the lift).
|
|
604
|
+
this.move(time, travel);
|
|
605
|
+
if (this.drag?.flip.didFlip !== true && event === 'touchUpInside') {
|
|
606
|
+
// A tap, a hold or a short drag: the browser's click activates it.
|
|
607
|
+
// Only a finger's lift can come with no click (a hold). A mouse or a
|
|
608
|
+
// pen gets one whenever it is released on the input. With no click,
|
|
609
|
+
// it was released off the input, inside the tracking slop. The
|
|
610
|
+
// switch is then left alone, as the native checkbox is.
|
|
611
|
+
this.release(time, {
|
|
612
|
+
byBrowserClick: true,
|
|
613
|
+
clicksIfNoBrowserClick: touch.pointerType === 'touch',
|
|
614
|
+
});
|
|
615
|
+
this.endPressListeners(BROWSER_CLICK_WAIT_MS);
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
if (
|
|
619
|
+
touch.pointerType !== 'touch' &&
|
|
620
|
+
(event === 'touchUpOutside' || this.drag?.flip.didFlip !== true)
|
|
621
|
+
) {
|
|
622
|
+
// A mouse or a pen released outside the tracking bounds, flipped or
|
|
623
|
+
// not, leaves the switch alone, as the native checkbox does. So the
|
|
624
|
+
// press can be taken back (WCAG 2.5.2). Only a finger's press slid
|
|
625
|
+
// off toggles.
|
|
626
|
+
this.released = true;
|
|
627
|
+
this.abandon(time);
|
|
628
|
+
this.endPressListeners(0);
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
// A drag that flipped, or a press slid off and lifted outside, which
|
|
632
|
+
// toggles as UISwitch's does. The switch decides this lift itself, so
|
|
633
|
+
// the shared click guard swallows the browser's click, if one comes.
|
|
634
|
+
// A drag back past the press commits nothing, and its click must not
|
|
635
|
+
// toggle either.
|
|
636
|
+
if (this.elements != null) {
|
|
637
|
+
armClickGuard(this.elements.input.ownerDocument);
|
|
638
|
+
}
|
|
639
|
+
this.release(time);
|
|
640
|
+
this.endPressListeners(0);
|
|
641
|
+
return;
|
|
642
|
+
case 'touchCancel':
|
|
643
|
+
// The browser took the touch for its scroll, or a recognizer above
|
|
644
|
+
// took it: the switch yields.
|
|
645
|
+
this.isTracking = false;
|
|
646
|
+
if (!this.released) {
|
|
647
|
+
this.released = true;
|
|
648
|
+
this.abandon(time);
|
|
649
|
+
}
|
|
650
|
+
this.endPressListeners(0);
|
|
651
|
+
return;
|
|
652
|
+
case 'touchDragEnter':
|
|
653
|
+
case 'touchDragExit':
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/**
|
|
659
|
+
* One touch event of a held or dragged switch. It retargets the thumb at the
|
|
660
|
+
* finger (one style write) and steps the flip rule.
|
|
661
|
+
*/
|
|
662
|
+
private move(time: number, travel: number | null): void {
|
|
663
|
+
const thumb = this.thumb;
|
|
664
|
+
if (this.isStepping) {
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
this.isStepping = true;
|
|
668
|
+
queueMicrotask(() => {
|
|
669
|
+
this.isStepping = false;
|
|
670
|
+
});
|
|
671
|
+
const x = this.direction * (travel ?? this.lastTravel);
|
|
672
|
+
this.lastTravel = travel ?? this.lastTravel;
|
|
673
|
+
const drag = (this.drag ??= {
|
|
674
|
+
start: this.downOffset,
|
|
675
|
+
flip: startFlip(this.isOn),
|
|
676
|
+
});
|
|
677
|
+
// The thumb under the finger is direct manipulation, which reduced
|
|
678
|
+
// motion leaves alone. Under it the thumb is placed at once: the animator
|
|
679
|
+
// would cross-fade a move. Under a fine pointer nothing follows the
|
|
680
|
+
// pointer. The switch keeps its own look, and the value moves the thumb.
|
|
681
|
+
if (thumb != null) {
|
|
682
|
+
const target = this.toX(dragTarget(drag.start, x, this.geometry));
|
|
683
|
+
if (thumb.reducedMotion() === 'off') {
|
|
684
|
+
thumb.animate({x: target}, {spring: TRACKING});
|
|
685
|
+
} else {
|
|
686
|
+
thumb.set({x: target});
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
drag.flip = stepFlip(drag.flip, x, this.geometry);
|
|
690
|
+
if (drag.flip.isVisuallyOn !== this.isVisuallyOn) {
|
|
691
|
+
this.showWell(drag.flip.isVisuallyOn, time);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* The finger lifted after a tap, a hold or a drag. `byBrowserClick`: the
|
|
697
|
+
* browser's own click activates the input, and the switch clicks it only
|
|
698
|
+
* if that click never comes.
|
|
699
|
+
*/
|
|
700
|
+
private release(
|
|
701
|
+
time: number,
|
|
702
|
+
{
|
|
703
|
+
byBrowserClick = false,
|
|
704
|
+
clicksIfNoBrowserClick = true,
|
|
705
|
+
}: {byBrowserClick?: boolean; clicksIfNoBrowserClick?: boolean} = {},
|
|
706
|
+
): void {
|
|
707
|
+
if (this.released) {
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
this.released = true;
|
|
711
|
+
this.clearTimers();
|
|
712
|
+
const commit = commitOnRelease(this.drag?.flip ?? null, this.isOn);
|
|
713
|
+
this.drag = null;
|
|
714
|
+
if (this.pressTime == null) {
|
|
715
|
+
// A tap quicker than a scroller's content delay: the press begins now.
|
|
716
|
+
this.pressBegan(time);
|
|
717
|
+
}
|
|
718
|
+
if (
|
|
719
|
+
commit !== this.isOn &&
|
|
720
|
+
this.thumb != null &&
|
|
721
|
+
this.thumb.reducedMotion({touchRelease: true}) !== 'off'
|
|
722
|
+
) {
|
|
723
|
+
// Under reduced motion the travel is one cross-fade, played when the
|
|
724
|
+
// value commits (setValue). It lands in the new value's frame. The
|
|
725
|
+
// colour starts now.
|
|
726
|
+
this.pendingTravel = true;
|
|
727
|
+
this.showWell(commit, time, true);
|
|
728
|
+
} else {
|
|
729
|
+
this.showSide(commit, time, undefined, true);
|
|
730
|
+
}
|
|
731
|
+
this.pressEnded(time, true);
|
|
732
|
+
if (byBrowserClick) {
|
|
733
|
+
this.browserClickTimer = setTimeout(() => {
|
|
734
|
+
this.browserClickTimer = null;
|
|
735
|
+
if (clicksIfNoBrowserClick && commit !== this.isOn) {
|
|
736
|
+
this.clickInput();
|
|
737
|
+
}
|
|
738
|
+
this.scheduleReconcile();
|
|
739
|
+
}, BROWSER_CLICK_WAIT_MS);
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
742
|
+
if (commit !== this.isOn) {
|
|
743
|
+
// No click comes from the browser for this lift; the input's own click
|
|
744
|
+
// keeps every native path (form value, onChange, the label).
|
|
745
|
+
this.clickInput();
|
|
746
|
+
}
|
|
747
|
+
this.scheduleReconcile();
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
/**
|
|
751
|
+
* The switch's own click on its input. The shared click guard then swallows
|
|
752
|
+
* the browser's late click for the same lift, so the switch toggles once.
|
|
753
|
+
*/
|
|
754
|
+
private clickInput(): void {
|
|
755
|
+
const input = this.elements?.input;
|
|
756
|
+
if (input == null) {
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
armClickGuard(input.ownerDocument);
|
|
760
|
+
withoutClickGuard(() => input.click());
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
/** If the owner kept the old value, the visuals go back to it. */
|
|
764
|
+
private scheduleReconcile(): void {
|
|
765
|
+
if (this.reconcileTimer != null) {
|
|
766
|
+
clearTimeout(this.reconcileTimer);
|
|
767
|
+
}
|
|
768
|
+
this.reconcileTimer = setTimeout(() => {
|
|
769
|
+
this.reconcileTimer = null;
|
|
770
|
+
if (this.drag == null && this.isVisuallyOn !== this.isOn) {
|
|
771
|
+
this.pendingTravel = false;
|
|
772
|
+
this.showSide(this.isOn, now(), undefined, true);
|
|
773
|
+
}
|
|
774
|
+
}, 0);
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
/** The touch ended without a release (cancelled, or the switch turned off). */
|
|
778
|
+
private abandon(time: number): void {
|
|
779
|
+
this.drag = null;
|
|
780
|
+
this.showSide(this.isOn, time, undefined, true);
|
|
781
|
+
this.pressEnded(time, true);
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// --- Visuals ---------------------------------------------------------------
|
|
785
|
+
|
|
786
|
+
private rest(isOn: boolean): number {
|
|
787
|
+
return isOn ? this.geometry.travel : 0;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
/** The animator's x for a thumb offset, relative to its CSS place. */
|
|
791
|
+
private toX(offset: number): number {
|
|
792
|
+
return this.direction * (offset - this.rest(this.isOn));
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
/** The thumb's offset on screen at `time`, from the curve. */
|
|
796
|
+
private offsetAt(time: number): number {
|
|
797
|
+
const x = this.thumb?.get('x', time) ?? 0;
|
|
798
|
+
return this.rest(this.isOn) + this.direction * x;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
private showWell(isOn: boolean, time: number, touchRelease = false): void {
|
|
802
|
+
this.isVisuallyOn = isOn;
|
|
803
|
+
this.paintFade();
|
|
804
|
+
this.animateFade(time, undefined, touchRelease);
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
/**
|
|
808
|
+
* The fade layer heads for the visuals, on UIKit's well springs (on:
|
|
809
|
+
* 0.68 s, off: 0.40 s). It is clear when they show the value. It is opaque,
|
|
810
|
+
* the other fill, when they show the other side.
|
|
811
|
+
*/
|
|
812
|
+
private animateFade(
|
|
813
|
+
time: number,
|
|
814
|
+
velocity?: number,
|
|
815
|
+
touchRelease = false,
|
|
816
|
+
): void {
|
|
817
|
+
// Under reduced motion the animator plays this as its cross-fade.
|
|
818
|
+
this.fade?.animate(
|
|
819
|
+
{opacity: this.isVisuallyOn === this.isOn ? 0 : 1},
|
|
820
|
+
{
|
|
821
|
+
spring: this.isVisuallyOn
|
|
822
|
+
? switchControl.wellOn
|
|
823
|
+
: switchControl.wellOff,
|
|
824
|
+
velocity: velocity == null ? undefined : {opacity: velocity},
|
|
825
|
+
},
|
|
826
|
+
{touchRelease},
|
|
827
|
+
);
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
/**
|
|
831
|
+
* The track now shows the new value's fill. The fade layer swaps to the
|
|
832
|
+
* other fill and to the complement of its opacity, so nothing changes on
|
|
833
|
+
* screen. It keeps heading where the visuals head, on the running clock.
|
|
834
|
+
*/
|
|
835
|
+
private swapFade(time: number): void {
|
|
836
|
+
const fade = this.fade;
|
|
837
|
+
if (fade == null) {
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
840
|
+
const opacity = fade.get('opacity', time);
|
|
841
|
+
const fadeVelocity = fade.getVelocity('opacity', time);
|
|
842
|
+
this.paintFade();
|
|
843
|
+
fade.rebase({opacity: 1 - 2 * opacity});
|
|
844
|
+
this.animateFade(time, -fadeVelocity);
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
/**
|
|
848
|
+
* Holds the track's own fill at the previous value's (the other state's,
|
|
849
|
+
* now) until the fade lands. False when there is nothing to read it from.
|
|
850
|
+
*/
|
|
851
|
+
private holdBase(): boolean {
|
|
852
|
+
const track = this.elements?.track;
|
|
853
|
+
const fade = this.fade;
|
|
854
|
+
if (track == null || fade == null || this.otherTrack == null) {
|
|
855
|
+
return false;
|
|
856
|
+
}
|
|
857
|
+
track.style.setProperty(BASE_FILL, readFillOf(track, this.otherTrack));
|
|
858
|
+
this.baseHeld = true;
|
|
859
|
+
const hold = ++this.baseHold;
|
|
860
|
+
void fade.atRest(['opacity']).then(() => {
|
|
861
|
+
if (hold === this.baseHold) {
|
|
862
|
+
this.landBase(now());
|
|
863
|
+
}
|
|
864
|
+
});
|
|
865
|
+
return true;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
/**
|
|
869
|
+
* Ends a held fill: the track shows its value's fill again and the fade
|
|
870
|
+
* layer swaps under it. Once the fade has landed, the layer is opaque and
|
|
871
|
+
* nothing changes on screen. Called early, by a new touch, value or other
|
|
872
|
+
* state, the swap keeps the picture where it is.
|
|
873
|
+
*/
|
|
874
|
+
private landBase(time: number): void {
|
|
875
|
+
if (!this.baseHeld) {
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
this.baseHeld = false;
|
|
879
|
+
this.baseHold++;
|
|
880
|
+
this.elements?.track.style.removeProperty(BASE_FILL);
|
|
881
|
+
this.swapFade(time);
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
/** The fade layer's colour: the track's fill in its other state. */
|
|
885
|
+
private paintFade(): void {
|
|
886
|
+
const track = this.elements?.track;
|
|
887
|
+
if (track == null || this.fade == null || this.otherTrack == null) {
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
track.style.setProperty(FADE_FILL, readFillOf(track, this.otherTrack));
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
/** Track color and thumb head for one side. */
|
|
894
|
+
private showSide(
|
|
895
|
+
isOn: boolean,
|
|
896
|
+
time: number,
|
|
897
|
+
velocity?: number,
|
|
898
|
+
touchRelease = false,
|
|
899
|
+
): void {
|
|
900
|
+
if (isOn !== this.isVisuallyOn) {
|
|
901
|
+
this.showWell(isOn, time, touchRelease);
|
|
902
|
+
}
|
|
903
|
+
// Under reduced motion the animator cross-fades this travel: the thumb
|
|
904
|
+
// fades out where it is and back in at its rest.
|
|
905
|
+
this.thumb?.animate(
|
|
906
|
+
{x: this.toX(this.rest(isOn))},
|
|
907
|
+
{
|
|
908
|
+
spring: TRAVEL,
|
|
909
|
+
velocity: velocity == null ? undefined : {x: velocity},
|
|
910
|
+
},
|
|
911
|
+
{touchRelease},
|
|
912
|
+
);
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
/** The press began: the switch holds the touch and the thumb lifts. */
|
|
916
|
+
private pressBegan(time: number): void {
|
|
917
|
+
this.pressTime = time;
|
|
918
|
+
this.lift(time);
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
/** The press ended (a release, a cancel or the switch turning off). */
|
|
922
|
+
private pressEnded(time: number = now(), touchRelease = false): void {
|
|
923
|
+
this.pressTime = null;
|
|
924
|
+
this.settle(time, touchRelease);
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
/**
|
|
928
|
+
* The thumb grows about its center into half the gap around it, and its
|
|
929
|
+
* lifted shadow fades in. Under reduced motion nothing lifts: the lift is
|
|
930
|
+
* feedback with no fade of its own.
|
|
931
|
+
*/
|
|
932
|
+
private lift(time: number): void {
|
|
933
|
+
if (this.thumb == null || this.thumb.reducedMotion() !== 'off') {
|
|
934
|
+
return;
|
|
935
|
+
}
|
|
936
|
+
this.liftTime = time;
|
|
937
|
+
const lift = this.thumb.animate(
|
|
938
|
+
{scaleX: this.geometry.liftScaleX, scaleY: this.geometry.liftScaleY},
|
|
939
|
+
{spring: switchControl.lift},
|
|
940
|
+
);
|
|
941
|
+
this.shadow?.animate({opacity: 1}, {spring: switchControl.lift});
|
|
942
|
+
// A lift from rest starts at the frame that shows it: the minimum lift
|
|
943
|
+
// counts from there.
|
|
944
|
+
void lift.started.then(start => {
|
|
945
|
+
if (this.liftTime === time) {
|
|
946
|
+
this.liftTime = start;
|
|
947
|
+
}
|
|
948
|
+
});
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
/**
|
|
952
|
+
* The thumb settles back after the press, no sooner than the minimum lift
|
|
953
|
+
* time after it lifted. One curve per property, built now, starts the
|
|
954
|
+
* settle on the compositor's clock.
|
|
955
|
+
*/
|
|
956
|
+
private settle(time: number, touchRelease: boolean): void {
|
|
957
|
+
const liftTime = this.liftTime;
|
|
958
|
+
if (liftTime == null) {
|
|
959
|
+
return;
|
|
960
|
+
}
|
|
961
|
+
this.liftTime = null;
|
|
962
|
+
const lift = switchControl.lift;
|
|
963
|
+
const settle = switchControl.settle;
|
|
964
|
+
this.settleAt = settleTime(time, liftTime);
|
|
965
|
+
const hold = (this.settleAt - time) / 1000;
|
|
966
|
+
const play = (
|
|
967
|
+
animator: Animator | null,
|
|
968
|
+
property: 'scaleX' | 'scaleY' | 'opacity',
|
|
969
|
+
lifted: number,
|
|
970
|
+
rest: number,
|
|
971
|
+
threshold: number,
|
|
972
|
+
) => {
|
|
973
|
+
if (animator == null) {
|
|
974
|
+
return;
|
|
975
|
+
}
|
|
976
|
+
animator.play(
|
|
977
|
+
{
|
|
978
|
+
[property]: liftThenSettle({
|
|
979
|
+
from: animator.get(property, time),
|
|
980
|
+
velocity: animator.getVelocity(property, time),
|
|
981
|
+
lifted,
|
|
982
|
+
rest,
|
|
983
|
+
holdSeconds: hold,
|
|
984
|
+
threshold,
|
|
985
|
+
lift,
|
|
986
|
+
settle,
|
|
987
|
+
}),
|
|
988
|
+
},
|
|
989
|
+
{touchRelease},
|
|
990
|
+
);
|
|
991
|
+
};
|
|
992
|
+
play(this.thumb, 'scaleX', this.geometry.liftScaleX, 1, 1e-4);
|
|
993
|
+
play(this.thumb, 'scaleY', this.geometry.liftScaleY, 1, 1e-4);
|
|
994
|
+
play(this.shadow, 'opacity', 1, 0, 1e-3);
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
/**
|
|
998
|
+
* The thumb's box changed size with the value: the on circle is larger.
|
|
999
|
+
* Its scale carries the size it showed, so nothing jumps. Then it heads for
|
|
1000
|
+
* the new box through the settle, or to the lift a held press keeps.
|
|
1001
|
+
*/
|
|
1002
|
+
private rescaleThumb(
|
|
1003
|
+
before: SwitchTouchGeometry,
|
|
1004
|
+
after: SwitchTouchGeometry,
|
|
1005
|
+
time: number,
|
|
1006
|
+
): void {
|
|
1007
|
+
const thumb = this.thumb;
|
|
1008
|
+
if (
|
|
1009
|
+
thumb == null ||
|
|
1010
|
+
!(before.thumbWidth > 0 && before.thumbHeight > 0) ||
|
|
1011
|
+
!(after.thumbWidth > 0 && after.thumbHeight > 0) ||
|
|
1012
|
+
(before.thumbWidth === after.thumbWidth &&
|
|
1013
|
+
before.thumbHeight === after.thumbHeight)
|
|
1014
|
+
) {
|
|
1015
|
+
return;
|
|
1016
|
+
}
|
|
1017
|
+
const held = this.liftTime != null;
|
|
1018
|
+
const hold =
|
|
1019
|
+
this.settleAt == null ? 0 : Math.max(0, this.settleAt - time) / 1000;
|
|
1020
|
+
for (const [property, factor, lifted] of [
|
|
1021
|
+
['scaleX', before.thumbWidth / after.thumbWidth, before.liftScaleX],
|
|
1022
|
+
['scaleY', before.thumbHeight / after.thumbHeight, before.liftScaleY],
|
|
1023
|
+
] as const) {
|
|
1024
|
+
const keep = lifted * factor;
|
|
1025
|
+
thumb.play({
|
|
1026
|
+
[property]: liftThenSettle({
|
|
1027
|
+
from: thumb.get(property, time) * factor,
|
|
1028
|
+
velocity: thumb.getVelocity(property, time) * factor,
|
|
1029
|
+
lifted: keep,
|
|
1030
|
+
rest: held ? keep : 1,
|
|
1031
|
+
holdSeconds: held ? 0 : hold,
|
|
1032
|
+
threshold: 1e-4,
|
|
1033
|
+
lift: switchControl.lift,
|
|
1034
|
+
settle: switchControl.settle,
|
|
1035
|
+
}),
|
|
1036
|
+
});
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
private clearTimers(): void {
|
|
1041
|
+
if (this.browserClickTimer != null) {
|
|
1042
|
+
clearTimeout(this.browserClickTimer);
|
|
1043
|
+
this.browserClickTimer = null;
|
|
1044
|
+
}
|
|
1045
|
+
if (this.reconcileTimer != null) {
|
|
1046
|
+
clearTimeout(this.reconcileTimer);
|
|
1047
|
+
this.reconcileTimer = null;
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
/**
|
|
1053
|
+
* Switch's gestures, attached for every pointer, with the touch
|
|
1054
|
+
* presentation under a coarse primary pointer. Its parts
|
|
1055
|
+
* must be rendered (and styled for it) whether or not it is active; it only
|
|
1056
|
+
* animates and listens.
|
|
1057
|
+
*/
|
|
1058
|
+
export function useSwitchTouch(
|
|
1059
|
+
parts: SwitchTouchParts,
|
|
1060
|
+
{
|
|
1061
|
+
isOn,
|
|
1062
|
+
isInteractive,
|
|
1063
|
+
isDisabled,
|
|
1064
|
+
trackSize,
|
|
1065
|
+
otherTrackProps,
|
|
1066
|
+
}: SwitchTouchOptions,
|
|
1067
|
+
): void {
|
|
1068
|
+
const {isTouch} = useTouchPresentation();
|
|
1069
|
+
const [trackWidth, trackHeight] = trackSize;
|
|
1070
|
+
const geometry = useMemo(
|
|
1071
|
+
() => switchTouchGeometry(trackWidth, trackHeight),
|
|
1072
|
+
[trackWidth, trackHeight],
|
|
1073
|
+
);
|
|
1074
|
+
const otherTrack = trackStateOf(otherTrackProps);
|
|
1075
|
+
const [controller] = useState(
|
|
1076
|
+
() => new SwitchTouchController(isOn, geometry),
|
|
1077
|
+
);
|
|
1078
|
+
|
|
1079
|
+
// Syncs the touch controller's animators with the value. It runs first: the
|
|
1080
|
+
// thumb's CSS place and the track's fill have just changed with the value.
|
|
1081
|
+
useIsomorphicLayoutEffect(() => {
|
|
1082
|
+
controller.setOtherTrack(otherTrack);
|
|
1083
|
+
controller.setValue(isOn);
|
|
1084
|
+
});
|
|
1085
|
+
|
|
1086
|
+
// Syncs the controller's thumb geometry with the track's size.
|
|
1087
|
+
useIsomorphicLayoutEffect(() => {
|
|
1088
|
+
controller.setGeometry(geometry);
|
|
1089
|
+
}, [controller, geometry]);
|
|
1090
|
+
|
|
1091
|
+
// Syncs whether the controller's gestures take a press.
|
|
1092
|
+
useIsomorphicLayoutEffect(() => {
|
|
1093
|
+
controller.setInteractive(isInteractive);
|
|
1094
|
+
}, [controller, isInteractive]);
|
|
1095
|
+
|
|
1096
|
+
// Attaches the controller's DOM listeners and animators to the switch's
|
|
1097
|
+
// parts, and detaches them on cleanup.
|
|
1098
|
+
useIsomorphicLayoutEffect(() => {
|
|
1099
|
+
const surface = parts.surface.current;
|
|
1100
|
+
const input = parts.input.current;
|
|
1101
|
+
const thumb = parts.thumb.current;
|
|
1102
|
+
const track = parts.track.current;
|
|
1103
|
+
// A disabled switch takes no press: no listener is active. A busy one
|
|
1104
|
+
// stays attached (setInteractive ignores its presses),
|
|
1105
|
+
// so the change that made it busy keeps its motion.
|
|
1106
|
+
if (
|
|
1107
|
+
isDisabled ||
|
|
1108
|
+
surface == null ||
|
|
1109
|
+
input == null ||
|
|
1110
|
+
thumb == null ||
|
|
1111
|
+
track == null
|
|
1112
|
+
) {
|
|
1113
|
+
return undefined;
|
|
1114
|
+
}
|
|
1115
|
+
controller.attach(
|
|
1116
|
+
{surface, input, thumb, track},
|
|
1117
|
+
isTouch ? 'touch' : 'pointer',
|
|
1118
|
+
);
|
|
1119
|
+
return () => controller.detach();
|
|
1120
|
+
}, [controller, isTouch, isDisabled, parts]);
|
|
1121
|
+
}
|