-
Notifications
You must be signed in to change notification settings - Fork 3
Heading component and fluid size improvements #679
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
3383896
Fix fluid type function
tylersticka f8ba633
More dynamic heading size logic
tylersticka a13e903
Add WIP c-heading
tylersticka 9af2044
Add and clearly document heading component
tylersticka 7303cbb
Remove unused knobs
tylersticka ddedb53
Merge branch 'v-next' into feature/heading-improvements
tylersticka 1996048
Add explanatory comments to c-heading Sass vars
tylersticka b5b9774
c-heading__element → c-heading__content
tylersticka b235dd3
Fix typos
tylersticka 7dde2a0
More detailed em/rem comment
tylersticka e0bc32c
role="presentation" → aria-hidden="true"
tylersticka 0f1cbb4
Merge branch 'v-next' into feature/heading-improvements
tylersticka File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
{% embed '../../../objects/rhythm/rhythm.twig' with { | ||
class: 'o-rhythm--condensed' | ||
} only %} | ||
{% block content %} | ||
{% for i in -2..6 %} | ||
{% set class_level = i|abs %} | ||
{% if i < 0 %} | ||
{% set class_level = 'n' ~ class_level %} | ||
{% endif %} | ||
{% include '../heading.twig' with { | ||
level: i, | ||
content: 'c-heading--level-' ~ class_level | ||
} only %} | ||
{% endfor %} | ||
{% endblock %} | ||
{% endembed %} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
{% embed '../../../objects/container/container.twig' with { | ||
class: 'o-container--prose' | ||
} %} | ||
{% block content %} | ||
{% embed '../../../objects/rhythm/rhythm.twig' with { | ||
class: 'o-rhythm--generous' | ||
} only %} | ||
{% block content %} | ||
{% for i in 1..3 %} | ||
{% set class_level = i|abs %} | ||
{% if i < 0 %} | ||
{% set class_level = 'n' ~ class_level %} | ||
{% endif %} | ||
{% include '../heading.twig' with { | ||
level: i, | ||
content: 'c-heading--level-' ~ class_level, | ||
permalink: true | ||
} only %} | ||
{% endfor %} | ||
{% endblock %} | ||
{% endembed %} | ||
{% endblock %} | ||
{% endembed %} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,133 @@ | ||
@use '../../design-tokens/colors.yml'; | ||
@use '../../design-tokens/layout.yml'; | ||
@use "../../design-tokens/motion.yml"; | ||
@use '../../design-tokens/opacity.yml'; | ||
@use '../../design-tokens/sizes.yml'; | ||
@use '../../mixins/focus'; | ||
@use '../../mixins/headings'; | ||
@use '../../mixins/ms'; | ||
@use '../../mixins/unit'; | ||
@use 'sass:math'; | ||
|
||
// Variables that define the size and shape of the (optional) permalink element. | ||
// 1. The total size of the element. | ||
// 2. The size of the icon within the element. | ||
// 3. The element's rounded corners. | ||
$permalink-size: sizes.$control-height; // 1 | ||
$permalink-icon-size: sizes.$control-icon-size; // 2 | ||
$permalink-radius: sizes.$radius-full; // 3 | ||
|
||
// We want the permalink's position to change when there is enough room to | ||
// display a full line of prose _plus_ the permalink. To pull this off, we use | ||
// the prose width and permalink size (above) to build some component-specific | ||
// media query breakpoints. | ||
$min-width-permalink-shift: layout.$max-width-prose + $permalink-size * 4; | ||
$max-width-permalink-shift: ($min-width-permalink-shift * 16 - 1) / 16; | ||
|
||
/** | ||
* This containing element may be applied to a heading directly to apply a | ||
* heading style, or to a containing element if you want to display a heading | ||
* and permalink. | ||
* | ||
* 1. We default to the lowest heading level supported by HTML5, which forces | ||
* the template designer to specify an intended visual heading level. | ||
* 2. Sets up the optional permalink to position itself relative to the | ||
* containing element. | ||
*/ | ||
|
||
.c-heading { | ||
@include headings.level(6); /* 1 */ | ||
position: relative; /* 2 */ | ||
} | ||
|
||
/** | ||
* Level modifiers | ||
* | ||
* These will apply the styles for a specific heading level. | ||
* | ||
* We start at `-2` so template designers have three sizes larger than a default | ||
* `<h1>` to apply thoughtfully within templates. | ||
* | ||
* We end at 3 because that is the maximum depth we support with distinct | ||
* visual differences. | ||
tylersticka marked this conversation as resolved.
Show resolved
Hide resolved
|
||
*/ | ||
|
||
@for $level from -2 through headings.$max-level { | ||
$level-segment: ms.step-class-segment($level); | ||
|
||
.c-heading--level-#{$level-segment} { | ||
@include headings.level($level); | ||
} | ||
} | ||
|
||
/** | ||
* If `c-heading` is applied to a containing element, this class must be applied | ||
* to the inner heading to avoid unintended font sizes. | ||
*/ | ||
|
||
.c-heading__content { | ||
font: inherit; | ||
|
||
/** | ||
* Prevent heading from overlapping permalink until the viewport is wide | ||
* enough for the permalink to position itself outside the container. | ||
*/ | ||
|
||
@media (max-width: $max-width-permalink-shift) { | ||
padding-right: unit.swap($permalink-size, rem); | ||
} | ||
} | ||
|
||
/** | ||
* Optional permalink to be included adjacent to the heading itself. | ||
* | ||
* 1. We define the element's `font-size` using `rem` so its size will remain | ||
* consistent between differently sized headings in the same article. | ||
* 2. The link's shape is not visible unless focused, so applying some negative | ||
* offset based on the inner padding makes the link appear better aligned | ||
* with content above and below. | ||
* | ||
* @see http://codepen.io/marcysutton/pen/rLKvgZ | ||
*/ | ||
|
||
.c-heading__permalink { | ||
tylersticka marked this conversation as resolved.
Show resolved
Hide resolved
|
||
align-items: center; | ||
border-radius: $permalink-radius; | ||
bottom: 0; | ||
display: flex; | ||
font-size: unit.swap($permalink-icon-size, rem); /* 1 */ | ||
height: $permalink-size; | ||
justify-content: center; | ||
margin: auto; | ||
position: absolute; | ||
right: ($permalink-size - $permalink-icon-size) / -2; /* 2 */ | ||
text-decoration: none; | ||
top: 0; | ||
transition: color motion.$speed-quick motion.$ease-out, | ||
opacity motion.$speed-quick motion.$ease-out; | ||
width: $permalink-size; | ||
|
||
/** | ||
* Above a certain size, we display the icon to the left of the heading but | ||
* outside of the container (so it doesn't bump the heading to the right, | ||
* which can disrupt the flow of reading the content). | ||
*/ | ||
|
||
@media (min-width: $min-width-permalink-shift) { | ||
right: 100%; | ||
} | ||
|
||
/** | ||
* We use negation selectors to apply the non-hover style so the permalink | ||
* inherits any existing theme link styles for free. | ||
*/ | ||
|
||
&:not(:hover):not(:focus):not(:active) { | ||
color: inherit; | ||
opacity: opacity.$muted; | ||
} | ||
|
||
@include focus.focus-visible { | ||
box-shadow: 0 0 0 sizes.$edge-large colors.$primary-brand-lighter; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
import { Story, Preview, Meta } from '@storybook/addon-docs/blocks'; | ||
import levelsDemo from './demo/levels.twig'; | ||
import permalinksDemo from './demo/permalinks.twig'; | ||
|
||
<Meta title="Components/Heading" /> | ||
|
||
# Heading | ||
|
||
It's important for accessibility and for search engines that page sections include helpful heading elements in a logical, descending order ([more info](https://dequeuniversity.com/assets/html/jquery-summit/html5/slides/headings.html)). But there are times when [our default `<h1>` through `<h6>` styles](/?path=/docs/design-typography--headings) may not be enough: | ||
|
||
- The most appropriate heading element may look too small or too large visually. | ||
- You may want to apply heading styles to a different element: A short introductory `<p>`, a `<legend>`, etc. | ||
- Longer pages may benefit from additional features added to headings, such as permalinks. | ||
|
||
The `c-heading` component class addresses these scenarios by applying heading styles for the desired heading level, with a few bonus features not possible via heading elements alone. | ||
|
||
## Level Modifiers | ||
|
||
You can control the appearance of a `c-heading` component by including a `c-heading--level-{amount}` modifier, where `{amount}` is a heading level. For example, `c-heading--level-2` will make the element look like our default `<h2>`. | ||
|
||
`c-heading` also supports _larger_ levels than `<h1>` for top-level banners and page titles: | ||
|
||
| Level | Heading | Modifier | | ||
| ----- | ------- | --------------------- | | ||
| -2 | – | `c-heading--level-n2` | | ||
| -1 | – | `c-heading--level-n1` | | ||
| 0 | – | `c-heading--level-0` | | ||
| 1 | `<h1>` | `c-heading--level-1` | | ||
| 2 | `<h2>` | `c-heading--level-2` | | ||
| 3 | `<h3>` | `c-heading--level-3` | | ||
| 4 | `<h4>` | `c-heading--level-4` | | ||
| 5 | `<h5>` | `c-heading--level-5` | | ||
| 6 | `<h6>` | `c-heading--level-6` | | ||
|
||
<Preview> | ||
<Story name="Levels" height="475px"> | ||
{levelsDemo} | ||
</Story> | ||
</Preview> | ||
|
||
## Permalinks | ||
|
||
The `c-heading` class may include a permalink using [a technique from Marcy Sutton ](http://codepen.io/marcysutton/pen/rLKvgZ). Three elements are required: | ||
|
||
- `c-heading`: A container, ideally without any semantics (`<div>`). | ||
- `c-heading__content`: The actual semantic heading element (usually `<h1>` – `<h6>`), including the `id` referenced by `c-heading__permalink`. | ||
- `c-heading__permalink`: The `<a>` element that exposes the permalink to the user. This element is kept separate from the actual heading so it won't disrupt a user's ability to navigate via headings. | ||
|
||
Note that the layout of this pattern is optimized for [our prose containers](/?path=/docs/objects-container--prose#prose) with generous horizontal whitespace. | ||
|
||
<Preview> | ||
<Story name="Permalinks" height="300px"> | ||
{permalinksDemo} | ||
</Story> | ||
</Preview> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
{# | ||
If a `level` property isn't provided but a `tag_name` is and it is a heading | ||
element, we can figure out the intended level from that tag. | ||
#} | ||
|
||
{% if level is empty and tag_name is not empty and tag_name|first == 'h' %} | ||
{% set level = tag_name|last|number_format %} | ||
{% endif %} | ||
|
||
{# | ||
If a `tag_name` property isn't provided but a `level` is, we construct the | ||
`tag_name` from the level. | ||
#} | ||
|
||
{% if tag_name is empty and level is not empty %} | ||
{% set tag_name = 'h' ~ max(1, min(level, 6)) %} | ||
{% endif %} | ||
|
||
{# | ||
Build the class name based on the level. | ||
#} | ||
|
||
{% set _level_class = 'c-heading' %} | ||
|
||
{% if level is not empty %} | ||
{% set _level_modifier = ('level-' ~ level)|replace({ '--': '-n' }) %} | ||
{% set _level_class = _level_class ~ ' ' ~ _level_class ~ '--' ~ _level_modifier %} | ||
{% endif %} | ||
|
||
{% if class %} | ||
{% set _level_class = _level_class ~ ' ' ~ class %} | ||
{% endif %} | ||
|
||
{# | ||
Cache the content block, supporting either a block or a `content` property. | ||
#} | ||
|
||
{% set _content %} | ||
{% block content %} | ||
{{content}} | ||
{% endblock %} | ||
{% endset %} | ||
|
||
{# | ||
If a `permalink` is desired but no `permalink_id` is specified, we try | ||
creating a `permalink_id` from the value of `content`. | ||
|
||
Since Twig lacks a built-in slugify function, this is pretty fragile and | ||
should be considered a fallback. | ||
#} | ||
|
||
{% if permalink and not permalink_id and content is not empty %} | ||
{% set permalink_id = content|lower|escape('url')|replace({'%20': '-'}) %} | ||
{% endif %} | ||
|
||
{# | ||
Permalink heading markup based on this example from accessibility expert | ||
Marcy Sutton: http://codepen.io/marcysutton/pen/rLKvgZ | ||
#} | ||
|
||
{% if permalink and permalink_id %} | ||
<div class="{{_level_class}}"> | ||
<a class="c-heading__permalink" href="#{{permalink_id}}"> | ||
{% include '../../assets/icons/link.svg.twig' with { | ||
class: 'c-icon', | ||
aria_hidden: 'true' | ||
} only %} | ||
<span class="u-hidden-visually">Permalink to {{_content}}</span> | ||
tylersticka marked this conversation as resolved.
Show resolved
Hide resolved
|
||
</a> | ||
<{{tag_name}} class="c-heading__content" id="{{permalink_id}}"> | ||
{{_content}} | ||
</{{tag_name}}> | ||
</div> | ||
{% else %} | ||
<{{tag_name}} class="{{_level_class}}"> | ||
{{_content}} | ||
</{{tag_name}}> | ||
{% endif %} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,7 @@ | ||
{% for i in 1..6 %} | ||
<h{{i}}>Heading Level {{i}}</h{{i}}> | ||
{% endfor %} | ||
{% embed '../../../objects/rhythm/rhythm.twig' %} | ||
{% block content %} | ||
{% for i in 1..6 %} | ||
<h{{i}}>Heading Level {{i}}</h{{i}}> | ||
{% endfor %} | ||
{% endblock %} | ||
{% endembed %} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,29 +1,53 @@ | ||
@use "unit"; | ||
|
||
/** | ||
* Dynamically adjust font size between a minimum and maximum, starting at a | ||
* minimum breakpoint width and capping at a maximum. | ||
* Mixins and functions for dynamically adjusting a CSS property from a minimum | ||
* value to a maximum, starting at a minimum breakpoint width and capping at a | ||
* maximum breakpoint. | ||
* | ||
* Similar to Bootstrap's RFS, except it's mobile-first and only contains the | ||
* logic we need. | ||
* | ||
* @see https://blog.typekit.com/2016/08/17/flexible-typography-with-css-locks/ | ||
* @see https://betterwebtype.com/articles/2019/05/14/the-state-of-fluid-web-typography/ | ||
* @see https://github.com/twbs/rfs | ||
*/ | ||
|
||
/** | ||
* Although the `calc` statement is only part of the equation, breaking it into | ||
* its own function makes it easier to add more fluid mixins in the future. | ||
* | ||
* 1. `$min-width` and `$max-width` should be in `em` units to avoid cross- | ||
* browser inconsistencies with `rem` units in some browsers. But we need to | ||
* convert the `min-width` value to `rem` so the `calc` won't be influenced | ||
* by the current `font-size`, which would cause the fluid transformation to | ||
* fall out of step with the viewport. | ||
* | ||
* @see https://zellwk.com/blog/media-query-units/ | ||
*/ | ||
|
||
@mixin font-size($min-width, $max-width, $min-font-size, $max-font-size) { | ||
$delta-font-size: unit.strip($max-font-size - $min-font-size); | ||
@function fluid-calc($min-width, $max-width, $min, $max) { | ||
$delta: unit.strip($max - $min); | ||
$delta-width: unit.strip($max-width - $min-width); | ||
$min-width-rem: unit.swap($min-width, rem); /* 1 */ | ||
|
||
& { | ||
font-size: $min-font-size; | ||
@return calc( | ||
#{$min} + #{$delta} * ((100vw - #{$min-width-rem}) / #{$delta-width}) | ||
); | ||
} | ||
|
||
/** | ||
* Fluid font-size | ||
*/ | ||
|
||
@mixin font-size($min-width, $max-width, $min, $max) { | ||
font-size: $min; | ||
|
||
@media (min-width: $min-width) { | ||
font-size: calc( | ||
#{$min-font-size} + #{$delta-font-size} * | ||
((100vw - #{$min-width}) / #{$delta-width}) | ||
); | ||
} | ||
@media (min-width: $min-width) { | ||
font-size: fluid-calc($min-width, $max-width, $min, $max); | ||
} | ||
|
||
@media (min-width: $max-width) { | ||
font-size: $max-font-size; | ||
} | ||
@media (min-width: $max-width) { | ||
font-size: $max; | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These var names don't quite explain to me what they are and how they're used. An additional comment might be nice.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(After reading through the rest of the code, or viewing a demo it's clear, but up top where these are placed it's not)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.