Skip to content

Commit 791afc4

Browse files
chore(runway): cherry-pick fix: Updated tabsbar to update when font size pref changes (#22477)
- fix: Updated tabsbar to update when font size pref changes (#22208) ## **Description** Users were reporting that the TabsBar component in the Wallet view was not horizontally scrollable when tabs overflowed, particularly when device font size settings were increased. **Root causes identified:** 1. **Overflow detection didn't account for container padding** - The component has `px-4` (32px total) padding, but the overflow calculation compared total content width against the full container width, not the available space minus padding. 2. **Underline positioning race condition** - With font scaling, layout measurements would race with font rendering, causing the underline to have inconsistent width/position on initial load. 3. **No dynamic recalculation** - When font size changed after initial load, the overflow detection would not recalculate, leaving tabs non-scrollable even when they overflowed. **Solution:** - Fixed overflow detection to account for container's 32px horizontal padding in both calculation points - Added layout change detection to re-animate underline when tabs resize due to font scaling - Implemented dynamic recalculation of scroll state when any tab's layout changes significantly (>1px) - Used refs to avoid circular dependencies and performance issues ## **Changelog** CHANGELOG entry: Fixed TabsBar not being horizontally scrollable when tabs overflow, especially with increased font size settings ## **Related issues** Fixes: ## **Manual testing steps** ```gherkin Feature: TabsBar horizontal scrolling with increased font size Scenario: user increases device font size with many tabs Given user has multiple tabs visible in the Wallet view (Tokens, Perps, Predictions, NFTs) And tabs fit within the screen width at default font size When user increases device font size in Settings > Display & Brightness > Text Size Then tabs should become horizontally scrollable And the underline indicator should correctly match the width of active tab And user should be able to swipe horizontally to see all tabs Scenario: user views tabs with default font size Given user has multiple tabs in the Wallet view And device font size is at default setting And tabs do not overflow the screen width When user views the Wallet screen Then tabs should not be scrollable And all tabs should be visible without scrolling And the underline should correctly align with active tab Scenario: user switches between tabs after font size increase Given user has increased device font size And tabs are horizontally scrollable due to overflow When user taps on a different tab or swipes to navigate Then the underline should animate smoothly to the new tab And the ScrollView should automatically scroll to show the active tab And the underline width should match the tab width accurately ``` ## **Screenshots/Recordings** ### **Before** https://github.com/user-attachments/assets/3359232b-c54a-4d6b-9124-c6e194de7fc1 ### **After** https://github.com/user-attachments/assets/b83a5cc9-9232-4fb1-bbde-3e73d6c75332 ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I've included tests if applicable - [x] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I've applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > TabsBar now accounts for container padding in overflow detection and re-animates the active tab underline when tab layouts change (e.g., font-size changes), with RAF scheduling and proper cleanup. > > - **TabsBar (`app/component-library/components-temp/Tabs/TabsBar/TabsBar.tsx`)**: > - Adjust overflow detection to subtract container padding (`px-4`, 32px). > - Detect significant tab layout changes and re-animate underline for the active tab. > - Add `activeIndexRef` to sync active index and `rafCallbackId` to schedule re-animation via `requestAnimationFrame`; cancel on cleanup. > - Recompute `scrollEnabled` when layouts change; set `layoutsReady` appropriately. > - Update dependencies for callbacks/effects to ensure correct re-animation behavior. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 2ad0a2a. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> [6dbe7e8](6dbe7e8) Co-authored-by: Brian August Nguyen <brianacnguyen@gmail.com>
1 parent be2054e commit 791afc4

1 file changed

Lines changed: 52 additions & 14 deletions

File tree

  • app/component-library/components-temp/Tabs/TabsBar

app/component-library/components-temp/Tabs/TabsBar/TabsBar.tsx

Lines changed: 52 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,20 @@ const TabsBar: React.FC<TabsBarProps> = ({
3838
const underlineWidthAnimated = useRef(new Animated.Value(0)).current;
3939
const tabLayouts = useRef<{ x: number; width: number }[]>([]);
4040
const currentAnimation = useRef<Animated.CompositeAnimation | null>(null);
41+
const rafCallbackId = useRef<number | null>(null);
4142
const [isInitialized, setIsInitialized] = useState(false);
4243
const [layoutsReady, setLayoutsReady] = useState(false);
44+
const activeIndexRef = useRef(activeIndex);
4345

4446
// State for automatic overflow detection
4547
const [scrollEnabled, setScrollEnabled] = useState(false);
4648
const [containerWidth, setContainerWidth] = useState(0);
4749

50+
// Keep activeIndexRef in sync with activeIndex
51+
useEffect(() => {
52+
activeIndexRef.current = activeIndex;
53+
}, [activeIndex]);
54+
4855
// Reset layout data when tabs change structurally (count or content)
4956
const tabKeys = useMemo(() => tabs.map((tab) => tab.key).join(','), [tabs]);
5057
const prevTabKeys = useRef<string>('');
@@ -176,7 +183,8 @@ const TabsBar: React.FC<TabsBarProps> = ({
176183
const gapsWidth = (tabs.length - 1) * 24; // Account for gaps between tabs
177184
const calculatedContentWidth = totalTabsWidth + gapsWidth;
178185

179-
const shouldScroll = calculatedContentWidth > containerWidth;
186+
// Account for container's px-4 padding (16px * 2 = 32px)
187+
const shouldScroll = calculatedContentWidth > containerWidth - 32;
180188
setScrollEnabled(shouldScroll);
181189
}
182190
}
@@ -197,6 +205,13 @@ const TabsBar: React.FC<TabsBarProps> = ({
197205
return;
198206
}
199207

208+
// Check if this is a significant change (more than 1px difference)
209+
const previousLayout = tabLayouts.current[index];
210+
const hasSignificantChange =
211+
!previousLayout ||
212+
Math.abs(previousLayout.width - width) > 1 ||
213+
Math.abs(previousLayout.x - x) > 1;
214+
200215
// Store layout data
201216
tabLayouts.current[index] = { x, width };
202217

@@ -205,22 +220,41 @@ const TabsBar: React.FC<TabsBarProps> = ({
205220
(layout, i) => i >= tabs.length || (layout && layout.width > 0),
206221
);
207222

208-
if (allLayoutsReady && !layoutsReady) {
209-
setLayoutsReady(true);
210-
211-
// Update scroll detection
212-
if (containerWidth > 0) {
213-
const totalWidth = tabLayouts.current.reduce(
214-
(sum, layout) => sum + (layout?.width || 0),
215-
0,
216-
);
217-
const gapsWidth = (tabs.length - 1) * 24;
218-
const shouldScroll = totalWidth + gapsWidth > containerWidth;
219-
setScrollEnabled(shouldScroll);
223+
if (allLayoutsReady) {
224+
// Recalculate scroll detection on initial load OR when any layout changes significantly
225+
if (!layoutsReady || hasSignificantChange) {
226+
if (!layoutsReady) {
227+
setLayoutsReady(true);
228+
}
229+
230+
// If layouts were already ready and any tab changed, re-animate the active tab
231+
// This ensures re-animation triggers regardless of which tab's callback fires last
232+
if (layoutsReady && hasSignificantChange) {
233+
// Cancel any pending RAF to avoid multiple callbacks
234+
if (rafCallbackId.current !== null) {
235+
cancelAnimationFrame(rafCallbackId.current);
236+
}
237+
rafCallbackId.current = requestAnimationFrame(() => {
238+
rafCallbackId.current = null;
239+
animateToTab(activeIndexRef.current);
240+
});
241+
}
242+
243+
// Update scroll detection
244+
if (containerWidth > 0) {
245+
const totalWidth = tabLayouts.current.reduce(
246+
(sum, layout) => sum + (layout?.width || 0),
247+
0,
248+
);
249+
const gapsWidth = (tabs.length - 1) * 24;
250+
// Account for container's px-4 padding (16px * 2 = 32px)
251+
const shouldScroll = totalWidth + gapsWidth > containerWidth - 32;
252+
setScrollEnabled(shouldScroll);
253+
}
220254
}
221255
}
222256
},
223-
[tabs.length, layoutsReady, containerWidth],
257+
[tabs.length, layoutsReady, containerWidth, animateToTab],
224258
);
225259

226260
// Cleanup effect
@@ -230,6 +264,10 @@ const TabsBar: React.FC<TabsBarProps> = ({
230264
currentAnimation.current.stop();
231265
currentAnimation.current = null;
232266
}
267+
if (rafCallbackId.current !== null) {
268+
cancelAnimationFrame(rafCallbackId.current);
269+
rafCallbackId.current = null;
270+
}
233271
},
234272
[],
235273
);

0 commit comments

Comments
 (0)