diff --git a/src/aria/private/tree/tree.ts b/src/aria/private/tree/tree.ts index eacc7326cef8..eb75d69d43a6 100644 --- a/src/aria/private/tree/tree.ts +++ b/src/aria/private/tree/tree.ts @@ -168,6 +168,9 @@ export interface TreeInputs extends Omit, V>, ' /** The aria-current type. */ currentType: SignalLike<'page' | 'step' | 'location' | 'date' | 'time' | 'true' | 'false'>; + + /** The text direction of the tree. */ + textDirection: SignalLike<'ltr' | 'rtl'>; } export interface TreePattern extends TreeInputs {} @@ -226,7 +229,8 @@ export class TreePattern { if (this.inputs.orientation() === 'horizontal') { return 'ArrowUp'; } - return this.inputs.textDirection() === 'rtl' ? 'ArrowRight' : 'ArrowLeft'; + const isRtl = this.inputs.textDirection() === 'rtl'; + return isRtl ? 'ArrowRight' : 'ArrowLeft'; }); /** The key for expanding an item or moving to its first child. */ @@ -234,7 +238,8 @@ export class TreePattern { if (this.inputs.orientation() === 'horizontal') { return 'ArrowDown'; } - return this.inputs.textDirection() === 'rtl' ? 'ArrowLeft' : 'ArrowRight'; + const isRtl = this.inputs.textDirection() === 'rtl'; + return isRtl ? 'ArrowLeft' : 'ArrowRight'; }); /** Represents the space key. Does nothing when the user is actively using typeahead. */ @@ -360,9 +365,6 @@ export class TreePattern { /** The orientation of the tree. */ orientation: SignalLike<'vertical' | 'horizontal'>; - /** The text direction of the tree. */ - textDirection: SignalLike<'ltr' | 'rtl'>; - /** Whether multiple items can be selected at the same time. */ multi: SignalLike; @@ -386,7 +388,6 @@ export class TreePattern { this.softDisabled = inputs.softDisabled; this.wrap = inputs.wrap; this.orientation = inputs.orientation; - this.textDirection = inputs.textDirection; this.multi = computed(() => (this.nav() ? false : this.inputs.multi())); this.selectionMode = inputs.selectionMode; this.typeaheadDelay = inputs.typeaheadDelay; diff --git a/src/aria/tree/tree.ts b/src/aria/tree/tree.ts index 6b876da5263c..44938746f529 100644 --- a/src/aria/tree/tree.ts +++ b/src/aria/tree/tree.ts @@ -22,6 +22,7 @@ import { untracked, afterNextRender, } from '@angular/core'; +import {toSignal} from '@angular/core/rxjs-interop'; import {_IdGenerator} from '@angular/cdk/a11y'; import {Directionality} from '@angular/cdk/bidi'; import { @@ -131,8 +132,13 @@ export class Tree { /** Selected item values. */ readonly value = model([]); + /** The directionality (LTR / RTL) context for the application. */ + private readonly _directionality = inject(Directionality); + /** Text direction. */ - readonly textDirection = inject(Directionality).valueSignal; + readonly textDirection = toSignal(this._directionality.change, { + initialValue: this._directionality.value, + }); /** Whether the tree is in navigation mode. */ readonly nav = input(false); @@ -158,6 +164,7 @@ export class Tree { activeItem: signal | undefined>(undefined), element: () => this._elementRef.nativeElement, combobox: () => this._popup?.combobox?._pattern, + textDirection: this.textDirection, }; this._pattern = this._popup?.combobox diff --git a/src/components-examples/aria/tree/BUILD.bazel b/src/components-examples/aria/tree/BUILD.bazel index cd934741369b..028e1bb51979 100644 --- a/src/components-examples/aria/tree/BUILD.bazel +++ b/src/components-examples/aria/tree/BUILD.bazel @@ -14,6 +14,7 @@ ng_project( "//:node_modules/@angular/core", "//:node_modules/@angular/forms", "//src/aria/tree", + "//src/cdk/a11y", "//src/material/checkbox", "//src/material/form-field", "//src/material/icon", diff --git a/src/components-examples/aria/tree/index.ts b/src/components-examples/aria/tree/index.ts index d017046af3c0..04196c49716e 100644 --- a/src/components-examples/aria/tree/index.ts +++ b/src/components-examples/aria/tree/index.ts @@ -6,5 +6,6 @@ export {TreeDisabledSkippedExample} from './tree-disabled-skipped/tree-disabled- export {TreeMultiSelectExample} from './tree-multi-select/tree-multi-select-example'; export {TreeMultiSelectFollowFocusExample} from './tree-multi-select-follow-focus/tree-multi-select-follow-focus-example'; export {TreeNavExample} from './tree-nav/tree-nav-example'; +export {TreeRtlActiveDescendantExample} from './tree-rtl-active-descendant/tree-rtl-active-descendant-example'; export {TreeSingleSelectExample} from './tree-single-select/tree-single-select-example'; export {TreeSingleSelectFollowFocusExample} from './tree-single-select-follow-focus/tree-single-select-follow-focus-example'; diff --git a/src/components-examples/aria/tree/tree-common.css b/src/components-examples/aria/tree/tree-common.css index 643fb5ab385d..a6ea2a7d3c61 100644 --- a/src/components-examples/aria/tree/tree-common.css +++ b/src/components-examples/aria/tree/tree-common.css @@ -44,6 +44,14 @@ transform: rotate(90deg); } +.example-tree[dir='rtl'] .example-tree-item .example-parent-icon { + transform: scaleX(-1); +} + +.example-tree[dir='rtl'] .example-tree-item[aria-expanded='true'] .example-parent-icon { + transform: scaleX(-1) rotate(90deg); +} + .example-selected-icon { visibility: hidden; margin-left: auto; diff --git a/src/components-examples/aria/tree/tree-rtl-active-descendant/tree-rtl-active-descendant-example.html b/src/components-examples/aria/tree/tree-rtl-active-descendant/tree-rtl-active-descendant-example.html new file mode 100644 index 000000000000..26641b1251ab --- /dev/null +++ b/src/components-examples/aria/tree/tree-rtl-active-descendant/tree-rtl-active-descendant-example.html @@ -0,0 +1,35 @@ +
    + +
+ + + @for (node of nodes; track node.value) { +
  • + + + {{ node.name }} + +
  • + + @if (node.children) { +
      + + + +
    + } } +
    diff --git a/src/components-examples/aria/tree/tree-rtl-active-descendant/tree-rtl-active-descendant-example.ts b/src/components-examples/aria/tree/tree-rtl-active-descendant/tree-rtl-active-descendant-example.ts new file mode 100644 index 000000000000..5922c7d97ba9 --- /dev/null +++ b/src/components-examples/aria/tree/tree-rtl-active-descendant/tree-rtl-active-descendant-example.ts @@ -0,0 +1,26 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import {Component} from '@angular/core'; +import {Dir} from '@angular/cdk/bidi'; +import {NgTemplateOutlet} from '@angular/common'; +import {Tree, TreeItem, TreeItemGroup} from '@angular/aria/tree'; +import {TreeNode, NODES} from '../tree-data'; + +/** + * @title Tree with active descendant focus. + */ +@Component({ + selector: 'tree-rtl-active-descendant-example', + templateUrl: 'tree-rtl-active-descendant-example.html', + styleUrl: '../tree-common.css', + imports: [Dir, Tree, TreeItem, TreeItemGroup, NgTemplateOutlet], +}) +export class TreeRtlActiveDescendantExample { + nodes: TreeNode[] = NODES; +} diff --git a/src/dev-app/aria-tree/tree-demo.html b/src/dev-app/aria-tree/tree-demo.html index 3003bb1817ea..28c8773983b2 100644 --- a/src/dev-app/aria-tree/tree-demo.html +++ b/src/dev-app/aria-tree/tree-demo.html @@ -44,6 +44,11 @@

    Active Descendant

    Nav Mode

    + +
    +

    RTL Active Descendant

    + +
    diff --git a/src/dev-app/aria-tree/tree-demo.ts b/src/dev-app/aria-tree/tree-demo.ts index 9807633d0653..3706433648a0 100644 --- a/src/dev-app/aria-tree/tree-demo.ts +++ b/src/dev-app/aria-tree/tree-demo.ts @@ -16,6 +16,7 @@ import { TreeMultiSelectExample, TreeMultiSelectFollowFocusExample, TreeNavExample, + TreeRtlActiveDescendantExample, TreeSingleSelectExample, TreeSingleSelectFollowFocusExample, } from '@angular/components-examples/aria/tree'; @@ -31,6 +32,7 @@ import { TreeMultiSelectExample, TreeMultiSelectFollowFocusExample, TreeNavExample, + TreeRtlActiveDescendantExample, TreeSingleSelectExample, TreeSingleSelectFollowFocusExample, ],