diff --git a/projects/addon-editor/components/toolbar/toolbar-navigation-manager.directive.ts b/projects/addon-editor/components/toolbar/toolbar-navigation-manager.directive.ts index 29ec732ff614..73a15bb0d9f3 100644 --- a/projects/addon-editor/components/toolbar/toolbar-navigation-manager.directive.ts +++ b/projects/addon-editor/components/toolbar/toolbar-navigation-manager.directive.ts @@ -50,7 +50,7 @@ export class TuiToolbarNavigationManagerDirective { for (const el of tools) { const focusableElement = tuiIsNativeMouseFocusable(el) ? el - : tuiGetClosestFocusable(el, el, false, false); + : tuiGetClosestFocusable({initial: el, root: el, keyboard: false}); if (focusableElement) { return focusableElement; @@ -65,22 +65,30 @@ export class TuiToolbarNavigationManagerDirective { return wrapper; } - const lookedInside = tuiGetClosestFocusable(wrapper, wrapper, false, false); + const lookedInside = tuiGetClosestFocusable({ + initial: wrapper, + root: wrapper, + keyboard: false, + }); return ( lookedInside || - tuiGetClosestFocusable(wrapper, this.elementRef.nativeElement, true, false) + tuiGetClosestFocusable({ + initial: wrapper, + root: this.elementRef.nativeElement, + previous: true, + keyboard: false, + }) ); } private findNextTool(wrapper: HTMLElement): HTMLElement | null { return tuiIsNativeMouseFocusable(wrapper) ? wrapper - : tuiGetClosestFocusable( - wrapper, - this.elementRef.nativeElement, - false, - false, - ); + : tuiGetClosestFocusable({ + initial: wrapper, + root: this.elementRef.nativeElement, + keyboard: false, + }); } } diff --git a/projects/cdk/directives/focus-trap/focus-trap.directive.ts b/projects/cdk/directives/focus-trap/focus-trap.directive.ts index 94ce6e29fc9a..b68848d559ec 100644 --- a/projects/cdk/directives/focus-trap/focus-trap.directive.ts +++ b/projects/cdk/directives/focus-trap/focus-trap.directive.ts @@ -53,7 +53,10 @@ export class TuiFocusTrapDirective implements OnDestroy { return; } - const focusable = tuiGetClosestFocusable(nativeElement, nativeElement); + const focusable = tuiGetClosestFocusable({ + initial: nativeElement, + root: nativeElement, + }); if (focusable) { focusable.focus(); diff --git a/projects/cdk/schematics/ng-update/constants/deprecated-functions.ts b/projects/cdk/schematics/ng-update/constants/deprecated-functions.ts index 2866b66f28b7..842148c886cc 100644 --- a/projects/cdk/schematics/ng-update/constants/deprecated-functions.ts +++ b/projects/cdk/schematics/ng-update/constants/deprecated-functions.ts @@ -318,7 +318,7 @@ export const DEPRECATED_FUNCTIONS: readonly TypeToRename[] = [ }, { from: 'getClosestKeyboardFocusable', - to: 'tuiGetClosestKeyboardFocusable', + to: 'tuiGetClosestFocusable', moduleSpecifier: ['@taiga-ui/cdk'], }, { diff --git a/projects/cdk/schematics/ng-update/steps/replace-functions.ts b/projects/cdk/schematics/ng-update/steps/replace-functions.ts index 81a69f4f7006..4841274c9677 100644 --- a/projects/cdk/schematics/ng-update/steps/replace-functions.ts +++ b/projects/cdk/schematics/ng-update/steps/replace-functions.ts @@ -17,8 +17,9 @@ export function replaceFunctions() { replaceFallbackValue(getNamedImportReferences('fallbackValue', '@taiga-ui/cdk')); replaceCustomEvent(getNamedImportReferences('tuiCustomEvent', '@taiga-ui/cdk')); replaceClosestElement(getNamedImportReferences('getClosestElement', '@taiga-ui/cdk')); - modifyFormatNumberArgs(); replaceDeprecatedFunction(); + modifyFormatNumberArgs(); + modifyClosestFocusable(); successLog(`${SMALL_TAB_SYMBOL}${SUCCESS_SYMBOL} functions replaced \n`); } @@ -136,3 +137,22 @@ function modifyFormatNumberArgs(): void { } }); } + +function modifyClosestFocusable(): void { + getNamedImportReferences('tuiGetClosestFocusable', '@taiga-ui/cdk') + .map(ref => ref.getParent()) + .filter(Node.isCallExpression) + .forEach(fn => { + const args = fn.getArguments(); + + if (args.length > 1) { + const [initial, prev = false, root, keyboard = true] = args.map(arg => + arg.getText(), + ); + + fn.replaceWithText( + `tuiGetClosestFocusable({initial: ${initial}, root: ${root}, previous: ${prev}, keyboard: ${keyboard}})`, + ); + } + }); +} diff --git a/projects/cdk/schematics/ng-update/tests/schematic-replace-consts.spec.ts b/projects/cdk/schematics/ng-update/tests/schematic-replace-consts.spec.ts index 1ad2fe954f5d..6b3aacfc635e 100644 --- a/projects/cdk/schematics/ng-update/tests/schematic-replace-consts.spec.ts +++ b/projects/cdk/schematics/ng-update/tests/schematic-replace-consts.spec.ts @@ -37,7 +37,7 @@ export class AppComponent extends AbstractTuiController { control = new FormControl('', [Validators.nullValidator]); onMouseDown(event: MouseEvent, target: HTMLElement) { - if (tuiGetClosestFocusable(target, 'button')) { + if (tuiGetClosestFocusable({initial: target, root: 'button', previous: false, keyboard: true})) { return null; } } @@ -87,7 +87,7 @@ export class AppComponent extends TuiController { control = new FormControl('', [EMPTY_VALIDATOR]); onMouseDown(event: MouseEvent, target: HTMLElement) { - if (tuiGetClosestFocusable(target, 'button')) { + if (tuiGetClosestFocusable(target, false, 'button')) { return null; } } diff --git a/projects/cdk/schematics/ng-update/tests/schematic-replace-functions.spec.ts b/projects/cdk/schematics/ng-update/tests/schematic-replace-functions.spec.ts index 8254a6f85754..02b676e5b583 100644 --- a/projects/cdk/schematics/ng-update/tests/schematic-replace-functions.spec.ts +++ b/projects/cdk/schematics/ng-update/tests/schematic-replace-functions.spec.ts @@ -14,6 +14,7 @@ import {createAngularJson} from '../../utils/create-angular-json'; const collectionPath = join(__dirname, '../../migration.json'); const AFTER = `import {Component} from '@angular/core'; +import { tuiGetClosestFocusable } from '@taiga-ui/cdk'; import { TUI_NUMBER_FORMAT, tuiFormatNumber, @@ -26,6 +27,11 @@ tuiFormatNumber(123.45, {decimalLimit: 3, decimalSeparator: '.', thousandSeparat tuiFormatNumber(12345.67, {decimalLimit: 4, decimalSeparator: ',', thousandSeparator: '.', zeroPadding: true}); tuiFormatNumber(27, {decimalLimit: 5, decimalSeparator: ',', thousandSeparator: '.', zeroPadding: false}); +tuiGetClosestFocusable({initial: el, root: el, previous: false, keyboard: false}); +tuiGetClosestFocusable({initial: host, root: root, previous: previous, keyboard: true}); +tuiGetClosestFocusable({initial: button, root: wrapper, previous: prev, keyboard: true}); +tuiGetClosestFocusable({initial: host, root: host, previous: true, keyboard: true}); + const dynamicDecimalLimit = Math.random() > 0.5; const decimalSeparatorVariable = ','; const thousandSeparatorVariable = '_'; @@ -49,6 +55,13 @@ export class AppComponent extends AbstractTuiController { return String(this.day).padStart(2, '0'); } + test(): void { + tuiGetClosestFocusable({initial: this.host, root: this.elementRef.nativeElement, previous: false, keyboard: true}); + tuiGetClosestFocusable({initial: wrapper, root: this.elementRef.nativeElement, previous: true, keyboard: false}); + const focusable = tuiGetClosestFocusable({initial: this.elementRef.nativeElement, root: this.elementRef.nativeElement, previous: false, keyboard: true}); + const focusable = tuiGetClosestFocusable({initial: initial, root: this.wrapper.nativeElement, previous: !first, keyboard: true}); + } + constructor(@Inject(TUI_NUMBER_FORMAT) private readonly numberFormat: TuiNumberFormatSettings) {} private hasClosest(suitableNode: any, selector: string): void { @@ -67,7 +80,7 @@ const event = new CustomEvent("hello", { `; const BEFORE = `import {Component} from '@angular/core'; -import { fallbackValue, tuiCustomEvent, getClosestElement, padStart } from '@taiga-ui/cdk'; +import { fallbackValue, tuiCustomEvent, getClosestElement, padStart, tuiGetClosestFocusable } from '@taiga-ui/cdk'; import { TUI_NUMBER_FORMAT, tuiFormatNumber, @@ -80,6 +93,11 @@ tuiFormatNumber(123.45, 3, '.'); tuiFormatNumber(12345.67, 4, ',', '.'); tuiFormatNumber(27, 5, ',', '.', false); +tuiGetClosestFocusable(el, false, el, false); +tuiGetClosestFocusable(host, previous, root); +tuiGetClosestFocusable(button, prev, wrapper); +tuiGetClosestFocusable(host, true, host, true); + const dynamicDecimalLimit = Math.random() > 0.5; const decimalSeparatorVariable = ','; const thousandSeparatorVariable = '_'; @@ -108,6 +126,21 @@ export class AppComponent extends AbstractTuiController { return padStart(String(this.day), 2, '0'); } + test(): void { + tuiGetClosestFocusable(this.host, false, this.elementRef.nativeElement); + tuiGetClosestFocusable(wrapper, true, this.elementRef.nativeElement, false); + const focusable = tuiGetClosestFocusable( + this.elementRef.nativeElement, + false, + this.elementRef.nativeElement, + ); + const focusable = tuiGetClosestFocusable( + initial, + !first, + this.wrapper.nativeElement, + ); + } + constructor(@Inject(TUI_NUMBER_FORMAT) private readonly numberFormat: TuiNumberFormatSettings) {} private hasClosest(suitableNode: any, selector: string): void { diff --git a/projects/cdk/utils/focus/get-closest-keyboard-focusable.ts b/projects/cdk/utils/focus/get-closest-focusable.ts similarity index 51% rename from projects/cdk/utils/focus/get-closest-keyboard-focusable.ts rename to projects/cdk/utils/focus/get-closest-focusable.ts index 724228e0e63d..c69a5caae98d 100644 --- a/projects/cdk/utils/focus/get-closest-keyboard-focusable.ts +++ b/projects/cdk/utils/focus/get-closest-focusable.ts @@ -4,21 +4,42 @@ import {tuiIsHTMLElement} from '@taiga-ui/cdk/utils/dom'; import {tuiIsNativeKeyboardFocusable} from './is-native-keyboard-focusable'; import {tuiIsNativeMouseFocusable} from './is-native-mouse-focusable'; +export interface TuiGetClosestFocusableOptions { + /** + * @description: + * current HTML element + */ + initial: Element; + + /** + * @description: + * top Node limiting the search area + */ + root: Node; + + /** + * @description: + * should it look backwards instead (find item that will be focused with Shift + Tab) + */ + previous?: boolean; + + /** + * @description: + * determine if only keyboard focus is of interest + */ + keyboard?: boolean; +} + /** + * @description: * Finds the closest element that can be focused with a keyboard or mouse in theory - * - * @param initial current HTML element - * @param prev should it look backwards instead (find item that will be focused with Shift + Tab) - * @param root top Node limiting the search area - * @param keyboard determine if only keyboard focus is of interest - * */ -export function tuiGetClosestFocusable( - initial: Element, - root: Node, - prev: boolean = false, - keyboard: boolean = true, -): HTMLElement | null { +export function tuiGetClosestFocusable({ + initial, + root, + previous = false, + keyboard = true, +}: TuiGetClosestFocusableOptions): HTMLElement | null { if (!root.ownerDocument) { return null; } @@ -32,12 +53,12 @@ export function tuiGetClosestFocusable( treeWalker.currentNode = initial; - while (prev ? treeWalker.previousNode() : treeWalker.nextNode()) { + while (previous ? treeWalker.previousNode() : treeWalker.nextNode()) { if (tuiIsHTMLElement(treeWalker.currentNode)) { initial = treeWalker.currentNode; } - if (check(initial) && tuiIsHTMLElement(initial)) { + if (tuiIsHTMLElement(initial) && check(initial)) { return initial; } } diff --git a/projects/cdk/utils/focus/index.ts b/projects/cdk/utils/focus/index.ts index 3b157055b12b..05e189b2dd5a 100644 --- a/projects/cdk/utils/focus/index.ts +++ b/projects/cdk/utils/focus/index.ts @@ -1,5 +1,5 @@ export * from './blur-native-focused'; -export * from './get-closest-keyboard-focusable'; +export * from './get-closest-focusable'; export * from './get-native-focused'; export * from './is-native-focused'; export * from './is-native-focused-in'; diff --git a/projects/cdk/utils/focus/tests/get-closest-keyboard-focusable.spec.ts b/projects/cdk/utils/focus/tests/get-closest-keyboard.spec.ts similarity index 73% rename from projects/cdk/utils/focus/tests/get-closest-keyboard-focusable.spec.ts rename to projects/cdk/utils/focus/tests/get-closest-keyboard.spec.ts index 4fe886699265..bc76cdff5b0f 100644 --- a/projects/cdk/utils/focus/tests/get-closest-keyboard-focusable.spec.ts +++ b/projects/cdk/utils/focus/tests/get-closest-keyboard.spec.ts @@ -1,11 +1,11 @@ -import {tuiGetClosestFocusable} from '../get-closest-keyboard-focusable'; +import {tuiGetClosestFocusable} from '@taiga-ui/cdk'; -describe(`getClosestKeyboardFocusable`, () => { +describe(`tuiGetClosestFocusable`, () => { it(`returns null if root has no document`, () => { const root: Node = {} as unknown as Node; const divElement = document.createElement(`div`); - expect(tuiGetClosestFocusable(divElement, root)).toBe(null); + expect(tuiGetClosestFocusable({initial: divElement, root})).toBe(null); }); it(`returns closest focusable going backwards`, () => { @@ -17,7 +17,9 @@ describe(`getClosestKeyboardFocusable`, () => { root.appendChild(divElement); document.body.appendChild(root); - expect(tuiGetClosestFocusable(divElement, root, true)).toBe(buttonElement); + expect(tuiGetClosestFocusable({initial: divElement, root, previous: true})).toBe( + buttonElement, + ); document.body.removeChild(root); }); @@ -31,7 +33,7 @@ describe(`getClosestKeyboardFocusable`, () => { root.appendChild(buttonElement); document.body.appendChild(root); - expect(tuiGetClosestFocusable(divElement, root)).toBe(buttonElement); + expect(tuiGetClosestFocusable({initial: divElement, root})).toBe(buttonElement); document.body.removeChild(root); }); @@ -43,7 +45,7 @@ describe(`getClosestKeyboardFocusable`, () => { root.appendChild(divElement); document.body.appendChild(root); - expect(tuiGetClosestFocusable(divElement, root)).toBe(null); + expect(tuiGetClosestFocusable({initial: divElement, root})).toBe(null); document.body.removeChild(root); }); diff --git a/projects/core/components/hosted-dropdown/hosted-dropdown.component.ts b/projects/core/components/hosted-dropdown/hosted-dropdown.component.ts index fff1ef2d5a8e..841042fc4a77 100644 --- a/projects/core/components/hosted-dropdown/hosted-dropdown.component.ts +++ b/projects/core/components/hosted-dropdown/hosted-dropdown.component.ts @@ -113,7 +113,10 @@ export class TuiHostedDropdownComponent implements TuiFocusableElementAccessor { get nativeFocusableElement(): TuiNativeFocusableElement | null { return tuiIsNativeKeyboardFocusable(this.host) ? this.host - : tuiGetClosestFocusable(this.host, this.elementRef.nativeElement); + : tuiGetClosestFocusable({ + initial: this.host, + root: this.elementRef.nativeElement, + }); } @HostBinding(`class._hosted_dropdown_focused`) @@ -225,11 +228,11 @@ export class TuiHostedDropdownComponent implements TuiFocusableElementAccessor { const initial = first ? this.wrapper.nativeElement : this.wrapper.nativeElement.nextElementSibling; - const focusable = tuiGetClosestFocusable( + const focusable = tuiGetClosestFocusable({ initial, - this.wrapper.nativeElement, - !first, - ); + root: this.wrapper.nativeElement, + previous: !first, + }); if (!focusable) { return; diff --git a/projects/core/directives/dropdown/dropdown.component.ts b/projects/core/directives/dropdown/dropdown.component.ts index 20547d595502..6452c1c9ff3a 100644 --- a/projects/core/directives/dropdown/dropdown.component.ts +++ b/projects/core/directives/dropdown/dropdown.component.ts @@ -127,10 +127,10 @@ export class TuiDropdownComponent { const host = document.createElement(`div`); const {ownerDocument} = host; const root = ownerDocument ? ownerDocument.body : host; - let focusable = tuiGetClosestFocusable(host, root, previous); + let focusable = tuiGetClosestFocusable({initial: host, root, previous}); while (focusable !== null && host.contains(focusable)) { - focusable = tuiGetClosestFocusable(focusable, root, previous); + focusable = tuiGetClosestFocusable({initial: focusable, root, previous}); } focusable?.focus(); diff --git a/projects/kit/components/tabs/tabs-with-more/tabs-with-more.component.ts b/projects/kit/components/tabs/tabs-with-more/tabs-with-more.component.ts index 154059da7258..5143a1bb7f25 100644 --- a/projects/kit/components/tabs/tabs-with-more/tabs-with-more.component.ts +++ b/projects/kit/components/tabs/tabs-with-more/tabs-with-more.component.ts @@ -179,9 +179,9 @@ export class TuiTabsWithMoreComponent implements AfterViewInit { } } - onWrapperArrow(event: Event, wrapper: HTMLElement, prev: boolean): void { + onWrapperArrow(event: Event, wrapper: HTMLElement, previous: boolean): void { const button: HTMLButtonElement = event.target as HTMLButtonElement; - const target = tuiGetClosestFocusable(button, wrapper, prev); + const target = tuiGetClosestFocusable({initial: button, root: wrapper, previous}); if (target) { target.focus(); diff --git a/projects/kit/directives/data-list-dropdown-manager/data-list-dropdown-manager.directive.ts b/projects/kit/directives/data-list-dropdown-manager/data-list-dropdown-manager.directive.ts index daae12285f90..f941a9c86f9a 100644 --- a/projects/kit/directives/data-list-dropdown-manager/data-list-dropdown-manager.directive.ts +++ b/projects/kit/directives/data-list-dropdown-manager/data-list-dropdown-manager.directive.ts @@ -154,8 +154,11 @@ export class TuiDataListDropdownManagerDirective implements AfterViewInit { } // First item is focus trap - const focusTrap = tuiGetClosestFocusable(content, content); - const item = tuiGetClosestFocusable(focusTrap || content, content); + const focusTrap = tuiGetClosestFocusable({initial: content, root: content}); + const item = tuiGetClosestFocusable({ + initial: focusTrap || content, + root: content, + }); if (item) { item.focus();