From ef2eb94801e4bf7f60873a46dac2b0c256ab57ee Mon Sep 17 00:00:00 2001 From: Brandy Smith <6577830+brandyscarney@users.noreply.github.com> Date: Tue, 27 Jan 2026 15:38:33 -0500 Subject: [PATCH 01/11] feat(range): add knob parts for A and B when dualKnobs is enabled --- core/api.txt | 5 ++++ core/src/components/range/range.tsx | 41 +++++++++++++++++++++-------- 2 files changed, 35 insertions(+), 11 deletions(-) diff --git a/core/api.txt b/core/api.txt index 745d82786af..2ddaf648e63 100644 --- a/core/api.txt +++ b/core/api.txt @@ -1475,6 +1475,11 @@ ion-range,css-prop,--pin-color,md ion-range,part,bar ion-range,part,bar-active ion-range,part,knob +ion-range,part,knob-a +ion-range,part,knob-b +ion-range,part,knob-handle +ion-range,part,knob-handle-a +ion-range,part,knob-handle-b ion-range,part,label ion-range,part,pin ion-range,part,tick diff --git a/core/src/components/range/range.tsx b/core/src/components/range/range.tsx index 431dbe4b7fe..c9bf8d983b2 100644 --- a/core/src/components/range/range.tsx +++ b/core/src/components/range/range.tsx @@ -32,7 +32,12 @@ import type { * @part tick - An inactive tick mark. * @part tick-active - An active tick mark. * @part pin - The counter that appears above a knob. - * @part knob - The handle that is used to drag the range. + * @part knob-handle - The container element that wraps the knob and handles drag interactions. + * @part knob-handle-a - The container element for the lower/left knob. Only available when `dualKnobs` is `true`. + * @part knob-handle-b - The container element for the upper/right knob. Only available when `dualKnobs` is `true`. + * @part knob - The visual knob element that appears on the range track. + * @part knob-a - The visual knob element for the lower/left knob. Only available when `dualKnobs` is `true`. + * @part knob-b - The visual knob element for the upper/right knob. Only available when `dualKnobs` is `true`. * @part bar - The inactive part of the bar. * @part bar-active - The active part of the bar. * @part label - The label text describing the range. @@ -616,9 +621,9 @@ export class Range implements ComponentInterface { private setFocus(knob: KnobName) { if (this.el.shadowRoot) { - const knobEl = this.el.shadowRoot.querySelector(knob === 'A' ? '.range-knob-a' : '.range-knob-b') as - | HTMLElement - | undefined; + const knobEl = this.el.shadowRoot.querySelector( + knob === 'A' ? '.range-knob-handle-a' : '.range-knob-handle-b' + ) as HTMLElement | undefined; if (knobEl) { knobEl.focus(); } @@ -647,8 +652,8 @@ export class Range implements ComponentInterface { // Manually manage ion-focused class for dual knobs if (this.dualKnobs && this.el.shadowRoot) { - const knobA = this.el.shadowRoot.querySelector('.range-knob-a'); - const knobB = this.el.shadowRoot.querySelector('.range-knob-b'); + const knobA = this.el.shadowRoot.querySelector('.range-knob-handle-a'); + const knobB = this.el.shadowRoot.querySelector('.range-knob-handle-b'); // Remove ion-focused from both knobs first knobA?.classList.remove('ion-focused'); @@ -675,8 +680,8 @@ export class Range implements ComponentInterface { // Remove ion-focused from both knobs when focus leaves the range if (this.dualKnobs && this.el.shadowRoot) { - const knobA = this.el.shadowRoot.querySelector('.range-knob-a'); - const knobB = this.el.shadowRoot.querySelector('.range-knob-b'); + const knobA = this.el.shadowRoot.querySelector('.range-knob-handle-a'); + const knobB = this.el.shadowRoot.querySelector('.range-knob-handle-b'); knobA?.classList.remove('ion-focused'); knobB?.classList.remove('ion-focused'); } @@ -848,6 +853,7 @@ export class Range implements ComponentInterface { {renderKnob(rtl, { knob: 'A', + dualKnobs: this.dualKnobs, pressed: pressedKnob === 'A', value: this.valA, ratio: this.ratioA, @@ -865,6 +871,7 @@ export class Range implements ComponentInterface { {this.dualKnobs && renderKnob(rtl, { knob: 'B', + dualKnobs: this.dualKnobs, pressed: pressedKnob === 'B', value: this.valB, ratio: this.ratioB, @@ -924,6 +931,7 @@ export class Range implements ComponentInterface { [mode]: true, 'in-item': inItem, 'range-disabled': disabled, + 'range-dual-knobs': dualKnobs, 'range-pressed': pressedKnob !== undefined, 'range-has-pin': pin, [`range-label-placement-${labelPlacement}`]: true, @@ -956,6 +964,7 @@ export class Range implements ComponentInterface { interface RangeKnob { knob: KnobName; + dualKnobs: boolean; value: number; ratio: number; min: number; @@ -974,6 +983,7 @@ const renderKnob = ( rtl: boolean, { knob, + dualKnobs, value, ratio, min, @@ -1019,14 +1029,15 @@ const renderKnob = ( onBlur={onKnobBlur} class={{ 'range-knob-handle': true, - 'range-knob-a': knob === 'A', - 'range-knob-b': knob === 'B', + 'range-knob-handle-a': knob === 'A', + 'range-knob-handle-b': knob === 'B', 'range-knob-pressed': pressed, 'range-knob-min': value === min, 'range-knob-max': value === max, 'ion-activatable': true, 'ion-focusable': true, }} + part={dualKnobs ? (knob === 'A' ? 'knob-handle knob-handle-a' : 'knob-handle knob-handle-b') : 'knob-handle'} style={knobStyle()} role="slider" tabindex={disabled ? -1 : 0} @@ -1042,7 +1053,15 @@ const renderKnob = ( {pinFormatter(value)} )} -