Skip to content

feat(tabs): add the headerPosition input for the headers - #17884

Open
viktorkombov wants to merge 12 commits into
masterfrom
vkombov/tabs-header-position
Open

viktorkombov wants to merge 12 commits into
masterfrom
vkombov/tabs-header-position

Conversation

@viktorkombov

@viktorkombov viktorkombov commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Closes #1982

The Tabs Specification now includes the new option.

Description

Adds headerPosition to igx-tabs. top, the default, keeps the headers in a row above the content, and start and end place them in a column before or after the content:

<igx-tabs headerPosition="start" style="height: 320px">
    <igx-tab-item>
        <igx-tab-header><span igxTabHeaderLabel>Inbox</span></igx-tab-header>
        <igx-tab-content>Inbox content</igx-tab-content>
    </igx-tab-item>
    ...
</igx-tabs>

In a column:

  • start and end follow the text direction. In right-to-left layouts start is on the right, where side navigation usually is, so the column mirrors as the row of headers does.
  • The column is as wide as its widest header, within the minimum and maximum width of a header. When the tabs have a height and the headers don't fit, the scroll buttons above and below the headers scroll them up and down.
  • Up and Down move to the next and previous enabled tab, and Home and End work as in a row. Left and Right aren't handled. activation applies as in a row.
  • tabAlignment aligns the headers along the column. With justify, they share the free height of the column.
  • The active indicator, and the active border in the Bootstrap theme, are on the side next to the content. The Indigo and Bootstrap borders and corners follow the side as well.

headerPosition can change at runtime. The selection, the focus and the rendered headers and panels stay the same, and the focused header, or else the selected one, is scrolled into view.

A few changes also apply to the default row of headers:

  • With manual activation, a header focused with the keyboard is now scrolled into view.
  • The selected header is scrolled into view when hidden tabs are shown, for example in a dialog.
  • A selected header that is wider than the visible area is scrolled so that its start is visible, instead of its end. This is under Behavioral Changes in the CHANGELOG.
  • The headers are measured to the last header that is displayed, so a last header with display: none no longer hides the scroll buttons.

The /tabs dev demo has a new Header position option in its properties panel. A column of headers gets a fixed height there, so that the alignment and the scroll buttons can be seen.

Motivation / Context

Requested in #1982. The same feature is logged for Web Components in IgniteUI/igniteui-webcomponents#2553.

The keyboard freeze when all tabs are disabled is fixed separately in #17881, backported in #17882 and #17885. Both PRs change the keyboard handling of the tab headers, so the one merged second needs a small rebase. After that, Up and Down in a column use the fixed search for the next enabled tab as well.

Not changed here: after the headers are scrolled with the buttons, Tab can move the focus to a header that is out of view, and the previous button then scrolls the wrong way. That happens in the default row of headers too and is tracked in #17883.

Type of Change (check all that apply):

  • Bug fix
  • New functionality
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Refactoring (no functional changes)
  • Documentation
  • Demos
  • CI/CD
  • Tests
  • Changelog
  • Skills/Agents

Component(s) / Area(s) Affected:

Tabs

How Has This Been Tested?

  • Unit tests
  • Manual testing
  • Automated e2e tests

The new tests cover the column on both sides in LTR and RTL, changing the position at runtime, keyboard navigation, scrolling, tabAlignment, the indicator, the theme-specific borders of all four themes in light and dark, hidden tabs, and zoneless change detection.

Three of them cover edge cases found in review, and each fails without its fix:

  • Server-side rendering. The server renders the scroll buttons without the inline styles that hide them, so a column without a set height was taller until hydration hid them. In a column, the buttons are now hidden until the headers are measured.
  • Justified headers moving to a column. The Material, Fluent and Indigo themes animate the headers with transition: all, so the selected header was scrolled into view against the sizes at the start of the animation and ended up partly hidden. When the position changes, the headers are now measured without their transitions.
  • Removing the height limit of a column. The scroll buttons were hidden inside the resize notification, which shrank the column again and logged ResizeObserver loop completed with undelivered notifications. A column now updates its scroll buttons in the next frame.

A second review found three more problems:

  • Destroying the tabs on the server. ngOnDestroy called cancelAnimationFrame, which the server does not have, so a server render failed with a ReferenceError. It is now called only when a frame is pending. A browser test cannot show this, so I checked it with renderApplication from @angular/platform-server against the production build, and the render now completes.
  • Hidden headers and the scroll buttons. A header with display: none has an offset of 0, so Previous jumped to the start. The scroll buttons now skip hidden headers.
  • Inline transitions of the headers. Changing the position cleared a transition set inline on a header. It is now restored.

The tests of the last two were extended and fail without the fix.

The existing RTL test "should position scroll buttons properly" now waits for the layout after it changes the width. The scroll buttons are set right after the first render now, instead of on the first resize notification, so they are hidden while the headers fit.

All tabs and bottom navigation tests pass, also in repeated runs. npm run lint:lib, npm run test:styles and the production build pass.

Test Configuration:

  • Angular version: 22.2.3
  • Browser(s): Chromium (Vitest browser mode)
  • OS: Windows 11

Screenshots / Recordings

N/A

Checklist:

  • All relevant labels have been applied to this PR
  • This PR includes unit tests covering all the new code (test guidelines)
  • This PR includes API docs for newly added methods/properties (api docs guidelines)
  • This PR includes feature/README.MD updates for the feature docs
  • This PR includes general feature table updates in the root README.MD
  • This PR includes CHANGELOG.MD updates for newly added functionality
  • This PR contains breaking changes
  • This PR includes ng update migrations for the breaking changes (migrations guidelines)
  • This PR includes behavioral changes and the feature specification has been updated with them
  • Accessibility (ARIA, keyboard navigation, focus management) has been verified

🤖 Generated with Claude Code

@viktorkombov viktorkombov added ❌ status: awaiting-test PRs awaiting manual verification tabs labels Oct 27, 2026
@viktorkombov
viktorkombov changed the base branch from vkombov/fix-tabs-disabled-freeze to master October 27, 2026 13:41
@viktorkombov viktorkombov reopened this Oct 27, 2026
@viktorkombov viktorkombov removed the ❌ status: awaiting-test PRs awaiting manual verification label Oct 27, 2026
@viktorkombov
viktorkombov marked this pull request as draft October 27, 2026 13:51
headerPosition places the tab headers in a row above the content (top,
the default) or in a column on the left or right side of the content,
also in right-to-left layouts. In a column, the headers are navigated
with the Up and Down arrow keys, the scroll buttons scroll them up and
down, tabAlignment aligns them along the column and the active
indicator is on the side next to the content.

A header focused with the keyboard in manual activation is scrolled
into view, and so is the selected header when hidden tabs become
visible.

Closes #1982

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@viktorkombov
viktorkombov force-pushed the vkombov/tabs-header-position branch from a556eb0 to 0b151c3 Compare October 27, 2026 14:29
viktorkombov and others added 3 commits October 27, 2026 19:01
Add a Header position option to the tabs demo. A column of headers gets
a fixed height, so that the alignment and the scroll buttons show.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Cancel the pending layout frame on destroy only when there is one,
  so that a server render no longer fails on cancelAnimationFrame.
- Skip headers that are not displayed when scrolling with the buttons,
  so that Previous no longer jumps to the start after a hidden header.
- Restore the inline transitions of the headers after a position change.
- Simplify the code and the tests, and shorten the comments.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@viktorkombov
viktorkombov marked this pull request as ready for review October 28, 2026 11:20
@viktorkombov
viktorkombov requested review from mddragnev and a balanced review from Copilot October 28, 2026 11:20
@viktorkombov viktorkombov added ❌ status: awaiting-test PRs awaiting manual verification version: 22.3.x labels Oct 28, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota.

@simeonoff
simeonoff requested a review from desig9stein October 28, 2026 11:31
@simeonoff

Copy link
Copy Markdown
Member

@desig9stein can you take a look at the changes this PR introduces as you are way more familiar with the Tab component? Thank you!

desig9stein
desig9stein previously approved these changes Oct 28, 2026
@desig9stein desig9stein self-assigned this Oct 28, 2026
@desig9stein desig9stein added the 💥 status: in-test PRs currently being tested label Oct 28, 2026

@desig9stein desig9stein left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

In RTL apps, side navigation is normally mirrored: Material Design moves navigation drawers and rails to the right. Arabic and Hebrew users expect a column of tabs on the right, next to where reading starts.

  • headerPosition="left" ties the app to one side. To get correct behavior in both directions, the app has to switch the value itself when the language changes.
  • With the default top, the tabs already mirror in RTL (the first tab is on the right). A column that doesn't mirror is inconsistent with that.

Suggested fix for the PR: add logical values start and end that follow the direction, and possibly make one of them the documented recommendation. left and right can stay as physical values for the cases that really need a fixed side. This is a small change: in the styles, start is just flex-direction: row with no :dir(rtl) override, because the browser then mirrors it on its own. It's much easier to add before release than after, because changing what left means later would be a breaking change.

@desig9stein desig9stein removed the 💥 status: in-test PRs currently being tested label Oct 28, 2026
@simeonoff

Copy link
Copy Markdown
Member

In RTL apps, side navigation is normally mirrored: Material Design moves navigation drawers and rails to the right. Arabic and Hebrew users expect a column of tabs on the right, next to where reading starts.

  • headerPosition="left" ties the app to one side. To get correct behavior in both directions, the app has to switch the value itself when the language changes.
  • With the default top, the tabs already mirror in RTL (the first tab is on the right). A column that doesn't mirror is inconsistent with that.

Suggested fix for the PR: add logical values start and end that follow the direction, and possibly make one of them the documented recommendation. left and right can stay as physical values for the cases that really need a fixed side. This is a small change: in the styles, start is just flex-direction: row with no :dir(rtl) override, because the browser then mirrors it on its own. It's much easier to add before release than after, because changing what left means later would be a breaking change.

To add on top of what Marin said, we should avoid verbiage such as "left" and "right"... it should be "start" or "end" to cater for direction specifically.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@viktorkombov

Copy link
Copy Markdown
Contributor Author

Look at comments left by me and Marin.

@simeonoff, @desig9stein, headerPosition now takes top, start and end, and left/right are removed. start and end follow the text direction, so in RTL start puts the column on the right, the same way the row of headers mirrors.

@viktorkombov viktorkombov added ❌ status: awaiting-test PRs awaiting manual verification and removed 🛠️ status: in-development Issues and PRs with active development on them labels Oct 28, 2026
desig9stein
desig9stein previously approved these changes Oct 29, 2026

@desig9stein desig9stein left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@viktorkombov Great job on this! Please note that we’ll need to mirror this update in Web Components to ensure both frameworks remain in sync.

@desig9stein desig9stein added ✅ status: verified Applies to PRs that have passed manual verification and removed ❌ status: awaiting-test PRs awaiting manual verification labels Oct 29, 2026
@viktorkombov

Copy link
Copy Markdown
Contributor Author

@viktorkombov Great job on this! Please note that we’ll need to mirror this update in Web Components to ensure both frameworks remain in sync.

@desig9stein, done! I’ve opened an issue in the igniteui-webcomponents repository and referenced it in the PR description.

@simeonoff simeonoff left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@viktorkombov I left a comment on the scroll buttons, please take a look. It affects the default row of headers too, not just the column. Apart from that, the row looks the same as on master: the compiled CSS for all four themes doesn't change outside the new position classes.

@mddragnev mddragnev left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Could you also check your other tests. I think that some of the tests can go because the behavior is already tested in others.

- Leave the display of the scroll buttons to the stylesheet again, so
  that application styles that hide them still apply. A column hides
  its buttons with a class until the headers are measured instead.
- Make headerPosition an @input, like the other inputs of the tabs.
- Pick the arrow keys of the next and previous tab once in keyDown.
- Move the width check of a justified column into the column size test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mddragnev
mddragnev previously approved these changes Oct 30, 2026
@teodosiah

Copy link
Copy Markdown
Contributor

Not introduced with this feature, but it would be nice to be fixed- in the tab dev demo, each tab is one-way bound in tabs-showcase.sample.html: <igx-tab-item [selected]="contact.id === selectedTabId". On a second click on the 'Select by index' button, this leads to unexpected behavior, so it would be nice to fix it by handling the selectedIndexChange event.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@viktorkombov

Copy link
Copy Markdown
Contributor Author

@teodosiah Done! It is fixed with the last commit.

@ChronosSF
ChronosSF enabled auto-merge (squash) October 30, 2026 17:20

This branch has not been deployed

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

Labels

tabs version: 22.3.x ✅ status: verified Applies to PRs that have passed manual verification

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature Request: Vertical tabs

7 participants