diff --git a/src/cdk/bidi/bidi.md b/src/cdk/bidi/bidi.md index 272c92e1b466..3c79bdf5709d 100644 --- a/src/cdk/bidi/bidi.md +++ b/src/cdk/bidi/bidi.md @@ -1,34 +1,34 @@ The `bidi` package provides a common system for components to get and respond to change in the -application's LTR/RTL layout direction. +application's LTR/RTL layout direction. ### Directionality - + When including the CDK's `BidiModule`, components can inject `Directionality` to get the current text direction (RTL or LTR); #### Example ```ts -@Component({ ... }) +@Component({ ... }) export class MyWidget implements OnDestroy { /** Whether the widget is in RTL mode or not. */ private isRtl: boolean; - + /** Subscription to the Directionality change EventEmitter. */ - private _dirChangeSubscription = Subscription.EMPTY; - + private _dirChangeSubscription = Subscription.EMPTY; + constructor(dir: Directionality) { this.isRtl = dir.value === 'rtl'; - + this._dirChangeSubscription = dir.change.subscribe(() => { this.flipDirection(); }); } - + ngOnDestroy() { this._dirChangeSubscription.unsubscribe(); } -} +} ``` ### The `Dir` directive @@ -36,3 +36,11 @@ The `BidiModule` also includes a directive that matches any elements with a `dir directive has the same API as Directionality and provides itself _as_ `Directionality`. By doing this, any component that injects `Directionality` will get the closest ancestor layout direction context. + +### Interpreting the `auto` value +The CDK also supports the native `auto` value for the `dir` attribute, however there's a difference +in how it is interpreted. Some parts of the CDK, like overlays and keyboard navigation, need to know +if the element is in an RTL or LTR layout in order to work correctly. For performance reasons, we +resolve the `auto` value by looking at the browser's language (`navigator.language`) and matching +it against a set of known RTL locales. This differs from the way the browser handles it, which is +based on the text content of the element. diff --git a/src/cdk/bidi/dir.ts b/src/cdk/bidi/dir.ts index 1ce6b20802a3..249fab27b467 100644 --- a/src/cdk/bidi/dir.ts +++ b/src/cdk/bidi/dir.ts @@ -8,7 +8,7 @@ import {Directive, Output, Input, EventEmitter, AfterContentInit, OnDestroy} from '@angular/core'; -import {Direction, Directionality} from './directionality'; +import {Direction, Directionality, _resolveDirectionality} from './directionality'; /** * Directive to listen for changes of direction of part of the DOM. @@ -40,14 +40,16 @@ export class Dir implements Directionality, AfterContentInit, OnDestroy { get dir(): Direction { return this._dir; } - set dir(value: Direction) { - const old = this._dir; - const normalizedValue = value ? value.toLowerCase() : value; + set dir(value: Direction | 'auto') { + const previousValue = this._dir; + // Note: `_resolveDirectionality` resolves the language based on the browser's language, + // whereas the browser does it based on the content of the element. Since doing so based + // on the content can be expensive, for now we're doing the simpler matching. + this._dir = _resolveDirectionality(value); this._rawDir = value; - this._dir = normalizedValue === 'ltr' || normalizedValue === 'rtl' ? normalizedValue : 'ltr'; - if (old !== this._dir && this._isInitialized) { + if (previousValue !== this._dir && this._isInitialized) { this.change.emit(this._dir); } } diff --git a/src/cdk/bidi/directionality.ts b/src/cdk/bidi/directionality.ts index 91d0fa358443..3eda68cfc6b1 100644 --- a/src/cdk/bidi/directionality.ts +++ b/src/cdk/bidi/directionality.ts @@ -11,6 +11,21 @@ import {DIR_DOCUMENT} from './dir-document-token'; export type Direction = 'ltr' | 'rtl'; +/** Regex that matches locales with an RTL script. Taken from `goog.i18n.bidi.isRtlLanguage`. */ +const RTL_LOCALE_PATTERN = + /^(ar|ckb|dv|he|iw|fa|nqo|ps|sd|ug|ur|yi|.*[-_](Adlm|Arab|Hebr|Nkoo|Rohg|Thaa))(?!.*[-_](Latn|Cyrl)($|-|_))($|-|_)/i; + +/** Resolves a string value to a specific direction. */ +export function _resolveDirectionality(rawValue: string): Direction { + const value = rawValue?.toLowerCase() || ''; + + if (value === 'auto' && typeof navigator !== 'undefined' && navigator?.language) { + return RTL_LOCALE_PATTERN.test(navigator.language) ? 'rtl' : 'ltr'; + } + + return value === 'rtl' ? 'rtl' : 'ltr'; +} + /** * The directionality (LTR / RTL) context for the application (or a subtree of it). * Exposes the current direction and a stream of direction changes. @@ -25,14 +40,9 @@ export class Directionality implements OnDestroy { constructor(@Optional() @Inject(DIR_DOCUMENT) _document?: any) { if (_document) { - // TODO: handle 'auto' value - - // We still need to account for dir="auto". - // It looks like HTMLElemenet.dir is also "auto" when that's set to the attribute, - // but getComputedStyle return either "ltr" or "rtl". avoiding getComputedStyle for now const bodyDir = _document.body ? _document.body.dir : null; const htmlDir = _document.documentElement ? _document.documentElement.dir : null; - const value = bodyDir || htmlDir; - this.value = value === 'ltr' || value === 'rtl' ? value : 'ltr'; + this.value = _resolveDirectionality(bodyDir || htmlDir || 'ltr'); } } diff --git a/tools/public_api_guard/cdk/bidi.md b/tools/public_api_guard/cdk/bidi.md index fb394856c8e3..3529507f406e 100644 --- a/tools/public_api_guard/cdk/bidi.md +++ b/tools/public_api_guard/cdk/bidi.md @@ -24,7 +24,7 @@ export class BidiModule { export class Dir implements Directionality, AfterContentInit, OnDestroy { readonly change: EventEmitter; get dir(): Direction; - set dir(value: Direction); + set dir(value: Direction | 'auto'); ngAfterContentInit(): void; // (undocumented) ngOnDestroy(): void;