@@ -13,6 +13,7 @@ import { roundToMaxDecimalPlaces } from '../../utils/floating-point';
1313
1414import type {
1515 KnobName ,
16+ KnobPosition ,
1617 RangeChangeEventDetail ,
1718 RangeKnobMoveEndEventDetail ,
1819 RangeKnobMoveStartEventDetail ,
@@ -35,14 +36,20 @@ import type {
3536 * @part bar - The inactive part of the bar.
3637 * @part bar-active - The active part of the bar.
3738 * @part knob-handle - The container element that wraps the knob and handles drag interactions.
38- * @part knob-handle-a - The container element for the lower/left knob. Only available when `dualKnobs` is `true`.
39- * @part knob-handle-b - The container element for the upper/right knob. Only available when `dualKnobs` is `true`.
39+ * @part knob-handle-a - The container element for the first knob. Only available when `dualKnobs` is `true`.
40+ * @part knob-handle-b - The container element for the second knob. Only available when `dualKnobs` is `true`.
41+ * @part knob-handle-lower - The container element for the lower knob. Only available when `dualKnobs` is `true`.
42+ * @part knob-handle-upper - The container element for the upper knob. Only available when `dualKnobs` is `true`.
4043 * @part pin - The counter that appears above a knob.
41- * @part pin-a - The counter that appears above the lower/left knob. Only available when `dualKnobs` is `true`.
42- * @part pin-b - The counter that appears above the upper/right knob. Only available when `dualKnobs` is `true`.
44+ * @part pin-a - The counter that appears above the first knob. Only available when `dualKnobs` is `true`.
45+ * @part pin-b - The counter that appears above the second knob. Only available when `dualKnobs` is `true`.
46+ * @part pin-lower - The counter that appears above the lower knob. Only available when `dualKnobs` is `true`.
47+ * @part pin-upper - The counter that appears above the upper knob. Only available when `dualKnobs` is `true`.
4348 * @part knob - The visual knob element that appears on the range track.
44- * @part knob-a - The visual knob element for the lower/left knob. Only available when `dualKnobs` is `true`.
45- * @part knob-b - The visual knob element for the upper/right knob. Only available when `dualKnobs` is `true`.
49+ * @part knob-a - The visual knob element for the first knob. Only available when `dualKnobs` is `true`.
50+ * @part knob-b - The visual knob element for the second knob. Only available when `dualKnobs` is `true`.
51+ * @part knob-lower - The visual knob element for the lower knob. Only available when `dualKnobs` is `true`.
52+ * @part knob-upper - The visual knob element for the upper knob. Only available when `dualKnobs` is `true`.
4653 * @part pressed - Added to the knob-handle, knob, and pin that is currently being pressed to drag. Only one set has this part at a time.
4754 * @part focused - Added to the knob-handle, knob, and pin that currently has focus. Only one set has this part at a time.
4855 */
@@ -72,7 +79,7 @@ export class Range implements ComponentInterface {
7279 @State ( ) private ratioA = 0 ;
7380 @State ( ) private ratioB = 0 ;
7481 @State ( ) private pressedKnob : KnobName ;
75- @State ( ) private focusedKnob : KnobName | undefined ;
82+ @State ( ) private focusedKnob : KnobName ;
7683
7784 /**
7885 * The color to use from your application's color palette.
@@ -525,6 +532,13 @@ export class Range implements ComponentInterface {
525532
526533 // update the active knob's position
527534 this . update ( currentX ) ;
535+
536+ /**
537+ * Blur the knob so focus styles are cleared.
538+ */
539+ const knobEl = this . getKnobElement ( this . pressedKnob ) ;
540+ knobEl ?. blur ( ) ;
541+
528542 /**
529543 * Reset the pressed knob to undefined since the user
530544 * may start dragging a different knob in the next gesture event.
@@ -573,6 +587,15 @@ export class Range implements ComponentInterface {
573587 this . setFocus ( this . pressedKnob ) ;
574588 }
575589
590+ /**
591+ * Returns the DOM element for the given knob.
592+ */
593+ private getKnobElement ( knob : KnobName ) : HTMLElement | null {
594+ return this . el . shadowRoot ?. querySelector (
595+ knob === 'A' ? '.range-knob-handle-a' : '.range-knob-handle-b'
596+ ) as HTMLElement | null ;
597+ }
598+
576599 private get valA ( ) {
577600 return ratioToValue ( this . ratioA , this . min , this . max , this . step ) ;
578601 }
@@ -602,9 +625,26 @@ export class Range implements ComponentInterface {
602625 private updateRatio ( ) {
603626 const value = this . getValue ( ) as any ;
604627 const { min, max } = this ;
628+
629+ /**
630+ * For dual knobs, value gives lower/upper but not which is A vs B.
631+ * Assign (lowerRatio, upperRatio) to (ratioA, ratioB) in the way that
632+ * minimizes change from the current ratios so the knobs don't swap.
633+ */
605634 if ( this . dualKnobs ) {
606- this . ratioA = valueToRatio ( value . lower , min , max ) ;
607- this . ratioB = valueToRatio ( value . upper , min , max ) ;
635+ const lowerRatio = valueToRatio ( value . lower , min , max ) ;
636+ const upperRatio = valueToRatio ( value . upper , min , max ) ;
637+
638+ if (
639+ Math . abs ( this . ratioA - lowerRatio ) + Math . abs ( this . ratioB - upperRatio ) <=
640+ Math . abs ( this . ratioA - upperRatio ) + Math . abs ( this . ratioB - lowerRatio )
641+ ) {
642+ this . ratioA = lowerRatio ;
643+ this . ratioB = upperRatio ;
644+ } else {
645+ this . ratioA = upperRatio ;
646+ this . ratioB = lowerRatio ;
647+ }
608648 } else {
609649 this . ratioA = valueToRatio ( value , min , max ) ;
610650 }
@@ -626,12 +666,8 @@ export class Range implements ComponentInterface {
626666
627667 private setFocus ( knob : KnobName ) {
628668 if ( this . el . shadowRoot ) {
629- const knobEl = this . el . shadowRoot . querySelector (
630- knob === 'A' ? '.range-knob-handle-a' : '.range-knob-handle-b'
631- ) as HTMLElement | undefined ;
632- if ( knobEl ) {
633- knobEl . focus ( ) ;
634- }
669+ const knobEl = this . getKnobElement ( knob ) ;
670+ knobEl ?. focus ( ) ;
635671 }
636672 }
637673
@@ -656,20 +692,6 @@ export class Range implements ComponentInterface {
656692 this . hasFocus = true ;
657693 this . ionFocus . emit ( ) ;
658694 }
659-
660- // Manually manage ion-focused class for dual knobs
661- if ( this . dualKnobs && this . el . shadowRoot ) {
662- const knobA = this . el . shadowRoot . querySelector ( '.range-knob-handle-a' ) ;
663- const knobB = this . el . shadowRoot . querySelector ( '.range-knob-handle-b' ) ;
664-
665- // Remove ion-focused from both knobs first
666- knobA ?. classList . remove ( 'ion-focused' ) ;
667- knobB ?. classList . remove ( 'ion-focused' ) ;
668-
669- // Add ion-focused only to the focused knob
670- const focusedKnobEl = knob === 'A' ? knobA : knobB ;
671- focusedKnobEl ?. classList . add ( 'ion-focused' ) ;
672- }
673695 } ;
674696
675697 private onKnobBlur = ( ) => {
@@ -685,14 +707,6 @@ export class Range implements ComponentInterface {
685707 this . focusedKnob = undefined ;
686708 this . ionBlur . emit ( ) ;
687709 }
688-
689- // Remove ion-focused from both knobs when focus leaves the range
690- if ( this . dualKnobs && this . el . shadowRoot ) {
691- const knobA = this . el . shadowRoot . querySelector ( '.range-knob-handle-a' ) ;
692- const knobB = this . el . shadowRoot . querySelector ( '.range-knob-handle-b' ) ;
693- knobA ?. classList . remove ( 'ion-focused' ) ;
694- knobB ?. classList . remove ( 'ion-focused' ) ;
695- }
696710 }
697711 } , 0 ) ;
698712 } ;
@@ -862,6 +876,7 @@ export class Range implements ComponentInterface {
862876
863877 { renderKnob ( rtl , {
864878 knob : 'A' ,
879+ position : this . dualKnobs ? ( this . ratioA <= this . ratioB ? 'lower' : 'upper' ) : 'lower' ,
865880 dualKnobs : this . dualKnobs ,
866881 pressed : pressedKnob === 'A' ,
867882 focused : focusedKnob === 'A' ,
@@ -881,6 +896,7 @@ export class Range implements ComponentInterface {
881896 { this . dualKnobs &&
882897 renderKnob ( rtl , {
883898 knob : 'B' ,
899+ position : this . ratioB <= this . ratioA ? 'lower' : 'upper' ,
884900 dualKnobs : this . dualKnobs ,
885901 pressed : pressedKnob === 'B' ,
886902 focused : focusedKnob === 'B' ,
@@ -975,6 +991,7 @@ export class Range implements ComponentInterface {
975991
976992interface RangeKnob {
977993 knob : KnobName ;
994+ position : KnobPosition ;
978995 dualKnobs : boolean ;
979996 value : number ;
980997 ratio : number ;
@@ -995,6 +1012,7 @@ const renderKnob = (
9951012 rtl : boolean ,
9961013 {
9971014 knob,
1015+ position,
9981016 dualKnobs,
9991017 value,
10001018 ratio,
@@ -1026,6 +1044,12 @@ const renderKnob = (
10261044
10271045 return (
10281046 < div
1047+ onMouseDown = { ( ev ) => {
1048+ /**
1049+ * Prevent the knob from being focused when the user clicks on it.
1050+ */
1051+ ev . preventDefault ( ) ;
1052+ } }
10291053 onKeyDown = { ( ev : KeyboardEvent ) => {
10301054 const key = ev . key ;
10311055 if ( key === 'ArrowLeft' || key === 'ArrowDown' ) {
@@ -1049,11 +1073,14 @@ const renderKnob = (
10491073 'range-knob-max' : value === max ,
10501074 'ion-activatable' : true ,
10511075 'ion-focusable' : true ,
1076+ 'ion-focused' : focused ,
10521077 } }
10531078 part = { [
10541079 'knob-handle' ,
10551080 dualKnobs && knob === 'A' && 'knob-handle-a' ,
10561081 dualKnobs && knob === 'B' && 'knob-handle-b' ,
1082+ dualKnobs && position === 'lower' && 'knob-handle-lower' ,
1083+ dualKnobs && position === 'upper' && 'knob-handle-upper' ,
10571084 pressed && 'pressed' ,
10581085 focused && 'focused' ,
10591086 ]
@@ -1077,6 +1104,8 @@ const renderKnob = (
10771104 'pin' ,
10781105 dualKnobs && knob === 'A' && 'pin-a' ,
10791106 dualKnobs && knob === 'B' && 'pin-b' ,
1107+ dualKnobs && position === 'lower' && 'pin-lower' ,
1108+ dualKnobs && position === 'upper' && 'pin-upper' ,
10801109 pressed && 'pressed' ,
10811110 focused && 'focused' ,
10821111 ]
@@ -1087,16 +1116,14 @@ const renderKnob = (
10871116 </ div >
10881117 ) }
10891118 < div
1090- class = { {
1091- 'range-knob' : true ,
1092- 'range-knob-a' : knob === 'A' ,
1093- 'range-knob-b' : knob === 'B' ,
1094- } }
1119+ class = "range-knob"
10951120 role = "presentation"
10961121 part = { [
10971122 'knob' ,
10981123 dualKnobs && knob === 'A' && 'knob-a' ,
10991124 dualKnobs && knob === 'B' && 'knob-b' ,
1125+ dualKnobs && position === 'lower' && 'knob-lower' ,
1126+ dualKnobs && position === 'upper' && 'knob-upper' ,
11001127 pressed && 'pressed' ,
11011128 focused && 'focused' ,
11021129 ]
0 commit comments