Skip to content

Commit 3c4bc8a

Browse files
authored
Merge pull request #2915 from squidfunk/feature/scrollable-content-tabs
Prototype: scrollable content tabs
2 parents b6fdd52 + 270abe1 commit 3c4bc8a

File tree

13 files changed

+321
-46
lines changed

13 files changed

+321
-46
lines changed

material/assets/javascripts/bundle.2248a2bd.min.js

+29
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

material/assets/javascripts/bundle.2248a2bd.min.js.map

+7
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

material/assets/javascripts/bundle.5e5cfff9.min.js

-29
This file was deleted.

material/assets/javascripts/bundle.5e5cfff9.min.js.map

-7
This file was deleted.

material/assets/stylesheets/main.8b42a75e.min.css renamed to material/assets/stylesheets/main.55b1b295.min.css

+2-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

material/assets/stylesheets/main.55b1b295.min.css.map

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

material/assets/stylesheets/main.8b42a75e.min.css.map

-1
This file was deleted.

material/base.html

+2-2
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
{% endif %}
4040
{% endblock %}
4141
{% block styles %}
42-
<link rel="stylesheet" href="{{ 'assets/stylesheets/main.8b42a75e.min.css' | url }}">
42+
<link rel="stylesheet" href="{{ 'assets/stylesheets/main.55b1b295.min.css' | url }}">
4343
{% if config.theme.palette %}
4444
{% set palette = config.theme.palette %}
4545
<link rel="stylesheet" href="{{ 'assets/stylesheets/palette.3f5d1f46.min.css' | url }}">
@@ -225,7 +225,7 @@ <h1>{{ page.title | d(config.site_name, true)}}</h1>
225225
</script>
226226
{% endblock %}
227227
{% block scripts %}
228-
<script src="{{ 'assets/javascripts/bundle.5e5cfff9.min.js' | url }}"></script>
228+
<script src="{{ 'assets/javascripts/bundle.2248a2bd.min.js' | url }}"></script>
229229
{% for path in config["extra_javascript"] %}
230230
<script src="{{ path | url }}"></script>
231231
{% endfor %}

requirements.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,5 +22,5 @@
2222
mkdocs>=1.2.2
2323
Pygments>=2.4
2424
markdown>=3.2
25-
pymdown-extensions>=7.0
25+
pymdown-extensions>=8.2
2626
mkdocs-material-extensions>=1.0

src/assets/javascripts/components/content/_/index.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { Component } from "../../_"
2828
import { CodeBlock, mountCodeBlock } from "../code"
2929
import { Details, mountDetails } from "../details"
3030
import { DataTable, mountDataTable } from "../table"
31+
import { ContentTabs, mountContentTabs } from "../tabs"
3132

3233
/* ----------------------------------------------------------------------------
3334
* Types
@@ -37,6 +38,7 @@ import { DataTable, mountDataTable } from "../table"
3738
* Content
3839
*/
3940
export type Content =
41+
| ContentTabs
4042
| CodeBlock
4143
| DataTable
4244
| Details
@@ -84,6 +86,10 @@ export function mountContent(
8486

8587
/* Details */
8688
...getElements("details", el)
87-
.map(child => mountDetails(child, { target$, print$ }))
89+
.map(child => mountDetails(child, { target$, print$ })),
90+
91+
/* Content tabs */
92+
...getElements("[data-tabs]", el)
93+
.map(child => mountContentTabs(child))
8894
)
8995
}

src/assets/javascripts/components/content/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,4 @@ export * from "./_"
2424
export * from "./code"
2525
export * from "./details"
2626
export * from "./table"
27+
export * from "./tabs"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/*
2+
* Copyright (c) 2016-2021 Martin Donath <[email protected]>
3+
*
4+
* Permission is hereby granted, free of charge, to any person obtaining a copy
5+
* of this software and associated documentation files (the "Software"), to
6+
* deal in the Software without restriction, including without limitation the
7+
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
8+
* sell copies of the Software, and to permit persons to whom the Software is
9+
* furnished to do so, subject to the following conditions:
10+
*
11+
* The above copyright notice and this permission notice shall be included in
12+
* all copies or substantial portions of the Software.
13+
*
14+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16+
* FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
17+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
19+
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
20+
* IN THE SOFTWARE.
21+
*/
22+
23+
import { NEVER, Observable, Subject, fromEvent, merge } from "rxjs"
24+
import { finalize, map, mapTo, tap } from "rxjs/operators"
25+
26+
import { getElementOrThrow, getElements } from "~/browser"
27+
28+
import { Component } from "../../_"
29+
30+
/* ----------------------------------------------------------------------------
31+
* Types
32+
* ------------------------------------------------------------------------- */
33+
34+
/**
35+
* Content tabs
36+
*/
37+
export interface ContentTabs {
38+
active: HTMLLabelElement /* Active tab label */
39+
}
40+
41+
/* ----------------------------------------------------------------------------
42+
* Functions
43+
* ------------------------------------------------------------------------- */
44+
45+
/**
46+
* Watch content tabs
47+
*
48+
* @param el - Content tabs element
49+
*
50+
* @returns Content tabs observable
51+
*/
52+
export function watchContentTabs(
53+
el: HTMLElement
54+
): Observable<ContentTabs> {
55+
if (!el.classList.contains(".tabbed-alternate"))
56+
return NEVER
57+
else
58+
return merge(...getElements(":scope > input", el)
59+
.map(input => fromEvent(input, "change").pipe(mapTo(input.id)))
60+
)
61+
.pipe(
62+
map(id => ({
63+
active: getElementOrThrow<HTMLLabelElement>(`label[for=${id}]`)
64+
}))
65+
)
66+
}
67+
68+
/**
69+
* Mount content tabs
70+
*
71+
* @param el - Content tabs element
72+
*
73+
* @returns Content tabs component observable
74+
*/
75+
export function mountContentTabs(
76+
el: HTMLElement
77+
): Observable<Component<ContentTabs>> {
78+
const internal$ = new Subject<ContentTabs>()
79+
internal$.subscribe(({ active }) => {
80+
active.scrollIntoView({ behavior: "smooth", block: "nearest" })
81+
})
82+
83+
/* Create and return component */
84+
return watchContentTabs(el)
85+
.pipe(
86+
tap(state => internal$.next(state)),
87+
finalize(() => internal$.complete()),
88+
map(state => ({ ref: el, ...state }))
89+
)
90+
}

src/assets/stylesheets/main/extensions/pymdownx/_tabbed.scss

+181-3
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,13 @@
2121
////
2222

2323
// ----------------------------------------------------------------------------
24-
// Rules
24+
// Rules: legacy implementation (deprecated, removed in v8)
2525
// ----------------------------------------------------------------------------
2626

2727
// Scoped in typesetted content to match specificity of regular content
2828
.md-typeset {
2929

30-
// Tabbed block content
30+
// Tabbed content
3131
.tabbed-content {
3232
display: none;
3333
order: 99;
@@ -60,7 +60,7 @@
6060
}
6161
}
6262

63-
// Tabbed block container
63+
// Tabbed container
6464
.tabbed-set {
6565
position: relative;
6666
display: flex;
@@ -121,3 +121,181 @@
121121
}
122122
}
123123
}
124+
125+
// ----------------------------------------------------------------------------
126+
// Placeholders: improve colocation for better compression
127+
// ----------------------------------------------------------------------------
128+
129+
// Tab label placeholder
130+
%tabbed-label {
131+
132+
// [screen]: Show active state
133+
@media screen {
134+
color: var(--md-accent-fg-color);
135+
border-color: var(--md-accent-fg-color);
136+
}
137+
}
138+
139+
// Tab label on keyboard focus placeholder
140+
%tabbed-label-focus-visible {
141+
background-color: var(--md-accent-fg-color--transparent);
142+
}
143+
144+
// Tab content placeholder
145+
%tabbed-content {
146+
display: block;
147+
}
148+
149+
// ----------------------------------------------------------------------------
150+
// Rules
151+
// ----------------------------------------------------------------------------
152+
153+
// Scoped in typesetted content to match specificity of regular content
154+
.md-typeset { // stylelint-disable-line
155+
156+
// Tabbed labels
157+
.tabbed-labels {
158+
display: flex;
159+
max-width: 100vw;
160+
overflow: auto;
161+
box-shadow: 0 px2rem(-1px) var(--md-default-fg-color--lightest) inset;
162+
scroll-snap-type: x proximity;
163+
-ms-overflow-style: none; // IE, Edge
164+
scrollbar-width: none; // Firefox
165+
166+
// [print]: Move one layer up for ordering
167+
@media print {
168+
display: contents;
169+
}
170+
171+
// Webkit scrollbar
172+
&::-webkit-scrollbar {
173+
display: none; // Chrome, Safari
174+
}
175+
176+
// Tab label
177+
> label {
178+
z-index: 1;
179+
width: auto;
180+
padding: px2em(12px, 12.8px) 1.25em px2em(10px, 12.8px);
181+
color: var(--md-default-fg-color--light);
182+
font-weight: 700;
183+
font-size: px2rem(12.8px);
184+
white-space: nowrap;
185+
border-bottom: px2rem(2px) solid transparent;
186+
scroll-snap-align: start;
187+
border-top-left-radius: px2rem(2px);
188+
border-top-right-radius: px2rem(2px);
189+
cursor: pointer;
190+
transition:
191+
background-color 250ms,
192+
color 250ms;
193+
194+
// [print]: Intersperse labels with containers
195+
@media print {
196+
197+
// Ensure correct order of labels
198+
@for $i from 1 through 10 {
199+
&:nth-child(#{$i}) {
200+
order: $i;
201+
}
202+
}
203+
}
204+
205+
// Tab label on hover
206+
&:hover {
207+
color: var(--md-accent-fg-color);
208+
}
209+
}
210+
}
211+
212+
// [mobile -]: Align with body copy
213+
@include break-to-device(mobile) {
214+
215+
// Top-level tabbed labels
216+
> .tabbed-alternate .tabbed-labels {
217+
margin: 0 px2rem(-16px);
218+
padding: 0 px2rem(16px);
219+
scroll-padding: 0 px2rem(16px);
220+
}
221+
}
222+
223+
// Tabbed container
224+
.tabbed-alternate {
225+
flex-direction: column;
226+
227+
// Tabbed content
228+
.tabbed-content {
229+
display: initial;
230+
order: initial;
231+
width: 100%;
232+
box-shadow: initial;
233+
234+
// [print]: Move one layer up for ordering
235+
@media print {
236+
display: contents;
237+
}
238+
}
239+
240+
// Tabbed block
241+
.tabbed-block {
242+
display: none;
243+
244+
// [print]: Intersperse labels with containers
245+
@media print {
246+
display: block;
247+
248+
// Ensure correct order of containers
249+
@for $i from 1 through 10 {
250+
&:nth-child(#{$i}) {
251+
order: $i;
252+
}
253+
}
254+
}
255+
256+
// Code block is the only child of a tab - remove margin and mirror
257+
// previous (now deprecated) SuperFences code block grouping behavior
258+
> pre:only-child,
259+
> .highlight:only-child pre,
260+
> .highlighttable:only-child {
261+
margin: 0;
262+
263+
// Omit rounded borders
264+
> code {
265+
border-top-left-radius: 0;
266+
border-top-right-radius: 0;
267+
}
268+
}
269+
270+
// Adjust spacing for nested tabbed container
271+
> .tabbed-set {
272+
margin: 0;
273+
}
274+
}
275+
276+
// Tab label states
277+
@for $i from 10 through 1 {
278+
input:nth-child(#{$i}) {
279+
280+
// Tab is active
281+
&:checked {
282+
283+
// Tab label
284+
~ .tabbed-labels > :nth-child(#{$i}) {
285+
@extend %tabbed-label;
286+
}
287+
288+
// Tab content
289+
~ .tabbed-content > :nth-child(#{$i}) {
290+
@extend %tabbed-content;
291+
}
292+
}
293+
294+
// Tab label on keyboard focus
295+
&.focus-visible ~ .tabbed-labels > :nth-child(#{$i}) {
296+
@extend %tabbed-label-focus-visible;
297+
}
298+
}
299+
}
300+
}
301+
}

0 commit comments

Comments
 (0)