Skip to content

Commit ccc8c3c

Browse files
committed
fix(filter-bar): restore focus after closing (#DS-4187)
1 parent c5c1a45 commit ccc8c3c

File tree

7 files changed

+131
-16
lines changed

7 files changed

+131
-16
lines changed

packages/components/filter-bar/filter-bar-button.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,21 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
33
import { KbqButton, KbqButtonStyles } from '@koobiq/components/button';
44
import { KbqComponentColors } from '@koobiq/components/core';
55
import { KbqFilterBar } from './filter-bar';
6+
import { KbqFilters } from './filters';
67

78
@Directive({
8-
selector: '[kbqFilterBarButton]'
9+
selector: '[kbqFilterBarButton]',
10+
host: {
11+
'(click)': 'saveFocusedElement()',
12+
'(keydown)': 'saveFocusedElement()'
13+
}
914
})
1015
export class KbqFilterBarButton {
1116
private readonly button = inject(KbqButton);
1217
/** KbqFilterBar instance */
1318
private readonly filterBar = inject(KbqFilterBar);
19+
/** KbqFilters instance */
20+
protected readonly filters = inject(KbqFilters);
1421

1522
constructor() {
1623
this.filterBar.changes.pipe(takeUntilDestroyed()).subscribe(() => {
@@ -23,4 +30,9 @@ export class KbqFilterBarButton {
2330
}
2431
});
2532
}
33+
34+
/** @docs-private */
35+
saveFocusedElement() {
36+
this.filters.saveFocusedElement(this.button);
37+
}
2638
}

packages/components/filter-bar/filters.html

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
<button
2+
#mainButton
23
kbq-button
34
kbq-title
45
kbqFilterBarButton
@@ -13,8 +14,8 @@
1314
[kbqPopoverPlacementPriority]="[placements.BottomLeft, placements.TopLeft]"
1415
[kbqPopoverSize]="popoverSize"
1516
[kbqTrigger]="'manual'"
16-
[class.kbq-active]="opened"
1717
[disabled]="isSaving"
18+
[class.kbq-active]="focusedElementBeforeIs(mainButton) && opened"
1819
(dropdownClosed)="searchControl.setValue('')"
1920
(dropdownOpened)="onDropdownOpen()"
2021
>
@@ -32,10 +33,12 @@
3233

3334
@if (filterBar.isChanged && !filterBar.isSaved) {
3435
<button
36+
#saveNewFilterButton
3537
kbqTooltip="{{ localeData.saveNewFilterTooltip }}"
3638
kbq-button
3739
kbqFilterBarButton
3840
class="kbq-button_action"
41+
[class.kbq-active]="focusedElementBeforeIs(saveNewFilterButton) && opened"
3942
[color]="colors.Empty"
4043
[disabled]="isSaving"
4144
(click)="openSaveAsNewFilterPopover()"
@@ -46,12 +49,14 @@
4649

4750
@if (filterBar.isSaved) {
4851
<button
52+
#filterActionsButton
4953
kbq-button
5054
kbqFilterBarButton
5155
class="kbq-button_action"
5256
[color]="colors.Empty"
5357
[kbqDropdownTriggerFor]="filterActions"
5458
[disabled]="isSaving"
59+
[class.kbq-active]="focusedElementBeforeIs(filterActionsButton) && filterActionsOpened"
5560
[ngClass]="{ 'kbq-button_changed-saved-filter': filterBar.isSavedAndChanged }"
5661
>
5762
<i kbq-icon="kbq-ellipsis-vertical_16"></i>
@@ -142,7 +147,7 @@
142147
</button>
143148
}
144149
@if (!filterBar.isReadOnly) {
145-
<button kbq-dropdown-item (click)="this.onRemoveFilter.next(this.filter!)">
150+
<button kbq-dropdown-item (click)="removeFilter()">
146151
<i kbq-icon="kbq-trash_16"></i>
147152
{{ localeData.remove }}
148153
</button>
@@ -191,7 +196,7 @@
191196
>
192197
{{ localeData.saveButton }}
193198
</button>
194-
<button kbq-button [color]="'contrast-fade'" [kbqStyle]="'filled'" [disabled]="isSaving" (click)="closePopover()">
199+
<button kbq-button [color]="'contrast-fade'" [kbqStyle]="'filled'" [disabled]="isSaving" (click)="popover.hide()">
195200
{{ localeData.cancelButton }}
196201
</button>
197202
</ng-template>

packages/components/filter-bar/filters.ts

Lines changed: 79 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
import { FocusMonitor, FocusOrigin } from '@angular/cdk/a11y';
12
import { AsyncPipe, NgClass } from '@angular/common';
23
import {
34
ChangeDetectionStrategy,
45
ChangeDetectorRef,
56
Component,
7+
DestroyRef,
68
ElementRef,
79
EventEmitter,
810
inject,
@@ -12,6 +14,7 @@ import {
1214
ViewChild,
1315
ViewEncapsulation
1416
} from '@angular/core';
17+
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
1518
import { FormControl, FormsModule, ReactiveFormsModule, UntypedFormControl, Validators } from '@angular/forms';
1619
import { KbqAlertModule } from '@koobiq/components/alert';
1720
import { KbqButton, KbqButtonModule, KbqButtonStyles } from '@koobiq/components/button';
@@ -60,6 +63,12 @@ import { KbqFilter, KbqSaveFilterError, KbqSaveFilterEvent, KbqSaveFilterStatuse
6063
}
6164
})
6265
export class KbqFilters implements OnInit {
66+
/** @docs-private */
67+
protected readonly elementRef = inject(ElementRef);
68+
/** @docs-private */
69+
protected readonly destroyRef = inject(DestroyRef);
70+
/** @docs-private */
71+
protected readonly focusMonitor = inject(FocusMonitor);
6372
/** @docs-private */
6473
protected readonly placements = PopUpPlacements;
6574
/** @docs-private */
@@ -73,9 +82,20 @@ export class KbqFilters implements OnInit {
7382
/** @docs-private */
7483
private readonly changeDetectorRef = inject(ChangeDetectorRef);
7584

76-
@ViewChild(KbqButton) private button: KbqButton;
77-
@ViewChild(KbqPopoverTrigger) private popover: KbqPopoverTrigger;
78-
@ViewChild(KbqDropdownTrigger) private dropdown: KbqDropdownTrigger;
85+
/** @docs-private */
86+
@ViewChild('mainButton') protected mainButton: KbqButton;
87+
/** @docs-private */
88+
@ViewChild('saveNewFilterButton') protected saveNewFilterButton: KbqButton;
89+
/** @docs-private */
90+
@ViewChild('filterActionsButton') protected filterActionsButton: KbqButton;
91+
92+
/** @docs-private */
93+
@ViewChild(KbqPopoverTrigger) protected popover: KbqPopoverTrigger;
94+
/** @docs-private */
95+
@ViewChild(KbqDropdownTrigger) protected dropdown: KbqDropdownTrigger;
96+
/** @docs-private */
97+
@ViewChild('filterActionsButton') protected filterActionsDropdown: KbqDropdownTrigger;
98+
7999
@ViewChild('search') private search: ElementRef;
80100
@ViewChild('newFilterName') private newFilterName: ElementRef;
81101
@ViewChild('saveFilterButton') private saveFilterButton: KbqButton;
@@ -125,6 +145,11 @@ export class KbqFilters implements OnInit {
125145
return this.popover?.isOpen || this.dropdown?.opened;
126146
}
127147

148+
/** Component state. true if opened dropdown or popup of filterActions */
149+
get filterActionsOpened(): boolean {
150+
return this.popover?.isOpen || this.filterActionsDropdown?.opened;
151+
}
152+
128153
/** Selected filter */
129154
get filter(): KbqFilter | null {
130155
return this.filterBar.filter;
@@ -141,6 +166,16 @@ export class KbqFilters implements OnInit {
141166
return this.filterBar.configuration.filters;
142167
}
143168

169+
/** Current focus origin state.
170+
* @docs-private */
171+
get focusOrigin(): FocusOrigin {
172+
return this._focusOrigin;
173+
}
174+
175+
private _focusOrigin: FocusOrigin = null;
176+
177+
private focusedElementBeforeOpen: KbqButton | null;
178+
144179
constructor() {
145180
this.filterBar.changes.subscribe(() => this.changeDetectorRef.markForCheck());
146181
}
@@ -150,6 +185,19 @@ export class KbqFilters implements OnInit {
150185
of(this.filters),
151186
this.searchControl.valueChanges.pipe(map((value) => this.getFilteredOptions(value)))
152187
);
188+
189+
this.focusMonitor
190+
.monitor(this.elementRef, true)
191+
.pipe(
192+
filter((origin) => !!origin),
193+
takeUntilDestroyed(this.destroyRef)
194+
)
195+
.subscribe((origin) => (this._focusOrigin = origin));
196+
}
197+
198+
/** @docs-private */
199+
focusedElementBeforeIs(button: KbqButton): boolean {
200+
return this.focusedElementBeforeOpen === button;
153201
}
154202

155203
selectFilter(filter: KbqFilter) {
@@ -210,7 +258,10 @@ export class KbqFilters implements OnInit {
210258
}
211259

212260
restoreFocus() {
213-
this.button.focus();
261+
if (this.focusedElementBeforeOpen && !this.focusedElementBeforeOpen.disabled) {
262+
this.focusMonitor.focusVia(this.focusedElementBeforeOpen.elementRef, this.focusOrigin);
263+
this.focusedElementBeforeOpen = null;
264+
}
214265
}
215266

216267
preparePopover() {
@@ -221,11 +272,19 @@ export class KbqFilters implements OnInit {
221272
this.popover.show();
222273

223274
merge(...this.popover.defaultClosingActions())
224-
.pipe(filter(() => !this.isSaving))
275+
.pipe(
276+
filter(() => !this.isSaving),
277+
takeUntilDestroyed(this.popover.instanceDestroyRef)
278+
)
279+
.subscribe(() => this.closePopover(false));
280+
281+
this.popover.visibleChange
282+
.pipe(
283+
filter((state) => !state),
284+
takeUntilDestroyed(this.popover.instanceDestroyRef)
285+
)
225286
.subscribe(this.closePopover);
226287

227-
this.popover.visibleChange.pipe(filter((state) => !state)).subscribe(this.closePopover);
228-
229288
setTimeout(() => {
230289
this.newFilterName.nativeElement.focus();
231290
this.filterName.setErrors(null);
@@ -244,10 +303,15 @@ export class KbqFilters implements OnInit {
244303
this.preparePopover();
245304
}
246305

247-
closePopover = () => {
306+
/** @docs-private */
307+
saveFocusedElement(button?: KbqButton) {
308+
this.focusedElementBeforeOpen = button || null;
309+
}
310+
311+
closePopover = (restoreFocus: boolean = true) => {
248312
this.popover.hide();
249313

250-
this.restoreFocus();
314+
if (restoreFocus) this.restoreFocus();
251315

252316
setTimeout(() => this.changeDetectorRef.detectChanges());
253317

@@ -280,6 +344,12 @@ export class KbqFilters implements OnInit {
280344
this.onResetFilterChanges.emit(this.filter!);
281345
}
282346

347+
removeFilter() {
348+
this.onRemoveFilter.next(this.filter!);
349+
350+
setTimeout(() => this.focusMonitor.focusVia(this.mainButton.elementRef, this.focusOrigin), 0);
351+
}
352+
283353
/** Hide the popup and restore focus.
284354
* Use this method in the onSave, onSaveAsNew, or onChangeFilter events after the data has been successfully saved. */
285355
filterSavedSuccessfully() {

packages/components/popover/popover.component.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
AfterViewInit,
1515
ChangeDetectionStrategy,
1616
Component,
17+
DestroyRef,
1718
Directive,
1819
ElementRef,
1920
EventEmitter,
@@ -374,6 +375,11 @@ export class KbqPopoverTrigger extends KbqPopUpTrigger<KbqPopoverComponent> impl
374375
return this.trigger.includes(PopUpTriggers.Click);
375376
}
376377

378+
/** @docs-private */
379+
get instanceDestroyRef(): DestroyRef {
380+
return this.instance.destroyRef;
381+
}
382+
377383
@Input() backdropClass: string = 'cdk-overlay-transparent-backdrop';
378384

379385
// @TODO add realization for arrow (#DS-2514)

packages/components/title/title.directive.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export class KbqTitleDirective extends KbqTooltipTrigger implements AfterViewIni
4444
/** For special cases where the difference is a fraction of a pixel */
4545
if (
4646
!this.isVerticalOverflown &&
47-
(this.child.scrollWidth === 0 || this.parent.offsetWidth === this.child.scrollWidth)
47+
(this.child.scrollWidth === 0 || this.parent?.offsetWidth === this.child.scrollWidth)
4848
) {
4949
if (this.hasOnlyText) {
5050
const wrapper = this.renderer.createElement('span');
@@ -66,7 +66,7 @@ export class KbqTitleDirective extends KbqTooltipTrigger implements AfterViewIni
6666
}
6767

6868
get isHorizontalOverflown(): boolean {
69-
return this.parent.offsetWidth < this.child.scrollWidth;
69+
return this.parent?.offsetWidth < this.child.scrollWidth;
7070
}
7171

7272
get isVerticalOverflown(): boolean {

tools/public_api_guard/components/filter-bar.api.md

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import { DestroyRef } from '@angular/core';
1212
import { ElementRef } from '@angular/core';
1313
import { EventEmitter } from '@angular/core';
1414
import { FlatTreeControl } from '@koobiq/components/tree';
15+
import { FocusMonitor } from '@angular/cdk/a11y';
16+
import { FocusOrigin } from '@angular/cdk/a11y';
1517
import { FormControl } from '@angular/forms';
1618
import { FormGroup } from '@angular/forms';
1719
import * as i0 from '@angular/core';
@@ -21,6 +23,7 @@ import { InputSignalWithTransform } from '@angular/core';
2123
import { KbqButton } from '@koobiq/components/button';
2224
import { KbqButtonStyles } from '@koobiq/components/button';
2325
import { KbqComponentColors } from '@koobiq/components/core';
26+
import { KbqDropdownTrigger } from '@koobiq/components/dropdown';
2427
import { KbqListSelection } from '@koobiq/components/list';
2528
import { KbqLocaleService } from '@koobiq/components/core';
2629
import { KbqOption } from '@koobiq/components/core';
@@ -210,6 +213,8 @@ export class KbqFilterBar {
210213
// @public (undocumented)
211214
export class KbqFilterBarButton {
212215
constructor();
216+
protected readonly filters: KbqFilters;
217+
saveFocusedElement(): void;
213218
// (undocumented)
214219
static ɵdir: i0.ɵɵDirectiveDeclaration<KbqFilterBarButton, "[kbqFilterBarButton]", never, {}, {}, never, never, true, never>;
215220
// (undocumented)
@@ -289,9 +294,15 @@ export class KbqFilterReset {
289294
export class KbqFilters implements OnInit {
290295
constructor();
291296
// (undocumented)
292-
closePopover: () => void;
297+
closePopover: (restoreFocus?: boolean) => void;
293298
protected readonly colors: typeof KbqComponentColors;
299+
protected readonly destroyRef: DestroyRef;
300+
protected dropdown: KbqDropdownTrigger;
301+
protected readonly elementRef: ElementRef<any>;
294302
get filter(): KbqFilter | null;
303+
protected filterActionsButton: KbqButton;
304+
protected filterActionsDropdown: KbqDropdownTrigger;
305+
get filterActionsOpened(): boolean;
295306
protected readonly filterBar: KbqFilterBar;
296307
filteredOptions: Observable<KbqFilter[]>;
297308
filterName: FormControl<string | null>;
@@ -301,10 +312,14 @@ export class KbqFilters implements OnInit {
301312
filterSavedUnsuccessfully(error?: KbqSaveFilterError): void;
302313
// (undocumented)
303314
filterSavingErrorText: string;
315+
focusedElementBeforeIs(button: KbqButton): boolean;
316+
protected readonly focusMonitor: FocusMonitor;
317+
get focusOrigin(): FocusOrigin;
304318
get isEmpty(): boolean;
305319
// (undocumented)
306320
isSaving: boolean;
307321
get localeData(): any;
322+
protected mainButton: KbqButton;
308323
// (undocumented)
309324
ngOnInit(): void;
310325
readonly onChangeFilter: EventEmitter<KbqSaveFilterEvent>;
@@ -321,19 +336,24 @@ export class KbqFilters implements OnInit {
321336
// (undocumented)
322337
openSaveAsNewFilterPopover(): void;
323338
protected readonly placements: typeof PopUpPlacements;
339+
protected popover: KbqPopoverTrigger;
324340
get popoverHeader(): string;
325341
popoverSize: PopUpSizes;
326342
// (undocumented)
327343
preparePopover(): void;
328344
// (undocumented)
345+
removeFilter(): void;
346+
// (undocumented)
329347
resetFilterChanges(): void;
330348
// (undocumented)
331349
restoreFocus(): void;
332350
// (undocumented)
333351
saveAsNew(event?: Event): void;
334352
// (undocumented)
335353
saveChanges(): void;
354+
saveFocusedElement(button?: KbqButton): void;
336355
saveNewFilter: boolean;
356+
protected saveNewFilterButton: KbqButton;
337357
searchControl: UntypedFormControl;
338358
searchKeydownHandler(event: KeyboardEvent): void;
339359
// (undocumented)

0 commit comments

Comments
 (0)