Skip to content
Merged
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
10 changes: 10 additions & 0 deletions data/structures/nav.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,20 @@ arguments:
vertical:
word-wrap:
release: v1.0.0
fade:
group: shortcode
release: v3.24.0
comment: >-
Flag to make the tab panes fade in. Cascades to every nav-item that does
not set it, which is the only way to reach a pane rendered by this
shortcode.
class:
pane:
responsive:
release: v1.4.0
comment: >-
Flag to collapse the nav controls into a dropdown below the configured
breakpoint. Applies to every tab type, "buttons" included.
width:
default: 12
group: partial
Expand Down
67 changes: 43 additions & 24 deletions layouts/_partials/assets/nav.html
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,39 @@

{{/* The companion dropdown mirrors the nav, so it has to start on the same entry. `nav-show` names
that entry by item id rather than position, so resolve it to an index here - built from the same
`{args.id}-btn-{index}` expression the nav's own buttons use below, so the two cannot disagree.
Falls back to the first entry when `nav-show` is unset or names an item that does not exist. */}}
`{id}-btn-{index}` expression the nav's own controls use below, so the two cannot disagree.
Falls back to the first entry when `nav-show` is unset or names an item that does not exist.

It is `$id` rather than `$args.id`, because `$id` carries the fallback a caller that omits the
argument gets. Every `-btn-` expression in this file has to spell the identifier the same way or
a nav rendered without an id addresses buttons that were emitted under another name. */}}
{{- $active := 0 -}}
{{- if $args.navShow -}}
{{- range $index, $item := $titles -}}
{{- if eq $args.navShow (printf "%s-btn-%d" $args.id $index) -}}{{- $active = $index -}}{{- end -}}
{{- if eq $args.navShow (printf "%s-btn-%d" $id $index) -}}{{- $active = $index -}}{{- end -}}
{{- end -}}
{{- end -}}

{{/* Arguments of the companion dropdown that stands in for the control group below the breakpoint.
Every nav type gets one. `responsive` promises the controls collapse rather than disappear, and
the group is hidden below the breakpoint whatever its type - so a type left without a dropdown
loses its controls outright at narrow widths, with the panes still rendered and no way to reach
any but the active one.

The toggle takes the button treatment for a button group and the borderless text treatment for a
standard nav, which is the split assets/table.html already makes for its filter group: a control
standing in for buttons has to read as a button rather than as body copy. */}}
{{- $dropdownArgs := dict
"id" $id
"class" (printf "d-%s-none py-%d" $breakpoint.current $padding.y)
"titles" $titles
"wrap" $wrap
"active" $active
-}}
{{- if $isBtnGroup -}}
{{- $dropdownArgs = merge $dropdownArgs (dict "toggleClass" "btn btn-outline-primary") -}}
{{- end -}}

{{/* Tab-content class — spacing depends on type and controls-placement.
tabs/callout: border acts as the visual separator, no margin needed.
vertical: left margin to offset from the vertical nav rail.
Expand All @@ -64,18 +88,18 @@

{{/* Main code */}}
{{- if not $error -}}
<div class="col col-12 col-{{ $breakpoint.current }}-{{ $args.width }} mx-auto">
{{- /* The wrapper carries the caller's `id` verbatim, which is the only element that does: every
other identifier here is derived from it (`nav-{id}` on the list, `dropdown-{id}` on the
companion, `{id}-btn-{index}` on the controls), so without this the argument documented as the
"unique identifier of the current element" named no element at all and `#{id}` resolved to
nothing. Emitted only when the caller supplied one - `$id` falls back to a constant, and
stamping that on the wrapper would repeat one id across every nav on the page. */ -}}
<div {{ with $args.id }}id="{{ . }}" {{ end }}class="col col-12 col-{{ $breakpoint.current }}-{{ $args.width }} mx-auto">
{{- if $isVertical }}<div class="d-{{ if $args.responsive }}{{ $breakpoint.current }}{{ end }}-flex align-items-start">{{ end -}}

{{/* Responsive dropdown (standard nav types only, controls-top position) */}}
{{- if and $args.responsive (not $isBtnGroup) (ne $controlsPlacement "bottom") -}}
{{ partial "assets/panel-dropdown.html" (dict
"id" $id
"class" (printf "d-%s-none py-%d" $breakpoint.current $padding.y)
"titles" $titles
"wrap" $wrap
"active" $active
) }}
{{/* Responsive dropdown, controls-top position */}}
{{- if and $args.responsive (ne $controlsPlacement "bottom") -}}
{{ partial "assets/panel-dropdown.html" $dropdownArgs }}
{{- end -}}

{{/* Tab content — rendered first when controls-placement=below */}}
Expand All @@ -90,7 +114,8 @@
{{- if $isBtnGroup -}}
{{/* buttons tab-type */}}
<div class="btn-group{{ $alignClass }}" role="tablist"
{{- range $key, $val := $args.attributes }} {{ printf "%s=%q" $key $val | safeHTMLAttr }}{{ end -}}
{{- range $key, $val := $args.attributes }} {{ printf "%s=%q" $key $val | safeHTMLAttr }}{{ end }}
data-companion="dropdown-{{ $id }}"
>
{{- range $index, $item := $titles -}}
{{- $itemID := printf "%s-btn-%d" $id $index -}}
Expand Down Expand Up @@ -130,7 +155,7 @@
data-companion="dropdown-{{ $id }}"
>
{{- range $index, $item := $titles -}}
{{ $itemID := printf "%s-btn-%d" $args.id $index -}}
{{ $itemID := printf "%s-btn-%d" $id $index -}}
{{- $show := eq $index 0 -}}
{{- if $args.navShow }}{{ $show = eq $args.navShow $itemID }}{{ end -}}
{{- $disabled := in $args.navDisabled $itemID -}}
Expand Down Expand Up @@ -160,15 +185,9 @@
</div>
{{- end -}}

{{/* Responsive dropdown (standard nav types only, controls-below position) */}}
{{- if and $args.responsive (not $isBtnGroup) (eq $controlsPlacement "bottom") -}}
{{ partial "assets/panel-dropdown.html" (dict
"id" $id
"class" (printf "d-%s-none py-%d" $breakpoint.current $padding.y)
"titles" $titles
"wrap" $wrap
"active" $active
) }}
{{/* Responsive dropdown, controls-below position */}}
{{- if and $args.responsive (eq $controlsPlacement "bottom") -}}
{{ partial "assets/panel-dropdown.html" $dropdownArgs }}
{{- end -}}

{{- if $isVertical }}</div>{{ end -}}
Expand Down
20 changes: 15 additions & 5 deletions layouts/_shortcodes/nav-item.html
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,15 @@
{{- $current := "" -}}

{{/* Main code */}}
{{- $output := partial "assets/nav-item.html" (dict
{{/*
The pane is described here and rendered by the nav shortcode rather than emitted here.
Its `active` class is the one property an item cannot settle on its own: when no item
carries `show` the group falls back to its first entry, and an item rendered in document
order cannot know whether a later sibling will claim that. Handing the parent a
description instead of markup is what lets the fallback reach the pane as well as the
control - it is the only place that sees the whole set before anything is emitted.
*/}}
{{- $itemArgs := dict
"page" .Page
"id" $id
"parent-id" $parent
Expand All @@ -63,29 +71,31 @@
"show" $args.show
"disabled" $args.disabled
"_default" $args.default
)
-}}

{{- $current := .Parent.Scratch.Get "inner" -}}
{{- $current := .Parent.Scratch.Get "inner-args" -}}
{{- $titles := .Parent.Scratch.Get "inner-title" -}}
{{- $disabled := .Parent.Scratch.Get "inner-disabled" -}}
{{- $icons := .Parent.Scratch.Get "inner-icon" -}}
{{- $itemAttrs := .Parent.Scratch.Get "inner-attrs" -}}
{{- if $args.show }}{{ .Parent.Scratch.Set "inner-show" $itemID }}{{ end -}}
{{- if $current -}}
{{- .Parent.Scratch.Set "inner" (print $current $output) -}}
{{- .Parent.Scratch.Set "inner-args" ($current | append $itemArgs) -}}
{{- .Parent.Scratch.Set "inner-title" ($titles | append $title) -}}
{{- .Parent.Scratch.Set "inner-disabled" ($disabled | append $disabledID) -}}
{{- .Parent.Scratch.Set "inner-icon" ($icons | append $args.icon) -}}
{{- .Parent.Scratch.Set "inner-attrs" ($itemAttrs | append $args.attributes) -}}
{{- else -}}
{{- .Parent.Scratch.Set "inner" (print $output) -}}
{{- .Parent.Scratch.Set "inner-args" (slice $itemArgs) -}}
{{- .Parent.Scratch.Set "inner-title" (slice $title) -}}
{{- .Parent.Scratch.Set "inner-disabled" (slice $disabledID) -}}
{{- .Parent.Scratch.Set "inner-icon" (slice $args.icon) -}}
{{- .Parent.Scratch.Set "inner-attrs" (slice $args.attributes) -}}
{{- end }}

{{/* The accordion rendering is kept eager. Nothing in this theme reads `alternative`, but it
reaches an override of the nav shortcode the same way `inner` used to, so it stays as it
was rather than changing a contract this fix has no reason to touch. */}}
{{- $alternative := partial "assets/nav-item.html" (dict
"page" .Page
"id" $id
Expand Down
23 changes: 21 additions & 2 deletions layouts/_shortcodes/nav.html
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
) -}}
{{- end -}}

{{- $inner := .Scratch.Get "inner" -}}
{{- $innerArgs := .Scratch.Get "inner-args" -}}
{{- $innerTitles := .Scratch.Get "inner-title" -}}
{{- $innerDisabled := .Scratch.Get "inner-disabled" -}}
{{- $innerShow := .Scratch.Get "inner-show" -}}
Expand All @@ -39,14 +39,33 @@
{{- .Scratch.Set "resolved-id" $navID -}}
{{- end -}}
{{- end -}}
{{/*
Resolve the entry the group starts on before any pane is rendered, so a control and its
pane cannot disagree about which one it is. An item reports an explicit `show` through
`inner-show`; with none, the group falls back to its first entry - which assets/nav.html
has always done for the control and no pane ever did, so a nav whose items were all left
unmarked rendered an active tab above an empty content area. Passing the resolved value
on as `nav-show` keeps that fallback in one place: the control, the pane and the
companion dropdown all read the same decision rather than each defaulting on its own.
*/}}
{{- $show := $innerShow -}}
{{- if and (not $show) $innerArgs -}}
{{- $show = printf "%s-btn-0" $navID -}}
{{- end -}}
{{- $inner := "" -}}
{{- range $itemArgs := $innerArgs -}}
{{- $inner = print $inner (partial "assets/nav-item.html" (merge $itemArgs (dict
"show" (eq $show (printf "%s-btn-%v" $navID $itemArgs.id))
))) -}}
{{- end -}}
{{- partial "assets/nav.html" (dict
"id" $navID
"page" .Page
"list" $args.list
"nav-items" $inner
"nav-titles" $innerTitles
"nav-disabled" $innerDisabled
"nav-show" $innerShow
"nav-show" $show
"nav-icons" $innerIcons
"nav-item-attrs" $innerAttrs
"tab-type" (or $args.tabType $args.type)
Expand Down
8 changes: 8 additions & 0 deletions tests/templates/content/blog/nav-fade.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
title: Nav Fade
---

{{< nav tab-type="pills" id="faded" fade=true >}}
{{< nav-item title="Alpha" >}}Alpha pane body.{{< /nav-item >}}
{{< nav-item title="Beta" show=true >}}Beta pane body.{{< /nav-item >}}
{{< /nav >}}
8 changes: 8 additions & 0 deletions tests/templates/content/blog/nav-shortcode.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
title: Nav Shortcode
---

{{< nav tab-type="buttons" id="probe-nav" >}}
{{< nav-item title="First" >}}First pane.{{< /nav-item >}}
{{< nav-item title="Second" show=true >}}Second pane.{{< /nav-item >}}
{{< /nav >}}
8 changes: 8 additions & 0 deletions tests/templates/content/blog/nav-unmarked.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
title: Nav Unmarked
---

{{< nav tab-type="pills" id="unmarked" >}}
{{< nav-item title="Alpha" >}}Alpha pane body.{{< /nav-item >}}
{{< nav-item title="Beta" >}}Beta pane body.{{< /nav-item >}}
{{< /nav >}}
31 changes: 31 additions & 0 deletions tests/templates/hugo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -157,3 +157,34 @@ disableKinds = ['taxonomy', 'term', 'RSS', 'sitemap', 'robotsTXT', '404']
[[module.mounts]]
source = '../../data/structures/pagination.yml'
target = 'data/structures/pagination.yml'

# assets/nav.html is the element under test for the nav assertions. Its companion dropdown is
# rendered by assets/panel-dropdown.html and the identifiers and classes on both are what the
# assertions read, so neither is stubbed. No case sets `nav-icons`, so the icon call site inside
# the button group is never reached and the vendored icon partial above is not exercised here.
[[module.mounts]]
source = '../../layouts/_partials/assets/nav.html'
target = 'layouts/_partials/assets/nav.html'
[[module.mounts]]
source = '../../layouts/_partials/assets/panel-dropdown.html'
target = 'layouts/_partials/assets/panel-dropdown.html'
[[module.mounts]]
source = '../../data/structures/nav.yml'
target = 'data/structures/nav.yml'

# The nav shortcodes are mounted for the one thing a direct partial call cannot show: that the
# identifier a page writes reaches every element that derives one from it. The shortcode resolves
# the id and assembles the panes, assets/nav-item.html renders them, and the chain the companion
# dropdown depends on - wrapper, control, pane, data-link - only exists once all four run.
[[module.mounts]]
source = '../../layouts/_shortcodes/nav.html'
target = 'layouts/_shortcodes/nav.html'
[[module.mounts]]
source = '../../layouts/_shortcodes/nav-item.html'
target = 'layouts/_shortcodes/nav-item.html'
[[module.mounts]]
source = '../../layouts/_partials/assets/nav-item.html'
target = 'layouts/_partials/assets/nav-item.html'
[[module.mounts]]
source = '../../data/structures/nav-item.yml'
target = 'data/structures/nav-item.yml'
8 changes: 8 additions & 0 deletions tests/templates/layouts/_partials/assets/hero-image.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{{- /* Stub for the illustration hook of assets/nav-item.html.

The partial calls its `hook` whether or not the item carries an illustration, so the real
hero image partial is on the path of every nav pane - and it pulls the featured-illustration
partial and the image pipeline behind it. None of that bears on what the nav assertions read,
which is the identifier a pane is addressed by. Stubbing it keeps the mount list to the
templates actually under test; a nav case that needs a rendered illustration would have to
mount the real one instead. */ -}}
Loading