Skip to content
This repository has been archived by the owner on Feb 22, 2023. It is now read-only.

Commit

Permalink
Add utilities for consistent focus rings (#2067)
Browse files Browse the repository at this point in the history
  • Loading branch information
dhruvkb authored Dec 29, 2022
1 parent 029bd63 commit a228738
Show file tree
Hide file tree
Showing 28 changed files with 289 additions and 37 deletions.
12 changes: 12 additions & 0 deletions .storybook/decorators/with-screenshot-area.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export const WithScreenshotArea = (story) => {
return {
template: `
<div
class="screenshot-area"
:style="{ display: 'inline-block', padding: '2rem' }"
>
<story />
</div>`,
components: { story },
}
}
11 changes: 9 additions & 2 deletions nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -363,16 +363,23 @@ const config: NuxtConfig = {
{
name: "@storybook/addon-essentials",
options: {
backgrounds: false,
backgrounds: true,
viewport: true,
toolbars: true,
},
},
],
parameters: {
backgrounds: {
default: "White",
values: [
{ name: "White", value: "#ffffff" },
{ name: "Dark charcoal", value: "#30272e" },
],
},
options: {
storySort: {
order: ["Introduction", ["Openverse UI"]],
order: ["Introduction", ["Openverse UI"], "Meta"],
},
},
viewport: {
Expand Down
6 changes: 4 additions & 2 deletions src/assets/icons/check.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion src/components/VAllResultsGrid/VImageCellSquare.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
itemprop="contentUrl"
:title="image.title"
:href="'/image/' + image.id"
class="group block rounded-sm focus:outline-none focus:ring-[3px] focus:ring-pink focus:ring-offset-[3px]"
class="group block rounded-sm focus-bold-filled"
>
<figure
itemprop="image"
Expand Down
8 changes: 3 additions & 5 deletions src/components/VAudioTrack/VAudioTrack.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<Component
:is="isComposite ? 'VLink' : 'VWarningSuppressor'"
v-bind="containerAttributes"
class="audio-track group block overflow-hidden rounded-sm ring-pink hover:no-underline focus:border-tx focus:bg-white focus:outline-none"
class="audio-track group block overflow-hidden rounded-sm ring-pink hover:no-underline"
:aria-label="ariaLabel"
:role="isComposite ? 'application' : undefined"
@keydown.native.shift.tab.exact="$emit('shift-tab', $event)"
Expand Down Expand Up @@ -445,10 +445,8 @@ export default defineComponent({
class: [
"cursor-pointer",
{
"focus:ring-offset-[3px] focus:ring-[3px]":
props.layout === "box",
"focus:ring-offset-0 focus:ring-[1.5px]":
props.layout === "row",
"focus-bold-filled": props.layout === "box",
"focus-slim-tx": props.layout === "row",
},
],
}
Expand Down
34 changes: 15 additions & 19 deletions src/components/VCheckbox/VCheckbox.vue
Original file line number Diff line number Diff line change
@@ -1,17 +1,25 @@
<template>
<label :for="id" class="checkbox-label" :class="labelClasses">
<label
:for="id"
class="relative flex text-sm leading-5"
:class="labelClasses"
>
<!--
The checkbox focus style is a slight variation on the `focus-slim-tx` style.
Because it becomes filled when checked, it also needs the
`checked:focus-visible:border-white` class.
-->
<input
:id="id"
type="checkbox"
class="checkbox bg-white"
class="checkbox h-5 w-5 flex-shrink-0 appearance-none rounded-sm border border-dark-charcoal bg-white me-3 focus-slim-tx checked:bg-dark-charcoal checked:focus-visible:border-white disabled:border-dark-charcoal-40 disabled:bg-dark-charcoal-10 checked:disabled:border-dark-charcoal-40 checked:disabled:bg-dark-charcoal-40"
v-bind="inputAttrs"
@change="onChange"
/>
<VIcon
v-show="localCheckedState"
class="absolute text-white start-0"
:icon-path="checkmark"
view-box="0 0 20 20"
class="absolute transform text-white"
:icon-path="checkIcon"
:size="5"
/>
<!-- @slot The checkbox label --><slot />
Expand All @@ -25,7 +33,7 @@ import { defineEvent } from "~/types/emits"
import VIcon from "~/components/VIcon/VIcon.vue"
import checkmark from "~/assets/icons/checkmark.svg"
import checkIcon from "~/assets/icons/check.svg"
type CheckboxAttrs = {
name: string
Expand Down Expand Up @@ -136,7 +144,7 @@ export default defineComponent({
})
}
return {
checkmark,
checkIcon,
localCheckedState,
labelClasses,
inputAttrs,
Expand All @@ -145,15 +153,3 @@ export default defineComponent({
},
})
</script>
<style scoped>
.checkbox-label {
@apply relative flex text-sm leading-5;
}
.checkbox {
@apply relative h-5 w-5 flex-shrink-0 appearance-none rounded-sm border border-dark-charcoal me-3;
@apply focus:outline-none focus:ring focus:ring-pink focus:ring-offset-2;
@apply disabled:border-dark-charcoal-40 disabled:bg-dark-charcoal-10;
@apply checked:bg-dark-charcoal;
@apply checked:disabled:bg-dark-charcoal-40;
}
</style>
3 changes: 3 additions & 0 deletions src/components/VCheckbox/meta/VCheckbox.stories.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@ import {
Story,
} from "@storybook/addon-docs"

import { WithScreenshotArea } from "~~/.storybook/decorators/with-screenshot-area"

import VCheckbox from "~/components/VCheckbox/VCheckbox.vue"
import VLicense from "~/components/VLicense/VLicense.vue"

<Meta
title="Components/VCheckbox"
components={VCheckbox}
decorators={[WithScreenshotArea]}
argTypes={{
change: {
action: "change",
Expand Down
81 changes: 81 additions & 0 deletions src/components/meta/Focus.stories.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { Meta, Canvas, Story } from "@storybook/addon-docs"

import { WithScreenshotArea } from "~~/.storybook/decorators/with-screenshot-area"

<Meta title="Meta/Focus" decorators={[WithScreenshotArea]} />

export const GetTemplate = (irrelevantClassNames) => (args) => ({
template: `
<div
class="h-30 w-30 flex items-center justify-center ${irrelevantClassNames}"
:class="args.classNames"
data-testid="focus-target"
tabindex="0"
>
Focus on me
</div>`,
setup() {
return { args }
},
})

# Focus

Focus styles are defined across two axes: thickness (slim or bold) and style
(transparent or filled). However, there is no bold-transparent variant.

The transparent style works best if the element has a border and no background
color. On the other hand the filled style works with elements that have a
background color and no border.

Both the outer and inner strokes in filled border variants are box-shadows.

## Slim transparent

An outer stroke (colored) of 1.5px is applied to the component and the existing
border is turned transparent.

<Canvas>
<Story name="Slim transparent" args={{ classNames: ["focus-slim-tx"] }}>
{GetTemplate(
"border border-dark-charcoal-40 hover:border-dark-charcoal"
).bind({})}
</Story>
</Canvas>

## Slim filled

A colored outer stroke of 1.5px and a white inner stroke of 1.5px are both
applied to the component.

<Canvas>
<Story name="Slim filled" args={{ classNames: ["focus-slim-filled"] }}>
{GetTemplate("bg-pink text-white").bind({})}
</Story>
</Canvas>

## Bold filled

A colored outer stroke of 3.0px and a white inner stroke of 3.0px are both
applied to the component.

<Canvas>
<Story name="Bold filled" args={{ classNames: ["focus-bold-filled"] }}>
{GetTemplate("bg-yellow text-dark-charcoal").bind({})}
</Story>
</Canvas>

## Colored

The ring may be colored with a `-<color>` suffix after the appropriate class. If
no color is set, the default is assumed to be pink.

<Canvas>
<Story
name="Colored"
args={{ classNames: ["focus-slim-tx-yellow"] }}
parameters={{ backgrounds: { default: "Dark charcoal" } }}
>
{GetTemplate("bg-dark-charcoal text-white").bind({})}
</Story>
</Canvas>
44 changes: 44 additions & 0 deletions src/styles/tailwind.css
Original file line number Diff line number Diff line change
Expand Up @@ -127,4 +127,48 @@ Time - 11px - xs semibold (default leading-[120%])
.time {
@apply text-xs font-semibold;
}

/**
* Focus styles are defined across across two axes.
*
* Thickness:
* - Slim: 1.5px outline
* - Bold: 3.0px outline
*
* Style:
* - Transparent: Replaces existing border on the element
* - Filled: Adds a ring around element with some offset
*
* There is no bold-transparent variant.
*
* The color of the ring is set by a plugin defined in `tailwind.config.js`.
*/

[class*="focus-slim-tx"] {
@apply focus-visible:outline-none; /* UA styles: none */
@apply focus-visible:ring; /* outer-stroke: box-shadow */
@apply focus-visible:border-tx; /* inner-stroke: transparent border */

/* It is up to the component to apply border utilities. */
@apply focus-visible:hover:border-tx; /* focus prevails over hover */
}

[class*="focus-slim-filled"],
[class*="focus-bold-filled"] {
@apply relative;
@apply after:pointer-events-none after:absolute after:inset-0 after:-z-10 after:rounded-inherit;

@apply focus-visible:outline-none; /* UA styles: none */
@apply focus-visible:after:z-10; /* inner-stroke: pseudo-element */
}

[class*="focus-slim-filled"] {
@apply after:shadow-slim-filled;
@apply focus-visible:ring; /* outer-stroke: box-shadow */
}

[class*="focus-bold-filled"] {
@apply after:shadow-bold-filled;
@apply focus-visible:ring-bold; /* outer-stroke: box-shadow */
}
}
21 changes: 20 additions & 1 deletion tailwind.config.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const defaultTheme = require("tailwindcss/defaultTheme")
const plugin = require("tailwindcss/plugin")

const { SCREENS } = require("./src/constants/screens")
const { Z_INDICES } = require("./src/constants/z-indices")
Expand Down Expand Up @@ -96,7 +97,8 @@ module.exports = {
120: "30.00rem",
},
ringWidth: {
DEFAULT: "1.5px",
DEFAULT: "1.5px", // aka slim
bold: "3.0px",
0: 0,
},
borderWidth: {
Expand Down Expand Up @@ -197,6 +199,11 @@ module.exports = {
ring: "inset 0 0 0 1px white",
"ring-1.5": "inset 0 0 0 1.5px white",
"el-2": "0 0.125rem 0.25rem rgba(0, 0, 0, 0.1)",
"slim-filled": "inset 0 0 0 1.5px white",
"bold-filled": "inset 0 0 0 3px white",
},
borderRadius: {
inherit: "inherit",
},
typography: (theme) => ({
DEFAULT: {
Expand All @@ -220,5 +227,17 @@ module.exports = {
require("@tailwindcss/line-clamp"),
require("@tailwindcss/typography"),
require("tailwindcss-labeled-groups")(["waveform"]),
// Focus styles
// This plugin has related stylesheets in `src/styles/tailwind.css`.
plugin(({ matchUtilities, theme }) => {
matchUtilities(
Object.fromEntries(
["focus-slim-tx", "focus-slim-filled", "focus-bold-filled"].map(
(item) => [item, (value) => ({ "--tw-ring-color": value })]
)
),
{ values: { ...theme("colors"), DEFAULT: theme("colors.pink") } }
)
}),
],
}
1 change: 1 addition & 0 deletions test/playwright/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ const config: PlaywrightTestConfig = {
maxDiffPixelRatio: 0,
},
},
retries: 2,
}

export default config
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
23 changes: 23 additions & 0 deletions test/storybook/visual-regression/focus.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { test, expect, Page, Locator } from "@playwright/test"

const goTo = async (page: Page, slug: string) => {
await page.goto(`/iframe.html?id=meta-focus--${slug}`)
}

const expectSnapshot = async (name: string, elem: Locator) => {
expect(await elem.screenshot()).toMatchSnapshot({ name: `${name}.png` })
}

const allSlugs = ["slim-transparent", "slim-filled", "bold-filled", "colored"]

test.describe.configure({ mode: "parallel" })

test.describe("Focus", () => {
for (const slug of allSlugs) {
test(`focus-${slug}`, async ({ page }) => {
await goTo(page, slug)
await page.focus('[data-testid="focus-target"]')
await expectSnapshot(`focus-${slug}`, page.locator(".screenshot-area"))
})
}
})
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit a228738

Please sign in to comment.