Skip to content

Define minimum tap target size for interactive elements (#234) #242

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 1 commit into from
Mar 4, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/docs/customize/theming/forms.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,7 @@ and [Toggle](/components/ui/toggle).
| `--rui-form-field-check-input-size` | Size of check inputs |
| `--rui-form-field-check-input-border-width` | Border width of check inputs |
| `--rui-form-field-check-input-focus-box-shadow` | Box shadow to highlight focused inputs |
| `--rui-form-field-check-tap-target-size` | Minimum tap target size |

Interaction states:

Expand Down
59 changes: 59 additions & 0 deletions src/docs/foundation/accessibility.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
---
name: Accessibility
menu: Foundation
route: /foundation/accessibility
---

# Accessibility

React UI bakes accessibility principles right into its core.

## Touch Friendliness

The active area of interactive elements should be properly sized so that the
elements can be easily targeted on touch screens. Recommended dimensions may
vary from platform to platform, however a commonly used size is 7–10 mm.

Default tap target size in React UI is set to **10 mm** and is used by all
potentially small interactive components like [Alert](/components/ui/alert)
close button, [CheckboxField](/components/ui/checkbox-field), or
[Toggle](/components/ui/toggle). Tap target size can be adjusted via the
`--rui-tap-target-size` custom property (see
[Theming](/customize/theming/overview) to learn how).

📖 [Read more about touch targets at Norman Nielsen Group.](https://www.nngroup.com/articles/touch-target-size/)

### Form Fields and Reserved Space

Note that form fields with potentially small inputs (like
[CheckboxField](/components/ui/checkbox-field) or
[Toggle](/components/ui/toggle)) reserve vertical space corresponding to the
minimum tap target size. In other words, form fields **box model is taller.**
The reason behind this behaviour is that in many cases the minimum tap target
size could overflow its component's box model and tap targets of neighboring
components could collide. The extra added space prevents this.

However, if placed inside [FormLayout](/components/layout/form-layout), form
fields do not add any extra vertical space because it is already provided by
`FormLayout` row gap. Remember to check that form fields in your `FormLayout`
are properly spaced and interactive elements do not collide should you decide to
make any changes to `--rui-tap-target-size`,
`--rui-form-field-check-tap-target-size` or `--rui-form-layout-row-gap` options.

Horizontal padding is never added to form fields box model so it does not make
their horizontal alignment complicated.

## Keyboard Friendliness

Many people use keyboard to control their computer. Interactive elements in
React UI are **highlighted on focus** so keyboard users can easily tab over
them and see what control currently has focus.

Check form fields like [CheckboxField](/components/ui/checkbox-field) or
[Toggle](/components/ui/toggle) obtain a blue outline on focus (which is to be
[spread over all interactive elements](https://github.com/react-ui-org/react-ui/issues/240)
eventually). Appearance of focus highlight can be adjusted via the
`--rui-focus-box-shadow` custom property (see [Theming](/customize/theming)
to learn how).

📖 [Read more about keyboard accessibility at MDN.](https://developer.mozilla.org/en-US/docs/Web/Accessibility/Understanding_WCAG/Keyboard)
2 changes: 2 additions & 0 deletions src/lib/components/ui/Alert/Alert.scss
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
@use '../../../styles/theme/typography';
@use '../../../styles/tools/accessibility';
@use '../../../styles/tools/reset';
@use 'settings';
@use 'theme';
Expand Down Expand Up @@ -46,6 +47,7 @@

.close {
@include reset.button();
@include accessibility.min-tap-target();

padding: theme.$padding;
font-size: map-get(typography.$size-values, 3);
Expand Down
2 changes: 1 addition & 1 deletion src/lib/components/ui/Alert/README.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ click on the close button:
) : (
<Button
clickHandler={() => setIsAlertVisible(true)}
label="Show alert"
label="Bring the alert back!"
/>
)}
</>
Expand Down
1 change: 1 addition & 0 deletions src/lib/components/ui/CheckboxField/CheckboxField.scss
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
.root {
@include foundation.root();
@include inline-field-layout.root();
@include inline-field-elements.min-tap-target($type: checkbox);
@include variants.visual(check);
@include relationships.horizontal-neighbor();
}
Expand Down
5 changes: 3 additions & 2 deletions src/lib/components/ui/Modal/Modal.scss
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
@use '../../../styles/theme/typography';
@use '../../../styles/tools/accessibility';
@use '../../../styles/tools/breakpoint';
@use '../../../styles/tools/reset';
@use '../../../styles/tools/spacing';
Expand Down Expand Up @@ -31,7 +32,7 @@
flex: none;
align-items: baseline;
justify-content: space-between;
padding: settings.$padding-y spacing.of(3) settings.$padding-y settings.$padding-x;
padding: settings.$padding-y spacing.of(4) settings.$padding-y settings.$padding-x;
}

.headTitle {
Expand All @@ -41,8 +42,8 @@

.close {
@include reset.button();
@include accessibility.min-tap-target();

padding: spacing.of(1) spacing.of(2);
font-size: map-get(typography.$size-values, 3);
line-height: 1;
color: inherit;
Expand Down
3 changes: 2 additions & 1 deletion src/lib/components/ui/Radio/Radio.scss
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@

.option {
@include inline-field-layout.field();
@include inline-field-elements.min-tap-target($type: radio);
}

.input {
Expand Down Expand Up @@ -67,7 +68,7 @@
}

.rootLayoutHorizontal {
@include box-field-layout.horizontal();
@include box-field-layout.horizontal($has-min-tap-target: true);
}

.isRootFullWidth {
Expand Down
2 changes: 2 additions & 0 deletions src/lib/components/ui/ScrollView/ScrollView.scss
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
// 5. Prevent undesired vertical scrolling that may occur with tables inside.
// 6. Make `ScrollView` adjust to flexible layouts.

@use '../../../styles/tools/accessibility';
@use '../../../styles/tools/caret';
@use '../../../styles/tools/reset';
@use '../../../styles/tools/scrollbar';
Expand Down Expand Up @@ -75,6 +76,7 @@ $_arrow-outer-spacing: spacing.of(4);
.arrowPrev,
.arrowNext {
@include reset.button();
@include accessibility.min-tap-target();
@include transition.add((visibility, opacity, transform));

position: absolute; // 3.
Expand Down
1 change: 1 addition & 0 deletions src/lib/components/ui/Toggle/Toggle.scss
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
.root {
@include foundation.root();
@include inline-field-layout.root();
@include inline-field-elements.min-tap-target($type: toggle);
@include variants.visual(check);
@include relationships.horizontal-neighbor();
}
Expand Down
1 change: 1 addition & 0 deletions src/lib/styles/theme/_accessibility.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
$tap-target-size: var(--rui-tap-target-size);
1 change: 1 addition & 0 deletions src/lib/styles/theme/_form-fields.scss
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ $box-sizes: (
$check-input-size: var(--rui-form-field-check-input-size);
$check-input-border-width: var(--rui-form-field-check-input-border-width);
$check-input-focus-box-shadow: var(--rui-form-field-check-input-focus-box-shadow);
$check-tap-target-size: var(--rui-form-field-check-tap-target-size);

// Form fields: check fields, component specific
$check-input-checkbox-border-radius: var(--rui-form-field-check-input-checkbox-border-radius);
Expand Down
29 changes: 26 additions & 3 deletions src/lib/styles/tools/_accessibility.scss
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
// Screen readers only
// Inspired by Bootstrap
// https://github.com/twbs/bootstrap/blob/master/scss/mixins/_screen-reader.scss
// 1. Screen readers only, inspired by Bootstrap.
// https://github.com/twbs/bootstrap/blob/master/scss/mixins/_screen-reader.scss
//
// 2. Make tap target big enough to improve accessibility on touch screens.

@use '../theme/accessibility' as theme;

// 1.
@mixin hide-text() {
position: absolute;
width: 1px;
Expand All @@ -11,3 +16,21 @@
white-space: nowrap;
border: 0;
}

// 2.
@mixin min-tap-target($size: theme.$tap-target-size, $center: true) {
position: relative;

&::before {
content: '';
position: absolute;
width: $size;
height: $size;

@if ($center) {
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
}
}
26 changes: 19 additions & 7 deletions src/lib/styles/tools/form-fields/_box-field-layout.scss
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,15 @@
// unfortunately doesn't work for blank text inputs in Safari. Default to zero when
// `--rui-local-padding-y` is not defined.
//
// 12. Reset width previously set by inline field layout (see `_inline-field-layout.scss`).
// 12. Reset `width` previously set by inline field layout (see `_inline-field-layout.scss`).
//
// 13. Make fields just as wide as necessary. Fields should be interactive only where their visible
// content is.

@use '../../settings/forms';
@use '../../settings/form-fields' as settings;
@use '../../theme/form-fields' as theme;
@use '../../theme/typography';
@use '../breakpoint';

@mixin vertical($has-list: false) {
Expand Down Expand Up @@ -75,7 +76,7 @@
}
}

@mixin horizontal() {
@mixin horizontal($has-min-tap-target: false) {
@include breakpoint.up(forms.$horizontal-breakpoint) {
display: inline-grid; // 2.
grid-template-columns: theme.$horizontal-label-width min-content; // 2.
Expand All @@ -85,11 +86,22 @@
.label {
grid-area: label;
min-width: theme.$horizontal-label-min-width;
padding-top:
calc(
#{theme.$box-border-width}
+ var(--rui-local-padding-y, -1 * #{theme.$box-border-width})
); // 11.

@if ($has-min-tap-target) {
padding-top:
calc(
(#{theme.$check-tap-target-size} - #{typography.$line-height-base})
/ 2
); // 11.
}

@else {
padding-top:
calc(
#{theme.$box-border-width}
+ var(--rui-local-padding-y, -1 * #{theme.$box-border-width})
); // 11.
}

padding-right: settings.$horizontal-inner-gap; // 4.
padding-bottom: 0; // 4.
Expand Down
44 changes: 44 additions & 0 deletions src/lib/styles/tools/form-fields/_inline-field-elements.scss
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
// Custom input styling inspired by Bootstrap 5.
//
// 1. Keep themed appearance for print.
//
// 2. Make tap target of inline fields big enough to improve their accessibility on touch screens.
//
// 3. Reset extra space previously added to symmetrically pad the field in FormLayout context as
// there already is a sufficient row gap provided by FormLayout.

@use '../../theme/typography';
@use '../../theme/form-fields' as theme;
@use '../../tools/accessibility';
@use '../transition';

@mixin check-input($type) {
Expand Down Expand Up @@ -54,3 +60,41 @@
}
}
}

// 2.
@mixin min-tap-target($type) {
$input-width: theme.$check-input-size;

@if ($type == 'toggle') {
$input-width: theme.$check-input-toggle-width;
}

$tap-target-offset: calc((#{$input-width} - #{theme.$check-tap-target-size}) / 2);

@include accessibility.min-tap-target($size: theme.$check-tap-target-size, $center: false);

min-height: theme.$check-tap-target-size;
padding-top: calc((#{theme.$check-tap-target-size} - #{typography.$line-height-base}) / 2);

&::before {
top: 0;
left: $tap-target-offset;
}

@if ($type == 'checkbox' or $type == 'toggle') {
&.hasRootLabelBefore::before {
right: $tap-target-offset;
left: auto;
}

// 3.
&.isRootInFormLayout {
min-height: 0;
padding-top: 0;

&::before {
top: calc((#{typography.$line-height-base} - #{theme.$check-tap-target-size}) / 2);
}
}
}
}
2 changes: 2 additions & 0 deletions src/lib/theme.scss
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@
--rui-border-radius: 0.25rem;

// Accessibility
--rui-tap-target-size: 10mm;
--rui-focus-box-shadow: 0 0 0 0.2em var(--rui-color-active-focus);

//
Expand Down Expand Up @@ -569,6 +570,7 @@
--rui-form-field-check-input-size: 1.125rem;
--rui-form-field-check-input-border-width: var(--rui-form-field-box-border-width);
--rui-form-field-check-input-focus-box-shadow: var(--rui-focus-box-shadow);
--rui-form-field-check-tap-target-size: var(--rui-tap-target-size);

// Form fields: check fields, component specific
// stylelint-disable function-url-quotes
Expand Down
2 changes: 1 addition & 1 deletion webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ const StyleLintPlugin = require('stylelint-webpack-plugin');
const VisualizerPlugin = require('webpack-visualizer-plugin');

const MAX_DEVELOPMENT_OUTPUT_SIZE = 2000000;
const MAX_PRODUCTION_OUTPUT_SIZE = 238000;
const MAX_PRODUCTION_OUTPUT_SIZE = 240000;

module.exports = (env, argv) => ({
devtool: argv.mode === 'production'
Expand Down