Skip to content

Commit e61e23e

Browse files
committed
DEV: add generic-ish tab control
1 parent 11c8777 commit e61e23e

File tree

6 files changed

+354
-1
lines changed

6 files changed

+354
-1
lines changed

assets/css/index.css

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -739,6 +739,58 @@ input[type="radio"] {
739739
appearance: none;
740740
}
741741

742+
/* Generic GitHub-style tabs */
743+
.generic-tabs {
744+
@apply w-full mb-6;
745+
}
746+
747+
.generic-tabs .tab-nav {
748+
@apply flex border-b border-redis-pen-300 bg-redis-neutral-200 rounded-t-md;
749+
}
750+
751+
.generic-tabs .tab-radio {
752+
@apply sr-only;
753+
}
754+
755+
.generic-tabs .tab-label {
756+
@apply px-4 py-3 cursor-pointer text-sm font-medium text-redis-pen-600
757+
bg-redis-neutral-200 border-r border-redis-pen-300
758+
hover:bg-white hover:text-redis-ink-900
759+
transition-colors duration-150 ease-in-out
760+
focus:outline-none focus:ring-2 focus:ring-redis-red-500 focus:ring-inset
761+
first:rounded-tl-md select-none;
762+
}
763+
764+
.generic-tabs .tab-label:last-child {
765+
@apply border-r-0 rounded-tr-md;
766+
}
767+
768+
.generic-tabs .tab-radio:checked + .tab-label {
769+
@apply bg-white text-redis-ink-900 border-b-2 border-b-redis-red-500 -mb-px relative z-10;
770+
}
771+
772+
.generic-tabs .tab-radio:focus + .tab-label {
773+
@apply ring-2 ring-redis-red-500 ring-inset;
774+
}
775+
776+
.generic-tabs .tab-content {
777+
@apply hidden p-6 bg-white border border-t-0 border-redis-pen-300 rounded-b-md shadow-sm;
778+
}
779+
780+
.generic-tabs .tab-content.active {
781+
@apply block;
782+
}
783+
784+
/* Ensure proper stacking and borders */
785+
.generic-tabs .tab-content-container {
786+
@apply relative -mt-px;
787+
}
788+
789+
/* Single content box styling (when no explicit tabs are provided) */
790+
.generic-tabs-single .tab-content-single {
791+
@apply prose prose-lg max-w-none;
792+
}
793+
742794
.stack-logo-inline {
743795
display: inline;
744796
max-height: 1em;

content/develop/multitabs-demo.md

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
---
2+
title: "Multi-Tabs Test"
3+
description: "Testing the simpler multi-tab syntax"
4+
weight: 995
5+
---
6+
7+
# Multi-Tabs Shortcode Test
8+
9+
This page tests a simpler approach to multi-tab syntax that works reliably with Hugo.
10+
11+
## Multi-Tab Example
12+
13+
{{< multitabs id="example-tabs" tab1="Getting Started" tab2="Features" tab3="Usage Guide" >}}
14+
Welcome to the **Getting Started** tab! This demonstrates the simpler multi-tab syntax.
15+
16+
### Quick Setup
17+
1. Include the tab component files
18+
2. Use the `multitabs` shortcode with tab parameters
19+
3. Separate content with `---` dividers
20+
21+
This approach avoids Hugo's nested shortcode parsing issues while still providing clean multi-tab functionality.
22+
23+
- - -
24+
25+
## Key Features
26+
27+
The tab control includes:
28+
29+
- **GitHub-style design**: Clean, professional appearance
30+
- **Accessibility**: Full keyboard navigation and ARIA support
31+
- **Responsive**: Works on all screen sizes
32+
- **Markdown support**: Full markdown rendering within tabs
33+
- **Simple syntax**: Uses parameter-based tab titles and content separators
34+
35+
### Code Example
36+
```javascript
37+
// Example of tab initialization
38+
document.addEventListener('DOMContentLoaded', () => {
39+
const tabs = document.querySelectorAll('.generic-tabs');
40+
tabs.forEach(tab => new GenericTabs(tab));
41+
});
42+
```
43+
44+
- - -
45+
46+
## How to Use
47+
48+
The multi-tab syntax uses parameters for tab titles and separates content with triple dashes.
49+
50+
**Syntax structure:**
51+
1. Define tab titles as parameters: `tab1="Title 1" tab2="Title 2"`
52+
2. Separate content sections with `---` on its own line
53+
3. Each section becomes the content for the corresponding tab
54+
55+
### Benefits
56+
- **Reliable parsing**: No nested shortcode issues
57+
- **Clean syntax**: Easy to read and write
58+
- **Flexible content**: Any markdown content works
59+
- **Maintainable**: Clear separation between tabs
60+
- **Accessible**: Proper semantic structure
61+
62+
Perfect for organizing documentation, tutorials, and reference materials!
63+
{{< /multitabs >}}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
{{/*
2+
Generic GitHub-style tabs component
3+
4+
Usage:
5+
{{ partial "components/generic-tabs.html" (dict "id" "my-tabs" "tabs" $tabs) }}
6+
7+
Where $tabs is an array of dictionaries with "title" and "content" keys:
8+
$tabs := slice
9+
(dict "title" "Tab 1" "content" "Content for tab 1")
10+
(dict "title" "Tab 2" "content" "Content for tab 2")
11+
*/}}
12+
13+
{{ $id := .id | default (printf "tabs-%s" (substr (.tabs | jsonify | md5) 0 8)) }}
14+
{{ $tabs := .tabs | default (slice (dict "title" "Error" "content" "No tabs provided")) }}
15+
16+
<div class="generic-tabs" id="{{ $id }}">
17+
<!-- Tab Navigation -->
18+
<div class="tab-nav" role="tablist" aria-label="Tab navigation">
19+
{{ range $index, $tab := $tabs }}
20+
{{ $tabId := printf "%s-tab-%d" $id $index }}
21+
{{ $panelId := printf "%s-panel-%d" $id $index }}
22+
23+
<input
24+
type="radio"
25+
name="{{ $id }}"
26+
id="{{ $tabId }}"
27+
class="tab-radio"
28+
{{ if eq $index 0 }}checked{{ end }}
29+
aria-controls="{{ $panelId }}"
30+
data-tab-index="{{ $index }}"
31+
/>
32+
<label
33+
for="{{ $tabId }}"
34+
class="tab-label"
35+
role="tab"
36+
aria-selected="{{ if eq $index 0 }}true{{ else }}false{{ end }}"
37+
aria-controls="{{ $panelId }}"
38+
tabindex="{{ if eq $index 0 }}0{{ else }}-1{{ end }}"
39+
>
40+
{{ $tab.title }}
41+
</label>
42+
{{ end }}
43+
</div>
44+
45+
<!-- Tab Content -->
46+
<div class="tab-content-container">
47+
{{ range $index, $tab := $tabs }}
48+
{{ $tabId := printf "%s-tab-%d" $id $index }}
49+
{{ $panelId := printf "%s-panel-%d" $id $index }}
50+
51+
<div
52+
id="{{ $panelId }}"
53+
class="tab-content {{ if eq $index 0 }}active{{ end }}"
54+
role="tabpanel"
55+
aria-labelledby="{{ $tabId }}"
56+
tabindex="0"
57+
data-tab-index="{{ $index }}"
58+
{{ if ne $index 0 }}aria-hidden="true"{{ end }}
59+
>
60+
{{ $tab.content | safeHTML }}
61+
</div>
62+
{{ end }}
63+
</div>
64+
</div>

layouts/partials/scripts.html

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,4 +141,7 @@
141141
}
142142
}
143143
}
144-
</script>
144+
</script>
145+
146+
<!-- Generic tabs functionality -->
147+
<script src="{{ "js/generic-tabs.js" | relURL }}"></script>

layouts/shortcodes/multitabs.html

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
{{/*
2+
Multi-tabs shortcode with simpler syntax
3+
4+
Usage:
5+
{{< multitabs id="my-tabs"
6+
tab1="Tab Title 1"
7+
tab2="Tab Title 2"
8+
tab3="Tab Title 3" >}}
9+
10+
Content for tab 1
11+
12+
- - -
13+
14+
Content for tab 2
15+
16+
- - -
17+
18+
Content for tab 3
19+
{{< /multitabs >}}
20+
*/}}
21+
22+
{{ $id := .Get "id" | default (printf "tabs-%s" (substr (.Inner | md5) 0 8)) }}
23+
{{ $tabs := slice }}
24+
25+
{{/* Split content by --- separator */}}
26+
{{ $sections := split .Inner "- - -" }}
27+
28+
{{/* Get tab titles from parameters */}}
29+
{{ $tabTitles := slice }}
30+
{{ range $i := seq 1 10 }}
31+
{{ $tabParam := printf "tab%d" $i }}
32+
{{ $title := $.Get $tabParam }}
33+
{{ if $title }}
34+
{{ $tabTitles = $tabTitles | append $title }}
35+
{{ end }}
36+
{{ end }}
37+
38+
{{/* Create tabs from sections and titles */}}
39+
{{ range $index, $section := $sections }}
40+
{{ $title := "Tab" }}
41+
{{ if lt $index (len $tabTitles) }}
42+
{{ $title = index $tabTitles $index }}
43+
{{ else }}
44+
{{ $title = printf "Tab %d" (add $index 1) }}
45+
{{ end }}
46+
47+
{{ $content := $section | strings.TrimSpace | markdownify }}
48+
{{ if ne $content "" }}
49+
{{ $tabs = $tabs | append (dict "title" $title "content" $content) }}
50+
{{ end }}
51+
{{ end }}
52+
53+
{{/* Render tabs if we have any */}}
54+
{{ if gt (len $tabs) 0 }}
55+
{{ partial "components/generic-tabs.html" (dict "id" $id "tabs" $tabs) }}
56+
{{ else }}
57+
{{/* Fallback to single content box */}}
58+
<div class="generic-tabs-single mb-6">
59+
<div class="tab-content-single p-6 bg-white border border-redis-pen-300 rounded-md shadow-sm">
60+
{{ .Inner | markdownify }}
61+
</div>
62+
</div>
63+
{{ end }}

static/js/generic-tabs.js

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/**
2+
* Generic GitHub-style tabs functionality
3+
* Handles tab switching, keyboard navigation, and accessibility
4+
*/
5+
6+
class GenericTabs {
7+
constructor(container) {
8+
this.container = container;
9+
this.tabRadios = container.querySelectorAll('.tab-radio');
10+
this.tabLabels = container.querySelectorAll('.tab-label');
11+
this.tabPanels = container.querySelectorAll('.tab-content');
12+
13+
this.init();
14+
}
15+
16+
init() {
17+
// Add event listeners for radio button changes
18+
this.tabRadios.forEach((radio, index) => {
19+
radio.addEventListener('change', (e) => {
20+
if (e.target.checked) {
21+
this.switchToTab(index);
22+
}
23+
});
24+
});
25+
26+
// Add keyboard navigation for tab labels
27+
this.tabLabels.forEach((label, index) => {
28+
label.addEventListener('keydown', (e) => {
29+
this.handleKeydown(e, index);
30+
});
31+
});
32+
33+
// Set initial state
34+
const checkedRadio = this.container.querySelector('.tab-radio:checked');
35+
if (checkedRadio) {
36+
const index = parseInt(checkedRadio.dataset.tabIndex);
37+
this.switchToTab(index);
38+
}
39+
}
40+
41+
switchToTab(index) {
42+
// Update radio buttons
43+
this.tabRadios.forEach((radio, i) => {
44+
radio.checked = i === index;
45+
});
46+
47+
// Update tab labels
48+
this.tabLabels.forEach((label, i) => {
49+
const isSelected = i === index;
50+
label.setAttribute('aria-selected', isSelected);
51+
label.setAttribute('tabindex', isSelected ? '0' : '-1');
52+
});
53+
54+
// Update tab panels
55+
this.tabPanels.forEach((panel, i) => {
56+
const isActive = i === index;
57+
panel.classList.toggle('active', isActive);
58+
panel.setAttribute('aria-hidden', !isActive);
59+
});
60+
}
61+
62+
handleKeydown(event, currentIndex) {
63+
let newIndex = currentIndex;
64+
65+
switch (event.key) {
66+
case 'ArrowLeft':
67+
event.preventDefault();
68+
newIndex = currentIndex > 0 ? currentIndex - 1 : this.tabLabels.length - 1;
69+
break;
70+
case 'ArrowRight':
71+
event.preventDefault();
72+
newIndex = currentIndex < this.tabLabels.length - 1 ? currentIndex + 1 : 0;
73+
break;
74+
case 'Home':
75+
event.preventDefault();
76+
newIndex = 0;
77+
break;
78+
case 'End':
79+
event.preventDefault();
80+
newIndex = this.tabLabels.length - 1;
81+
break;
82+
case 'Enter':
83+
case ' ':
84+
event.preventDefault();
85+
this.tabRadios[currentIndex].checked = true;
86+
this.switchToTab(currentIndex);
87+
return;
88+
default:
89+
return;
90+
}
91+
92+
// Focus and activate the new tab
93+
this.tabLabels[newIndex].focus();
94+
this.tabRadios[newIndex].checked = true;
95+
this.switchToTab(newIndex);
96+
}
97+
}
98+
99+
// Initialize all generic tabs on page load
100+
document.addEventListener('DOMContentLoaded', () => {
101+
const tabContainers = document.querySelectorAll('.generic-tabs');
102+
tabContainers.forEach(container => {
103+
new GenericTabs(container);
104+
});
105+
});
106+
107+
// Export for potential external use
108+
window.GenericTabs = GenericTabs;

0 commit comments

Comments
 (0)