Skip to content

Commit

Permalink
Add consistent keyboard focus ring across interactive components (#2050)
Browse files Browse the repository at this point in the history
* πŸ’„ Apply focus-within to button

* πŸ’„ Fix checkbox border radius assignment

* πŸ’„ Apply focus-within to checkbox

* πŸ’„ Apply focus-within to radio

* πŸ’„ Apply focus-within to toggle

* πŸ’„ Change global focus ring color

* πŸ’„ Apply focus-within to card

* πŸ’„ Apply focus-within to dropdown

* πŸ’„ Apply focus-within to input (formfield)

* πŸ’„ Apply focus-within to textarea (formfield)

* πŸ’„ Apply focus-within to link

* πŸ’„ Apply focus-within to knob in range

* πŸ’„ Specify padding on segment btn (closes #1744)

This is a prerequisite for properly styling the segment button with a
focus ring

* πŸ’„ Apply focus-within to segmented-control

* πŸ’„ Use element-as-button directive with chip

* πŸ’„ Apply focus-within to chip

* 🚧  Remove outline test from elm as btn directive

This is part of restructuring focus ring tests. It is the responsibility
of components themselves not this directive to apply focus styling.

* πŸ’„ Introduce apply-focus mixin

* πŸ’„ Use interaction state in button

* πŸ’„ Extend apply-focus mixin with gap and shadow

* πŸ’„ Use apply-focus in card

* πŸ’„ Use apply-focus in link

* βͺ  Revert accidental color change

* ♻️  Remove accidental double pseudo class for link

* πŸ’„ Adjust link focus ring border radius

* πŸ’„ Use background color getter from utils

* πŸ’„ Use apply-focus in dropdown

* πŸ’„ Split mixin in two to support css shadow parts

* πŸ’„ QoL improvements to focus mixin

* πŸ’„ Use apply-focus-part in toggle

* ♻️  Clean up link, card and dropdown

* πŸ’„ Fix missing string interpolation

* ♻️  Move apply-focus to shared form field styles

* πŸ’„ Use apply-focus in checkbox

* πŸ’„ Use apply-focus in chip

* πŸ’„ Use apply-focus in radio

* πŸ’„ Use apply-focus in range

* πŸ’„ Use apply-focus in segmented-control

* πŸ’„ Use apply-focus-part in Segmented Control

* πŸ’„ Support elevation of four in card

* πŸ’„ Add focus-visible mixin with fallbacks

* πŸ’„ Replace apply-focus with focus-visible solution

* πŸ’„ Apply mixins only on non-touch devices

* ♻️  Refactor navbar to leave room for focus ring

* πŸ’„ Document and cleanup focus mixins

* 🎨  Formatting to improve readability

* Remove typo in focus mixin comment

* ♻️ Add content-block to mixin

* ♻️ Reorder imports alphabetically

* ♻️  Use focus-ring-color variable via utils

* πŸ“  Update comments for focus to be more specific

* ♻️ Document toggle style changes

* ♻️ Reorder for clarity

* πŸ”₯ Remove todo
  • Loading branch information
RasmusKjeldgaard authored and jkaltoft committed Mar 17, 2022
1 parent 5b7b7d6 commit 11486d9
Show file tree
Hide file tree
Showing 20 changed files with 156 additions and 72 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
<kirby-card
(keydown.ArrowUp)="onLinksArrowUpDown($event)"
(keydown.ArrowDown)="onLinksArrowUpDown($event)"
[hasPadding]="true"
>
<ng-container *ngFor="let item of filteredShowcaseRoutes">
<a
Expand Down
7 changes: 1 addition & 6 deletions apps/cookbook/src/app/page/side-nav/side-nav.component.scss
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
@use '~@kirbydesign/core/src/scss/utils';

$list-margin: utils.size('s');
$link-padding: 5px utils.size('s');
$link-padding: 5px;
$link-border-radius: 3px;
$menu-padding-mobile: utils.size('m');
$divider-inset: utils.size('xxxxs');
Expand Down Expand Up @@ -68,10 +67,6 @@ section {
padding: 0 0;
pointer-events: none;
}

&:first-child {
margin-top: $list-margin;
}
}
}

Expand Down
6 changes: 6 additions & 0 deletions libs/core/src/scss/_global-styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
@use 'base/typography';
@use 'base/line-clamp';
@use 'base/html-list';
@use 'interaction-state';

:root,
:host {
Expand Down Expand Up @@ -245,6 +246,11 @@ ion-loading.kirby-loading-overlay {
*/
:link,
:visited {
@include interaction-state.apply-focus-visible($gap: 3px) {
outline: none;
border-radius: #{utils.size('xxxxs')};
}

color: inherit;
cursor: pointer;
text-decoration: underline;
Expand Down
66 changes: 66 additions & 0 deletions libs/core/src/scss/interaction-state/_focus.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
@use '../utils';

// ---------------------------------------------------------------------------
// The apply-focus-visible mixin is used by most interactive elements to apply
// a focus ring around the element when navigated to via a keyboard on a
// non-touch device.

/// @param {box-shadow} $shadow
/// Add/preserve additional box-shadow (e.g. elevation) alongside the focus ring.
/// @param {number} $gap
/// The distance (in pixels) from the element boundary to the focus ring
// ---------------------------------------------------------------------------
@mixin apply-focus-visible($shadow: null, $gap: utils.size('xxxxs')) {
@include utils.not-touch {
$reset-focus-ring: 0 0 0 0 transparent;

&:focus {
// This is a fallback for safari and other browsers, so we still show the focus
// ring on older versions that do not support :focus-visible.
@include _focus-ring($shadow, $gap);
}

&:focus:not(:focus-visible) {
// On browsers that do support focus-visible, we dont want anything to show on the
// standard focus pseudo-class, so we reset it here, while still allowing to preserve
// any shadow set on the element by the user.
box-shadow: $shadow, $reset-focus-ring;
}

&:focus-visible {
@include _focus-ring($shadow, $gap);
}

@content;
}
}

// ---------------------------------------------------------------------------
// The apply-focus-part mixin is used where ionics components do not allow
// us to apply styling directly to the tabbed element, as it is hidden inside
// a shadow root and not exposed as a part. In some of these cases, we are
// unable to provide focus-visible like functionality, and thus the focus ring
// will always show.

/// @param {string} $part
/// Add focus styles to a specific CSS Shadow Part.
/// @param {box-shadow} $shadow
/// Add/preserve additional box-shadow (e.g. elevation) alongside the focus ring.
/// @param {number} $gap
/// The distance (in pixels) from the element boundary to the focus ring
// ---------------------------------------------------------------------------
@mixin apply-focus-part($part, $shadow: null, $gap: utils.size('xxxxs')) {
@include utils.not-touch {
&:focus-within::part(#{$part}) {
@include _focus-ring($shadow, $gap);
}
@content;
}
}

@mixin _focus-ring($shadow, $gap) {
$stroke-width: utils.size('xxxxs');
box-shadow: #{$shadow}, 0 0 0 $gap #{utils.get-color('background-color')},
0 0 0 $gap + $stroke-width utils.$focus-ring-color;
transition: box-shadow utils.get-transition-duration('quick');
}
1 change: 1 addition & 0 deletions libs/core/src/scss/interaction-state/_index.scss
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
@forward 'focus';
@forward 'hover';
2 changes: 1 addition & 1 deletion libs/core/src/scss/themes/_colors.scss
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ $text-colors: (
'danger': #ee0d0d,
);

$focus-ring-color: rgb(77, 144, 254);
$focus-ring-color: #228bec;

@function getAllColors() {
@return map.merge(map.merge($brand-colors, $notification-colors), $system-colors);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ $button-width: (

:host {
@include utils.accessible-target-size();
@include interaction-state.apply-focus-visible();

font-family: var(--kirby-font-family);
background-color: var(--kirby-button-background-color, initial);
Expand Down Expand Up @@ -207,12 +208,6 @@ $button-width: (
width: 100%;
}

// Only apply focus ring if pointer device can hover
// (effectively desktop/mouse devices):
@include utils.focus() {
--kirby-button-border-color: #{utils.$focus-ring-color};
}

&:active {
opacity: 0.8;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
@use '~@kirbydesign/core/src/scss/interaction-state';
@use '~@kirbydesign/core/src/scss/utils';

:host {
Expand Down Expand Up @@ -37,8 +38,12 @@
}

&[role='button'] {
@include interaction-state.apply-focus-visible($shadow: utils.get-elevation(2));
&.highlighted {
@include interaction-state.apply-focus-visible($shadow: utils.get-elevation(4));
}
cursor: pointer;
outline-offset: utils.size('xxxxs');
outline: none;
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
@use "sass:map";
@use '~@kirbydesign/core/src/scss/interaction-state';
@use '~@kirbydesign/core/src/scss/utils';

utils.$border-radius: 6px;
$border-radius: 6px;
$checkbox-icon-size: utils.size('m');
$spacing-to-edge: map.get(utils.$checkbox-radio-spacing, 'to-edge');
$spacing-to-label: map.get(utils.$checkbox-radio-spacing, 'to-label');
Expand Down Expand Up @@ -55,12 +56,14 @@ $default-checkbox-radio-size: map.get(utils.$checkbox-radio-sizes, 'md');
}

ion-checkbox {
@include interaction-state.apply-focus-part($part: 'container');

--size: #{$checkbox-icon-size};
--checkmark-width: #{utils.size('xxxs')};
--background: #{utils.get-color('white')};
--border-width: 1px;
--border-color: #{utils.get-color('semi-dark')};
--border-radius: #{utils.$border-radius};
--border-radius: #{$border-radius};

margin-left: $spacing-to-edge;
margin-right: $spacing-to-label;
Expand All @@ -69,12 +72,6 @@ $default-checkbox-radio-size: map.get(utils.$checkbox-radio-sizes, 'md');
padding: utils.size('xxxs'); // Spacing between checkmark and container box
}

&:focus-within::part(container) {
@media (hover: hover) {
box-shadow: 0 0 0 1px var(--kirby-background-color), 0 0 0 2px #{utils.$focus-ring-color};
border-radius: utils.$border-radius;
}
}
&:active {
--background: #{utils.get-color('white-shade')};
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
@use '~@kirbydesign/core/src/scss/interaction-state';
@use '~@kirbydesign/core/src/scss/utils';

@mixin selected-and-hover($background-color) {
Expand All @@ -15,6 +16,13 @@
@include selected-and-hover('black');
@include utils.accessible-target-size();

&[role='button'] {
@include interaction-state.apply-focus-visible() {
cursor: pointer;
outline: none;
}
}

white-space: nowrap;
padding: 0 var(--inline-padding, utils.size('s'));
height: utils.size('l');
Expand All @@ -24,7 +32,6 @@
align-items: center;
border-radius: utils.$border-radius-round;
margin: utils.size('xxxs') var(--inline-margin, utils.size('xxxs'));
cursor: pointer;
user-select: none;

:host-context(.kirby-color-brightness-dark) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
@use '~@kirbydesign/core/src/scss/interaction-state';
@use '~@kirbydesign/core/src/scss/utils';

$dropdown-max-height: 8 * utils.$dropdown-item-height;
Expand All @@ -6,6 +7,11 @@ $min-screen-width-small: 320px;
$min-screen-width: 375px;

:host {
@include interaction-state.apply-focus-visible() {
outline: none;
border-radius: utils.$border-radius-round;
}

display: inline-block;
position: relative;
max-width: calc(100vw - #{$margin-horizontal-total});
Expand All @@ -20,19 +26,6 @@ $min-screen-width: 375px;
}
}

// Outline is applied on button border instead,
// to keep the rounded shape:
outline: none;

// Only apply focus ring if pointer device can hover
// (effectively desktop/mouse devices):
@include utils.focus() {
> button,
> button.attention-level3 {
border-color: utils.$focus-ring-color;
}
}

&.error,
&.ng-touched.ng-invalid {
> button {
Expand All @@ -46,7 +39,6 @@ $min-screen-width: 375px;
margin: 0;
outline: none;
width: 100%;
transition: box-shadow 0.2s, border-color 0.2s;

// Temporary fix for button-width as attention level 3 has border,
// and attention level 2 does not:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
@use '~@kirbydesign/core/src/scss/interaction-state';
@use '~@kirbydesign/core/src/scss/utils';

$fab-sheet-margin: utils.size('s');
Expand All @@ -10,22 +11,16 @@ $fab-sheet-margin: utils.size('s');
// https://ionicframework.com/docs/api/fab-button
ion-fab-button {
--box-shadow: #{utils.get-elevation(8)};

@include interaction-state.apply-focus-visible() {
outline: none;
border-radius: utils.$border-radius-round;
}

width: 64px;
height: 64px;
user-select: none;

// Outline is applied on button border instead,
// to keep the rounded shape:
outline: none;

// Only apply focus ring if pointer device can hover
// (effectively desktop/mouse devices):
@include utils.focus() {
--border-width: 1px;
--border-style: solid;
--border-color: #{utils.$focus-ring-color};
}

&[disabled] {
--background: #{utils.get-color('semi-light')};
--box-shadow: none;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
@use '~@kirbydesign/core/src/scss/utils';
@use '~@kirbydesign/core/src/scss/interaction-state';

$form-field-label-height: 24px;
$form-field-input-font-family: var(--kirby-font-family);
Expand Down Expand Up @@ -26,6 +27,8 @@ $form-field-input-padding: utils.size('s');
}

:host {
@include interaction-state.apply-focus-visible($shadow: utils.get-elevation(2));

background-color: utils.get-color('white');
color: utils.get-color('white-contrast');
border: none;
Expand Down
13 changes: 3 additions & 10 deletions libs/designsystem/src/lib/components/radio/radio.component.scss
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
@use "sass:map";
@use '~@kirbydesign/core/src/scss/interaction-state';
@use '~@kirbydesign/core/src/scss/utils';

$radio-icon-padding: utils.size('xxxxs');
Expand Down Expand Up @@ -61,6 +62,8 @@ $default-checkbox-radio-size: map.get(utils.$checkbox-radio-sizes, 'md');
}

ion-radio {
@include interaction-state.apply-focus-part($part: 'container');

min-height: $radio-icon-size;
min-width: $radio-icon-size;
padding: $radio-icon-padding;
Expand Down Expand Up @@ -94,16 +97,6 @@ ion-radio {
}
}

// Only apply focus ring if pointer device can hover
// (effectively desktop/mouse devices):
@include utils.focus() {
&::part(container) {
// Apply focus ring through box-shadow to keep circle shape and default border:
box-shadow: 0 0 0 1px #{utils.get-color('background-color')},
0 0 0 3px #{utils.$focus-ring-color};
}
}

&.radio-checked {
--border-width: 0px;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
@use '~@kirbydesign/core/src/scss/interaction-state';
@use '~@kirbydesign/core/src/scss/utils';

$tickHeight: 6px;
$tickWidth: 6px;
$knob-shadow: 0 5px 10px 0px rgba(28, 28, 28, 0.3), 0 0 5px 0 rgba(28, 28, 28, 0.08);

:host {
display: flex;
Expand All @@ -10,8 +12,10 @@ $tickWidth: 6px;
}

ion-range {
@include interaction-state.apply-focus-part($part: 'knob', $shadow: $knob-shadow);

--knob-background: #{utils.get-color('white')};
--knob-box-shadow: 0 5px 10px 0px rgba(28, 28, 28, 0.3), 0 0 5px 0 rgba(28, 28, 28, 0.08);
--knob-box-shadow: #{$knob-shadow};
--knob-size: 24px;

--pin-color: #{utils.get-text-color('black')};
Expand Down
Loading

0 comments on commit 11486d9

Please sign in to comment.