Skip to content

Conversation

@michaldudak
Copy link
Member

Fixes a bug where the onValueChange callback was not invoked when the selected tab changed automatically due to external factors.

Previously, onValueChange only fired for user-initiated tab changes (clicks, keyboard navigation). It did not fire when:

  • A selected tab became disabled
  • A selected tab was removed from the DOM
  • All tabs became disabled

Now, onValueChange fires in all these scenarios with appropriate reason values.

It also fires during initial render when no value or defaultValue is provided, and the component automatically selects the first enabled tab.


New reason values in onValueChange:

  • 'initial': First automatic selection on mount (no value/defaultValue provided)
  • 'disabled': Selected tab became disabled after initial render
  • 'missing': Selected tab was removed from the DOM

The existing 'none' reason is used for user-initiated changes.

Fixes #2097

@pkg-pr-new
Copy link

pkg-pr-new bot commented Jan 15, 2026

  • vite-css-base-ui-example

    pnpm add https://pkg.pr.new/mui/base-ui/@base-ui/react@3758
    
    pnpm add https://pkg.pr.new/mui/base-ui/@base-ui/utils@3758
    

commit: 20af3b9

@mui-bot
Copy link

mui-bot commented Jan 15, 2026

Bundle size report

Bundle Parsed size Gzip size
@base-ui/react 🔺+276B(+0.07%) 🔺+108B(+0.08%)

Details of bundle changes


Check out the code infra dashboard for more information about this PR.

@netlify
Copy link

netlify bot commented Jan 15, 2026

Deploy Preview for base-ui ready!

Name Link
🔨 Latest commit 20af3b9
🔍 Latest deploy log https://app.netlify.com/projects/base-ui/deploys/696df786f8aa120008d7a4b3
😎 Deploy Preview https://deploy-preview-3758--base-ui.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@michaldudak michaldudak added the component: tabs Changes related to the tabs component. label Jan 16, 2026
@michaldudak michaldudak marked this pull request as ready for review January 19, 2026 09:21
@greptile-apps
Copy link

greptile-apps bot commented Jan 19, 2026

Greptile Summary

This PR fixes issue #2097 by ensuring onValueChange fires for all automatic tab selections, not just user-initiated changes. The implementation adds three new reason values ('initial', 'disabled', 'missing') to distinguish different automatic selection scenarios.

Key changes:

  • onValueChange now fires on initial render when no explicit value/defaultValue is provided and the first enabled tab is automatically selected
  • onValueChange fires when the selected tab becomes disabled or is removed from the DOM
  • Respects explicit defaultValue pointing to a disabled tab if it was disabled from the start
  • Supports cancellation through eventDetails.cancel() for automatic selections
  • Does not fire in controlled mode for automatic fallbacks (parent controls the value)

Issue found:

  • The reason assignment logic uses 'initial' for any first-run automatic selection, even when an explicit defaultValue is provided but the tab doesn't exist. The PR description states 'initial' should only be used when "no value/defaultValue provided", suggesting this edge case may need refinement for semantic correctness.

The test coverage is thorough and validates the main scenarios. The implementation properly uses useIsoLayoutEffect and follows the repository's code guidelines.

Confidence Score: 4/5

  • This PR is safe to merge with minor considerations
  • The implementation correctly handles the main use cases for firing onValueChange during automatic tab selection. Comprehensive test coverage validates the behavior. However, there's a minor logic inconsistency where the 'initial' reason can be used even when an explicit defaultValue is provided (if that tab doesn't exist), which doesn't match the documented behavior. This edge case is unlikely to occur in practice but should be addressed for semantic correctness.
  • packages/react/src/tabs/root/TabsRoot.tsx - review the reason assignment logic for edge case handling

Important Files Changed

Filename Overview
packages/react/src/tabs/root/TabsRoot.tsx Added logic to fire onValueChange for automatic tab selections with new reason values (initial, disabled, missing). Implementation handles most scenarios correctly but has a potential logic issue with reason assignment when a non-existent tab is specified as defaultValue.
packages/react/src/tabs/root/TabsRoot.test.tsx Comprehensive test coverage added for all automatic selection scenarios including initial selection, disabled tabs, removed tabs, cancellation, and controlled mode behavior.
packages/react/src/utils/reason-parts.ts Added two new reason constants (missing and initial) to support the new automatic selection events in Tabs component.
docs/reference/generated/tabs-root.json Updated type signature for onValueChange callback to reflect simplified ChangeEventDetails type (generic rather than hardcoded to 'none' reason).

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

4 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

Comment on lines +261 to +268
let reason: TabsRoot.ChangeEventReason;
if (isInitialRun) {
reason = REASONS.initial;
} else if (selectionIsDisabled) {
reason = REASONS.disabled;
} else {
reason = REASONS.missing;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: The reason assignment logic always uses 'initial' for the first run, even when an explicit defaultValue is provided but the corresponding tab doesn't exist. According to the PR description, 'initial' should only be used when "no value/defaultValue provided", but this code will fire with 'initial' reason even if defaultValue={1} is provided but tab 1 is missing.

Consider refining the logic:

Suggested change
let reason: TabsRoot.ChangeEventReason;
if (isInitialRun) {
reason = REASONS.initial;
} else if (selectionIsDisabled) {
reason = REASONS.disabled;
} else {
reason = REASONS.missing;
}
// Determine the appropriate reason based on context:
// - 'initial': First automatic selection (no value/defaultValue provided)
// - 'disabled': Tab became disabled after initial render
// - 'missing': Tab was removed from DOM or never existed
let reason: TabsRoot.ChangeEventReason;
if (isInitialRun && isAutomaticDefault) {
reason = REASONS.initial;
} else if (selectionIsDisabled) {
reason = REASONS.disabled;
} else {
reason = REASONS.missing;
}

This ensures 'initial' is only used when no explicit value was provided, making the behavior match the documentation.

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/react/src/tabs/root/TabsRoot.tsx
Line: 261:268

Comment:
**logic:** The reason assignment logic always uses `'initial'` for the first run, even when an explicit `defaultValue` is provided but the corresponding tab doesn't exist. According to the PR description, `'initial'` should only be used when "no value/defaultValue provided", but this code will fire with `'initial'` reason even if `defaultValue={1}` is provided but tab 1 is missing.

Consider refining the logic:
```suggestion
    // Determine the appropriate reason based on context:
    // - 'initial': First automatic selection (no value/defaultValue provided)
    // - 'disabled': Tab became disabled after initial render
    // - 'missing': Tab was removed from DOM or never existed
    let reason: TabsRoot.ChangeEventReason;
    if (isInitialRun && isAutomaticDefault) {
      reason = REASONS.initial;
    } else if (selectionIsDisabled) {
      reason = REASONS.disabled;
    } else {
      reason = REASONS.missing;
    }
```

This ensures `'initial'` is only used when no explicit value was provided, making the behavior match the documentation.

How can I resolve this? If you propose a fix, please make it concise.

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

Labels

component: tabs Changes related to the tabs component.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[tabs] onValueChange does not fire when the initially selected tab is picked by the Tabs component

2 participants