Skip to content

feat(tabs): Adds headerPosition (header-position) to igc-tabs - #2556

Open
rkaraivanov wants to merge 7 commits into
masterfrom
rkaraivanov/vertical-tabs
Open

rkaraivanov wants to merge 7 commits into
masterfrom
rkaraivanov/vertical-tabs

Conversation

@rkaraivanov

@rkaraivanov rkaraivanov commented Oct 30, 2026 •

Copy link
Copy Markdown
Member

Description

Adds headerPosition (header-position) to igc-tabs. start and end put the headers in a column on that side of the panels. The default, top, keeps the row above the panels. The sides follow the direction, so a start column is on the right in RTL. The behavior follows the Angular PR IgniteUI/igniteui-angular#17884.

In a column

  • Up and Down move between the tabs, and the tab list has aria-orientation="vertical". Left and Right are not bound, so the browser keeps them.
  • alignment applies along the column. In every theme, the indicator is on the side of the panels, and Bootstrap and Indigo draw their borders on that side.
  • The column is as wide as the widest header. Each header is at least as tall as a header in a row.
  • Without a height limit, the selected panel sets the height of the tabs. A height limit is a height or a max-height on the tabs, or a height from the parent. With a limit, a taller panel scrolls its content. When the headers do not fit, scroll buttons show above and below them. These buttons use the prev/next icons turned by 90°, so the icon registry does not change.
  • Only the scroll buttons and the keyboard scroll the headers, so the wheel over a panel never moves them.

How it works

  • igc-tab stays display: contents. The tabs grid places the parts of each tab through inherited private custom properties, so a server-rendered or slotted tab gets the column layout before hydration.
  • A Lit context gives each tab the position as the header-start and header-end custom states, which the theme side rules use.
  • Two zero-size probes read a definite height and the max-height of the host. The max-height limit does not include the padding and the border of the tabs. A ResizeObserver updates the layout when they change, and in a column it waits for the next animation frame.

Shared changes

  • Key bindings: a new when option. While when returns false, the key is not bound, and the browser default runs.
  • Roving focus: an axis with a predicate binds its keys only while the predicate returns true, and it keeps the when of the host's binding defaults. So the button group and the stepper no longer cancel the arrow keys of the axis they do not use.
  • Internals controller: setState does nothing when the state does not change, because Chromium restyles the host on each add().

Row fixes (these bugs are in 7.4.1, so they have CHANGELOG "Fixed" entries)

  • With justify, a hidden tab no longer leaves an empty space.
  • Tabs in a panel of justify tabs keep the width limit of a header.
  • The scroll buttons and the selection of a hidden tab no longer scroll the strip by the position of the tabs on the page.

Also: the new TabsHeaderPosition type, spec.md (revision 11 and scenarios 34-57), story controls and column scenarios, and the CHANGELOG.

Type of Change

  • New feature (non-breaking change that adds functionality)
  • Bug fix (non-breaking change that fixes an issue)

Related Issues

Closes #2553

Testing

  • Full suite: 5,328 passed, 99.49% line coverage. New tabs specs cover the column keys and ARIA, the layout in LTR and RTL, height and max-height limits (also with padding and border), the scroll buttons, the indicator, a change of position in the same task as a selection, and a tab that leaves its tabs. The key-bindings and roving-focus specs cover when and the axis that is not used.
  • A comparison of the computed styles of the row in the four themes shows no change.
  • npm run test:ssr: a server-rendered column has the column layout and no scroll buttons
  • Storybook in Firefox (tested) and Safari (not tested): the sticky panel, the scroll buttons, RTL
  • Forced colors in each theme

Notes for review

  • Limits: a max-height on another ancestor does not limit the tabs. A max-height in percent limits a column only with the default height, because CSS cannot give the tabs the value that the percent resolves to with height: auto.
  • Design must confirm the turned arrows, the centered header content and the Bootstrap and Indigo column borders. Until then, they look as in the Angular PR.

Checklist

  • My code follows the project's coding standards
  • I have tested my changes locally
  • I have updated documentation if needed
  • If I changed the public API on purpose, I have run npm run public-api:update and committed public-api.json
  • I have added a CHANGELOG.md entry under [Unreleased]

Add `headerPosition` (`header-position`): `top` (default), `start` and
`end`. In a column, Up and Down move between the tabs, and the tab list
has `aria-orientation="vertical"`. The arrow keys of the other axis stay
with the browser, so a row still does not cancel Up and Down.

The column layout follows in the next commits. #2553
Add `headerPosition` (`header-position`): `top` (default), `start` and
`end`. In a column, Up and Down move between the tabs, and the tab list
has `aria-orientation="vertical"`. The arrow keys of the other axis stay
with the browser, so a row still does not cancel Up and Down.

The column layout follows in the next commits. #2553
In a column with a set height, the scroll buttons show above and below
the headers when they do not fit, and scroll them one header at a time.
A selected or focused tab scrolls into view in the column. The
indicator moves along the column with the selected header. A change of
`headerPosition` starts the scroll offsets over, keeps the focused
header in view, and moves the indicator without a transition.

In a column, the scroll buttons and the height limit size the
container, so a resize waits a frame before the column is measured
again, and the resize observer reports no loop. Only the arrow icons of
the scroll buttons turn, so the buttons add no overflow. #2553
In a column, the selected indicator lies over the side of the headers
that faces the panels, in every theme. The themes give its thickness as
custom properties, and the tabs place it on the axis of the headers.
Bootstrap draws the edges of a column with logical borders, and the
selected tab is open to the panels. Indigo draws the header line, the
disabled scroll button line and the indicator on that side. Each tab
gets the position as an internal state for these rules.

The panel of a column with a set height starts at the top. The header
strip covers only the header row of a row, or the header column of a
column: as an absolutely positioned item, its end lines were the edge of
the grid, so in a row it also painted the panels. #2553
The specification gets revision 10. It covers the column layout, the Up
and Down keys, the vertical orientation, the scroll buttons above and
below the headers, the limits of a column, and the test scenarios. The
CHANGELOG lists headerPosition. The alignment description says that in a
column, start is the top and end is the bottom.

The Default story has a headerPosition control. It shows a column in a
frame that the user can make shorter, so the panel scroll and the scroll
buttons show. The account settings put the headers in a start column
with a set height. #2553
- Build the row and column alignment templates and fillers in one loop.
- Scope the right-to-left arrow flip and the scroll-button insets to a
  row, so a column does not reset them.
- Place the header strip of a column through the header area variable.
- Measure with a zoomed DOMRect, and set the indicator in one call.
- Put the indicator size and color that the themes share in the common
  sheet, and remove repeated Indigo column hover rules.
- In a column, a change of the container width no longer lays out the
  headers again.
- setState changes a custom state only when the value changes, because
  Chromium restyles the host on each add(). #2553
- The tab gets the header position from a Lit context, so a change of
  position keeps a `selected` change made in the same task. A tab that
  leaves its tabs drops the position.
- A key binding can have a `when` predicate. The roving-focus controller
  binds a predicate axis only while it is active, and keeps the `when`
  of the host's binding defaults, so the tabs, the button group and the
  stepper leave the other arrow keys to the browser.
- A `max-height` on the tabs limits a column as a definite height does,
  without the padding and the border of the tabs. A probe that takes the
  `max-height` reports every change of it.
- In a column, each header has the minimum height of a header row, and
  the indicator follows the headers that `center` or `end` moves.
- With `justify`, a hidden tab leaves no gap, and tabs in a panel keep
  the width limit of a header. A hidden tab no longer moves the strip.
- Simplify: one edge function for the scroll math, refs owned by the
  layout helpers, a `tracks()` function for the grid templates, fewer
  Bootstrap rules, and the shared `hasModifiers`. #2553

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature Request] Vertical tabs

2 participants