Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 42 additions & 9 deletions src/components/SegmentedTopBar/Indicator.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useEffect } from "react";
import React, { useEffect, useRef } from "react";
import { StyleSheet } from "react-native";

import PropTypes from "prop-types";
Expand All @@ -14,32 +14,65 @@ import { theme } from "@theme";

const springConfig = {
mass: 1,
overshootClamping: true,
damping: 18,
stiffness: 100,
overshootClamping: false,
restSpeedThreshold: 0.5,
restDisplacementThreshold: 0.5,
};

const spacing = moderateScale(6);

export const Indicator = ({ measure }) => {
const { x, width, height } = measure;

const firstRender = useRef(true);
const translateX = useSharedValue(0);
const animatedWidth = useSharedValue(0);

// Define hook functions regardless of whether they'll be used
useEffect(() => {
cancelAnimation(translateX);
cancelAnimation(animatedWidth);
if (!measure) return;

const { x, width } = measure;

if (firstRender.current) {
if (translateX.value === 0 && x > 0) {
translateX.value = x + spacing / 2;
}

if (animatedWidth.value === 0 && width > 0) {
animatedWidth.value = width - spacing;
}

translateX.value = withSpring(x + spacing / 2, springConfig);
animatedWidth.value = withSpring(width - spacing, springConfig);
}, [animatedWidth, translateX, width, x]);
firstRender.current = false;
} else {
// Handle value changes after first render
// Cancel any ongoing animations first
cancelAnimation(translateX);
cancelAnimation(animatedWidth);

// Use spring animation for smooth transitions
translateX.value = withSpring(x + spacing / 2, springConfig);
animatedWidth.value = withSpring(width - spacing, springConfig);
}
}, [measure]);

const tabTranslateAnimatedStyles = useAnimatedStyle(() => ({
transform: [{ translateX: translateX.value }],
width: animatedWidth.value,
}));

// Guard against undefined measure
if (
!measure ||
typeof measure.x !== "number" ||
typeof measure.width !== "number" ||
typeof measure.height !== "number"
) {
return null;
}

const { height } = measure;

return (
<Animated.View
style={[
Expand Down
20 changes: 18 additions & 2 deletions src/components/SegmentedTopBar/Tab.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { forwardRef, useCallback } from "react";
import { View } from "react-native";
import { View, StyleSheet } from "react-native";

import PropTypes from "prop-types";
import { moderateScale } from "react-native-size-matters";
Expand All @@ -16,7 +16,13 @@ export const Tab = forwardRef(
return (
<View
ref={ref}
style={{ flex, minWidth: count !== undefined && moderateScale(56) }}
style={[
styles.container,
{
flex,
minWidth: count !== undefined ? moderateScale(56) : undefined,
},
]}
>
<Touchable
alignItems="center"
Expand Down Expand Up @@ -48,6 +54,12 @@ export const Tab = forwardRef(
}
);

const styles = StyleSheet.create({
container: {
height: "100%",
},
});

Tab.displayName = "Tab";

Tab.propTypes = {
Expand All @@ -57,3 +69,7 @@ Tab.propTypes = {
navigation: PropTypes.object,
count: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
};

Tab.defaultProps = {
flex: 1,
};
70 changes: 58 additions & 12 deletions src/components/SegmentedTopBar/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,8 @@ export const SegmentedTopBar = ({
descriptors,
}) => {
const containerRef = useRef();

const [measures, setMeasures] = useState([]);
const [prevRoutesLength, setPrevRoutesLength] = useState(routes.length);

const tabsData = useMemo(
() =>
Expand All @@ -79,27 +79,73 @@ export const SegmentedTopBar = ({
[descriptors, routes]
);

// Reset measures when routes length changes to force re-measure
useLayoutEffect(() => {
if (prevRoutesLength !== routes.length) {
setMeasures([]);
setPrevRoutesLength(routes.length);
}
}, [routes.length, prevRoutesLength]);

// Measure tab elements
useLayoutEffect(() => {
const _measures = [];
let isMounted = true;

// Use setTimeout to ensure the DOM is ready
const timerId = setTimeout(() => {
if (!isMounted || !containerRef.current) return;

const _measures = [];

tabsData.forEach(({ ref }) => {
ref.current.measureLayout(containerRef.current, (x, y, width, height) => {
_measures.push({ x, y, width, height });
tabsData.forEach(({ ref }) => {
if (ref.current && containerRef.current) {
ref.current.measureLayout(
containerRef.current,
(x, y, width, height) => {
if (!isMounted) return;

if (_measures.length === tabsData.length) {
setMeasures(_measures);
_measures.push({ x, y, width, height });

if (_measures.length === tabsData.length) {
setMeasures(_measures);
}
},
() => {
// Error callback - if measurement fails
// eslint-disable-next-line no-console
console.warn("Failed to measure tab element");
}
);
}
});
});
}, [measures.length]);
}, 50); // Increased timeout for better reliability

return () => {
isMounted = false;
clearTimeout(timerId);
};
}, [tabsData, routes.length]);

// Determine flex values for tabs based on number of tabs
const getTabFlex = label => {
// When we have 4 tabs, use equal flex to ensure they fit properly
if (routes.length > 3) {
return 1;
}

// For 3 or fewer tabs, use label length as flex to make them proportional
return label.length;
};

return (
<View height={height} ref={containerRef} style={styles.container}>
{measures.length > 0 && <Indicator measure={measures[index]} />}
{tabsData.map(({ label, value, ref, count }) => (
{measures.length > 0 && index < measures.length && measures[index] && (
<Indicator measure={measures[index]} />
)}
{tabsData.map(({ label, value, ref, count }, i) => (
<Tab
count={count}
flex={tabsData.length > 3 ? label.length : 1}
flex={getTabFlex(label, i)}
key={value}
label={label}
navigation={navigation}
Expand Down