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.
|
// 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 PILL_NUDGE_PX = 4;
|
||||||
|
|
||||||
const restrictToXAxis: Modifier = ({ transform }) => ({
|
const restrictToXAxis: Modifier = ({ transform }) => ({
|
||||||
@@ -124,16 +124,18 @@ export const SortableTabsStrip: React.FC<SortableTabsStripProps> = ({
|
|||||||
const tabRefs = React.useRef<Map<string, HTMLElement>>(new Map());
|
const tabRefs = React.useRef<Map<string, HTMLElement>>(new Map());
|
||||||
const [pillRect, setPillRect] = React.useState<{ left: number; top: number; width: number; height: number } | null>(null);
|
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
|
// 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 [pillTransitionEnabled, setPillTransitionEnabled] = React.useState(false);
|
||||||
const [pressedId, setPressedId] = React.useState<string | null>(null);
|
const [pressedId, setPressedId] = React.useState<string | null>(null);
|
||||||
const pillTransitionReadyRef = React.useRef(false);
|
const pillTransitionReadyRef = React.useRef(false);
|
||||||
const lastSwitchAtRef = React.useRef(0);
|
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.
|
// commits, which is what makes the control feel physical rather than snappy.
|
||||||
const pillNudge = React.useMemo(() => {
|
const pillNudge = React.useMemo(() => {
|
||||||
if (!usesActivePillIndicator || !pressedId || !activeId || pressedId === activeId) {
|
if (!usesIndicator || !pressedId || !activeId || pressedId === activeId) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
const pressedIndex = items.findIndex((item) => item.id === pressedId);
|
const pressedIndex = items.findIndex((item) => item.id === pressedId);
|
||||||
@@ -142,7 +144,7 @@ export const SortableTabsStrip: React.FC<SortableTabsStripProps> = ({
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
return pressedIndex > activeIndex ? PILL_NUDGE_PX : -PILL_NUDGE_PX;
|
return pressedIndex > activeIndex ? PILL_NUDGE_PX : -PILL_NUDGE_PX;
|
||||||
}, [activeId, items, pressedId, usesActivePillIndicator]);
|
}, [activeId, items, pressedId, usesIndicator]);
|
||||||
|
|
||||||
const sensors = useSensors(
|
const sensors = useSensors(
|
||||||
useSensor(PointerSensor, { activationConstraint: { distance: 8 } }),
|
useSensor(PointerSensor, { activationConstraint: { distance: 8 } }),
|
||||||
@@ -284,6 +286,8 @@ export const SortableTabsStrip: React.FC<SortableTabsStripProps> = ({
|
|||||||
updateActivePillRect();
|
updateActivePillRect();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const itemOrderKey = itemIDs.join(' | ||||||