diff --git a/frontend/src/app/components/mining-dashboard/mining-dashboard.component.ts b/frontend/src/app/components/mining-dashboard/mining-dashboard.component.ts index df4713374..6353ab8b8 100644 --- a/frontend/src/app/components/mining-dashboard/mining-dashboard.component.ts +++ b/frontend/src/app/components/mining-dashboard/mining-dashboard.component.ts @@ -1,6 +1,8 @@ -import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; +import { AfterViewInit, ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { SeoService } from '../../services/seo.service'; import { WebsocketService } from '../../services/websocket.service'; +import { StateService } from '../../services/state.service'; +import { EventType, NavigationStart, Router } from '@angular/router'; @Component({ selector: 'app-mining-dashboard', @@ -8,10 +10,12 @@ import { WebsocketService } from '../../services/websocket.service'; styleUrls: ['./mining-dashboard.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, }) -export class MiningDashboardComponent implements OnInit { +export class MiningDashboardComponent implements OnInit, AfterViewInit { constructor( private seoService: SeoService, private websocketService: WebsocketService, + private stateService: StateService, + private router: Router ) { this.seoService.setTitle($localize`:@@a681a4e2011bb28157689dbaa387de0dd0aa0c11:Mining Dashboard`); } @@ -19,4 +23,15 @@ export class MiningDashboardComponent implements OnInit { ngOnInit(): void { this.websocketService.want(['blocks', 'mempool-blocks', 'stats']); } + + ngAfterViewInit(): void { + this.stateService.focusSearchInputDesktop(); + this.router.events.subscribe((e: NavigationStart) => { + if (e.type === EventType.NavigationStart) { + if (e.url.indexOf('graphs') === -1) { // The mining dashboard and the graph component are part of the same module so we can't use ngAfterViewInit in graphs.component.ts to blur the input + this.stateService.focusSearchInputDesktop(); + } + } + }); + } } diff --git a/frontend/src/app/components/search-form/search-form.component.html b/frontend/src/app/components/search-form/search-form.component.html index cdfcfe015..3fc03c83a 100644 --- a/frontend/src/app/components/search-form/search-form.component.html +++ b/frontend/src/app/components/search-form/search-form.component.html @@ -1,7 +1,7 @@
- +
diff --git a/frontend/src/app/components/search-form/search-form.component.ts b/frontend/src/app/components/search-form/search-form.component.ts index 2361f8873..61b3351b7 100644 --- a/frontend/src/app/components/search-form/search-form.component.ts +++ b/frontend/src/app/components/search-form/search-form.component.ts @@ -1,6 +1,6 @@ import { Component, OnInit, ChangeDetectionStrategy, EventEmitter, Output, ViewChild, HostListener, ElementRef } from '@angular/core'; import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; -import { Router } from '@angular/router'; +import { EventType, NavigationStart, Router } from '@angular/router'; import { AssetsService } from '../../services/assets.service'; import { StateService } from '../../services/state.service'; import { Observable, of, Subject, zip, BehaviorSubject, combineLatest } from 'rxjs'; @@ -47,6 +47,8 @@ export class SearchFormComponent implements OnInit { this.handleKeyDown($event); } + @ViewChild('searchInput') searchInput: ElementRef; + constructor( private formBuilder: UntypedFormBuilder, private router: Router, @@ -55,11 +57,26 @@ export class SearchFormComponent implements OnInit { private electrsApiService: ElectrsApiService, private apiService: ApiService, private relativeUrlPipe: RelativeUrlPipe, - private elementRef: ElementRef, - ) { } + private elementRef: ElementRef + ) { + } ngOnInit(): void { this.stateService.networkChanged$.subscribe((network) => this.network = network); + + this.router.events.subscribe((e: NavigationStart) => { // Reset search focus when changing page + if (this.searchInput && e.type === EventType.NavigationStart) { + this.searchInput.nativeElement.blur(); + } + }); + + this.stateService.searchFocus$.subscribe(() => { + if (!this.searchInput) { // Try again a bit later once the view is properly initialized + setTimeout(() => this.searchInput.nativeElement.focus(), 100); + } else if (this.searchInput) { + this.searchInput.nativeElement.focus(); + } + }); this.searchForm = this.formBuilder.group({ searchText: ['', Validators.required], diff --git a/frontend/src/app/dashboard/dashboard.component.ts b/frontend/src/app/dashboard/dashboard.component.ts index aca3593d7..05381453d 100644 --- a/frontend/src/app/dashboard/dashboard.component.ts +++ b/frontend/src/app/dashboard/dashboard.component.ts @@ -1,7 +1,7 @@ -import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core'; +import { AfterViewInit, ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core'; import { combineLatest, merge, Observable, of, Subscription } from 'rxjs'; import { filter, map, scan, share, switchMap, tap } from 'rxjs/operators'; -import { BlockExtended, OptimizedMempoolStats, RbfTree } from '../interfaces/node-api.interface'; +import { BlockExtended, OptimizedMempoolStats } from '../interfaces/node-api.interface'; import { MempoolInfo, TransactionStripped, ReplacementInfo } from '../interfaces/websocket.interface'; import { ApiService } from '../services/api.service'; import { StateService } from '../services/state.service'; @@ -31,7 +31,7 @@ interface MempoolStatsData { styleUrls: ['./dashboard.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush }) -export class DashboardComponent implements OnInit, OnDestroy { +export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit { featuredAssets$: Observable; network$: Observable; mempoolBlocksData$: Observable; @@ -57,6 +57,10 @@ export class DashboardComponent implements OnInit, OnDestroy { private seoService: SeoService ) { } + ngAfterViewInit(): void { + this.stateService.focusSearchInputDesktop(); + } + ngOnDestroy(): void { this.currencySubscription.unsubscribe(); this.websocketService.stopTrackRbfSummary(); diff --git a/frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.ts b/frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.ts index 6fa4b454c..e58d5f124 100644 --- a/frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.ts +++ b/frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.ts @@ -1,4 +1,4 @@ -import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; +import { AfterViewInit, ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { Observable } from 'rxjs'; import { share } from 'rxjs/operators'; import { INodesRanking } from '../../interfaces/node-api.interface'; @@ -12,7 +12,7 @@ import { LightningApiService } from '../lightning-api.service'; styleUrls: ['./lightning-dashboard.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, }) -export class LightningDashboardComponent implements OnInit { +export class LightningDashboardComponent implements OnInit, AfterViewInit { statistics$: Observable; nodesRanking$: Observable; officialMempoolSpace = this.stateService.env.OFFICIAL_MEMPOOL_SPACE; @@ -30,4 +30,7 @@ export class LightningDashboardComponent implements OnInit { this.statistics$ = this.lightningApiService.getLatestStatistics$().pipe(share()); } + ngAfterViewInit(): void { + this.stateService.focusSearchInputDesktop(); + } } diff --git a/frontend/src/app/services/state.service.ts b/frontend/src/app/services/state.service.ts index ea00f12ab..9ab8a7e93 100644 --- a/frontend/src/app/services/state.service.ts +++ b/frontend/src/app/services/state.service.ts @@ -7,6 +7,7 @@ import { Router, NavigationStart } from '@angular/router'; import { isPlatformBrowser } from '@angular/common'; import { filter, map, scan, shareReplay } from 'rxjs/operators'; import { StorageService } from './storage.service'; +import { hasTouchScreen } from '../shared/pipes/bytes-pipe/utils'; export interface MarkBlockState { blockHeight?: number; @@ -139,6 +140,8 @@ export class StateService { fiatCurrency$: BehaviorSubject; rateUnits$: BehaviorSubject; + searchFocus$: Subject = new Subject(); + constructor( @Inject(PLATFORM_ID) private platformId: any, @Inject(LOCALE_ID) private locale: string, @@ -356,4 +359,10 @@ export class StateService { this.blocks = this.blocks.slice(0, this.env.KEEP_BLOCKS_AMOUNT); this.blocksSubject$.next(this.blocks); } + + focusSearchInputDesktop() { + if (!hasTouchScreen()) { + this.searchFocus$.next(true); + } + } } diff --git a/frontend/src/app/shared/pipes/bytes-pipe/utils.ts b/frontend/src/app/shared/pipes/bytes-pipe/utils.ts index fc8c2b08f..2700be45d 100644 --- a/frontend/src/app/shared/pipes/bytes-pipe/utils.ts +++ b/frontend/src/app/shared/pipes/bytes-pipe/utils.ts @@ -309,3 +309,28 @@ export function takeWhile(input: any[], predicate: CollectionPredicate) { return takeUntil(input, (item: any, index: number | undefined, collection: any[] | undefined) => !predicate(item, index, collection)); } + +// https://developer.mozilla.org/en-US/docs/Web/HTTP/Browser_detection_using_the_user_agent +export function hasTouchScreen(): boolean { + let hasTouchScreen = false; + if ('maxTouchPoints' in navigator) { + hasTouchScreen = navigator.maxTouchPoints > 0; + } else if ('msMaxTouchPoints' in navigator) { + // @ts-ignore + hasTouchScreen = navigator.msMaxTouchPoints > 0; + } else { + const mQ = matchMedia?.('(pointer:coarse)'); + if (mQ?.media === '(pointer:coarse)') { + hasTouchScreen = !!mQ.matches; + } else if ('orientation' in window) { + hasTouchScreen = true; // deprecated, but good fallback + } else { + // @ts-ignore - Only as a last resort, fall back to user agent sniffing + const UA = navigator.userAgent; + hasTouchScreen = + /\b(BlackBerry|webOS|iPhone|IEMobile)\b/i.test(UA) || + /\b(Android|Windows Phone|iPad|iPod)\b/i.test(UA); + } + } + return hasTouchScreen; +} \ No newline at end of file