Skip to content

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 12 commits into from
May 5, 2020
16 changes: 16 additions & 0 deletions src/components/heading/demo/levels.twig
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 %}
23 changes: 23 additions & 0 deletions src/components/heading/demo/permalinks.twig
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 %}
133 changes: 133 additions & 0 deletions src/components/heading/heading.scss
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;
Copy link
Contributor

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.

Copy link
Contributor

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)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Added more context to this

$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.
*/

@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 {
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;
}
}
55 changes: 55 additions & 0 deletions src/components/heading/heading.stories.mdx
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>
78 changes: 78 additions & 0 deletions src/components/heading/heading.twig
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>
</a>
<{{tag_name}} class="c-heading__content" id="{{permalink_id}}">
{{_content}}
</{{tag_name}}>
</div>
{% else %}
<{{tag_name}} class="{{_level_class}}">
{{_content}}
</{{tag_name}}>
{% endif %}
10 changes: 7 additions & 3 deletions src/design/typography/demo/headings.twig
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 %}
54 changes: 39 additions & 15 deletions src/mixins/_fluid.scss
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;
}
}
Loading