Fix search typeahead accessibility

pull/6449/head
Chocobozzz 2024-06-10 16:20:50 +02:00
parent 9c215124d1
commit 44eedafed4
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
6 changed files with 49 additions and 69 deletions

View File

@ -2,7 +2,7 @@
<input <input
type="search" id="search-video" name="search-video" #searchVideo i18n-placeholder placeholder="Search videos, playlists, channels…" type="search" id="search-video" name="search-video" #searchVideo i18n-placeholder placeholder="Search videos, playlists, channels…"
[(ngModel)]="search" (ngModelChange)="onSearchChange()" (keydown)="handleKey($event)" [(ngModel)]="search" (ngModelChange)="onSearchChange()" (keydown)="handleKey($event)"
autocomplete="off" autocomplete="off" aria-describedby="typeahead-input-help"
> >
<button class="border-0 search-button" title="Search" i18n-title (click)="doSearch()"> <button class="border-0 search-button" title="Search" i18n-title (click)="doSearch()">
@ -17,40 +17,38 @@
role="option" aria-selected="true" tabindex="0" role="option" aria-selected="true" tabindex="0"
(mouseenter)="onSuggestionHover(i)" (click)="onSuggestionClicked(result)" (keyup.enter)="onSuggestionClicked(result)" (mouseenter)="onSuggestionHover(i)" (click)="onSuggestionClicked(result)" (keyup.enter)="onSuggestionClicked(result)"
> >
<my-suggestion [result]="result" [highlight]="search"></my-suggestion> <my-suggestion
[result]="result" [highlight]="search"
[describedby]="showSearchGlobalHelp() ? 'typeahead-help' : undefined"
></my-suggestion>
</li> </li>
</ul> </ul>
<!-- suggestion help, not shown until one of the suggestions is selected and specific to that suggestion --> <!-- suggestion help, not shown until one of the suggestions is selected and specific to that suggestion -->
<div *ngIf="showSearchGlobalHelp()" id="typeahead-help" class="overflow-hidden"> <div *ngIf="showSearchGlobalHelp()" id="typeahead-suggestion-help" class="overflow-hidden">
<div class="d-flex justify-content-between"> <div class="d-flex justify-content-between">
<div class="small-title" i18n>GLOBAL SEARCH</div> <div class="small-title mb-2" i18n>GLOBAL SEARCH</div>
<div class="advanced-search-status muted">
<span *ngIf="serverConfig" class="me-1" i18n>using {{ serverConfig.search.searchIndex.url }}</span> <span *ngIf="serverConfig" class="muted" i18n>using {{ serverConfig.search.searchIndex.url }}</span>
</div>
</div> </div>
<div class="muted" i18n>Results will be augmented with those of a third-party index. Only data necessary to make the query will be sent.</div> <div class="muted" i18n>Results will be augmented with those of a third-party index. Only data necessary to make the query will be sent.</div>
</div> </div>
<!-- search instructions, when search input is empty --> <!-- search instructions, when search input is empty -->
<div *ngIf="areInstructionsDisplayed()" id="typeahead-instructions" class="overflow-hidden"> <div [hidden]="this.search" id="typeahead-input-help" class="overflow-hidden">
<span class="muted" i18n>Your query will be matched against video names or descriptions, channel names.</span> <span class="muted" i18n>Your query will be matched against video names or descriptions, channel names.</span>
<div class="d-flex justify-content-between mt-3"> <div class="mt-3 mb-2 small-title" i18n>ADVANCED SEARCH</div>
<div class="small-title" i18n>ADVANCED SEARCH</div>
<div class="advanced-search-status c-help">
<span [ngClass]="canSearchAnyURI ? 'text-success' : 'muted'" i18n-title title="Determines whether you can resolve any distant content, or if this instance only allows doing so for instances it follows.">
<span *ngIf="canSearchAnyURI()" class="me-1" i18n>any instance</span>
<span *ngIf="!canSearchAnyURI()" class="me-1" i18n>only followed instances</span>
</span>
</div>
</div>
<ul> <ul>
<li> <li>
<em>&#64;channel_id&#64;domain</em> <span class="flex-auto muted" i18n>will list the matching channel</span> <em>&#64;channel_id&#64;domain</em> <span class="flex-auto muted" i18n>will list the matching channel</span>
</li> </li>
<li> <li>
<em>URL</em> <span class="muted" i18n>will list the matching channel</span> <em>URL</em> <span class="muted" i18n>will list the matching channel</span>
</li> </li>
<li> <li>
<em>UUID</em> <span class="muted" i18n>will list the matching video</span> <em>UUID</em> <span class="muted" i18n>will list the matching video</span>
</li> </li>

View File

@ -37,27 +37,25 @@
width: 100%; width: 100%;
} }
#typeahead-help, #typeahead-suggestion-help,
#typeahead-instructions, #typeahead-input-help,
li.suggestion { .suggestion {
border: 1px solid pvar(--mainBackgroundColor); border: 1px solid pvar(--mainBackgroundColor);
background: pvar(--mainBackgroundColor); background: pvar(--mainBackgroundColor);
transition: .3s ease; transition: .3s ease;
transition-property: box-shadow; transition-property: box-shadow;
cursor: pointer; cursor: pointer;
// soft border-radius for the last suggestion and the link inside
&:last-of-type {
&,
::ng-deep a {
border-bottom-right-radius: 3px;
border-bottom-left-radius: 3px;
}
}
} }
#typeahead-help, #typeahead-suggestion-help,
#typeahead-instructions { #typeahead-input-help,
.suggestion:last-of-type {
border-bottom-right-radius: 3px;
border-bottom-left-radius: 3px;
}
#typeahead-suggestion-help,
#typeahead-input-help {
margin-top: 9px; margin-top: 9px;
width: 100%; width: 100%;
padding: .5rem 1rem; padding: .5rem 1rem;
@ -110,21 +108,20 @@ li.suggestion {
display: none; display: none;
} }
&:focus, &:focus-within {
::ng-deep &:focus-within {
> div:last-child { > div:last-child {
@media screen and (min-width: $mobile-view) { @media screen and (min-width: $mobile-view) {
display: initial !important; display: initial !important;
} }
#typeahead-help, #typeahead-suggestion-help,
#typeahead-instructions, #typeahead-input-help,
li.suggestion { .suggestion {
box-shadow: rgba(0, 0, 0, 0.2) 0 10px 20px -5px; box-shadow: rgba(0, 0, 0, 0.2) 0 10px 20px -5px;
} }
} }
::ng-deep input { input {
box-shadow: rgba(0, 0, 0, 0.2) 0 1px 20px 0; box-shadow: rgba(0, 0, 0, 0.2) 0 1px 20px 0;
border-end-start-radius: 0; border-end-start-radius: 0;
border-end-end-radius: 0; border-end-end-radius: 0;
@ -136,21 +133,10 @@ li.suggestion {
} }
} }
.advanced-search-status {
height: max-content;
cursor: default;
&.c-help {
cursor: help;
}
}
.small-title { .small-title {
@include in-content-small-title; @include in-content-small-title;
margin-bottom: .5rem;
} }
::ng-deep my-suggestion { my-suggestion {
width: 100%; width: 100%;
} }

View File

@ -70,10 +70,6 @@ export class SearchTypeaheadComponent implements OnInit, AfterViewChecked, OnDes
if (this.keyboardEventsManager) this.keyboardEventsManager.change.unsubscribe() if (this.keyboardEventsManager) this.keyboardEventsManager.change.unsubscribe()
} }
areInstructionsDisplayed () {
return !this.search
}
showSearchGlobalHelp () { showSearchGlobalHelp () {
return this.search && this.areSuggestionsOpened && this.keyboardEventsManager?.activeItem?.result?.type === 'search-index' return this.search && this.areSuggestionsOpened && this.keyboardEventsManager?.activeItem?.result?.type === 'search-index'
} }

View File

@ -1,18 +1,18 @@
<a tabindex="-1" class="d-flex flex-auto flex-items-center p-2" [class.focus-visible]="active"> <a
<div class="flex-shrink-0 me-2 text-center"> tabindex="-1" class="d-flex flex-auto align-center p-2" [class.focus-visible]="active"
<my-global-icon iconName="search"></my-global-icon> [title]="getTitle()"
</div> [attr.aria-describedby]="describedby"
>
<img class="avatar me-2 flex-shrink-0 d-none" alt="" aria-label="Team" src="" width="28" height="28"> <my-global-icon class="me-2" iconName="search"></my-global-icon>
<div <div
class="flex-auto overflow-hidden text-start no-wrap css-truncate css-truncate-target" class="flex-auto overflow-hidden no-wrap d-flex align-center"
[attr.aria-label]="result.text" [attr.aria-label]="result.text"
> >
{{ result.text }} {{ result.text }}
</div> </div>
<div class="border rounded flex-shrink-0 px-1 bg-gray text-gray-light ms-1 f6"> <div class="result-type border rounded flex-shrink-0 px-1 ms-1 f6">
<span *ngIf="result.type === 'search-instance'" i18n>In this instance's network</span> <span *ngIf="result.type === 'search-instance'" i18n>In this instance's network</span>
<span *ngIf="result.type === 'search-index'" i18n>In the vidiverse</span> <span *ngIf="result.type === 'search-index'" i18n>In the vidiverse</span>
</div> </div>

View File

@ -17,11 +17,8 @@ a {
} }
} }
.bg-gray { .result-type {
background-color: pvar(--mainBackgroundColor); background-color: pvar(--mainBackgroundColor);
}
.text-gray-light {
color: pvar(--mainForegroundColor); color: pvar(--mainForegroundColor);
} }
@ -30,6 +27,5 @@ my-global-icon {
width: 17px; width: 17px;
position: relative; position: relative;
top: -2px; top: -1px;
margin: 5px;
} }

View File

@ -23,12 +23,16 @@ export type SuggestionPayloadType = 'search-instance' | 'search-index'
export class SuggestionComponent implements OnInit, ListKeyManagerOption { export class SuggestionComponent implements OnInit, ListKeyManagerOption {
@Input() result: SuggestionPayload @Input() result: SuggestionPayload
@Input() highlight: string @Input() highlight: string
@Input() describedby: string
disabled = false disabled = false
active = false active = false
getLabel () { getTitle () {
return this.result.text if (this.result.type === 'search-instance') return $localize`Search "${this.result.text}" in this instance's network`
if (this.result.type === 'search-index') return $localize`Search "${this.result.text}" in the vidiverse`
return undefined
} }
ngOnInit () { ngOnInit () {