Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions docs/snippets/myst/tab-vertical.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
::::{tab-set}
:orientation: vertical

:::{tab-item} Label1
Content 1
:::

:::{tab-item} Label2
Content 2
:::

::::
10 changes: 10 additions & 0 deletions docs/snippets/rst/tab-vertical.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
.. tab-set::
:orientation: vertical

.. tab-item:: Label1

Content 1

.. tab-item:: Label2

Content 2
38 changes: 38 additions & 0 deletions docs/tabs.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,44 @@ Content 2

See the [Material Design](https://material.io/components/tabs) description for further details.

## Vertical Tabs

::::{versionadded} 0.7.0
Add `:orientation: vertical` to a `tab-set` or `tab-set-code` to stack the labels down a rail on the left, with the selected content beside them.
This suits sets holding more labels than fit on one line, where the horizontal layout wraps them into a strip that is hard to scan.
::::

::::{tab-set}
:orientation: vertical

:::{tab-item} Label1
Content 1
:::

:::{tab-item} Label2
Content 2
:::

:::{tab-item} A longer label
Content 3
:::

::::

`````{dropdown-syntax}

````{tab-set-code}
```{literalinclude} ./snippets/myst/tab-vertical.txt
:language: markdown
```
```{literalinclude} ./snippets/rst/tab-vertical.txt
:language: rst
```
````
`````

Below the `md` breakpoint the set falls back to the horizontal layout, two columns being too narrow to read at that width.

## Synchronised Tabs

The Selection of tab items can be synchronised across multiple tab-sets.
Expand Down
2 changes: 1 addition & 1 deletion sphinx_design/static/sphinx-design.min.css

Large diffs are not rendered by default.

31 changes: 29 additions & 2 deletions sphinx_design/tabs.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,25 @@
create_component,
is_component,
is_ignorable_child,
make_choice,
)

LOGGER = getLogger(__name__)

ORIENTATIONS = ("horizontal", "vertical")
"""Layouts a tab set can be rendered in, the first one being the default."""


def orientation_classes(orientation: str | None) -> list[str]:
"""Return the modifier classes carrying a tab set's orientation.

:param orientation: One of :data:`ORIENTATIONS`, or ``None`` for the default.
:return: The classes to append to the tab set's own.
"""
if orientation == "vertical":
return ["sd-tab-set-vertical"]
return []


def setup_tabs(app: Sphinx) -> None:
app.add_directive("tab-set", TabSetDirective)
Expand All @@ -31,14 +46,20 @@ class TabSetDirective(SdDirective):

has_content = True
option_spec = {
"orientation": make_choice(ORIENTATIONS),
"sync-group": directives.unchanged_required,
"class": directives.class_option,
}

def run_with_defaults(self) -> list[nodes.Node]:
self.assert_has_content()
tab_set = create_component(
"tab-set", classes=["sd-tab-set", *self.options.get("class", [])]
"tab-set",
classes=[
"sd-tab-set",
*orientation_classes(self.options.get("orientation")),
*self.options.get("class", []),
],
)
self.set_source_info(tab_set)
self.state.nested_parse(self.content, self.content_offset, tab_set)
Expand Down Expand Up @@ -143,6 +164,7 @@ class TabSetCodeDirective(SdDirective):
has_content = True
option_spec = {
"no-sync": directives.flag,
"orientation": make_choice(ORIENTATIONS),
"sync-group": directives.unchanged_required,
"class-set": directives.class_option,
"class-item": directives.class_option,
Expand All @@ -151,7 +173,12 @@ class TabSetCodeDirective(SdDirective):
def run_with_defaults(self) -> list[nodes.Node]:
self.assert_has_content()
tab_set = create_component(
"tab-set", classes=["sd-tab-set", *self.options.get("class-set", [])]
"tab-set",
classes=[
"sd-tab-set",
*orientation_classes(self.options.get("orientation")),
*self.options.get("class-set", []),
],
)
self.set_source_info(tab_set)
self.state.nested_parse(self.content, self.content_offset, tab_set)
Expand Down
66 changes: 66 additions & 0 deletions style/tabs.css
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,69 @@ html .sd-tab-set > label:hover {
.sd-tab-content > .sd-tab-set {
margin: 0;
}

/* Vertical orientation, opted into with the tab-set's `:orientation: vertical`.

The labels, their radio inputs and the panes are flat siblings, so the visual
order cannot come from the source order. Grid placement supplies it instead:
every label is assigned the first column, and the one pane that is not
`display: none` takes the second across every row. No separate HTML structure
is needed for this mode.

Everything below is the horizontal layout turned a quarter turn: the label's
underline becomes a right border, and the pane's overline becomes a single
rule down its left edge, which the active label's border merges into. */
.sd-tab-set-vertical {
display: grid;
grid-template-columns: minmax(7rem, max-content) 1fr;
align-items: start;
}

/* The rail and the pane share an edge, so the active label's border lands on
the pane's rule instead of being held away from it by a column gap. Padding
opens the space between the two columns. */
.sd-tab-set-vertical > label {
grid-column: 1;
border-bottom: none;
border-right: .125rem solid var(--sd-color-tabs-underline-inactive);
padding-right: 1rem;
text-align: left;
}

.sd-tab-set-vertical > .sd-tab-content {
grid-column: 2;
/* Span every row rather than the first one alone, whose height the pane would
otherwise set, pushing the labels below it off the rail. `1 / -1` cannot say
this here: `-1` counts back from the last explicit row, and this grid
declares columns only. The span overshoots any real tab count instead;
surplus rows are empty and take no height. */
grid-row: 1 / span 99;
/* Stretch across those rows, or the rule runs only as deep as the pane's own
content instead of the full rail. */
align-self: stretch;
box-shadow: -0.0625rem 0 var(--sd-color-tabs-overline);
padding: 0 0 0 1.5rem;
}

/* Narrower than the `md` breakpoint, two columns leave the pane a ribbon: hand
every overridden property back to the horizontal layout. Each is restated
rather than unset, a shorthand having no way to fall back to an earlier
rule's value. */
@media (max-width: 767.98px) {
.sd-tab-set-vertical {
display: flex;
flex-wrap: wrap;
}

.sd-tab-set-vertical > label {
border-bottom: .125rem solid var(--sd-color-tabs-underline-inactive);
border-right: none;
padding: .5em 1.25em;
}

.sd-tab-set-vertical > .sd-tab-content {
align-self: auto;
box-shadow: 0 -0.0625rem var(--sd-color-tabs-overline),0 .0625rem var(--sd-color-tabs-underline);
padding: .75rem 0;
}
}
17 changes: 17 additions & 0 deletions tests/test_snippets/snippet_post_tab-vertical.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<document source="index">
<section ids="heading" names="heading">
<title>
Heading
<container classes="sd-tab-set sd-tab-set-vertical" design_component="tab-set" is_div="True">
<sd_tab_input checked="True" id="sd-tab-item-0" set_id="sd-tab-set-0" type="radio">
<sd_tab_label aria_controls="sd-tab-item-0-content" classes="sd-tab-label" input_id="sd-tab-item-0">
Label1
<container classes="sd-tab-content" design_component="tab-content" ids="sd-tab-item-0-content" is_div="True">
<paragraph>
Content 1
<sd_tab_input checked="False" id="sd-tab-item-1" set_id="sd-tab-set-0" type="radio">
<sd_tab_label aria_controls="sd-tab-item-1-content" classes="sd-tab-label" input_id="sd-tab-item-1">
Label2
<container classes="sd-tab-content" design_component="tab-content" ids="sd-tab-item-1-content" is_div="True">
<paragraph>
Content 2
17 changes: 17 additions & 0 deletions tests/test_snippets/snippet_pre_tab-vertical.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<document source="index">
<section ids="heading" names="heading">
<title>
Heading
<container classes="sd-tab-set sd-tab-set-vertical" design_component="tab-set" is_div="True">
<container classes="sd-tab-item" design_component="tab-item" is_div="True" selected="False">
<rubric classes="sd-tab-label">
Label1
<container classes="sd-tab-content" design_component="tab-content" is_div="True">
<paragraph>
Content 1
<container classes="sd-tab-item" design_component="tab-item" is_div="True" selected="False">
<rubric classes="sd-tab-label">
Label2
<container classes="sd-tab-content" design_component="tab-content" is_div="True">
<paragraph>
Content 2