From 6af662a5961b48ac12682df2b8b971060a2cc67d Mon Sep 17 00:00:00 2001 From: Rigel Kent Date: Sat, 25 Jan 2020 16:32:06 +0100 Subject: [PATCH] Add keyboard navigation and hepler to typeahead --- client/src/app/app.module.ts | 4 +- client/src/app/header/header.component.html | 2 +- client/src/app/header/header.component.ts | 5 +- client/src/app/header/index.ts | 2 + .../header/search-typeahead.component.html | 53 +++++--------- .../header/search-typeahead.component.scss | 45 ++++-------- .../app/header/search-typeahead.component.ts | 70 +++++++++++++++---- .../src/app/header/suggestion.component.html | 28 ++++++++ .../src/app/header/suggestion.component.scss | 32 +++++++++ client/src/app/header/suggestion.component.ts | 48 +++++++++++++ .../src/app/header/suggestions.component.ts | 31 ++++++++ 11 files changed, 234 insertions(+), 86 deletions(-) create mode 100644 client/src/app/header/suggestion.component.html create mode 100644 client/src/app/header/suggestion.component.scss create mode 100644 client/src/app/header/suggestion.component.ts create mode 100644 client/src/app/header/suggestions.component.ts diff --git a/client/src/app/app.module.ts b/client/src/app/app.module.ts index 2db33d638..9e220a383 100644 --- a/client/src/app/app.module.ts +++ b/client/src/app/app.module.ts @@ -9,7 +9,7 @@ import 'focus-visible' import { AppRoutingModule } from './app-routing.module' import { AppComponent } from './app.component' import { CoreModule } from './core' -import { HeaderComponent, SearchTypeaheadComponent } from './header' +import { HeaderComponent, SearchTypeaheadComponent, SuggestionsComponent, SuggestionComponent } from './header' import { LoginModule } from './login' import { AvatarNotificationComponent, LanguageChooserComponent, MenuComponent } from './menu' import { SharedModule } from './shared' @@ -42,6 +42,8 @@ export function metaFactory (serverService: ServerService): MetaLoader { AvatarNotificationComponent, HeaderComponent, SearchTypeaheadComponent, + SuggestionsComponent, + SuggestionComponent, WelcomeModalComponent, InstanceConfigWarningModalComponent diff --git a/client/src/app/header/header.component.html b/client/src/app/header/header.component.html index 38c87c642..074bebf21 100644 --- a/client/src/app/header/header.component.html +++ b/client/src/app/header/header.component.html @@ -1,6 +1,6 @@ diff --git a/client/src/app/header/header.component.ts b/client/src/app/header/header.component.ts index 92a7eded6..ca4a32cbc 100644 --- a/client/src/app/header/header.component.ts +++ b/client/src/app/header/header.component.ts @@ -2,7 +2,7 @@ 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, Notifier, ServerService } from '@app/core' +import { AuthService } from '@app/core' import { of } from 'rxjs' import { I18n } from '@ngx-translate/i18n-polyfill' @@ -20,9 +20,6 @@ export class HeaderComponent implements OnInit { private router: Router, private route: ActivatedRoute, private auth: AuthService, - private serverService: ServerService, - private authService: AuthService, - private notifier: Notifier, private i18n: I18n ) {} diff --git a/client/src/app/header/index.ts b/client/src/app/header/index.ts index bf1787103..a882d4d1f 100644 --- a/client/src/app/header/index.ts +++ b/client/src/app/header/index.ts @@ -1,2 +1,4 @@ export * from './header.component' export * from './search-typeahead.component' +export * from './suggestions.component' +export * from './suggestion.component' diff --git a/client/src/app/header/search-typeahead.component.html b/client/src/app/header/search-typeahead.component.html index fe3f6ff4d..2623ba337 100644 --- a/client/src/app/header/search-typeahead.component.html +++ b/client/src/app/header/search-typeahead.component.html @@ -3,17 +3,27 @@
-
    -
  • - -
  • -
+ + + +
+ +
+ +
+ using {{ globalSearchIndex }} + +
+
+
Results will be augmented with those of a third-party index. Only data necessary to make the query will be sent.
+
+
-
+
{URIPolicy, select, only-followed {only followed instances} other {any instance}} @@ -36,34 +46,3 @@
- - - -
- - -
- - - -
- -
- - {{ inThisChannelText }} - - - {{ inAllText }} - - -
- - -
-
\ No newline at end of file diff --git a/client/src/app/header/search-typeahead.component.scss b/client/src/app/header/search-typeahead.component.scss index 93f021e33..c410d4734 100644 --- a/client/src/app/header/search-typeahead.component.scss +++ b/client/src/app/header/search-typeahead.component.scss @@ -7,8 +7,9 @@ width: 100%; } +#typeahead-help, #typeahead-instructions, -#jump-to-results { +my-suggestions ::ng-deep ul { border: 1px solid var(--mainBackgroundColor); border-bottom-right-radius: 3px; border-bottom-left-radius: 3px; @@ -17,10 +18,12 @@ transition-property: box-shadow; } +#typeahead-help, #typeahead-instructions { margin-top: 10px; width: 100%; padding: .5rem 1rem; + white-space: normal; ul { list-style: none; @@ -58,8 +61,9 @@ & > div:last-child { display: initial !important; + #typeahead-help, #typeahead-instructions, - #jump-to-results { + my-suggestions ::ng-deep ul { box-shadow: rgba(0, 0, 0, 0.2) 0px 10px 20px -5px; } } @@ -76,33 +80,17 @@ } } -a.focus-visible { - background-color: var(--mainHoverColor); -} - -a { - @include disable-default-a-behaviour; - width: 100%; - - &, &:hover { - color: var(--mainForegroundColor); - } -} - -.bg-gray { - background-color: var(--mainBackgroundColor); -} - -.text-gray-light { - color: var(--mainForegroundColor); -} - .glyphicon { top: 3px; } .advanced-search-status { - cursor: help; + height: max-content; + cursor: default; + + &.c-help { + cursor: help; + } } .small-title { @@ -111,11 +99,6 @@ a { margin-bottom: .5rem; } -my-global-icon { - width: 17px; - position: relative; - top: -2px; - margin: 5px; - - @include apply-svg-color(var(--mainForegroundColor)) +::ng-deep my-suggestion { + width: 100%; } diff --git a/client/src/app/header/search-typeahead.component.ts b/client/src/app/header/search-typeahead.component.ts index d12a9682e..084bdd58b 100644 --- a/client/src/app/header/search-typeahead.component.ts +++ b/client/src/app/header/search-typeahead.component.ts @@ -1,23 +1,31 @@ -import { Component, ViewChild, ElementRef, AfterViewInit, OnInit } from '@angular/core' +import { + Component, + ViewChild, + ElementRef, + AfterViewInit, + OnInit, + OnDestroy, + QueryList +} from '@angular/core' import { Router, NavigationEnd } from '@angular/router' import { AuthService } from '@app/core' import { I18n } from '@ngx-translate/i18n-polyfill' import { filter } from 'rxjs/operators' -import { ListKeyManager, ListKeyManagerOption } from '@angular/cdk/a11y' -import { UP_ARROW, DOWN_ARROW, ENTER } from '@angular/cdk/keycodes' +import { ListKeyManager } from '@angular/cdk/a11y' +import { UP_ARROW, DOWN_ARROW, ENTER, TAB } from '@angular/cdk/keycodes' +import { SuggestionComponent } from './suggestion.component' @Component({ selector: 'my-search-typeahead', templateUrl: './search-typeahead.component.html', styleUrls: [ './search-typeahead.component.scss' ] }) -export class SearchTypeaheadComponent implements OnInit, AfterViewInit { +export class SearchTypeaheadComponent implements OnInit, OnDestroy, AfterViewInit { @ViewChild('contentWrapper', { static: true }) contentWrapper: ElementRef - @ViewChild('optionsList', { static: true }) optionsList: ElementRef hasChannel = false inChannel = false - keyboardEventsManager: ListKeyManager + newSearch = true searchInput: HTMLInputElement URIPolicy: 'only-followed' | 'any' = 'any' @@ -25,7 +33,9 @@ export class SearchTypeaheadComponent implements OnInit, AfterViewInit { URIPolicyText: string inAllText: string inThisChannelText: string + globalSearchIndex = 'https://index.joinpeertube.org' + keyboardEventsManager: ListKeyManager results: any[] = [] constructor ( @@ -33,7 +43,7 @@ export class SearchTypeaheadComponent implements OnInit, AfterViewInit { private router: Router, private i18n: I18n ) { - this.URIPolicyText = this.i18n('Determines whether you can resolve any distant content from its URL, or if your instance only allows doing so for instances it follows.') + this.URIPolicyText = this.i18n('Determines whether you can resolve any distant content, or if your instance only allows doing so for instances it follows.') this.inAllText = this.i18n('In all PeerTube') this.inThisChannelText = this.i18n('In this channel') } @@ -48,16 +58,30 @@ export class SearchTypeaheadComponent implements OnInit, AfterViewInit { }) } + ngOnDestroy () { + if (this.keyboardEventsManager) this.keyboardEventsManager.change.unsubscribe() + } + ngAfterViewInit () { this.searchInput = this.contentWrapper.nativeElement.childNodes[0] this.searchInput.addEventListener('input', this.computeResults.bind(this)) + this.searchInput.addEventListener('keyup', this.handleKeyUp.bind(this)) } get hasSearch () { return !!this.searchInput && !!this.searchInput.value } + get activeResult () { + return this.keyboardEventsManager && this.keyboardEventsManager.activeItem && this.keyboardEventsManager.activeItem.result + } + + get showHelp () { + return this.hasSearch && this.newSearch && this.activeResult && this.activeResult.type === 'search-global' || false + } + computeResults () { + this.newSearch = true let results = [ { text: 'Maître poney', @@ -71,6 +95,10 @@ export class SearchTypeaheadComponent implements OnInit, AfterViewInit { text: this.searchInput.value, type: 'search-channel' }, + { + text: this.searchInput.value, + type: 'search-instance' + }, { text: this.searchInput.value, type: 'search-global' @@ -90,20 +118,38 @@ export class SearchTypeaheadComponent implements OnInit, AfterViewInit { ) } + initKeyboardEventsManager (event: { items: QueryList, 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 + } + this.keyboardEventsManager.change.subscribe( + val => { + event.items.forEach(e => e.active = false) + this.keyboardEventsManager.activeItem.active = true + } + ) + } + isUserLoggedIn () { return this.authService.isLoggedIn() } - handleKeyUp (event: KeyboardEvent) { + handleKeyUp (event: KeyboardEvent, indexSelected?: number) { event.stopImmediatePropagation() if (this.keyboardEventsManager) { - if (event.keyCode === DOWN_ARROW || event.keyCode === UP_ARROW) { - // passing the event to key manager so we get a change fired + if (event.keyCode === TAB) { + this.keyboardEventsManager.setNextItemActive() + return false + } else if (event.keyCode === DOWN_ARROW || event.keyCode === UP_ARROW) { this.keyboardEventsManager.onKeydown(event) return false } else if (event.keyCode === ENTER) { - // when we hit enter, the keyboardManager should call the selectItem method of the `ListItemComponent` - // this.keyboardEventsManager.activeItem + this.newSearch = false + // this.router.navigate(this.keyboardEventsManager.activeItem.result) return false } } diff --git a/client/src/app/header/suggestion.component.html b/client/src/app/header/suggestion.component.html new file mode 100644 index 000000000..894cacb95 --- /dev/null +++ b/client/src/app/header/suggestion.component.html @@ -0,0 +1,28 @@ + +
+ + +
+ + + +
+ +
+ + {{ inThisChannelText }} + + + {{ inThisInstanceText }} + + + {{ inAllText }} + + +
+ + +
\ No newline at end of file diff --git a/client/src/app/header/suggestion.component.scss b/client/src/app/header/suggestion.component.scss new file mode 100644 index 000000000..1de2f43bd --- /dev/null +++ b/client/src/app/header/suggestion.component.scss @@ -0,0 +1,32 @@ +@import '_mixins'; + +a { + @include disable-default-a-behaviour; + width: 100%; + + &, &:hover { + color: var(--mainForegroundColor); + + &.focus-visible { + background-color: var(--mainHoverColor); + color: var(--mainBackgroundColor); + } + } +} + +.bg-gray { + background-color: var(--mainBackgroundColor); +} + +.text-gray-light { + color: var(--mainForegroundColor); +} + +my-global-icon { + width: 17px; + position: relative; + top: -2px; + margin: 5px; + + @include apply-svg-color(var(--mainForegroundColor)); +} diff --git a/client/src/app/header/suggestion.component.ts b/client/src/app/header/suggestion.component.ts new file mode 100644 index 000000000..75c44a583 --- /dev/null +++ b/client/src/app/header/suggestion.component.ts @@ -0,0 +1,48 @@ +import { Input, Component, Output, EventEmitter, OnInit } from '@angular/core' +import { RouterLink } from '@angular/router' +import { I18n } from '@ngx-translate/i18n-polyfill' +import { ListKeyManagerOption } from '@angular/cdk/a11y' + +type Result = { + text: string + type: 'channel' | 'suggestion' | 'search-channel' | 'search-instance' | 'search-global' | 'search-any' + routerLink?: RouterLink +} + +@Component({ + selector: 'my-suggestion', + templateUrl: './suggestion.component.html', + styleUrls: [ './suggestion.component.scss' ] +}) +export class SuggestionComponent implements OnInit, ListKeyManagerOption { + @Input() result: Result + @Input() highlight: string + @Output() selected = new EventEmitter() + + inAllText: string + inThisChannelText: string + inThisInstanceText: string + + disabled = false + active = false + + constructor ( + private i18n: I18n + ) { + this.inAllText = this.i18n('In the vidiverse') + this.inThisChannelText = this.i18n('In this channel') + this.inThisInstanceText = this.i18n('In this instance') + } + + getLabel () { + return this.result.text + } + + ngOnInit () { + this.active = false + } + + selectItem () { + this.selected.emit(this.result) + } +} diff --git a/client/src/app/header/suggestions.component.ts b/client/src/app/header/suggestions.component.ts new file mode 100644 index 000000000..122c09388 --- /dev/null +++ b/client/src/app/header/suggestions.component.ts @@ -0,0 +1,31 @@ +import { Input, QueryList, Component, Output, AfterViewInit, EventEmitter, ViewChildren } from '@angular/core' +import { SuggestionComponent } from './suggestion.component' + +@Component({ + selector: 'my-suggestions', + template: ` +
    +
  • + +
  • +
+ ` +}) +export class SuggestionsComponent implements AfterViewInit { + @Input() results: any[] + @Input() highlight: string + @ViewChildren(SuggestionComponent) listItems: QueryList + @Output() init = new EventEmitter() + + ngAfterViewInit () { + this.init.emit({ items: this.listItems }) + this.listItems.changes.subscribe( + val => this.init.emit({ items: this.listItems }) + ) + } + + hoverItem (index: number) { + this.init.emit({ items: this.listItems, index: index }) + } +}