Gracefully downsize search bar for mobile devices
authorRigel Kent <sendmemail@rigelk.eu>
Mon, 3 Feb 2020 13:04:42 +0000 (14:04 +0100)
committerRigel Kent <sendmemail@rigelk.eu>
Thu, 13 Feb 2020 15:32:58 +0000 (16:32 +0100)
client/src/app/app.component.html
client/src/app/header/header.component.html
client/src/app/header/header.component.scss
client/src/app/header/header.component.ts
client/src/app/header/search-typeahead.component.scss
client/src/app/header/search-typeahead.component.ts
client/src/app/header/suggestion.component.ts
client/src/app/header/suggestions.component.ts
client/src/app/menu/menu.component.scss
client/src/sass/application.scss

index 2660c537761018e38f572bf59e4d5f9d4d44f507..f5a8dbd34659ce99a9d43967c5638beb9c94798a 100644 (file)
@@ -15,7 +15,7 @@
     </div>
 
     <div class="header-right" [ngClass]="{ 'border-bottom': isMenuDisplayed === false }">
-      <my-header></my-header>
+      <my-header class="w-100 d-flex justify-content-end"></my-header>
     </div>
   </div>
 
index 074bebf21585e782157139f0f0c86c6a33972011..561ee6c1634cc62842bd162ec80748cd7b0365e8 100644 (file)
@@ -1,9 +1,9 @@
-<my-search-typeahead>
+<my-search-typeahead class="w-100 d-flex justify-content-end">
   <input
-    type="text" id="search-video" name="search-video" [attr.aria-label]="ariaLabelTextForSearch" i18n-placeholder placeholder="Search locally videos, channels…"
-    [(ngModel)]="searchValue" (keyup.enter)="doSearch()"
+    type="text" id="search-video" name="search-video" [attr.aria-label]="ariaLabelTextForSearch"
+    i18n-placeholder placeholder="Search videos, channels… known by this instance" [(ngModel)]="searchValue"
   >
-  <span (click)="doSearch()" class="icon icon-search"></span>
+  <span class="icon icon-search"></span>
 </my-search-typeahead>
 
 <a class="upload-button" routerLink="/videos/upload">
index b602cf0a884c4e02d4fcdc526153b4dffb8adb22..2f0a407fc9cd022440a363d78a8c0d9a559c18c6 100644 (file)
@@ -14,14 +14,6 @@ my-search-typeahead {
   &::placeholder {
     color: var(--inputPlaceholderColor);
   }
-
-  @media screen and (max-width: 800px) {
-    width: calc(100% - 150px);
-  }
-
-  @media screen and (max-width: 600px) {
-    width: calc(100% - 70px);
-  }
 }
 
 .icon.icon-search {
@@ -45,10 +37,6 @@ my-search-typeahead {
   color: var(--mainBackgroundColor) !important;
   margin-right: 25px;
 
-  @media screen and (max-width: 800px) {
-    margin-right: 0;
-  }
-
   @media screen and (max-width: 600px) {
     margin-right: 10px;
     padding: 0 10px;
index ca4a32cbc7732ca5fe1107472445d2891a26b60a..d9311c554a0934dd6d34e10f0461f99848cc2f6a 100644 (file)
@@ -1,9 +1,4 @@
-import { filter, first, map, tap } from 'rxjs/operators'
 import { Component, OnInit } from '@angular/core'
-import { ActivatedRoute, NavigationEnd, Params, Router } from '@angular/router'
-import { getParameterByName } from '../shared/misc/utils'
-import { AuthService } from '@app/core'
-import { of } from 'rxjs'
 import { I18n } from '@ngx-translate/i18n-polyfill'
 
 @Component({
@@ -17,46 +12,10 @@ export class HeaderComponent implements OnInit {
   ariaLabelTextForSearch = ''
 
   constructor (
-    private router: Router,
-    private route: ActivatedRoute,
-    private auth: AuthService,
     private i18n: I18n
   ) {}
 
   ngOnInit () {
     this.ariaLabelTextForSearch = this.i18n('Search videos, channels')
-
-    this.router.events
-        .pipe(
-          filter(e => e instanceof NavigationEnd),
-          map(() => getParameterByName('search', window.location.href))
-        )
-        .subscribe(searchQuery => this.searchValue = searchQuery || '')
-  }
-
-  doSearch () {
-    const queryParams: Params = {}
-
-    if (window.location.pathname === '/search' && this.route.snapshot.queryParams) {
-      Object.assign(queryParams, this.route.snapshot.queryParams)
-    }
-
-    Object.assign(queryParams, { search: this.searchValue })
-
-    const o = this.auth.isLoggedIn()
-      ? this.loadUserLanguagesIfNeeded(queryParams)
-      : of(true)
-
-    o.subscribe(() => this.router.navigate([ '/search' ], { queryParams }))
-  }
-
-  private loadUserLanguagesIfNeeded (queryParams: any) {
-    if (queryParams && queryParams.languageOneOf) return of(queryParams)
-
-    return this.auth.userInformationLoaded
-               .pipe(
-                 first(),
-                 tap(() => Object.assign(queryParams, { languageOneOf: this.auth.getUser().videoLanguages }))
-               )
   }
 }
index c410d47345b3e35ce43d50a12a703863e9f8a372..c2f5a1828c87a423ff33027637091ec8330bd9c9 100644 (file)
@@ -46,6 +46,18 @@ my-suggestions ::ng-deep ul {
     transition: box-shadow .3s ease, width .2s ease;
   }
 
+  @media screen and (min-width: 500px) {
+    margin-left: 10px;
+  }
+
+  @media screen and (max-width: 800px) {
+    flex: 1;
+
+    ::ng-deep input {
+      width: unset;
+    }
+  }
+
   ::ng-deep span {
     right: 10px;
   }
@@ -59,7 +71,9 @@ my-suggestions ::ng-deep ul {
   &:focus,
   ::ng-deep &:focus-within {
     & > div:last-child {
-      display: initial !important;
+      @media screen and (min-width: 500px) {
+        display: initial !important;
+      }
       
       #typeahead-help,
       #typeahead-instructions,
index 084bdd58be24a3b25b72a9f9ce06eb790a48c1ce..514c04704cfde1227f028411bfb634c26b513cab 100644 (file)
@@ -7,13 +7,15 @@ import {
   OnDestroy,
   QueryList
 } from '@angular/core'
-import { Router, NavigationEnd } from '@angular/router'
+import { Router, NavigationEnd, Params, ActivatedRoute } from '@angular/router'
 import { AuthService } from '@app/core'
 import { I18n } from '@ngx-translate/i18n-polyfill'
-import { filter } from 'rxjs/operators'
+import { filter, first, tap, map } from 'rxjs/operators'
 import { ListKeyManager } from '@angular/cdk/a11y'
 import { UP_ARROW, DOWN_ARROW, ENTER, TAB } from '@angular/cdk/keycodes'
-import { SuggestionComponent } from './suggestion.component'
+import { SuggestionComponent, Result } from './suggestion.component'
+import { of } from 'rxjs'
+import { getParameterByName } from '@app/shared/misc/utils'
 
 @Component({
   selector: 'my-search-typeahead',
@@ -41,6 +43,7 @@ export class SearchTypeaheadComponent implements OnInit, OnDestroy, AfterViewIni
   constructor (
     private authService: AuthService,
     private router: Router,
+    private route: ActivatedRoute,
     private i18n: I18n
   ) {
     this.URIPolicyText = this.i18n('Determines whether you can resolve any distant content, or if your instance only allows doing so for instances it follows.')
@@ -50,12 +53,19 @@ export class SearchTypeaheadComponent implements OnInit, OnDestroy, AfterViewIni
 
   ngOnInit () {
     this.router.events
-      .pipe(filter(event => event instanceof NavigationEnd))
+      .pipe(filter(e => e instanceof NavigationEnd))
       .subscribe((event: NavigationEnd) => {
         this.hasChannel = event.url.startsWith('/videos/watch')
         this.inChannel = event.url.startsWith('/video-channels')
         this.computeResults()
       })
+
+    this.router.events
+      .pipe(
+        filter(e => e instanceof NavigationEnd),
+        map(() => getParameterByName('search', window.location.href))
+      )
+      .subscribe(searchQuery => this.searchInput.value = searchQuery || '')
   }
 
   ngOnDestroy () {
@@ -82,33 +92,33 @@ export class SearchTypeaheadComponent implements OnInit, OnDestroy, AfterViewIni
 
   computeResults () {
     this.newSearch = true
-    let results = [
-      {
-        text: 'Maître poney',
-        type: 'channel'
-      }
-    ]
+    let results: Result[] = []
 
     if (this.hasSearch) {
       results = [
+        /* Channel search is still unimplemented. Uncomment when it is.
         {
           text: this.searchInput.value,
           type: 'search-channel'
         },
+        */
         {
           text: this.searchInput.value,
-          type: 'search-instance'
+          type: 'search-instance',
+          default: true
         },
+        /* Global search is still unimplemented. Uncomment when it is.
         {
           text: this.searchInput.value,
           type: 'search-global'
         },
+        */
         ...results
       ]
     }
 
     this.results = results.filter(
-      result => {
+      (result: Result) => {
         // if we're not in a channel or one of its videos/playlits, show all channel-related results
         if (!(this.hasChannel || this.inChannel)) return !result.type.includes('channel')
         // if we're in a channel, show all channel-related results except for the channel redirection itself
@@ -118,19 +128,26 @@ export class SearchTypeaheadComponent implements OnInit, OnDestroy, AfterViewIni
     )
   }
 
+  setEventItems (event: { items: QueryList<SuggestionComponent>, index?: number }) {
+    event.items.forEach(e => {
+      if (this.keyboardEventsManager.activeItem && this.keyboardEventsManager.activeItem === e) {
+        this.keyboardEventsManager.activeItem.active = true
+      } else {
+        e.active = false
+      }
+    })
+  }
+
   initKeyboardEventsManager (event: { items: QueryList<SuggestionComponent>, index?: number }) {
     if (this.keyboardEventsManager) this.keyboardEventsManager.change.unsubscribe()
     this.keyboardEventsManager = new ListKeyManager(event.items)
     if (event.index !== undefined) {
       this.keyboardEventsManager.setActiveItem(event.index)
-      event.items.forEach(e => e.active = false)
-      this.keyboardEventsManager.activeItem.active = true
+    } else {
+      this.keyboardEventsManager.setFirstItemActive()
     }
     this.keyboardEventsManager.change.subscribe(
-      val => {
-        event.items.forEach(e => e.active = false)
-        this.keyboardEventsManager.activeItem.active = true
-      }
+      _ => this.setEventItems(event)
     )
   }
 
@@ -141,17 +158,40 @@ export class SearchTypeaheadComponent implements OnInit, OnDestroy, AfterViewIni
   handleKeyUp (event: KeyboardEvent, indexSelected?: number) {
     event.stopImmediatePropagation()
     if (this.keyboardEventsManager) {
-      if (event.keyCode === TAB) {
-        this.keyboardEventsManager.setNextItemActive()
-        return false
-      } else if (event.keyCode === DOWN_ARROW || event.keyCode === UP_ARROW) {
+      if (event.keyCode === DOWN_ARROW || event.keyCode === UP_ARROW) {
         this.keyboardEventsManager.onKeydown(event)
         return false
       } else if (event.keyCode === ENTER) {
         this.newSearch = false
-        // this.router.navigate(this.keyboardEventsManager.activeItem.result)
+        this.doSearch()
         return false
       }
     }
   }
+
+  doSearch () {
+    const queryParams: Params = {}
+
+    if (window.location.pathname === '/search' && this.route.snapshot.queryParams) {
+      Object.assign(queryParams, this.route.snapshot.queryParams)
+    }
+
+    Object.assign(queryParams, { search: this.searchInput.value })
+
+    const o = this.authService.isLoggedIn()
+      ? this.loadUserLanguagesIfNeeded(queryParams)
+      : of(true)
+
+    o.subscribe(() => this.router.navigate([ '/search' ], { queryParams }))
+  }
+
+  private loadUserLanguagesIfNeeded (queryParams: any) {
+    if (queryParams && queryParams.languageOneOf) return of(queryParams)
+
+    return this.authService.userInformationLoaded
+               .pipe(
+                 first(),
+                 tap(() => Object.assign(queryParams, { languageOneOf: this.authService.getUser().videoLanguages }))
+               )
+  }
 }
index 75c44a5839037720154f43f90442636d5bb92914..bdcb3e03feff8c898e83e14bec60b3c6af57db97 100644 (file)
@@ -3,10 +3,11 @@ import { RouterLink } from '@angular/router'
 import { I18n } from '@ngx-translate/i18n-polyfill'
 import { ListKeyManagerOption } from '@angular/cdk/a11y'
 
-type Result = {
+export type Result = {
   text: string
   type: 'channel' | 'suggestion' | 'search-channel' | 'search-instance' | 'search-global' | 'search-any'
-  routerLink?: RouterLink
+  routerLink?: RouterLink,
+  default?: boolean
 }
 
 @Component({
@@ -39,7 +40,7 @@ export class SuggestionComponent implements OnInit, ListKeyManagerOption {
   }
 
   ngOnInit () {
-    this.active = false
+    if (this.result.default) this.active = true
   }
 
   selectItem () {
index 122c09388c5ebee01f6e0ded88b02f97133af85e..fac7fe2f97b00fe5b80b37e510bef938a3ee4f75 100644 (file)
@@ -19,7 +19,6 @@ export class SuggestionsComponent implements AfterViewInit {
   @Output() init = new EventEmitter()
 
   ngAfterViewInit () {
-    this.init.emit({ items: this.listItems })
     this.listItems.changes.subscribe(
       val => this.init.emit({ items: this.listItems })
     )
index 43654504c4f5696c8f19debf0bb22567d5dded44..cb5f907232f04ccb1155574d3f4395fc8ee647e5 100644 (file)
@@ -7,6 +7,7 @@
   padding: 0;
   width: $menu-width;
   z-index: z(menu);
+  scrollbar-color: var(--actionButtonColor) var(--menuBackgroundColor);
 }
 
 menu {
index e4840dd81fdaa2d0560c3e46dd5485c86c921b03..4ad14ea5bc89c5c0945d5e3570fc8fc6c83a752e 100644 (file)
@@ -47,6 +47,11 @@ body {
   font-size: 14px;
 }
 
+::selection {
+  color: var(--mainBackgroundColor);
+  background-color: var(--mainHoverColor);
+}
+
 #incompatible-browser {
   display: none;
   text-align: center;