From b5b053fa7a1e45ed524ab4ef309bed313d229779 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Sat, 28 Mar 2026 09:21:33 +0100 Subject: [PATCH] fix(material/slider): not picking up static direction The slider was reading its direction once on init and one when it changes, however the initial read was too early which meant that the `dir` directive might not have received its value yet. Fixes #32970. --- goldens/material/slider/index.api.md | 3 +- src/material/slider/slider-input.ts | 10 +++---- src/material/slider/slider-interface.ts | 4 +-- src/material/slider/slider.ts | 39 ++++++++++++------------- 4 files changed, 28 insertions(+), 28 deletions(-) diff --git a/goldens/material/slider/index.api.md b/goldens/material/slider/index.api.md index 5b7488870ca5..b7b959d28602 100644 --- a/goldens/material/slider/index.api.md +++ b/goldens/material/slider/index.api.md @@ -16,6 +16,7 @@ import { NgZone } from '@angular/core'; import { OnDestroy } from '@angular/core'; import { OnInit } from '@angular/core'; import { QueryList } from '@angular/core'; +import { Signal } from '@angular/core'; import { Subject } from 'rxjs'; import { WritableSignal } from '@angular/core'; @@ -56,7 +57,7 @@ export class MatSlider implements AfterViewInit, OnDestroy, _MatSlider { _isCursorOnSliderThumb(event: PointerEvent, rect: DOMRect): boolean; // (undocumented) _isRange: boolean; - _isRtl: boolean; + _isRtl: i0.Signal; _knobRadius: number; get max(): number; set max(v: number); diff --git a/src/material/slider/slider-input.ts b/src/material/slider/slider-input.ts index b01f80da862d..e0e82ff579a2 100644 --- a/src/material/slider/slider-input.ts +++ b/src/material/slider/slider-input.ts @@ -201,7 +201,7 @@ export class MatSliderThumb implements _MatSliderThumb, OnDestroy, ControlValueA /** The percentage of the slider that coincides with the value. */ get percentage(): number { if (this._slider.min >= this._slider.max) { - return this._slider._isRtl ? 1 : 0; + return this._slider._isRtl() ? 1 : 0; } return (this.value - this._slider.min) / (this._slider.max - this._slider.min); } @@ -209,7 +209,7 @@ export class MatSliderThumb implements _MatSliderThumb, OnDestroy, ControlValueA /** @docs-private */ get fillPercentage(): number { if (!this._slider._cachedWidth) { - return this._slider._isRtl ? 1 : 0; + return this._slider._isRtl() ? 1 : 0; } if (this._translateX === 0) { return 0; @@ -446,7 +446,7 @@ export class MatSliderThumb implements _MatSliderThumb, OnDestroy, ControlValueA const width = this._slider._cachedWidth; const step = this._slider.step === 0 ? 1 : this._slider.step; const numSteps = Math.floor((this._slider.max - this._slider.min) / step); - const percentage = this._slider._isRtl ? 1 - xPos / width : xPos / width; + const percentage = this._slider._isRtl() ? 1 - xPos / width : xPos / width; // To ensure the percentage is rounded to the necessary number of decimals. const fixedPercentage = Math.round(percentage * numSteps) / numSteps; @@ -507,7 +507,7 @@ export class MatSliderThumb implements _MatSliderThumb, OnDestroy, ControlValueA } _calcTranslateXByValue(): number { - if (this._slider._isRtl) { + if (this._slider._isRtl()) { return ( (1 - this.percentage) * (this._slider._cachedWidth - this._tickMarkOffset * 2) + this._tickMarkOffset @@ -652,7 +652,7 @@ export class MatSliderRangeThumb extends MatSliderThumb implements _MatSliderRan _setIsLeftThumb(): void { this._isLeftThumb = - (this._isEndThumb && this._slider._isRtl) || (!this._isEndThumb && !this._slider._isRtl); + (this._isEndThumb && this._slider._isRtl()) || (!this._isEndThumb && !this._slider._isRtl()); } /** Whether this slider corresponds to the input on the left hand side. */ diff --git a/src/material/slider/slider-interface.ts b/src/material/slider/slider-interface.ts index 404e871830b3..33016607c928 100644 --- a/src/material/slider/slider-interface.ts +++ b/src/material/slider/slider-interface.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.dev/license */ -import {InjectionToken, ChangeDetectorRef, WritableSignal} from '@angular/core'; +import {InjectionToken, ChangeDetectorRef, WritableSignal, Signal} from '@angular/core'; import {MatRipple, RippleGlobalOptions} from '../core'; /** @@ -107,7 +107,7 @@ export interface _MatSlider { _isRange: boolean; /** Whether the slider is rtl. */ - _isRtl: boolean; + _isRtl: Signal; /** The stored width of the host element's bounding client rect. */ _cachedWidth: number; diff --git a/src/material/slider/slider.ts b/src/material/slider/slider.ts index d027c782cf4c..76b3e267d792 100644 --- a/src/material/slider/slider.ts +++ b/src/material/slider/slider.ts @@ -9,11 +9,13 @@ import {Directionality} from '@angular/cdk/bidi'; import {Platform} from '@angular/cdk/platform'; import { + afterRenderEffect, AfterViewInit, booleanAttribute, ChangeDetectionStrategy, ChangeDetectorRef, Component, + computed, ContentChild, ContentChildren, ElementRef, @@ -34,7 +36,6 @@ import { RippleGlobalOptions, ThemePalette, } from '../core'; -import {Subscription} from 'rxjs'; import { _MatThumb, _MatTickMark, @@ -372,9 +373,6 @@ export class MatSlider implements AfterViewInit, OnDestroy, _MatSlider { /** Whether animations have been disabled. */ _noopAnimations = _animationsDisabled(); - /** Subscription to changes to the directionality (LTR / RTL) context for the application. */ - private _dirChangeSubscription: Subscription | undefined; - /** Observer used to monitor size changes in the slider. */ private _resizeObserver: ResizeObserver | null = null; @@ -401,7 +399,7 @@ export class MatSlider implements AfterViewInit, OnDestroy, _MatSlider { _isRange: boolean = false; /** Whether the slider is rtl. */ - _isRtl: boolean = false; + _isRtl = computed(() => this._dir?.valueSignal() === 'rtl'); private _hasViewInitialized: boolean = false; @@ -422,10 +420,19 @@ export class MatSlider implements AfterViewInit, OnDestroy, _MatSlider { constructor() { inject(_CdkPrivateStyleLoader).load(_StructuralStylesLoader); - if (this._dir) { - this._dirChangeSubscription = this._dir.change.subscribe(() => this._onDirChange()); - this._isRtl = this._dir.value === 'rtl'; - } + let prevIsRtl = this._isRtl(); + + afterRenderEffect(() => { + const isRtl = this._isRtl(); + + // The diffing is normally handled by the signal, but we don't want to + // fire on the first run, because it'll trigger unnecessary measurements. + if (isRtl !== prevIsRtl) { + prevIsRtl = isRtl; + this._isRange ? this._onDirChangeRange() : this._onDirChangeNonRange(); + this._updateTickMarkUI(); + } + }); } /** The radius of the native slider's knob. AFAIK there is no way to avoid hardcoding this. */ @@ -499,18 +506,10 @@ export class MatSlider implements AfterViewInit, OnDestroy, _MatSlider { } ngOnDestroy(): void { - this._dirChangeSubscription?.unsubscribe(); this._resizeObserver?.disconnect(); this._resizeObserver = null; } - /** Handles updating the slider ui after a dir change. */ - private _onDirChange(): void { - this._isRtl = this._dir?.value === 'rtl'; - this._isRange ? this._onDirChangeRange() : this._onDirChangeNonRange(); - this._updateTickMarkUI(); - } - private _onDirChangeRange(): void { const endInput = this._getInput(_MatThumb.END) as _MatSliderRangeThumb; const startInput = this._getInput(_MatThumb.START) as _MatSliderRangeThumb; @@ -600,7 +599,7 @@ export class MatSlider implements AfterViewInit, OnDestroy, _MatSlider { _calcTickMarkTransform(index: number): string { // TODO(wagnermaciel): See if we can avoid doing this and just using flex to position these. const offset = index * (this._tickMarkTrackWidth / (this._tickMarks.length - 1)); - const translateX = this._isRtl ? this._cachedWidth - 6 - offset : offset; + const translateX = this._isRtl() ? this._cachedWidth - 6 - offset : offset; return `translateX(${translateX}px)`; } @@ -852,7 +851,7 @@ export class MatSlider implements AfterViewInit, OnDestroy, _MatSlider { } private _updateTrackUINonRange(source: _MatSliderThumb): void { - this._isRtl + this._isRtl() ? this._setTrackActiveStyles({ left: 'auto', right: '0px', @@ -893,7 +892,7 @@ export class MatSlider implements AfterViewInit, OnDestroy, _MatSlider { const value = this._getValue(); let numActive = Math.max(Math.round((value - this.min) / step), 0) + 1; let numInactive = Math.max(Math.round((this.max - value) / step), 0) - 1; - this._isRtl ? numActive++ : numInactive++; + this._isRtl() ? numActive++ : numInactive++; this._tickMarks = Array(numActive) .fill(_MatTickMark.ACTIVE)