feat(ui): glide the underline tab indicator
Give the underline tab strips the same motion as the pill ones: the indicator travels to the newly selected tab and leans toward a neighbour on press, instead of jumping. Opening, closing, and reordering tabs shift the indicator without a user switch, so those repositions still snap. Also drop the overshoot from the shared travel curve — both indicators now decelerate into place rather than springing back.
This commit is contained in:
@@ -48,7 +48,7 @@ type SortableTabsStripProps = {
|
||||
};
|
||||
|
||||
// Keep in sync with `.pill-tabs__indicator--is-animated` in index.css.
|
||||
const PILL_SWITCH_ANIMATION_MS = 320;
|
||||
const PILL_SWITCH_ANIMATION_MS = 280;
|
||||
const PILL_NUDGE_PX = 4;
|
||||
|
||||
const restrictToXAxis: Modifier = ({ transform }) => ({
|
||||
@@ -124,16 +124,18 @@ export const SortableTabsStrip: React.FC<SortableTabsStripProps> = ({
|
||||
const tabRefs = React.useRef<Map<string, HTMLElement>>(new Map());
|
||||
const [pillRect, setPillRect] = React.useState<{ left: number; top: number; width: number; height: number } | null>(null);
|
||||
// Transitions stay off until the user actually switches tabs, so the initial
|
||||
// measurement (and any container resize) repositions the pill instantly.
|
||||
// measurement (and any container resize) repositions the indicator instantly.
|
||||
const [pillTransitionEnabled, setPillTransitionEnabled] = React.useState(false);
|
||||
const [pressedId, setPressedId] = React.useState<string | null>(null);
|
||||
const pillTransitionReadyRef = React.useRef(false);
|
||||
const lastSwitchAtRef = React.useRef(0);
|
||||
const previousActiveIdRef = React.useRef<string | null>(activeId);
|
||||
const previousOrderKeyRef = React.useRef<string | null>(null);
|
||||
|
||||
// Pressing a neighbouring tab leans the pill toward it before the selection
|
||||
// Pressing a neighbouring tab leans the indicator toward it before the selection
|
||||
// commits, which is what makes the control feel physical rather than snappy.
|
||||
const pillNudge = React.useMemo(() => {
|
||||
if (!usesActivePillIndicator || !pressedId || !activeId || pressedId === activeId) {
|
||||
if (!usesIndicator || !pressedId || !activeId || pressedId === activeId) {
|
||||
return 0;
|
||||
}
|
||||
const pressedIndex = items.findIndex((item) => item.id === pressedId);
|
||||
@@ -142,7 +144,7 @@ export const SortableTabsStrip: React.FC<SortableTabsStripProps> = ({
|
||||
return 0;
|
||||
}
|
||||
return pressedIndex > activeIndex ? PILL_NUDGE_PX : -PILL_NUDGE_PX;
|
||||
}, [activeId, items, pressedId, usesActivePillIndicator]);
|
||||
}, [activeId, items, pressedId, usesIndicator]);
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor, { activationConstraint: { distance: 8 } }),
|
||||
@@ -284,6 +286,8 @@ export const SortableTabsStrip: React.FC<SortableTabsStripProps> = ({
|
||||
updateActivePillRect();
|
||||
});
|
||||
|
||||
const itemOrderKey = itemIDs.join(' | ||||