use focus-visible polyfill to improve keyboard navigation
authorRigel Kent <sendmemail@rigelk.eu>
Sat, 8 Sep 2018 12:34:32 +0000 (14:34 +0200)
committerChocobozzz <me@florianbigard.com>
Tue, 11 Sep 2018 08:58:48 +0000 (10:58 +0200)
Only the homepage is concerned, but it should have decent keyboard
navigation support now.

12 files changed:
client/package.json
client/src/app/app.component.html
client/src/app/app.module.ts
client/src/app/menu/menu.component.html
client/src/app/menu/menu.component.scss
client/src/app/search/search.component.html
client/src/app/shared/video/video-miniature.component.html
client/src/app/shared/video/video-thumbnail.component.scss
client/src/app/videos/video-list/video-overview.component.scss
client/src/sass/include/_mixins.scss
client/yarn.lock
server/helpers/custom-validators/videos.ts

index 3a4605b56b1f0184140ae04dccafc640f67b9b26..4454e9a39944961251f255a223ea2622573d9abe 100644 (file)
     "webpack-cli": "^3.0.8",
     "webtorrent": "^0.102.1",
     "whatwg-fetch": "^2.0.4",
-    "zone.js": "~0.8.5"
+    "zone.js": "~0.8.5",
+    "focus-visible": "^4.1.5"
   }
 }
index 20e573de18317338027acf905befbf0edcc81abd..23ed04c2dad67b707ac25c9d5b11ebd761f82065 100644 (file)
@@ -22,7 +22,7 @@
   <div class="sub-header-container">
     <my-menu *ngIf="isMenuDisplayed"></my-menu>
 
-    <div id="right-container" class="main-col container-fluid" [ngClass]="{ expanded: isMenuDisplayed === false }">
+    <div id="content" tabindex="-1" class="main-col container-fluid" [ngClass]="{ expanded: isMenuDisplayed === false }">
 
       <div class="main-row">
         <router-outlet></router-outlet>
index b484a89e8a422d072c4bfd79909cad6b57993c94..ba16c072e2805b1df2b70ec5d4cd836e35d8dbd3 100644 (file)
@@ -6,6 +6,7 @@ import { ResetPasswordModule } from '@app/reset-password'
 import { MetaLoader, MetaModule, MetaStaticLoader, PageTitlePositioning } from '@ngx-meta/core'
 import { ClipboardModule } from 'ngx-clipboard'
 import { HotkeyModule, IHotkeyOptions } from 'angular2-hotkeys'
+import 'focus-visible'
 
 import { AppRoutingModule } from './app-routing.module'
 import { AppComponent } from './app.component'
index 63ff4a86f51407811646ff4c6cc2b6059edbcf59..139664534e03afc54cbbf46ccc052ec4fb8a263d 100644 (file)
 
     <div class="footer d-flex justify-content-between">
       <span class="language">
-        <span (click)="openLanguageChooser()" i18n-title title="Change the language" class="icon icon-language"></span>
+        <span tabindex="0" (keyup.enter)="openLanguageChooser()" (click)="openLanguageChooser()" i18n-title title="Change the language" class="icon icon-language"></span>
       </span>
       <span class="color-palette">
-        <span (click)="toggleDarkTheme()" i18n-title title="Toggle dark interface" class="icon icon-moonsun"></span>
+        <span tabindex="0" (keyup.enter)="toggleDarkTheme()" (click)="toggleDarkTheme()" i18n-title title="Toggle dark interface" class="icon icon-moonsun"></span>
       </span>
     </div>
   </menu>
index 592860e122da09b730a0b0ac9f9d182135020c28..f1b0a284f6a7bf12d6864862925d782f02a0b8a9 100644 (file)
@@ -130,7 +130,7 @@ menu {
       transition: background-color .1s ease-in-out;
       @include disable-default-a-behaviour;
 
-      &:hover {
+      &:hover, &.focus-visible {
         background-color: rgba(255, 255, 255, 0.15);
       }
 
@@ -202,6 +202,7 @@ menu {
       font-weight: $font-semibold;
 
       .icon {
+        @include disable-outline;
         @include icon(28px);
         opacity: 0.9;
 
index a258d4edd279b31ee2c9956514b10e204a745980..c4072b29106cba3acf2b7f34fe597d8f0abea8a9 100644 (file)
@@ -48,9 +48,9 @@
       <my-video-thumbnail [video]="result"></my-video-thumbnail>
 
       <div class="video-info">
-        <a class="video-info-name" [routerLink]="['/videos/watch', result.uuid]" [attr.title]="result.name">{{ result.name }}</a>
+        <a tabindex="-1" class="video-info-name" [routerLink]="['/videos/watch', result.uuid]" [attr.title]="result.name">{{ result.name }}</a>
         <span i18n class="video-info-date-views">{{ result.publishedAt | myFromNow }} - {{ result.views | myNumberFormatter }} views</span>
-        <a class="video-info-account" [routerLink]="[ '/accounts', result.byAccount ]">{{ result.byAccount }}</a>
+        <a tabindex="-1" class="video-info-account" [routerLink]="[ '/accounts', result.byAccount ]">{{ result.byAccount }}</a>
       </div>
     </div>
   </ng-container>
index de84bccf96a216015296e5095f8c4f5f7f7cc94e..9cf3fb32107a5817bf1d3d6aa465ca2ef72ebbed 100644 (file)
@@ -3,6 +3,7 @@
 
   <div class="video-miniature-information">
     <a
+      tabindex="-1"
       class="video-miniature-name"
       [routerLink]="[ '/videos/watch', video.uuid ]" [attr.title]="video.name" [ngClass]="{ 'blur-filter': isVideoBlur() }"
     >
 
     <span i18n class="video-miniature-created-at-views">{{ video.publishedAt | myFromNow }} - {{ video.views | myNumberFormatter }} views</span>
 
-    <a *ngIf="displayOwnerAccount()" class="video-miniature-account" [routerLink]="[ '/accounts', video.byAccount ]">
+    <a tabindex="-1" *ngIf="displayOwnerAccount()" class="video-miniature-account" [routerLink]="[ '/accounts', video.byAccount ]">
       {{ video.byAccount }}
     </a>
-    <a *ngIf="displayOwnerVideoChannel()" class="video-miniature-channel" [routerLink]="[ '/video-channels', video.byVideoChannel ]">
+    <a tabindex="-1" *ngIf="displayOwnerVideoChannel()" class="video-miniature-channel" [routerLink]="[ '/video-channels', video.byVideoChannel ]">
       {{ video.byVideoChannel }}
     </a>
   </div>
index c3cb1ec7520dca30b1d517cb29d0fc0fe0b83c68..1dd8e5338042a397ff97ef5269825f6777312ecd 100644 (file)
     text-decoration: none !important;
   }
 
+  @include disable-outline;
+  &.focus-visible {
+    box-shadow: 0 0 0 2px var(--mainColor);
+  }
+
   img {
     width: $video-thumbnail-width;
     height: $video-thumbnail-height;
index f5508cf614f78b05346c6631dc8b8b5c1602d479..eca8b230ff688d939404ce68a807cf87d704db97 100644 (file)
   margin-bottom: 20px;
 
   a {
-    @include disable-default-a-behaviour;
+    &:hover, &:focus:not(.focus-visible), &:active {
+      text-decoration: none;
+      outline: none;
+    }
 
     color: var(--mainForegroundColor);
   }
index 03cb337c2fc6edf4850720729f47736b6adbddd5..d755e7df399cfafc9f35c5fc6aaf80994396815f 100644 (file)
@@ -8,7 +8,9 @@
 }
 
 @mixin disable-outline {
-  outline: none;
+  &:focus:not(.focus-visible) {
+    outline: none;
+  }
 
   &::-moz-focus-inner {
     border: 0;
index dcd37c80d011d3dea2e75262f47a253bcab98236..6ecf26393cb17cea0b243220fdfbf17d9d684652 100644 (file)
   version "2.1.3"
   resolved "https://registry.yarnpkg.com/@angularclass/hmr/-/hmr-2.1.3.tgz#34e658ed3da37f23b0a200e2da5a89be92bb209f"
 
-"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.0.0-beta.35":
+"@babel/code-frame@^7.0.0-beta.35":
   version "7.0.0"
   resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.0.0.tgz#06e2ab19bdb535385559aabb5ba59729482800f8"
   dependencies:
     "@babel/highlight" "^7.0.0"
 
-"@babel/helper-module-imports@^7.0.0":
-  version "7.0.0"
-  resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.0.0.tgz#96081b7111e486da4d2cd971ad1a4fe216cc2e3d"
-  dependencies:
-    "@babel/types" "^7.0.0"
-
-"@babel/helper-module-transforms@^7.0.0":
-  version "7.0.0"
-  resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.0.0.tgz#b01ee7d543e81e8c3fc404b19c9f26acb6e4cf4c"
-  dependencies:
-    "@babel/helper-module-imports" "^7.0.0"
-    "@babel/helper-simple-access" "^7.0.0"
-    "@babel/helper-split-export-declaration" "^7.0.0"
-    "@babel/template" "^7.0.0"
-    "@babel/types" "^7.0.0"
-    lodash "^4.17.10"
-
-"@babel/helper-plugin-utils@^7.0.0":
-  version "7.0.0"
-  resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.0.0.tgz#bbb3fbee98661c569034237cc03967ba99b4f250"
-
-"@babel/helper-simple-access@^7.0.0":
-  version "7.0.0"
-  resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.0.0.tgz#ff36a27983ae4c27122da2f7f294dced80ecbd08"
-  dependencies:
-    "@babel/template" "^7.0.0"
-    "@babel/types" "^7.0.0"
-
-"@babel/helper-split-export-declaration@^7.0.0":
-  version "7.0.0"
-  resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.0.0.tgz#3aae285c0311c2ab095d997b8c9a94cad547d813"
-  dependencies:
-    "@babel/types" "^7.0.0"
-
 "@babel/highlight@^7.0.0":
   version "7.0.0"
   resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.0.0.tgz#f710c38c8d458e6dd9a201afb637fcb781ce99e4"
     esutils "^2.0.2"
     js-tokens "^4.0.0"
 
-"@babel/parser@^7.0.0":
-  version "7.0.0"
-  resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.0.0.tgz#697655183394facffb063437ddf52c0277698775"
-
-"@babel/plugin-transform-modules-commonjs@^7.0.0":
-  version "7.0.0"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.0.0.tgz#20b906e5ab130dd8e456b694a94d9575da0fd41f"
-  dependencies:
-    "@babel/helper-module-transforms" "^7.0.0"
-    "@babel/helper-plugin-utils" "^7.0.0"
-    "@babel/helper-simple-access" "^7.0.0"
-
-"@babel/template@^7.0.0":
-  version "7.0.0"
-  resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.0.0.tgz#c2bc9870405959c89a9c814376a2ecb247838c80"
-  dependencies:
-    "@babel/code-frame" "^7.0.0"
-    "@babel/parser" "^7.0.0"
-    "@babel/types" "^7.0.0"
-
-"@babel/types@^7.0.0":
-  version "7.0.0"
-  resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.0.0.tgz#6e191793d3c854d19c6749989e3bc55f0e962118"
-  dependencies:
-    esutils "^2.0.2"
-    lodash "^4.17.10"
-    to-fast-properties "^2.0.0"
-
 "@neos21/bootstrap3-glyphicons@^1.0.1":
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/@neos21/bootstrap3-glyphicons/-/bootstrap3-glyphicons-1.0.1.tgz#e5eeec43e0153d4b51effd9ecb58cdf7029924d7"
@@ -3299,6 +3237,10 @@ flush-write-stream@^1.0.0:
     inherits "^2.0.1"
     readable-stream "^2.0.4"
 
+focus-visible@^4.1.5:
+  version "4.1.5"
+  resolved "https://registry.yarnpkg.com/focus-visible/-/focus-visible-4.1.5.tgz#50b44e2e84c24b831ceca3cce84d57c2b311c855"
+
 follow-redirects@^1.0.0:
   version "1.5.1"
   resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.5.1.tgz#67a8f14f5a1f67f962c2c46469c79eaec0a90291"
@@ -8217,10 +8159,6 @@ to-fast-properties@^1.0.3:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.3.tgz#b83571fa4d8c25b82e231b06e3a3055de4ca1a47"
 
-to-fast-properties@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e"
-
 to-object-path@^0.3.0:
   version "0.3.0"
   resolved "https://registry.yarnpkg.com/to-object-path/-/to-object-path-0.3.0.tgz#297588b7b0e7e0ac08e04e672f85c1f4999e17af"
index 4b1f6c069898363490ace2ac011d1398cd6ea359..edafba6e2d17378506d69d1702be8cf39e30574a 100644 (file)
@@ -178,7 +178,7 @@ async function isVideoChannelOfAccountExist (channelId: number, user: UserModel,
     const videoChannel = await VideoChannelModel.loadAndPopulateAccount(channelId)
     if (videoChannel === null) {
       res.status(400)
-         .json({ error: 'Unknown video video channel on this instance.' })
+         .json({ error: 'Unknown video `video channel` on this instance.' })
          .end()
 
       return false
@@ -191,7 +191,7 @@ async function isVideoChannelOfAccountExist (channelId: number, user: UserModel,
   const videoChannel = await VideoChannelModel.loadByIdAndAccount(channelId, user.Account.id)
   if (videoChannel === null) {
     res.status(400)
-       .json({ error: 'Unknown video video channel for this account.' })
+       .json({ error: 'Unknown video `video channel` for this account.' })
        .end()
 
     return false