Skip to content

Commit c381a6c

Browse files
authored
feat(cdk/bidi): support auto direction value (#23906)
Adds support for the `auto` value of the `dir` attribute. The value gets resolved to `ltr` or `rtl` based on browser's locale. Fixes #10186.
1 parent d4a6c3b commit c381a6c

File tree

4 files changed

+42
-22
lines changed

4 files changed

+42
-22
lines changed

src/cdk/bidi/bidi.md

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,46 @@
11
The `bidi` package provides a common system for components to get and respond to change in the
2-
application's LTR/RTL layout direction.
2+
application's LTR/RTL layout direction.
33

44
### Directionality
5-
5+
66
When including the CDK's `BidiModule`, components can inject `Directionality` to get the current
77
text direction (RTL or LTR);
88

99
#### Example
1010
```ts
11-
@Component({ ... })
11+
@Component({ ... })
1212
export class MyWidget implements OnDestroy {
1313

1414
/** Whether the widget is in RTL mode or not. */
1515
private isRtl: boolean;
16-
16+
1717
/** Subscription to the Directionality change EventEmitter. */
18-
private _dirChangeSubscription = Subscription.EMPTY;
19-
18+
private _dirChangeSubscription = Subscription.EMPTY;
19+
2020
constructor(dir: Directionality) {
2121
this.isRtl = dir.value === 'rtl';
22-
22+
2323
this._dirChangeSubscription = dir.change.subscribe(() => {
2424
this.flipDirection();
2525
});
2626
}
27-
27+
2828
ngOnDestroy() {
2929
this._dirChangeSubscription.unsubscribe();
3030
}
31-
}
31+
}
3232
```
3333

3434
### The `Dir` directive
3535
The `BidiModule` also includes a directive that matches any elements with a `dir` attribute. This
3636
directive has the same API as Directionality and provides itself _as_ `Directionality`. By doing
3737
this, any component that injects `Directionality` will get the closest ancestor layout direction
3838
context.
39+
40+
### Interpreting the `auto` value
41+
The CDK also supports the native `auto` value for the `dir` attribute, however there's a difference
42+
in how it is interpreted. Some parts of the CDK, like overlays and keyboard navigation, need to know
43+
if the element is in an RTL or LTR layout in order to work correctly. For performance reasons, we
44+
resolve the `auto` value by looking at the browser's language (`navigator.language`) and matching
45+
it against a set of known RTL locales. This differs from the way the browser handles it, which is
46+
based on the text content of the element.

src/cdk/bidi/dir.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
import {Directive, Output, Input, EventEmitter, AfterContentInit, OnDestroy} from '@angular/core';
1010

11-
import {Direction, Directionality} from './directionality';
11+
import {Direction, Directionality, _resolveDirectionality} from './directionality';
1212

1313
/**
1414
* Directive to listen for changes of direction of part of the DOM.
@@ -40,14 +40,16 @@ export class Dir implements Directionality, AfterContentInit, OnDestroy {
4040
get dir(): Direction {
4141
return this._dir;
4242
}
43-
set dir(value: Direction) {
44-
const old = this._dir;
45-
const normalizedValue = value ? value.toLowerCase() : value;
43+
set dir(value: Direction | 'auto') {
44+
const previousValue = this._dir;
4645

46+
// Note: `_resolveDirectionality` resolves the language based on the browser's language,
47+
// whereas the browser does it based on the content of the element. Since doing so based
48+
// on the content can be expensive, for now we're doing the simpler matching.
49+
this._dir = _resolveDirectionality(value);
4750
this._rawDir = value;
48-
this._dir = normalizedValue === 'ltr' || normalizedValue === 'rtl' ? normalizedValue : 'ltr';
4951

50-
if (old !== this._dir && this._isInitialized) {
52+
if (previousValue !== this._dir && this._isInitialized) {
5153
this.change.emit(this._dir);
5254
}
5355
}

src/cdk/bidi/directionality.ts

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,21 @@ import {DIR_DOCUMENT} from './dir-document-token';
1111

1212
export type Direction = 'ltr' | 'rtl';
1313

14+
/** Regex that matches locales with an RTL script. Taken from `goog.i18n.bidi.isRtlLanguage`. */
15+
const RTL_LOCALE_PATTERN =
16+
/^(ar|ckb|dv|he|iw|fa|nqo|ps|sd|ug|ur|yi|.*[-_](Adlm|Arab|Hebr|Nkoo|Rohg|Thaa))(?!.*[-_](Latn|Cyrl)($|-|_))($|-|_)/i;
17+
18+
/** Resolves a string value to a specific direction. */
19+
export function _resolveDirectionality(rawValue: string): Direction {
20+
const value = rawValue?.toLowerCase() || '';
21+
22+
if (value === 'auto' && typeof navigator !== 'undefined' && navigator?.language) {
23+
return RTL_LOCALE_PATTERN.test(navigator.language) ? 'rtl' : 'ltr';
24+
}
25+
26+
return value === 'rtl' ? 'rtl' : 'ltr';
27+
}
28+
1429
/**
1530
* The directionality (LTR / RTL) context for the application (or a subtree of it).
1631
* Exposes the current direction and a stream of direction changes.
@@ -25,14 +40,9 @@ export class Directionality implements OnDestroy {
2540

2641
constructor(@Optional() @Inject(DIR_DOCUMENT) _document?: any) {
2742
if (_document) {
28-
// TODO: handle 'auto' value -
29-
// We still need to account for dir="auto".
30-
// It looks like HTMLElemenet.dir is also "auto" when that's set to the attribute,
31-
// but getComputedStyle return either "ltr" or "rtl". avoiding getComputedStyle for now
3243
const bodyDir = _document.body ? _document.body.dir : null;
3344
const htmlDir = _document.documentElement ? _document.documentElement.dir : null;
34-
const value = bodyDir || htmlDir;
35-
this.value = value === 'ltr' || value === 'rtl' ? value : 'ltr';
45+
this.value = _resolveDirectionality(bodyDir || htmlDir || 'ltr');
3646
}
3747
}
3848

tools/public_api_guard/cdk/bidi.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export class BidiModule {
2424
export class Dir implements Directionality, AfterContentInit, OnDestroy {
2525
readonly change: EventEmitter<Direction>;
2626
get dir(): Direction;
27-
set dir(value: Direction);
27+
set dir(value: Direction | 'auto');
2828
ngAfterContentInit(): void;
2929
// (undocumented)
3030
ngOnDestroy(): void;

0 commit comments

Comments
 (0)