Skip to content

Commit 12dd418

Browse files
authored
Footnote Patterns (#1518)
This adds three new components which are meant to be used together: - Footnote Link - Links to a footnote from the primary text - Footnote - the contents of the footnote. Has a link returning to the footnote link - Footnote Group - Handles laying out footnotes in a list and including a visually hidden heading
1 parent 2947ed2 commit 12dd418

File tree

15 files changed

+485
-13
lines changed

15 files changed

+485
-13
lines changed
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
@use '../../compiled/tokens/scss/color';
2+
@use '../../compiled/tokens/scss/size';
3+
@use '../../mixins/theme';
4+
@use '../../mixins/ms';
5+
6+
/// Footnote groups are used to group footnotes together in a list
7+
.c-footnote-group {
8+
--spacing-footnotes: #{ms.step(2)};
9+
10+
border-top: size.$edge-medium solid color.$base-gray-lighter;
11+
margin-top: var(--spacing-footnotes);
12+
padding-top: var(--spacing-footnotes);
13+
14+
@include theme.styles(dark) {
15+
border-color: color.$brand-primary-darker;
16+
}
17+
}
18+
19+
/// Compact footnote groups have smaller text, smaller gaps, and no top
20+
/// border/padding
21+
.c-footnote-group--compact {
22+
--spacing-footnotes: #{ms.step(1)};
23+
24+
border-top: none;
25+
font-size: ms.step(-1);
26+
padding-top: 0;
27+
}
28+
29+
.c-footnote-group__list > * + * {
30+
margin-top: var(--spacing-footnotes);
31+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { Story, Canvas, Meta } from '@storybook/addon-docs/blocks';
2+
import template from './footnote-group.twig';
3+
4+
<Meta
5+
title="Components/Footnote/Footnote Group"
6+
argTypes={{
7+
compact: { type: { name: 'boolean' } },
8+
id_suffix: { type: { name: 'string' } },
9+
}}
10+
/>
11+
12+
# Footnote Group
13+
14+
Used for displaying one or more [Footnotes](/docs/components-footnote-footnote--basic).
15+
16+
There are a couple things that are important to note when using Footnote Groups:
17+
18+
- If you have more than one Footnote Group on a page, you'll need to pass in a `suffix_id` to the Footnote Groups to ensure they use unique IDs.
19+
- Footnote Groups include a [visually hidden](/docs/utilities-display--hidden-visually) heading. By default it is an `h2` saying "Footnotes". This can be customized with the `heading_tag` and `heading_text` properties.
20+
21+
## Basic usage
22+
23+
<Canvas>
24+
<Story name="Basic">
25+
{(args) =>
26+
template({
27+
items: ['Footnote 1', 'Footnote 2', 'Footnote 3', 'Footnote 4'],
28+
...args,
29+
})
30+
}
31+
</Story>
32+
</Canvas>
33+
34+
## Compact
35+
36+
The Compact attribute can be added to reduce the size and spacing of footnote groups, as well as remove their top border. This can be helpful when styling footnote groups in blog comments.
37+
38+
<Canvas>
39+
<Story
40+
name="Compact"
41+
args={{
42+
compact: true,
43+
}}
44+
>
45+
{(args) =>
46+
template({
47+
items: ['Footnote 1', 'Footnote 2', 'Footnote 3', 'Footnote 4'],
48+
...args,
49+
})
50+
}
51+
</Story>
52+
</Canvas>
53+
54+
## Template Properties
55+
56+
- `items`: An array of strings
57+
- `id_suffix` (string) should be used whenever there are multiple sets of footnotes on one page, to ensure the components use unique IDs.
58+
- `heading_tag` (string) the tag to use for the heading. Defaults to `h1`.
59+
- `heading_text` (string) the heading text. Defaults to `Footnotes`.
60+
- `compact` (boolean) make the footnote group smaller
61+
62+
## Template Blocks
63+
64+
- `content`: The footnote contents (overwrites anything placed in the items property)
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<div class="c-footnote-group{% if compact %} c-footnote-group--compact{% endif %}">
2+
<{{heading_tag|default('h2')}} class="u-hidden-visually" id="footnote-group-heading{{id_suffix}}">
3+
{{heading_text|default('Footnotes')}}
4+
</{{heading_tag|default('h2')}}>
5+
6+
<ol class="c-footnote-group__list" aria-labelledby="footnote-group-heading{{id_suffix}}">
7+
{% block content %}
8+
{% for item in items %}
9+
{% set _id = loop.index %}
10+
11+
{% if id_suffix %}
12+
{% set _id = _id ~ id_suffix %}
13+
{% endif %}
14+
15+
{% embed '@cloudfour/components/footnote/footnote.twig' with {
16+
count: loop.index,
17+
id: _id,
18+
item: item
19+
} only %}
20+
{% block content %}
21+
{{ item|raw }}
22+
{% endblock %}
23+
{% endembed %}
24+
{% endfor %}
25+
{% endblock %}
26+
</ol>
27+
</div>
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<p>
2+
Micro-interactions are small, singular interactions that serve one purpose:
3+
communicate meaningful feedback to users in a positive and welcoming
4+
way.{% include '@cloudfour/components/footnote-link/footnote-link.twig' with {
5+
count: count
6+
} only %}
7+
People like to constantly know <em>what’s</em> going to happen when an
8+
action is performed and tend to expect something to happen when they
9+
interact with it. The pop of the heart when you favorite a post, the
10+
elasticity of pull-to-refresh, or even snoozing your alarm are all
11+
examples of micro-interactions we experience every day.
12+
I think it’s important to take the extra time to make these experiences
13+
feel more human.
14+
</p>
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
@use '../../compiled/tokens/scss/color';
2+
@use '../../compiled/tokens/scss/ease';
3+
@use "../../compiled/tokens/scss/font-weight";
4+
@use '../../compiled/tokens/scss/size';
5+
@use "../../compiled/tokens/scss/scale";
6+
@use '../../compiled/tokens/scss/transition';
7+
@use '../../mixins/focus';
8+
@use '../../mixins/ms';
9+
@use '../../mixins/theme';
10+
11+
/// Footnote links are small numbered circles within a body of text that
12+
/// link to a footnote below
13+
///
14+
/// 1. They start off as circles but if their content gets wide they turn into pills
15+
/// 2. Since they can be linked to we assign them a margin offset, so when they're
16+
/// linked they're not right at the top of the screen
17+
/// 3. They have a higher vertical baseline than the text around them. (This is
18+
/// similar to the `<sup>` element. Since `<sup>` has no semantic meaning, we
19+
/// apply these styles via CSS
20+
.c-footnote-link {
21+
background-color: color.$base-gray-lighter;
22+
border-radius: size.$border-radius-full; /* 1 */
23+
color: color.$text-action;
24+
display: inline-block;
25+
font-size: ms.step(-2);
26+
font-weight: font-weight.$medium;
27+
line-height: size.$square-footnote-link; /* 1 */
28+
min-width: size.$square-footnote-link; /* 1 */
29+
padding: 0 ms.step(-5); /* 1 */
30+
scroll-margin: ms.step(4); /* 2 */
31+
text-align: center;
32+
text-decoration: none;
33+
transition-duration: transition.$quick;
34+
transition-property: background-color, color, transform;
35+
transition-timing-function: ease.$out;
36+
vertical-align: super; /* 3 */
37+
38+
&:hover {
39+
background-color: color.$text-action;
40+
color: color.$text-light-emphasis;
41+
}
42+
43+
@include theme.styles(dark) {
44+
background-color: color.$brand-primary-dark;
45+
color: color.$text-light-emphasis;
46+
47+
&:hover {
48+
background-color: color.$text-light-emphasis;
49+
color: color.$text-action;
50+
}
51+
}
52+
53+
/// We add a subtle animation to the footnote link when it is targeted via the
54+
/// URL hash. This is meant to provide a visual cue about where you landed.
55+
/// We use a temporary animation to avoid it being confused with a focus or
56+
/// hover style.
57+
&:target {
58+
animation: footnote-link-target-circle transition.$glacial ease.$out;
59+
}
60+
61+
@include focus.focus-visible {
62+
box-shadow: 0 0 0 size.$edge-medium color.$brand-primary-lighter;
63+
}
64+
65+
&:active {
66+
transform: scale(scale.$effect-shrink);
67+
}
68+
}
69+
70+
@keyframes footnote-link-target-circle {
71+
from {
72+
box-shadow: 0 0 0 size.$edge-medium color.$brand-primary-light;
73+
}
74+
to {
75+
box-shadow: 0 0 0 size.$edge-medium transparent;
76+
}
77+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { Story, Canvas, Meta } from '@storybook/addon-docs/blocks';
2+
import template from './demo/demo.twig';
3+
// The '!!raw-loader!' syntax is a non-standard, Webpack-specific, syntax.
4+
// See: https://github.com/webpack-contrib/raw-loader#examples
5+
// For now, it seems likely Storybook is pretty tied to Webpack, therefore, we are
6+
// okay with the following Webpack-specific raw loader syntax. It's better to leave
7+
// the ESLint rule enabled globally, and only thoughtfully disable as needed (e.g.
8+
// within a Storybook docs page and not within an actual component).
9+
// This can be revisited in the future if Storybook no longer relies on Webpack.
10+
// eslint-disable-next-line @cloudfour/import/no-webpack-loader-syntax
11+
import footnoteLinkSource from '!!raw-loader!./demo/demo.twig';
12+
13+
<Meta
14+
title="Components/Footnote/Footnote Link"
15+
argTypes={{
16+
count: { type: { name: 'number' } },
17+
id: { type: { name: 'string' } },
18+
}}
19+
/>
20+
21+
# Footnote Link
22+
23+
A link to a footnote. Intended to be matched with a [Footnote](/docs/components-footnote-footnote--basic).
24+
25+
## Basic usage
26+
27+
<Canvas>
28+
<Story
29+
name="Basic"
30+
args={{ count: 1 }}
31+
parameters={{
32+
docs: {
33+
source: {
34+
code: footnoteLinkSource,
35+
},
36+
},
37+
}}
38+
>
39+
{(args) => template(args)}
40+
</Story>
41+
</Canvas>
42+
43+
## Template Properties
44+
45+
- `count` (number) the number of the current footnote we're on (e.g. this is the second footnote in the document)
46+
- `id` (string) a unique ID for a footnote. Defaults to the `count` but should be customized if there are multiple sets of footnotes on one page
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{% spaceless %}
2+
<a
3+
class="c-footnote-link"
4+
href="#footnote-{{id|default(count)}}"
5+
id="footnote-anchor-{{id|default(count)}}"
6+
>
7+
<span class="u-hidden-visually">Footnote</span>
8+
{{count}}
9+
</a>
10+
{% endspaceless %}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{% embed '@cloudfour/components/footnote-group/footnote-group.twig' with {
2+
count: count
3+
} only %}
4+
{% block content %}
5+
{% embed '@cloudfour/components/footnote/footnote.twig' with {
6+
count: count
7+
} only %}
8+
{% block content %}
9+
This is the footnote content.
10+
{% endblock %}
11+
{% endembed %}
12+
{% endblock %}
13+
{% endembed %}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
@use '../../compiled/tokens/scss/color';
2+
@use '../../compiled/tokens/scss/size';
3+
@use '../../mixins/icon';
4+
@use '../../mixins/theme';
5+
@use '../../mixins/focus';
6+
@use '../../mixins/ms';
7+
8+
$footnote-offset: ms.step(-2);
9+
$footnote-offset-negative: calc(#{$footnote-offset} * -1);
10+
$footnote-offset-left-negative: calc(
11+
#{$footnote-offset-negative} + #{ms.step(1)} * -1
12+
);
13+
14+
/// Footnotes are list items with return links after their content
15+
///
16+
/// 1. Since footnotes are linked to using hash links, we give them a little
17+
/// vertical offset so the text isn't right at the top of the screen
18+
.c-footnote {
19+
position: relative;
20+
scroll-margin: calc(#{$footnote-offset} + #{ms.step(-2)}); /* 1 */
21+
22+
/// When a footnote is targeted by the URL hash we apply a background color to it
23+
/// We use an absolutely positioned pseudo-element so our background will extend
24+
/// beneath the list item ::marker
25+
&:target::before {
26+
background-color: color.$base-gray-lighter;
27+
border-radius: size.$border-radius-medium;
28+
bottom: $footnote-offset-negative;
29+
content: '';
30+
left: $footnote-offset-left-negative;
31+
position: absolute;
32+
right: $footnote-offset-negative;
33+
top: $footnote-offset-negative;
34+
z-index: -1;
35+
36+
@include theme.styles(dark) {
37+
background-color: color.$brand-primary-dark;
38+
}
39+
}
40+
41+
/// We apply some basic styles to the return link.
42+
/// 1. Center its icon
43+
/// 2. We shift the link up slightly so its icon appears vertically aligned
44+
/// with the surrounding text. We apply that to the link instead of the
45+
/// inner icon to ensure the focus ring is centered correctly.
46+
/// 3. Add some focus styles
47+
.c-footnote-link-item__return-link {
48+
align-items: center; /* 1 */
49+
border-radius: size.$border-radius-full;
50+
display: inline-flex; /* 1 */
51+
justify-content: center; /* 1 */
52+
padding: ms.step(-6); /* 1 */
53+
position: relative; /* 2 */
54+
vertical-align: middle; /* 1 */
55+
56+
@include icon.inline(); /* 2 */
57+
58+
@include focus.focus-visible {
59+
box-shadow: 0 0 0 size.$edge-medium color.$brand-primary-lighter; /* 3 */
60+
}
61+
}
62+
}

0 commit comments

Comments
 (0)