@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
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
import React, {useCallback, useEffect, useRef, type ReactNode} from 'react';
|
|
20
20
|
import * as stylex from '@stylexjs/stylex';
|
|
21
21
|
import {useLayer, type ContextRenderProps} from '../Layer/useLayer';
|
|
22
|
-
import {
|
|
22
|
+
import {useFocusTrapInternal} from '../hooks/useFocusTrap';
|
|
23
23
|
import {LayerDepthProvider} from '../Layer/LayerDepthContext';
|
|
24
24
|
import type {StyleXStyles} from '@stylexjs/stylex';
|
|
25
25
|
import {
|
|
@@ -34,6 +34,7 @@ import {rtlStyles} from '../utils';
|
|
|
34
34
|
import {useDevWarning} from '../hooks/useDevWarning';
|
|
35
35
|
import {focusOutlineProps} from '../utils/focusOutline.stylex';
|
|
36
36
|
import {mergeProps} from '../utils/mergeProps';
|
|
37
|
+
import {isFocusDetached} from '../utils/focusReturn';
|
|
37
38
|
import {themeProps} from '../utils/themeProps';
|
|
38
39
|
import {paddingStyles} from '../Layout/padding.stylex';
|
|
39
40
|
import type {SpacingStep} from '../utils/types';
|
|
@@ -422,28 +423,51 @@ function usePopoverImplementation(
|
|
|
422
423
|
|
|
423
424
|
// Track whether to skip auto-focus for the current open event
|
|
424
425
|
const skipAutoFocusRef = useRef(false);
|
|
426
|
+
// Whether `show` focused a content control within the opening event.
|
|
427
|
+
const hasFocusedInGestureRef = useRef(false);
|
|
428
|
+
|
|
429
|
+
// Set when the layer reports it has shown, so `show` can tell a popover
|
|
430
|
+
// that opened within the call from one whose content mounts later.
|
|
431
|
+
const hasShownRef = useRef(false);
|
|
432
|
+
const handleLayerShow = useCallback(() => {
|
|
433
|
+
hasShownRef.current = true;
|
|
434
|
+
onShow?.();
|
|
435
|
+
}, [onShow]);
|
|
425
436
|
|
|
426
437
|
// Core layer for popover positioning
|
|
427
438
|
const layer = useLayer({
|
|
428
439
|
mode: 'context',
|
|
429
440
|
lightDismiss: hasLightDismiss,
|
|
430
|
-
onShow,
|
|
441
|
+
onShow: handleLayerShow,
|
|
431
442
|
onHide,
|
|
432
443
|
});
|
|
433
444
|
|
|
434
445
|
// Focus trap for the popover content. Escape stays registered while light
|
|
435
446
|
// dismiss is on (native popover="auto" closes on Escape regardless), so a
|
|
436
447
|
// host Dialog keeps deferring to this trap instead of double-dismissing.
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
448
|
+
// The element focused when `show` ran: initial focus may move inside before
|
|
449
|
+
// the trap activates, so the trap restores focus here instead.
|
|
450
|
+
const openerRef = useRef<HTMLElement | null>(null);
|
|
451
|
+
const getRestoreTarget = useCallback(() => openerRef.current, []);
|
|
452
|
+
const {containerRef: contentRef, focusFirst} =
|
|
453
|
+
useFocusTrapInternal<HTMLDivElement>({
|
|
454
|
+
isActive: layer.isOpen,
|
|
455
|
+
onEscape: hasEscapeDismiss || hasLightDismiss ? layer.hide : undefined,
|
|
456
|
+
getRestoreTarget,
|
|
457
|
+
});
|
|
441
458
|
|
|
442
459
|
const focusInitialTarget = useCallback(() => {
|
|
443
460
|
const container = contentRef.current;
|
|
444
461
|
if (!container) {
|
|
445
462
|
return;
|
|
446
463
|
}
|
|
464
|
+
// Focus already on a control inside (set in `onShow`, or a press in the
|
|
465
|
+
// content) is where the user or the caller wants it; moving it would
|
|
466
|
+
// undo that.
|
|
467
|
+
const active = container.ownerDocument.activeElement;
|
|
468
|
+
if (active != null && active !== container && container.contains(active)) {
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
447
471
|
if (focusFirstContentControl(container)) {
|
|
448
472
|
return;
|
|
449
473
|
}
|
|
@@ -459,16 +483,23 @@ function usePopoverImplementation(
|
|
|
459
483
|
// Reset the per-open focus preference after the popover closes.
|
|
460
484
|
if (!layer.isOpen) {
|
|
461
485
|
skipAutoFocusRef.current = false;
|
|
486
|
+
hasFocusedInGestureRef.current = false;
|
|
462
487
|
return;
|
|
463
488
|
}
|
|
464
489
|
if (hasAutoFocus && !skipAutoFocusRef.current) {
|
|
465
490
|
// Wait a frame so the content is mounted. A popover that closes or
|
|
466
491
|
// unmounts first cancels it: the frame would otherwise move focus into
|
|
467
492
|
// content that is no longer shown, after the close had left it alone.
|
|
468
|
-
const frame = requestAnimationFrame(
|
|
493
|
+
const frame = requestAnimationFrame(() => {
|
|
494
|
+
const doc = contentRef.current?.ownerDocument ?? document;
|
|
495
|
+
if (hasFocusedInGestureRef.current && !isFocusDetached(doc)) {
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
focusInitialTarget();
|
|
499
|
+
});
|
|
469
500
|
return () => cancelAnimationFrame(frame);
|
|
470
501
|
}
|
|
471
|
-
}, [layer.isOpen, hasAutoFocus, focusInitialTarget]);
|
|
502
|
+
}, [layer.isOpen, hasAutoFocus, focusInitialTarget, contentRef]);
|
|
472
503
|
|
|
473
504
|
// Combined ref for trigger element (layer anchor + our ref). It is keyed on
|
|
474
505
|
// the layer's own ref, not the whole layer: the layer object changes with
|
|
@@ -490,9 +521,40 @@ function usePopoverImplementation(
|
|
|
490
521
|
const show = useCallback(
|
|
491
522
|
(showOptions?: {skipAutoFocus?: boolean}) => {
|
|
492
523
|
skipAutoFocusRef.current = showOptions?.skipAutoFocus ?? false;
|
|
524
|
+
hasShownRef.current = false;
|
|
525
|
+
const activeElement = (
|
|
526
|
+
triggerElementRef.current?.ownerDocument ?? document
|
|
527
|
+
).activeElement as HTMLElement | null;
|
|
528
|
+
openerRef.current =
|
|
529
|
+
activeElement != null && !contentRef.current?.contains(activeElement)
|
|
530
|
+
? activeElement
|
|
531
|
+
: null;
|
|
493
532
|
layer.show();
|
|
533
|
+
// When the content is already mounted, the layer shows it within this
|
|
534
|
+
// call, so initial focus lands within the event that opened it: iOS
|
|
535
|
+
// raises its keyboard only for a focus made while the tap is still
|
|
536
|
+
// being handled. The frame in the effect above stays for content that
|
|
537
|
+
// mounts later.
|
|
538
|
+
// Only a content control already rendered takes focus here; content
|
|
539
|
+
// that renders on open (and the dialog-surface fallback) waits for the
|
|
540
|
+
// effect's frame.
|
|
541
|
+
const container = contentRef.current;
|
|
542
|
+
if (
|
|
543
|
+
hasShownRef.current &&
|
|
544
|
+
hasAutoFocus &&
|
|
545
|
+
!skipAutoFocusRef.current &&
|
|
546
|
+
container != null &&
|
|
547
|
+
!container.contains(container.ownerDocument.activeElement) &&
|
|
548
|
+
focusFirstContentControl(container)
|
|
549
|
+
) {
|
|
550
|
+
// Initial focus is done. The effect's frame stays only as a fallback
|
|
551
|
+
// for focus lost to the page (content swapped in the same handler
|
|
552
|
+
// unmounts the control just focused); it must not move focus the user
|
|
553
|
+
// has since moved elsewhere.
|
|
554
|
+
hasFocusedInGestureRef.current = true;
|
|
555
|
+
}
|
|
494
556
|
},
|
|
495
|
-
[layer],
|
|
557
|
+
[layer, hasAutoFocus, contentRef],
|
|
496
558
|
);
|
|
497
559
|
|
|
498
560
|
// Toggle function. Opening delegates to show so every route inherits the
|
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
type TokenizerOverflowBehavior,
|
|
32
32
|
} from '../Tokenizer';
|
|
33
33
|
import {TokenizerClearAllMessageContext} from '../Tokenizer/TokenizerClearAllMessageContext';
|
|
34
|
+
import {TokenizerItemRemovableContext} from '../Tokenizer/TokenizerItemRemovableContext';
|
|
34
35
|
import {TypeaheadItem} from '../Typeahead/TypeaheadItem';
|
|
35
36
|
import {Token} from '../Token';
|
|
36
37
|
import {Avatar} from '../Avatar';
|
|
@@ -751,6 +752,10 @@ export function PowerSearch({
|
|
|
751
752
|
[config, filters, onChange, setPopoverState],
|
|
752
753
|
);
|
|
753
754
|
|
|
755
|
+
// Tokenizer's Backspace does not go through renderToken. This predicate
|
|
756
|
+
// locks every token of a read-only field.
|
|
757
|
+
const isItemRemovable = useCallback(() => !isReadOnly, [isReadOnly]);
|
|
758
|
+
|
|
754
759
|
// Handle clicking a token to edit
|
|
755
760
|
const handleTokenClick = useCallback(
|
|
756
761
|
(index: number) => {
|
|
@@ -1117,37 +1122,39 @@ export function PowerSearch({
|
|
|
1117
1122
|
{/* The field's Clear all announces PowerSearch's own words
|
|
1118
1123
|
(component:PowerSearch; component:Tokenizer AR7). */}
|
|
1119
1124
|
<TokenizerClearAllMessageContext value={clearAllMessage}>
|
|
1120
|
-
<
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1125
|
+
<TokenizerItemRemovableContext value={isItemRemovable}>
|
|
1126
|
+
<Tokenizer
|
|
1127
|
+
handleRef={tokenizerRef}
|
|
1128
|
+
label={label}
|
|
1129
|
+
isLabelHidden={isLabelHidden}
|
|
1130
|
+
searchSource={searchSource}
|
|
1131
|
+
value={tokenizerValue}
|
|
1132
|
+
onChange={handleTokenizerChange}
|
|
1133
|
+
renderToken={renderToken}
|
|
1134
|
+
renderItem={renderItem}
|
|
1135
|
+
maxMenuItems={MAX_BROWSE_MENU_ITEMS}
|
|
1136
|
+
menuWidth={menuWidth}
|
|
1137
|
+
placeholder={filters.length === 0 ? placeholder : ''}
|
|
1138
|
+
hasAutoFocus={hasAutoFocus}
|
|
1139
|
+
hasClear={hasClear && !isReadOnly}
|
|
1140
|
+
startIcon={startIcon}
|
|
1141
|
+
endContent={combinedEndContent}
|
|
1142
|
+
isDisabled={isDisabled}
|
|
1143
|
+
disabledMessage={disabledMessage}
|
|
1144
|
+
size={size}
|
|
1145
|
+
tokenOverflowBehavior={tokenOverflowBehavior}
|
|
1146
|
+
hasEntriesOnFocus
|
|
1147
|
+
debounceMs={0}
|
|
1148
|
+
status={status}
|
|
1149
|
+
statusVariant={statusVariant}
|
|
1150
|
+
onFocus={onFocus}
|
|
1151
|
+
onBlur={onBlur}
|
|
1152
|
+
containerXstyle={xstyle}
|
|
1153
|
+
containerClassName={className}
|
|
1154
|
+
containerStyle={style}
|
|
1155
|
+
data-testid={testId}
|
|
1156
|
+
/>
|
|
1157
|
+
</TokenizerItemRemovableContext>
|
|
1151
1158
|
</TokenizerClearAllMessageContext>
|
|
1152
1159
|
</div>
|
|
1153
1160
|
{popover.render(popoverContent, {
|
|
@@ -101,7 +101,8 @@ export const docs = {
|
|
|
101
101
|
{
|
|
102
102
|
name: 'isIndeterminate',
|
|
103
103
|
type: 'boolean',
|
|
104
|
-
description:
|
|
104
|
+
description:
|
|
105
|
+
'Animated loading indicator for unknown progress. Under reduced motion it pulses in place instead of sliding.',
|
|
105
106
|
default: 'false',
|
|
106
107
|
},
|
|
107
108
|
{
|
|
@@ -160,7 +161,7 @@ export const docs = {
|
|
|
160
161
|
usage: {
|
|
161
162
|
anatomy,
|
|
162
163
|
description:
|
|
163
|
-
"A horizontal bar showing the completion progress of a task. Use it for operations where the duration is known, or as an animated indicator when progress can't be calculated. Supports semantic color variants, value labels, and custom formatting.",
|
|
164
|
+
"A horizontal bar showing the completion progress of a task. Use it for operations where the duration is known, or as an animated indicator when progress can't be calculated. Supports semantic color variants, value labels, and custom formatting. Under a finger a new value animates as on iOS: linearly, in proportion to how far it moves (a full track takes one second at the theme's default tempo), and the fill keeps a minimum visible length above zero.",
|
|
164
165
|
bestPractices: [
|
|
165
166
|
{
|
|
166
167
|
guidance: true,
|
|
@@ -304,7 +305,7 @@ export const docsZh = {
|
|
|
304
305
|
usage: {
|
|
305
306
|
anatomy,
|
|
306
307
|
description:
|
|
307
|
-
"A horizontal bar showing the completion progress of a task. Use it for operations where the duration is known, or as an animated indicator when progress can't be calculated. Supports semantic color variants, value labels, and custom formatting.",
|
|
308
|
+
"A horizontal bar showing the completion progress of a task. Use it for operations where the duration is known, or as an animated indicator when progress can't be calculated. Supports semantic color variants, value labels, and custom formatting. Under a finger a new value animates as on iOS: linearly, in proportion to how far it moves (a full track takes one second at the theme's default tempo), and the fill keeps a minimum visible length above zero.",
|
|
308
309
|
bestPractices: [
|
|
309
310
|
{
|
|
310
311
|
guidance: true,
|
|
@@ -347,7 +348,7 @@ export const docsDense = {
|
|
|
347
348
|
usage: {
|
|
348
349
|
anatomy,
|
|
349
350
|
description:
|
|
350
|
-
"A horizontal bar showing the completion progress of a task. Use it for operations where the duration is known, or as an animated indicator when progress can't be calculated. Supports semantic color variants, value labels, and custom formatting.",
|
|
351
|
+
"A horizontal bar showing the completion progress of a task. Use it for operations where the duration is known, or as an animated indicator when progress can't be calculated. Supports semantic color variants, value labels, and custom formatting. Under a finger a new value animates as on iOS: linearly, in proportion to how far it moves (a full track takes one second at the theme's default tempo), and the fill keeps a minimum visible length above zero.",
|
|
351
352
|
bestPractices: [
|
|
352
353
|
{
|
|
353
354
|
guidance: true,
|
|
@@ -390,7 +391,8 @@ export const docsDense = {
|
|
|
390
391
|
formatValueLabel:
|
|
391
392
|
'Custom value label formatter; defaults to percentage string.',
|
|
392
393
|
variant: 'Semantic color variant.',
|
|
393
|
-
isIndeterminate:
|
|
394
|
+
isIndeterminate:
|
|
395
|
+
'Animated loading indicator for unknown progress. Under reduced motion it pulses in place instead of sliding.',
|
|
394
396
|
marks:
|
|
395
397
|
'Fixed target marks ({value, label?}) drawn on the track in the 0..max scale; stay visible past the fill. Marks inside the fill take the variant on-color; marks on the bare track take the primary text color (secondary when disabled). A label reveals a tooltip on hover/focus. Ignored when indeterminate.',
|
|
396
398
|
isDisabled: 'Visually disabled: grays out fill and text.',
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* @file ProgressBar.tsx
|
|
7
|
-
* @input Uses React, useId, stylex,
|
|
7
|
+
* @input Uses React, useId, stylex, tokens, ProgressBarTouch and resolveMotion
|
|
8
8
|
* @output Exports ProgressBar component, ProgressBarProps, ProgressBarVariant types
|
|
9
9
|
* @position Core implementation; consumed by index.ts
|
|
10
10
|
*
|
|
@@ -35,6 +35,11 @@ import type {BaseProps} from '../BaseProps';
|
|
|
35
35
|
import {themeProps} from '../utils/themeProps';
|
|
36
36
|
import {VisuallyHidden} from '../VisuallyHidden';
|
|
37
37
|
import type {ProgressBarVariantMap} from './index';
|
|
38
|
+
import {useProgressBarTouch} from './ProgressBarTouch';
|
|
39
|
+
import {formatMotionRoleValue} from '../motion/roles';
|
|
40
|
+
import {resolveMotion} from '../motion/resolveMotion';
|
|
41
|
+
import {progressBar} from '../motion/uikit/progressBar';
|
|
42
|
+
import {timing} from '../motion/uikit/timing';
|
|
38
43
|
|
|
39
44
|
// Every mark is wrapped in a Tooltip (marks always carry a label), so load it
|
|
40
45
|
// lazily: a ProgressBar with no marks never bundles the Tooltip chunk. While it
|
|
@@ -116,7 +121,7 @@ export interface ProgressBarProps extends BaseProps<HTMLDivElement> {
|
|
|
116
121
|
* When true, renders an animated indeterminate progress indicator.
|
|
117
122
|
* Use when the progress amount is unknown (e.g. loading, processing).
|
|
118
123
|
* The `value` and `hasValueLabel` props are ignored in this mode.
|
|
119
|
-
*
|
|
124
|
+
* Under reduced motion it pulses in place instead of sliding.
|
|
120
125
|
* @default false
|
|
121
126
|
*/
|
|
122
127
|
isIndeterminate?: boolean;
|
|
@@ -169,6 +174,19 @@ const indeterminateSlideRtl = stylex.keyframes({
|
|
|
169
174
|
},
|
|
170
175
|
});
|
|
171
176
|
|
|
177
|
+
// Under reduced motion the indeterminate fill stops travelling and pulses in
|
|
178
|
+
// place. It goes from full opacity down to the pulse's floor
|
|
179
|
+
// (`--_progress-bar-pulse-floor`, the progress table's) and back. The
|
|
180
|
+
// animation alternates direction each pass.
|
|
181
|
+
const indeterminatePulse = stylex.keyframes({
|
|
182
|
+
from: {
|
|
183
|
+
opacity: 1,
|
|
184
|
+
},
|
|
185
|
+
to: {
|
|
186
|
+
opacity: 'var(--_progress-bar-pulse-floor)',
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
|
|
172
190
|
// =============================================================================
|
|
173
191
|
// Styles
|
|
174
192
|
// =============================================================================
|
|
@@ -249,10 +267,25 @@ const styles = stylex.create({
|
|
|
249
267
|
// no transform), as before: the track's clip would cut the CanvasText
|
|
250
268
|
// outline's start cap off a slid fill, and that outline is what keeps the
|
|
251
269
|
// fill's shape visible there (architecture:theme-tokens INV11).
|
|
270
|
+
//
|
|
271
|
+
// Under a finger (TOUCH_QUERY, on every touch device), the fill moves as
|
|
272
|
+
// UIProgressView's setProgress(_:animated: true) does. It moves linearly
|
|
273
|
+
// from wherever it is drawn. Its duration is the `progress.fillTrackMs` role
|
|
274
|
+
// times the share of the track the value moved (`--_progress-bar-move`).
|
|
275
|
+
// Its curve is the `progress.curves.fill` role (`--_progress-bar-curve`).
|
|
276
|
+
// The touch layer (ProgressBarTouch) writes both, and this rule scales the
|
|
277
|
+
// duration by the theme's tempo. Above zero, the fill never draws shorter
|
|
278
|
+
// than the touch minimum (`--_progress-bar-min`, 16px on the 8px track). At
|
|
279
|
+
// zero it paints nothing (`fillEmpty`). Under reduced motion it lands at
|
|
280
|
+
// once, as everywhere (below). A mouse or trackpad keeps the theme's timing.
|
|
252
281
|
fill: {
|
|
253
282
|
width: {
|
|
254
283
|
default: '100%',
|
|
255
|
-
'@media (forced-colors: active)':
|
|
284
|
+
'@media (forced-colors: active)': {
|
|
285
|
+
default: 'var(--_progress-bar-fill, 0%)',
|
|
286
|
+
'@media (pointer: coarse)':
|
|
287
|
+
'calc(max(var(--_progress-bar-fill, 0%), var(--_progress-bar-min, 0px)) + var(--_progress-bar-flip, 0) * 0.01px)',
|
|
288
|
+
},
|
|
256
289
|
},
|
|
257
290
|
height: '100%',
|
|
258
291
|
// Forced colors replace status fills with Canvas; this boundary keeps the
|
|
@@ -271,6 +304,19 @@ const styles = stylex.create({
|
|
|
271
304
|
default: 'translateX(calc(100% - var(--_progress-bar-fill, 0%)))',
|
|
272
305
|
'@media (forced-colors: active)': 'none',
|
|
273
306
|
},
|
|
307
|
+
'@media (pointer: coarse)': {
|
|
308
|
+
// `--_progress-bar-flip` steps 0, 1, 2, 0… on every change. It moves
|
|
309
|
+
// the fill by that many hundredths of a pixel. So a change back to the
|
|
310
|
+
// value it just left is never the running transition's own start
|
|
311
|
+
// value. CSS shortens a transition that reverses to its start, which
|
|
312
|
+
// would halve a return mid-flight (10 → 90 → 10). It takes three
|
|
313
|
+
// values because two would repeat on that return.
|
|
314
|
+
default:
|
|
315
|
+
'translateX(calc(max(var(--_progress-bar-fill, 0%), var(--_progress-bar-min, 0px)) - 100% + var(--_progress-bar-flip, 0) * 0.01px))',
|
|
316
|
+
':dir(rtl)':
|
|
317
|
+
'translateX(calc(min(calc(100% - var(--_progress-bar-fill, 0%)), calc(100% - var(--_progress-bar-min, 0px))) - var(--_progress-bar-flip, 0) * 0.01px))',
|
|
318
|
+
},
|
|
319
|
+
// After the touch key, so StyleX negates that key under forced colors.
|
|
274
320
|
'@media (forced-colors: active)': 'none',
|
|
275
321
|
},
|
|
276
322
|
willChange: {
|
|
@@ -281,17 +327,36 @@ const styles = stylex.create({
|
|
|
281
327
|
default: 'transform',
|
|
282
328
|
'@media (forced-colors: active)': 'width',
|
|
283
329
|
},
|
|
284
|
-
// Under reduced motion a new value lands at once,
|
|
285
|
-
// fill never travels
|
|
286
|
-
//
|
|
287
|
-
//
|
|
330
|
+
// Under reduced motion a new value lands at once, with a mouse as with a
|
|
331
|
+
// finger: the fill never travels. The keys are the shared reduced-motion
|
|
332
|
+
// queries, as the other transitions spell them. The coarse key comes after
|
|
333
|
+
// the touch key, so it wins there.
|
|
288
334
|
transitionDuration: {
|
|
289
335
|
default: durationVars['--duration-medium'],
|
|
336
|
+
// The touch layer's move (the `progress.fillTrackMs` role times the
|
|
337
|
+
// share moved), scaled by the theme's tempo.
|
|
338
|
+
'@media (pointer: coarse)': {
|
|
339
|
+
default:
|
|
340
|
+
'calc(var(--_progress-bar-move, 0ms) * var(--_astryx-motion-tempo, 1))',
|
|
341
|
+
// Under forced colors the fill is sized by `width` instead. Touch
|
|
342
|
+
// motion never animates a layout property, so with a finger the new
|
|
343
|
+
// size lands at once there.
|
|
344
|
+
'@media (forced-colors: active)': '0s',
|
|
345
|
+
},
|
|
290
346
|
'@media (prefers-reduced-motion: reduce) and (not (pointer: coarse))':
|
|
291
347
|
'0s',
|
|
292
348
|
'@media (prefers-reduced-motion: reduce) and (pointer: coarse)': '0s',
|
|
293
349
|
},
|
|
294
|
-
transitionTimingFunction:
|
|
350
|
+
transitionTimingFunction: {
|
|
351
|
+
default: easeVars['--ease-standard'],
|
|
352
|
+
'@media (pointer: coarse)': 'var(--_progress-bar-curve, linear)',
|
|
353
|
+
},
|
|
354
|
+
},
|
|
355
|
+
fillEmpty: {
|
|
356
|
+
opacity: {
|
|
357
|
+
default: null,
|
|
358
|
+
'@media (pointer: coarse)': 0,
|
|
359
|
+
},
|
|
295
360
|
},
|
|
296
361
|
indeterminateFill: {
|
|
297
362
|
height: '100%',
|
|
@@ -304,17 +369,37 @@ const styles = stylex.create({
|
|
|
304
369
|
'@media (forced-colors: active)': 'CanvasText',
|
|
305
370
|
},
|
|
306
371
|
outlineOffset: {default: null, '@media (forced-colors: active)': '-1px'},
|
|
307
|
-
|
|
372
|
+
// Under reduced motion, on every pointer, the fill spans the whole track
|
|
373
|
+
// as one solid bar, and only its opacity pulses. Each way takes the
|
|
374
|
+
// `progress.reducedMotionPulseMs` role on the `timing.curves.move` curve
|
|
375
|
+
// (`--_progress-bar-pulse`, `--_progress-bar-pulse-curve`, set inline from
|
|
376
|
+
// the progress table). A busy loop: tempo does not scale it.
|
|
377
|
+
width: {
|
|
378
|
+
default: '40%',
|
|
379
|
+
'@media (prefers-reduced-motion: reduce)': '100%',
|
|
380
|
+
},
|
|
308
381
|
borderRadius: radiusVars['--radius-full'],
|
|
309
382
|
animationName: {
|
|
310
383
|
default: indeterminateSlide,
|
|
311
|
-
':is([dir="rtl"] *)':
|
|
384
|
+
':is([dir="rtl"] *)': {
|
|
385
|
+
default: indeterminateSlideRtl,
|
|
386
|
+
'@media (prefers-reduced-motion: reduce)': indeterminatePulse,
|
|
387
|
+
},
|
|
388
|
+
'@media (prefers-reduced-motion: reduce)': indeterminatePulse,
|
|
312
389
|
},
|
|
313
390
|
animationDuration: {
|
|
314
391
|
default: '1.5s',
|
|
315
|
-
'@media (prefers-reduced-motion: reduce)': '
|
|
392
|
+
'@media (prefers-reduced-motion: reduce)': 'var(--_progress-bar-pulse)',
|
|
393
|
+
},
|
|
394
|
+
animationTimingFunction: {
|
|
395
|
+
default: 'ease-in-out',
|
|
396
|
+
'@media (prefers-reduced-motion: reduce)':
|
|
397
|
+
'var(--_progress-bar-pulse-curve)',
|
|
398
|
+
},
|
|
399
|
+
animationDirection: {
|
|
400
|
+
default: null,
|
|
401
|
+
'@media (prefers-reduced-motion: reduce)': 'alternate',
|
|
316
402
|
},
|
|
317
|
-
animationTimingFunction: 'ease-in-out',
|
|
318
403
|
animationIterationCount: 'infinite',
|
|
319
404
|
},
|
|
320
405
|
// A mark is a vertical tick centered on the track via their shared positioning container.
|
|
@@ -425,10 +510,41 @@ const markOnTrackStyles = stylex.create({
|
|
|
425
510
|
},
|
|
426
511
|
});
|
|
427
512
|
|
|
513
|
+
// The reduced-motion pulse's variables, from the progress table.
|
|
514
|
+
type PulseStyle = CSSProperties &
|
|
515
|
+
Record<
|
|
516
|
+
| '--_progress-bar-pulse'
|
|
517
|
+
| '--_progress-bar-pulse-curve'
|
|
518
|
+
| '--_progress-bar-pulse-floor',
|
|
519
|
+
string
|
|
520
|
+
>;
|
|
521
|
+
|
|
428
522
|
// `CSSProperties` has no index signature for custom properties, so the fill's
|
|
429
523
|
// inline style — which publishes the filled share as the private
|
|
430
524
|
// `--_progress-bar-fill` — needs this augmentation to typecheck.
|
|
431
|
-
type FillStyle = CSSProperties &
|
|
525
|
+
type FillStyle = CSSProperties &
|
|
526
|
+
Record<
|
|
527
|
+
| '--_progress-bar-fill'
|
|
528
|
+
| '--_progress-bar-move'
|
|
529
|
+
| '--_progress-bar-curve'
|
|
530
|
+
| '--_progress-bar-flip'
|
|
531
|
+
| '--_progress-bar-min',
|
|
532
|
+
string
|
|
533
|
+
>;
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* The reduced-motion pulse plays the `progress.reducedMotionPulseMs` role on
|
|
537
|
+
* the `timing.curves.move` curve, down to the progress table's floor.
|
|
538
|
+
*/
|
|
539
|
+
function pulseStyle(element: Element | null): PulseStyle {
|
|
540
|
+
const pulseMs = resolveMotion(element, progressBar, 'reducedMotionPulseMs');
|
|
541
|
+
const curve = resolveMotion(element, timing.curves.move);
|
|
542
|
+
return {
|
|
543
|
+
'--_progress-bar-pulse': `${pulseMs}ms`,
|
|
544
|
+
'--_progress-bar-pulse-curve': formatMotionRoleValue('curve', curve),
|
|
545
|
+
'--_progress-bar-pulse-floor': String(progressBar.reducedMotionPulseFloor),
|
|
546
|
+
};
|
|
547
|
+
}
|
|
432
548
|
|
|
433
549
|
function defaultFormatValueLabel(value: number, max: number): string {
|
|
434
550
|
const pct = max > 0 ? Math.round((value / max) * 100) : 0;
|
|
@@ -509,7 +625,16 @@ export function ProgressBar({
|
|
|
509
625
|
const showValueLabel = hasValueLabel && !isIndeterminate;
|
|
510
626
|
|
|
511
627
|
const fillVariant = isDisabled ? 'disabled' : variant;
|
|
512
|
-
|
|
628
|
+
// Under a finger the fill moves as UIProgressView's does and draws at least
|
|
629
|
+
// 16px; what that needs from script is the touch layer's (ProgressBarTouch).
|
|
630
|
+
const touch = useProgressBarTouch(
|
|
631
|
+
percentage,
|
|
632
|
+
!isIndeterminate && (marks?.length ?? 0) > 0,
|
|
633
|
+
);
|
|
634
|
+
const fillStyle: FillStyle = {
|
|
635
|
+
'--_progress-bar-fill': `${percentage}%`,
|
|
636
|
+
...touch.fillVars,
|
|
637
|
+
};
|
|
513
638
|
|
|
514
639
|
// Marks make no sense without a determinate value, so they are only drawn
|
|
515
640
|
// in determinate mode. Non-finite mark values are dropped; the rest are
|
|
@@ -532,7 +657,7 @@ export function ProgressBar({
|
|
|
532
657
|
value: mark.value,
|
|
533
658
|
label: mark.label,
|
|
534
659
|
pct,
|
|
535
|
-
isOnFill: percentage > 0 && pct <=
|
|
660
|
+
isOnFill: percentage > 0 && pct <= touch.drawnPercentage,
|
|
536
661
|
};
|
|
537
662
|
})
|
|
538
663
|
: [];
|
|
@@ -582,6 +707,7 @@ export function ProgressBar({
|
|
|
582
707
|
aria-valuemax={isIndeterminate ? undefined : safeMax}
|
|
583
708
|
aria-labelledby={labelId}
|
|
584
709
|
aria-valuetext={isIndeterminate ? undefined : valueText}
|
|
710
|
+
ref={touch.trackRef}
|
|
585
711
|
{...mergeProps(
|
|
586
712
|
themeProps('progress-bar-track'),
|
|
587
713
|
stylex.props(styles.track),
|
|
@@ -595,12 +721,17 @@ export function ProgressBar({
|
|
|
595
721
|
variantStyles[fillVariant],
|
|
596
722
|
),
|
|
597
723
|
)}
|
|
724
|
+
style={pulseStyle(null)}
|
|
598
725
|
/>
|
|
599
726
|
) : (
|
|
600
727
|
<div
|
|
601
728
|
{...mergeProps(
|
|
602
729
|
themeProps('progress-bar-fill', {variant: fillVariant}),
|
|
603
|
-
stylex.props(
|
|
730
|
+
stylex.props(
|
|
731
|
+
styles.fill,
|
|
732
|
+
percentage === 0 && styles.fillEmpty,
|
|
733
|
+
variantStyles[fillVariant],
|
|
734
|
+
),
|
|
604
735
|
)}
|
|
605
736
|
style={fillStyle}
|
|
606
737
|
/>
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
'use client';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @file ProgressBarTouch.ts
|
|
7
|
+
* @input Uses React, the touch presentation resolver, the shared resize
|
|
8
|
+
* observer, and the progress table's roles (through resolveMotion)
|
|
9
|
+
* @output useProgressBarTouch: the fill's touch variables (move length,
|
|
10
|
+
* curve, return offset, minimum length) and, under a finger with marks,
|
|
11
|
+
* the share of the track the fill draws
|
|
12
|
+
* @position ProgressBar's touch layer, imported only by ProgressBar.tsx
|
|
13
|
+
*
|
|
14
|
+
* SYNC: When modified, update /packages/core/src/ProgressBar/ProgressBar.tsx
|
|
15
|
+
* (its touch styles read these variables and the 16px minimum) and
|
|
16
|
+
* /packages/core/src/motion/uikit/progressBar.ts.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import {useEffect, useRef, useState, type RefObject} from 'react';
|
|
20
|
+
import {formatMotionRoleValue} from '../motion/roles';
|
|
21
|
+
import {resolveMotion} from '../motion/resolveMotion';
|
|
22
|
+
import {progressBar} from '../motion/uikit/progressBar';
|
|
23
|
+
import {observeResize} from '../utils/sharedResizeObserver';
|
|
24
|
+
import {useTouchPresentation} from '../utils/touchPresentation';
|
|
25
|
+
|
|
26
|
+
/** The track's height. */
|
|
27
|
+
const TRACK_HEIGHT_PX = 8;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Under a finger the fill never draws shorter than this above zero:
|
|
31
|
+
* UIProgressView's minimum, two track heights, on ProgressBar's 8px track.
|
|
32
|
+
*/
|
|
33
|
+
export const TOUCH_MIN_FILL_PX =
|
|
34
|
+
progressBar.progressView.minimumFillHeights * TRACK_HEIGHT_PX;
|
|
35
|
+
|
|
36
|
+
export interface ProgressBarTouch {
|
|
37
|
+
/** The fill's touch timing variables, set inline on the fill. */
|
|
38
|
+
fillVars: Record<
|
|
39
|
+
| '--_progress-bar-move'
|
|
40
|
+
| '--_progress-bar-curve'
|
|
41
|
+
| '--_progress-bar-flip'
|
|
42
|
+
| '--_progress-bar-min',
|
|
43
|
+
string
|
|
44
|
+
>;
|
|
45
|
+
/** The track, measured under a finger when the bar has marks. */
|
|
46
|
+
trackRef: RefObject<HTMLDivElement | null>;
|
|
47
|
+
/** The share of the track the fill draws (0–100), for placing marks. */
|
|
48
|
+
drawnPercentage: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function useProgressBarTouch(
|
|
52
|
+
percentage: number,
|
|
53
|
+
hasMarks: boolean,
|
|
54
|
+
): ProgressBarTouch {
|
|
55
|
+
// The touch fill's duration comes from how far the value moved since the
|
|
56
|
+
// last value, the model change that UIKit measures. It holds until the next
|
|
57
|
+
// change, so a re-render with the same value keeps it.
|
|
58
|
+
const [motion, setMotion] = useState({percentage, change: 0, flip: 0});
|
|
59
|
+
const isNewValue = motion.percentage !== percentage;
|
|
60
|
+
// The share of the track the value moved (0–1). `flip` steps through three
|
|
61
|
+
// values on every change (see the fill's touch transform).
|
|
62
|
+
const change = isNewValue
|
|
63
|
+
? Math.abs(percentage - motion.percentage) / 100
|
|
64
|
+
: motion.change;
|
|
65
|
+
const flip = isNewValue ? (motion.flip + 1) % 3 : motion.flip;
|
|
66
|
+
if (isNewValue) {
|
|
67
|
+
setMotion({percentage, change, flip});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Under a finger the fill draws at least TOUCH_MIN_FILL_PX long, so a mark
|
|
71
|
+
// it covers there sits on the fill. That length is a share of the track's
|
|
72
|
+
// width, which only layout knows. So the track is measured, but only under
|
|
73
|
+
// a finger and only when the bar has marks.
|
|
74
|
+
const {isTouch} = useTouchPresentation();
|
|
75
|
+
const watchesTrack = isTouch && hasMarks;
|
|
76
|
+
const trackRef = useRef<HTMLDivElement | null>(null);
|
|
77
|
+
const [trackWidth, setTrackWidth] = useState(0);
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
const element = trackRef.current;
|
|
80
|
+
if (!watchesTrack || element == null) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
// The shared observer; its first call (on registration) carries no
|
|
84
|
+
// contentRect, so that one reads the box.
|
|
85
|
+
return observeResize(element, entry => {
|
|
86
|
+
setTrackWidth(
|
|
87
|
+
entry.contentRect?.width ?? element.getBoundingClientRect().width,
|
|
88
|
+
);
|
|
89
|
+
});
|
|
90
|
+
}, [watchesTrack]);
|
|
91
|
+
const drawnPercentage =
|
|
92
|
+
watchesTrack && percentage > 0 && trackWidth > 0
|
|
93
|
+
? Math.max(percentage, (TOUCH_MIN_FILL_PX / trackWidth) * 100)
|
|
94
|
+
: percentage;
|
|
95
|
+
|
|
96
|
+
// CSS applies the theme's tempo to this duration.
|
|
97
|
+
const fillTrackMs = resolveMotion(null, progressBar, 'fillTrackMs');
|
|
98
|
+
const curve = resolveMotion(null, progressBar.curves.fill);
|
|
99
|
+
return {
|
|
100
|
+
fillVars: {
|
|
101
|
+
'--_progress-bar-move': `${Math.round(change * fillTrackMs * 1000) / 1000}ms`,
|
|
102
|
+
'--_progress-bar-curve': formatMotionRoleValue('curve', curve),
|
|
103
|
+
'--_progress-bar-flip': String(flip),
|
|
104
|
+
'--_progress-bar-min': `${TOUCH_MIN_FILL_PX}px`,
|
|
105
|
+
},
|
|
106
|
+
trackRef,
|
|
107
|
+
drawnPercentage,
|
|
108
|
+
};
|
|
109
|
+
}
|