mirror of
https://github.com/balena-io/etcher.git
synced 2025-07-19 17:26:34 +00:00
feat(GUI): use tabindex and focus to navigate (#1745)
* feat(GUI): use tabindex and focus to navigate We make navigating with the tab key easier by highlighting focused elements more visibly, adding `tabindex` attributes to elements, and making `open-external` links respond to keyboard events. Change-Type: minor Changelog-Entry: Improve tab-key navigation through tabindex and visual improvements. Connects-To: https://github.com/resin-io/etcher/issues/1734 * outline with 10s timeout * use orange "warning colour" as outline * smaller outline on settings buttons, fix order on settings page * allow selection in drive-selector * fix typo, better tabindexes
This commit is contained in:
parent
e5d69465ab
commit
f2f5955264
@ -251,4 +251,29 @@ module.exports = function (
|
|||||||
this.getDriveStatuses = this.memoizeImmutableListReference((drive) => {
|
this.getDriveStatuses = this.memoizeImmutableListReference((drive) => {
|
||||||
return this.constraints.getDriveImageCompatibilityStatuses(drive, this.state.getImage())
|
return this.constraints.getDriveImageCompatibilityStatuses(drive, this.state.getImage())
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @summary Keyboard event drive toggling
|
||||||
|
* @function
|
||||||
|
* @public
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Keyboard-event specific entry to the toggleDrive function.
|
||||||
|
*
|
||||||
|
* @param {Object} drive - drive
|
||||||
|
* @param {Object} $event - event
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* <div tabindex="1" ng-keypress="this.keyboardToggleDrive(drive, $event)">
|
||||||
|
* Tab-select me and press enter or space!
|
||||||
|
* </div>
|
||||||
|
*/
|
||||||
|
this.keyboardToggleDrive = (drive, $event) => {
|
||||||
|
console.log($event.keyCode)
|
||||||
|
const ENTER = 13
|
||||||
|
const SPACE = 32
|
||||||
|
if (_.includes([ ENTER, SPACE ], $event.keyCode)) {
|
||||||
|
this.toggleDrive(drive)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h4 class="modal-title">Select a Drive</h4>
|
<h4 class="modal-title">Select a Drive</h4>
|
||||||
<button class="close" ng-click="modal.closeModal()">×</button>
|
<button tabindex="14" class="close" ng-click="modal.closeModal()">×</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
@ -14,10 +14,14 @@
|
|||||||
ng-src="./assets/{{drive.icon}}.svg"
|
ng-src="./assets/{{drive.icon}}.svg"
|
||||||
width="25"
|
width="25"
|
||||||
height="30">
|
height="30">
|
||||||
<div class="list-group-item-section list-group-item-section-expanded">
|
<div
|
||||||
<h4 class="list-group-item-heading">
|
class="list-group-item-section list-group-item-section-expanded"
|
||||||
{{ drive.description }} <span class="word-keep"
|
tabindex="{{ 15 + $index }}"
|
||||||
ng-show="drive.size">- {{ drive.size | closestUnit }}</span>
|
ng-keypress="modal.keyboardToggleDrive(drive, $event)">
|
||||||
|
|
||||||
|
<h4 class="list-group-item-heading">{{ drive.description }} -
|
||||||
|
<span class="word-keep"
|
||||||
|
ng-show="drive.size">{{ drive.size | closestUnit }}</span>
|
||||||
</h4>
|
</h4>
|
||||||
<p class="list-group-item-text">{{ drive.displayName }}</p>
|
<p class="list-group-item-text">{{ drive.displayName }}</p>
|
||||||
|
|
||||||
@ -50,6 +54,7 @@
|
|||||||
|
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button class="button button-primary button-block"
|
<button class="button button-primary button-block"
|
||||||
|
tabindex="{{ 15 + modal.getDrives().length }}"
|
||||||
ng-click="modal.closeModal()"
|
ng-click="modal.closeModal()"
|
||||||
ng-disabled="!modal.state.hasDrive()">Continue</button>
|
ng-disabled="!modal.state.hasDrive()">Continue</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -3,7 +3,9 @@
|
|||||||
<span class="glyphicon glyphicon-exclamation-sign"></span>
|
<span class="glyphicon glyphicon-exclamation-sign"></span>
|
||||||
<span>Attention</span>
|
<span>Attention</span>
|
||||||
</h4>
|
</h4>
|
||||||
<button class="close" ng-click="modal.reject()">×</button>
|
<button class="close"
|
||||||
|
tabindex="11"
|
||||||
|
ng-click="modal.reject()">×</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
@ -13,8 +15,10 @@
|
|||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<div class="modal-menu">
|
<div class="modal-menu">
|
||||||
<button class="button button-danger button-block"
|
<button class="button button-danger button-block"
|
||||||
|
tabindex="13"
|
||||||
ng-click="modal.accept()">{{ ::modal.options.confirmationLabel }}</button>
|
ng-click="modal.accept()">{{ ::modal.options.confirmationLabel }}</button>
|
||||||
<button ng-if="modal.options.rejectionLabel" class="button button-block"
|
<button ng-if="modal.options.rejectionLabel" class="button button-block"
|
||||||
|
tabindex="12"
|
||||||
ng-click="modal.reject()">{{ ::modal.options.rejectionLabel }}</button>
|
ng-click="modal.reject()">{{ ::modal.options.rejectionLabel }}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -47,12 +47,6 @@ body {
|
|||||||
-webkit-overflow-scrolling: touch;
|
-webkit-overflow-scrolling: touch;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Prevent blue outline */
|
|
||||||
input:focus,
|
|
||||||
button:focus {
|
|
||||||
outline: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Titles don't have margins on desktop apps */
|
/* Titles don't have margins on desktop apps */
|
||||||
h1, h2, h3, h4, h5, h6 {
|
h1, h2, h3, h4, h5, h6 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
@ -6132,33 +6132,30 @@ body {
|
|||||||
.button-no-hover, .button[disabled], [disabled].progress-button, .progress-button[active="true"] {
|
.button-no-hover, .button[disabled], [disabled].progress-button, .progress-button[active="true"] {
|
||||||
pointer-events: none; }
|
pointer-events: none; }
|
||||||
|
|
||||||
.button-default,
|
.button-default {
|
||||||
.button-default:focus {
|
|
||||||
background-color: #ececec;
|
background-color: #ececec;
|
||||||
color: #b3b3b3;
|
color: #b3b3b3; }
|
||||||
outline: none; }
|
|
||||||
|
|
||||||
|
.button-default:focus,
|
||||||
.button-default:hover {
|
.button-default:hover {
|
||||||
background-color: lightgray;
|
background-color: lightgray;
|
||||||
color: #b3b3b3; }
|
color: #b3b3b3; }
|
||||||
|
|
||||||
.button-primary, .progress-button,
|
.button-primary, .progress-button {
|
||||||
.button-primary:focus,
|
|
||||||
.progress-button:focus {
|
|
||||||
background-color: #5793db;
|
background-color: #5793db;
|
||||||
color: #fff;
|
color: #fff; }
|
||||||
outline: none; }
|
|
||||||
|
|
||||||
.button-primary:hover, .progress-button:hover {
|
.button-primary:focus, .progress-button:focus,
|
||||||
|
.button-primary:hover,
|
||||||
|
.progress-button:hover {
|
||||||
background-color: #2d78d2;
|
background-color: #2d78d2;
|
||||||
color: #fff; }
|
color: #fff; }
|
||||||
|
|
||||||
.button-danger,
|
.button-danger {
|
||||||
.button-danger:focus {
|
|
||||||
background-color: #d9534f;
|
background-color: #d9534f;
|
||||||
color: #fff;
|
color: #fff; }
|
||||||
outline: none; }
|
|
||||||
|
|
||||||
|
.button-danger:focus,
|
||||||
.button-danger:hover {
|
.button-danger:hover {
|
||||||
background-color: #c9302c;
|
background-color: #c9302c;
|
||||||
color: #fff; }
|
color: #fff; }
|
||||||
@ -6741,3 +6738,12 @@ body {
|
|||||||
.section-header > .button, .section-header > .progress-button {
|
.section-header > .button, .section-header > .progress-button {
|
||||||
padding-left: 3px;
|
padding-left: 3px;
|
||||||
padding-right: 3px; }
|
padding-right: 3px; }
|
||||||
|
|
||||||
|
@keyframes focus-highlight {
|
||||||
|
from {
|
||||||
|
outline: 2px solid #ff912f; }
|
||||||
|
to {
|
||||||
|
outline: none; } }
|
||||||
|
|
||||||
|
[tabindex]:focus {
|
||||||
|
animation: focus-highlight 10s steps(2, end) forwards; }
|
||||||
|
@ -20,16 +20,24 @@
|
|||||||
</head>
|
</head>
|
||||||
<body ng-app="Etcher">
|
<body ng-app="Etcher">
|
||||||
<header class="section-header" ng-controller="HeaderController as header">
|
<header class="section-header" ng-controller="HeaderController as header">
|
||||||
<button class="button button-link" ng-click="header.openHelpPage()">
|
<button class="button button-link"
|
||||||
<span class="glyphicon glyphicon-question-sign"></span>
|
ng-click="header.openHelpPage()"
|
||||||
|
tabindex="-1">
|
||||||
|
<span tabindex="4" class="glyphicon glyphicon-question-sign"></span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button class="button button-link" ui-sref="settings" hide-if-state="settings">
|
<button class="button button-link"
|
||||||
<span class="glyphicon glyphicon-cog"></span>
|
ui-sref="settings"
|
||||||
|
hide-if-state="settings"
|
||||||
|
tabindex="-1">
|
||||||
|
<span tabindex="5" class="glyphicon glyphicon-cog"></span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button class="button button-link" ui-sref="main" show-if-state="settings">
|
<button class="button button-link"
|
||||||
<span class="glyphicon glyphicon-chevron-left"></span> Back
|
tabindex="-1"
|
||||||
|
ui-sref="main"
|
||||||
|
show-if-state="settings">
|
||||||
|
<span tabindex="5" class="glyphicon glyphicon-chevron-left"></span> Back
|
||||||
</button>
|
</button>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@ -37,23 +45,28 @@
|
|||||||
|
|
||||||
<footer class="section-footer" ng-controller="StateController as state"
|
<footer class="section-footer" ng-controller="StateController as state"
|
||||||
ng-hide="state.currentName === 'success'">
|
ng-hide="state.currentName === 'success'">
|
||||||
<span os-open-external="https://etcher.io?ref=etcher_footer">
|
<span os-open-external="https://etcher.io?ref=etcher_footer"
|
||||||
|
tabindex="100">
|
||||||
<svg-icon path="'../assets/etcher.svg'"
|
<svg-icon path="'../assets/etcher.svg'"
|
||||||
width="'83px'"
|
width="'83px'"
|
||||||
height="'13px'"></svg-icon>
|
height="'13px'"></svg-icon>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span class="caption">
|
<span class="caption">
|
||||||
is <span class="caption" os-open-external="https://github.com/resin-io/etcher">an open source project</span> by
|
is <span class="caption"
|
||||||
|
tabindex="101"
|
||||||
|
os-open-external="https://github.com/resin-io/etcher">an open source project</span> by
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span os-open-external="https://resin.io?ref=etcher">
|
<span os-open-external="https://resin.io?ref=etcher"
|
||||||
|
tabindex="102">
|
||||||
<svg-icon path="'../assets/resin.svg'"
|
<svg-icon path="'../assets/resin.svg'"
|
||||||
width="'79px'"
|
width="'79px'"
|
||||||
height="'23px'"></svg-icon>
|
height="'23px'"></svg-icon>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span class="caption footer-right"
|
<span class="caption footer-right"
|
||||||
|
tabindex="103"
|
||||||
manifest-bind="version"
|
manifest-bind="version"
|
||||||
os-open-external="https://github.com/resin-io/etcher/blob/master/CHANGELOG.md"></span>
|
os-open-external="https://github.com/resin-io/etcher/blob/master/CHANGELOG.md"></span>
|
||||||
</footer>
|
</footer>
|
||||||
|
@ -16,6 +16,8 @@
|
|||||||
|
|
||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
|
const _ = require('lodash')
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @summary OsOpenExternal directive
|
* @summary OsOpenExternal directive
|
||||||
* @function
|
* @function
|
||||||
@ -43,6 +45,17 @@ module.exports = (OSOpenExternalService) => {
|
|||||||
element.on('click', () => {
|
element.on('click', () => {
|
||||||
OSOpenExternalService.open(attributes.osOpenExternal)
|
OSOpenExternalService.open(attributes.osOpenExternal)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const ENTER_KEY = 13
|
||||||
|
const SPACE_KEY = 32
|
||||||
|
element.on('keypress', (event) => {
|
||||||
|
if (_.includes([ ENTER_KEY, SPACE_KEY ], event.keyCode)) {
|
||||||
|
// Don't spam the user with several tabs if the key is being held
|
||||||
|
if (!event.repeat) {
|
||||||
|
OSOpenExternalService.open(attributes.osOpenExternal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,7 @@
|
|||||||
<div ng-hide="main.selection.hasImage()">
|
<div ng-hide="main.selection.hasImage()">
|
||||||
<button
|
<button
|
||||||
class="button button-primary button-brick"
|
class="button button-primary button-brick"
|
||||||
|
tabindex="{{ main.selection.hasImage() ? -1 : 1 }}"
|
||||||
ng-click="image.openImageSelector()">Select image</button>
|
ng-click="image.openImageSelector()">Select image</button>
|
||||||
|
|
||||||
<p class="step-footer">
|
<p class="step-footer">
|
||||||
@ -29,6 +30,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button class="button button-link step-footer"
|
<button class="button button-link step-footer"
|
||||||
|
tabindex="1"
|
||||||
ng-click="image.reselectImage()"
|
ng-click="image.reselectImage()"
|
||||||
ng-hide="main.state.isFlashing()">Change</button>
|
ng-hide="main.state.isFlashing()">Change</button>
|
||||||
</div>
|
</div>
|
||||||
@ -51,6 +53,7 @@
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<button class="button button-primary button-brick"
|
<button class="button button-primary button-brick"
|
||||||
|
tabindex="{{ main.selection.hasDrive() ? -1 : 2 }}"
|
||||||
ng-disabled="main.shouldDriveStepBeDisabled()"
|
ng-disabled="main.shouldDriveStepBeDisabled()"
|
||||||
ng-click="drive.openDriveSelector()">Select drive</button>
|
ng-click="drive.openDriveSelector()">Select drive</button>
|
||||||
</div>
|
</div>
|
||||||
@ -70,6 +73,7 @@
|
|||||||
<span class="step-drive step-size">{{ main.selection.getDrive().size | closestUnit }}</span>
|
<span class="step-drive step-size">{{ main.selection.getDrive().size | closestUnit }}</span>
|
||||||
</div>
|
</div>
|
||||||
<button class="button button-link step-footer"
|
<button class="button button-link step-footer"
|
||||||
|
tabindex="{{ main.selection.hasDrive() ? 2 : -1 }}"
|
||||||
ng-click="drive.reselectDrive()"
|
ng-click="drive.reselectDrive()"
|
||||||
ng-hide="main.state.isFlashing()">Change</button>
|
ng-hide="main.state.isFlashing()">Change</button>
|
||||||
</div>
|
</div>
|
||||||
@ -86,6 +90,7 @@
|
|||||||
|
|
||||||
<div class="space-vertical-large">
|
<div class="space-vertical-large">
|
||||||
<progress-button class="button-brick"
|
<progress-button class="button-brick"
|
||||||
|
tabindex="3"
|
||||||
percentage="main.state.getFlashState().percentage"
|
percentage="main.state.getFlashState().percentage"
|
||||||
striped="{{ main.state.getFlashState().type == 'check' }}"
|
striped="{{ main.state.getFlashState().type == 'check' }}"
|
||||||
ng-attr-active="{{ main.state.isFlashing() }}"
|
ng-attr-active="{{ main.state.isFlashing() }}"
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
<div class="checkbox">
|
<div class="checkbox">
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox"
|
<input type="checkbox"
|
||||||
|
tabindex="6"
|
||||||
ng-model="settings.currentData.errorReporting"
|
ng-model="settings.currentData.errorReporting"
|
||||||
ng-change="settings.toggle('errorReporting')">
|
ng-change="settings.toggle('errorReporting')">
|
||||||
|
|
||||||
@ -14,6 +15,7 @@
|
|||||||
<div class="checkbox">
|
<div class="checkbox">
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox"
|
<input type="checkbox"
|
||||||
|
tabindex="7"
|
||||||
ng-model="settings.currentData.unmountOnSuccess"
|
ng-model="settings.currentData.unmountOnSuccess"
|
||||||
ng-change="settings.toggle('unmountOnSuccess')">
|
ng-change="settings.toggle('unmountOnSuccess')">
|
||||||
|
|
||||||
@ -33,6 +35,7 @@
|
|||||||
<div class="checkbox">
|
<div class="checkbox">
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox"
|
<input type="checkbox"
|
||||||
|
tabindex="8"
|
||||||
ng-model="settings.currentData.validateWriteOnSuccess"
|
ng-model="settings.currentData.validateWriteOnSuccess"
|
||||||
ng-change="settings.toggle('validateWriteOnSuccess')">
|
ng-change="settings.toggle('validateWriteOnSuccess')">
|
||||||
|
|
||||||
@ -43,6 +46,7 @@
|
|||||||
<div class="checkbox" ng-show="settings.model.get('updatesEnabled')">
|
<div class="checkbox" ng-show="settings.model.get('updatesEnabled')">
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox"
|
<input type="checkbox"
|
||||||
|
tabindex="9"
|
||||||
ng-model="settings.currentData.includeUnstableUpdateChannel"
|
ng-model="settings.currentData.includeUnstableUpdateChannel"
|
||||||
ng-change="settings.toggle('includeUnstableUpdateChannel')">
|
ng-change="settings.toggle('includeUnstableUpdateChannel')">
|
||||||
|
|
||||||
@ -53,6 +57,7 @@
|
|||||||
<div class="checkbox">
|
<div class="checkbox">
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox"
|
<input type="checkbox"
|
||||||
|
tabindex="10"
|
||||||
ng-model="settings.currentData.unsafeMode"
|
ng-model="settings.currentData.unsafeMode"
|
||||||
ng-change="settings.toggle('unsafeMode', {
|
ng-change="settings.toggle('unsafeMode', {
|
||||||
description: 'Are you sure you want to turn this on? You will be able to overwrite your system drives if you\'re not careful.',
|
description: 'Are you sure you want to turn this on? You will be able to overwrite your system drives if you\'re not careful.',
|
||||||
|
@ -75,19 +75,12 @@ $button-types-styles: (
|
|||||||
@each $style in map-keys($button-types-styles) {
|
@each $style in map-keys($button-types-styles) {
|
||||||
$button-styles: map-get($button-types-styles, $style);
|
$button-styles: map-get($button-types-styles, $style);
|
||||||
|
|
||||||
.button-#{$style},
|
.button-#{$style} {
|
||||||
|
|
||||||
// Undo `:focus` styles from Bootstrap.
|
|
||||||
// On Electron, the user can click and press over a button,
|
|
||||||
// then move the mouse away from the button and release,
|
|
||||||
// and the button will erroneusly keep the `:focus` state style.
|
|
||||||
.button-#{$style}:focus {
|
|
||||||
|
|
||||||
background-color: map-get($button-styles, "bg");
|
background-color: map-get($button-styles, "bg");
|
||||||
color: map-get($button-styles, "color");
|
color: map-get($button-styles, "color");
|
||||||
outline: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.button-#{$style}:focus,
|
||||||
.button-#{$style}:hover {
|
.button-#{$style}:hover {
|
||||||
background-color: darken(map-get($button-styles, "bg"), 10%);
|
background-color: darken(map-get($button-styles, "bg"), 10%);
|
||||||
color: map-get($button-styles, "color");
|
color: map-get($button-styles, "color");
|
||||||
|
@ -123,3 +123,19 @@ body {
|
|||||||
padding-right: 3px;
|
padding-right: 3px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes focus-highlight {
|
||||||
|
from {
|
||||||
|
outline: 2px solid $palette-theme-warning-background;
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[tabindex] {
|
||||||
|
&:focus {
|
||||||
|
animation: focus-highlight 10s steps(2, end) forwards;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user