Skip to content

fix: honour per-screen tab-bar hiding on the custom-Column chrome path - #347

Open
shanerbaner82 wants to merge 1 commit into
mainfrom
fix/tab-bar-hide-column-path
Open

fix: honour per-screen tab-bar hiding on the custom-Column chrome path#347
shanerbaner82 wants to merge 1 commit into
mainfrom
fix/tab-bar-hide-column-path

Conversation

@shanerbaner82

Copy link
Copy Markdown
Contributor

Fixes #250.

The bug

public function tabBarOptions(): ?TabBarOptions
{
    return TabBarOptions::make()->hidden();
}

No effect. Same for the protected bool $hidesTabBar = true; shortcut.

Cause

There are two chrome paths (NativeComponent.php:539), and per-screen tab hiding was only wired into one of them:

  • Native chromewrapWithNativeChrome() checks shouldHideTabBar() and folds hideTabBar onto the sentinel. Works.
  • Custom ColumnbuildChromeColumn() appends $tabBar->toElement() unconditionally. shouldHideTabBar() had exactly one caller in the codebase — inside wrapWithNativeChrome() — so nothing on this path could ever remove the bar.

Which path you get is the crux: NativeLayout::usesNativeChrome() returns false by default. A layout that defines tabBar() without also overriding it lands on the broken path, which is the common case and matches the report.

The asymmetry that makes this clearly a bug rather than a design choice: NavBar already had the equivalent guard, 24 lines earlier —

// NativeComponent.php:502
if ($navBar !== null && ! $usesNativeChrome && $this->shouldHideNavBar()) {
    $navBar = null;
}

so $hidesNavBar / navigationOptions()->hidden() worked where the TabBar equivalents did not.

Fix

Mirror that guard in the TabBar branch. Nulling the bar (rather than passing a flag) is deliberate and matches the NavBar comment — "On the custom-Column path hiding is identical to the layout returning null" — and it's also what buildChromeColumn() needs: it picks its safe-area variant from which bars exist (lines 726–730), so a null tab bar correctly hands the bottom edge back to the wrapper.

The native-chrome path is untouched: it keeps the bar config and folds hide_tab_bar onto the sentinel, because the TabView has to survive for tab switching.

Not the cause

The wire path was intact end to end, so despite the report being Android-only, neither renderer was at fault:

  • NativeRootTabs.php:109 maps hideTabBarhide_tab_bar
  • NativeRootTabsRenderer.kt:169,305 reads it and skips the bottom bar
  • NativeRootTabsRenderer.swift:463 does the same

TabBarOptions::hidden() / isHidden() were also correct.

Why it shipped

The only test covering this (TestingSuiteV2Test.php:223) used ChromeTabsLayout, whose usesNativeChrome() returns true — so it exercised only the working path, and only through the $hidesTabBar boolean, never the builder. The Column path had zero coverage for tab hiding.

Added:

  • tab hiding on the Column path, both spellings, with a visible control
  • tab hiding via the tabBarOptions() builder on the native path

The new Column-path test fails on main and passes with the fix. The new native-path builder test passes on main — it's pinning down behavior that already worked but was untested, and it's what confirms the builder itself was never broken.

assertTabBarHidden() / assertTabBarVisible() carried the same asymmetry as the source — they only understood the native sentinel and hard-failed with "No native tab chrome rendered" on the Column path. They now handle both, matching assertNavBarHidden().

Test run

Before: 901 tests / 3021 assertions / 0 failures. After: 903 / 3029 / 0 failures.

One caveat

The reporter didn't share their layout, so I can't be certain theirs doesn't override usesNativeChrome(). If it does, their symptom has a different cause and this is a separate bug found while investigating. The default-false behavior makes this by far the likelier explanation, and the missing guard is a real bug regardless.

🤖 Generated with Claude Code

#250)

`tabBarOptions()->hidden()` and the `$hidesTabBar` shortcut had no effect
unless the layout opted into native chrome. `NativeLayout::usesNativeChrome()`
returns false by default, so a layout that defines `tabBar()` without also
overriding it landed on the custom-Column path — the common case, and the
one reported.

`shouldHideTabBar()` had exactly one caller, inside wrapWithNativeChrome(),
so nothing on the Column path could ever remove the bar: buildChromeColumn()
appends `$tabBar->toElement()` unconditionally.

NavBar already had the equivalent guard 24 lines earlier, which is what made
the gap visible — `$hidesNavBar` worked where `$hidesTabBar` did not. This
mirrors it: null the TabBar on the Column path, which is also what
buildChromeColumn() needs to hand the bottom safe-area edge back to the
wrapper. The native-chrome path is untouched — it keeps the bar config and
folds `hide_tab_bar` onto the sentinel so the TabView survives for tab
switching.

The wire path was never at fault: NativeRootTabs maps `hideTabBar` →
`hide_tab_bar`, and both NativeRootTabsRenderer.kt and .swift honour it.

Coverage is why this shipped: the only existing test used ChromeTabsLayout
(usesNativeChrome() = true), exercising just the working path, and only via
the `$hidesTabBar` boolean — never the builder. Added:

  - tab hiding on the Column path, both spellings, plus a visible control
  - tab hiding via the tabBarOptions() builder on the native path

assertTabBarHidden()/assertTabBarVisible() carried the same asymmetry — they
only understood the native sentinel and hard-failed on the Column path. They
now handle both, matching assertNavBarHidden().

Fixes #250

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@mustafa-online

Copy link
Copy Markdown

Will this be merged soon?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

v4 - tabBarOptions on screen has no effect!

2 participants