Skip to content

Commit 03739af

Browse files
committed
Adds first template language chooser to the site. A working example showcasing 11ty#94 in action.
1 parent cbf070e commit 03739af

File tree

5 files changed

+300
-17
lines changed

5 files changed

+300
-17
lines changed
+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
seven-minute-tabs {
2+
display: block;
3+
border: 1px solid #444;
4+
padding: 0 1rem;
5+
border-radius: .5rem;
6+
margin: 0 -1rem 1rem;
7+
overflow: hidden;
8+
}
9+
seven-minute-tabs [role="tablist"] {
10+
padding: .5rem 1rem 0;
11+
margin: 0 -1rem .5rem;
12+
background-color: #eee;
13+
font-size: 0.8125em; /* 13px /16 */
14+
line-height: 1.8;
15+
}
16+
seven-minute-tabs [role="tab"] {
17+
display: inline-block;
18+
font-weight: 500;
19+
margin: 0 .5em;
20+
text-decoration: none;
21+
border-bottom: 6px solid transparent;
22+
}
23+
seven-minute-tabs [role="tab"][aria-selected="true"] {
24+
border-color: #222;
25+
}
26+
27+
/* Code samples in tabs */
28+
seven-minute-tabs [role="tabpanel"] pre {
29+
border-radius: 0;
30+
}
31+
seven-minute-tabs [role="tabpanel"] pre:last-child {
32+
margin-bottom: 0;
33+
}
+246
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
/*
2+
* This content is licensed according to the W3C Software License at
3+
* https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document
4+
* Heavily modified to web component by Zach Leatherman
5+
*/
6+
class SevenMinuteTabs extends HTMLElement {
7+
constructor() {
8+
super();
9+
10+
this.tablist = this.querySelector('[role="tablist"]');
11+
this.buttons = this.querySelectorAll('[role="tab"]');
12+
this.panels = this.querySelectorAll('[role="tabpanel"]');
13+
this.delay = this.determineDelay();
14+
15+
if(!this.tablist || !this.buttons.length || !this.panels.length) {
16+
return;
17+
}
18+
19+
this.initButtons();
20+
this.initPanels();
21+
}
22+
23+
get keys() {
24+
return {
25+
end: 35,
26+
home: 36,
27+
left: 37,
28+
up: 38,
29+
right: 39,
30+
down: 40
31+
};
32+
}
33+
34+
// Add or substract depending on key pressed
35+
get direction() {
36+
return {
37+
37: -1,
38+
38: -1,
39+
39: 1,
40+
40: 1
41+
};
42+
}
43+
44+
initButtons() {
45+
let count = 0;
46+
for(let button of this.buttons) {
47+
button.addEventListener('click', this.clickEventListener.bind(this));
48+
button.addEventListener('keydown', this.keydownEventListener.bind(this));
49+
button.addEventListener('keyup', this.keyupEventListener.bind(this));
50+
51+
button.index = count++;
52+
}
53+
}
54+
55+
initPanels() {
56+
let selectedPanelId = this.querySelector('[role="tab"][aria-selected="true"]').getAttribute("aria-controls");
57+
for(let panel of this.panels) {
58+
if(panel.getAttribute("id") !== selectedPanelId) {
59+
panel.setAttribute("hidden", "");
60+
}
61+
}
62+
}
63+
64+
clickEventListener(event) {
65+
let button = event.target;
66+
if(button.tagName === "A" || button.tagName === "BUTTON" && button.getAttribute("type") === "submit") {
67+
event.preventDefault();
68+
}
69+
70+
this.activateTab(button, false);
71+
}
72+
73+
// Handle keydown on tabs
74+
keydownEventListener(event) {
75+
var key = event.keyCode;
76+
77+
switch (key) {
78+
case this.keys.end:
79+
event.preventDefault();
80+
// Activate last tab
81+
this.activateTab(this.buttons[this.buttons.length - 1]);
82+
break;
83+
case this.keys.home:
84+
event.preventDefault();
85+
// Activate first tab
86+
this.activateTab(this.buttons[0]);
87+
break;
88+
89+
// Up and down are in keydown
90+
// because we need to prevent page scroll >:)
91+
case this.keys.up:
92+
case this.keys.down:
93+
this.determineOrientation(event);
94+
break;
95+
};
96+
}
97+
98+
// Handle keyup on tabs
99+
keyupEventListener(event) {
100+
var key = event.keyCode;
101+
102+
switch (key) {
103+
case this.keys.left:
104+
case this.keys.right:
105+
this.determineOrientation(event);
106+
break;
107+
};
108+
}
109+
110+
// When a tablist’s aria-orientation is set to vertical,
111+
// only up and down arrow should function.
112+
// In all other cases only left and right arrow function.
113+
determineOrientation(event) {
114+
var key = event.keyCode;
115+
var vertical = this.tablist.getAttribute('aria-orientation') == 'vertical';
116+
var proceed = false;
117+
118+
if (vertical) {
119+
if (key === this.keys.up || key === this.keys.down) {
120+
event.preventDefault();
121+
proceed = true;
122+
};
123+
}
124+
else {
125+
if (key === this.keys.left || key === this.keys.right) {
126+
proceed = true;
127+
};
128+
};
129+
130+
if (proceed) {
131+
this.switchTabOnArrowPress(event);
132+
};
133+
}
134+
135+
// Either focus the next, previous, first, or last tab
136+
// depending on key pressed
137+
switchTabOnArrowPress(event) {
138+
var pressed = event.keyCode;
139+
140+
for (let button of this.buttons) {
141+
button.addEventListener('focus', this.focusEventHandler.bind(this));
142+
};
143+
144+
if (this.direction[pressed]) {
145+
var target = event.target;
146+
if (target.index !== undefined) {
147+
if (this.buttons[target.index + this.direction[pressed]]) {
148+
this.buttons[target.index + this.direction[pressed]].focus();
149+
}
150+
else if (pressed === this.keys.left || pressed === this.keys.up) {
151+
this.focusLastTab();
152+
}
153+
else if (pressed === this.keys.right || pressed == this.keys.down) {
154+
this.focusFirstTab();
155+
}
156+
}
157+
}
158+
}
159+
160+
// Activates any given tab panel
161+
activateTab (tab, setFocus) {
162+
if(tab.getAttribute("role") !== "tab") {
163+
tab = tab.closest('[role="tab"]');
164+
}
165+
166+
setFocus = setFocus || true;
167+
168+
// Deactivate all other tabs
169+
this.deactivateTabs();
170+
171+
// Remove tabindex attribute
172+
tab.removeAttribute('tabindex');
173+
174+
// Set the tab as selected
175+
tab.setAttribute('aria-selected', 'true');
176+
177+
// Get the value of aria-controls (which is an ID)
178+
var controls = tab.getAttribute('aria-controls');
179+
180+
// Remove hidden attribute from tab panel to make it visible
181+
document.getElementById(controls).removeAttribute('hidden');
182+
183+
// Set focus when required
184+
if (setFocus) {
185+
tab.focus();
186+
}
187+
}
188+
189+
// Deactivate all tabs and tab panels
190+
deactivateTabs() {
191+
for (let button of this.buttons) {
192+
button.setAttribute('tabindex', '-1');
193+
button.setAttribute('aria-selected', 'false');
194+
button.removeEventListener('focus', this.focusEventHandler.bind(this));
195+
}
196+
197+
for (let panel of this.panels) {
198+
panel.setAttribute('hidden', 'hidden');
199+
}
200+
}
201+
202+
focusFirstTab() {
203+
this.buttons[0].focus();
204+
}
205+
206+
focusLastTab() {
207+
this.buttons[this.buttons.length - 1].focus();
208+
}
209+
210+
// Determine whether there should be a delay
211+
// when user navigates with the arrow keys
212+
determineDelay() {
213+
var hasDelay = this.tablist.hasAttribute('data-delay');
214+
var delay = 0;
215+
216+
if (hasDelay) {
217+
var delayValue = this.tablist.getAttribute('data-delay');
218+
if (delayValue) {
219+
delay = delayValue;
220+
}
221+
else {
222+
// If no value is specified, default to 300ms
223+
delay = 300;
224+
};
225+
};
226+
227+
return delay;
228+
}
229+
230+
focusEventHandler(event) {
231+
var target = event.target;
232+
233+
setTimeout(this.checkTabFocus.bind(this), this.delay, target);
234+
};
235+
236+
// Only activate tab on focus if it still has focus after the delay
237+
checkTabFocus(target) {
238+
let focused = document.activeElement;
239+
240+
if (target === focused) {
241+
this.activateTab(target, false);
242+
}
243+
}
244+
}
245+
246+
window.customElements.define("seven-minute-tabs", SevenMinuteTabs);

_includes/layouts/base.njk

+1
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ social:
4848
{% include 'components/breadcrumb.css' %}
4949
{% include 'components/prism-theme.css' %}
5050
{% include 'components/code.css' %}
51+
{% include 'components/seven-minute-tabs.css' %}
5152
{% for includeLocation in css %}
5253
{% include includeLocation %}
5354
{% endfor %}

docs/layouts.md

+19-16
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,14 @@ You can use any template language in your layout—it doesn’t need to match th
2828

2929
Next, we need to create a `mylayout.njk` file. It can contain any type of text, but here we’re using HTML:
3030

31-
{% codetitle "_includes/mylayout.njk" %}
32-
33-
{% raw %}
34-
```html
31+
<seven-minute-tabs>
32+
<div role="tablist" aria-label="Template Language Chooser">
33+
Template language: <a href="#njk-a" id="njk-a-btn" role="tab" aria-controls="njk-a" aria-selected="true" tabindex="0">Nunjucks</a>
34+
<a href="#11tyjs-a" id="11tyjs-a-btn" role="tab" aria-controls="11tyjs-a" aria-selected="false" tabindex="-1">11ty.js</a>
35+
</div>
36+
<div id="njk-a" role="tabpanel" tabindex="0" aria-labelledby="njk-a-btn">
37+
{%- codetitle "_includes/mylayout.njk" %}
38+
{%- highlight "html" %}
3539
---
3640
title: My Rad Blog
3741
---
@@ -46,15 +50,12 @@ title: My Rad Blog
4650
{{ content | safe }}
4751
</body>
4852
</html>
49-
```
50-
{% endraw %}
51-
52-
In case you prefer a [templating language](/docs/languages/) other than Nunjucks, here’s the same markup in JavaScript.
53-
54-
{% codetitle "_includes/mylayout.11ty.js" %}
55-
56-
{% raw %}
57-
```js
53+
{%- endhighlight %}
54+
<p>Note that the layout template will populate the <code>content</code> data with the child template’s content. Also note that we don’t want to double-escape the output, so we’re using the provided Nunjuck’s <code>safe</code> filter here (see more language double-escaping syntax below).</p>
55+
</div>
56+
<div id="11tyjs-a" role="tabpanel" tabindex="0" aria-labelledby="11tyjs-a-btn" hidden>
57+
{%- codetitle "_includes/mylayout.11ty.js" %}
58+
{%- highlight "js" %}
5859
exports.data = {
5960
title: "My Rad Blog"
6061
};
@@ -72,10 +73,12 @@ return `<!doctype html>
7273
</body>
7374
</html>`;
7475
};
75-
```
76-
{% endraw %}
76+
{%- endhighlight %}
77+
<p>Note that the layout template will populate the <code>data.content</code> variable with the child template’s content.
78+
</div>
79+
</seven-minute-tabs>
80+
7781

78-
Note that the layout template will populate the `content` data with the child template’s content. Also note that we don’t want to double-escape the output, so we’re using the provided Nunjuck’s `safe` filter here (see more language double-escaping syntax below).
7982

8083
{% callout "info" %}Layouts can contain their own front matter data! It’ll be merged with the content’s data on render. Content data takes precedence, if conflicting keys arise. Read more about <a href="/docs/data-cascade/">how Eleventy merges data in what we call the Data Cascade</a>.{% endcallout %}
8184

js/eleventy-js.njk

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,6 @@ eleventyExcludeFromCollections: true
77
{% include "components/auth/gotrue.js" %}
88
{% include "components/auth/auth.js" %}
99
{% include "components/search.js" %}
10-
{% include "../node_modules/instant.page/instantpage.js" %}
10+
{% include "components/seven-minute-tabs.js" %}
1111
{% endset %}
1212
{{ js | jsmin | safe }}

0 commit comments

Comments
 (0)