Skip to content
This repository was archived by the owner on May 20, 2023. It is now read-only.

Commit 1b5b3b5

Browse files
Googlernshahan
authored andcommitted
Modifies material-slider to support 2 sided slider.
A two sided slider seems to be part of the material slider spec so it is useful to add this feature to the component: https://material.io/design/components/sliders.html#usage PiperOrigin-RevId: 258871554
1 parent 223ae46 commit 1b5b3b5

File tree

6 files changed

+163
-22
lines changed

6 files changed

+163
-22
lines changed

angular_components/lib/material_slider/_mixins.scss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@
3939
/// }
4040
/// ```
4141
@mixin slider-track-color($selector, $left-color, $right-color: $mat-grey) {
42+
::ng-deep #{$selector} .double-sided-left-track-container > .track {
43+
background-color: $right-color;
44+
}
45+
4246
::ng-deep #{$selector} .left-track-container > .track {
4347
background-color: $left-color;
4448
}

angular_components/lib/material_slider/material_slider.dart

Lines changed: 114 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ import 'package:angular_components/utils/browser/dom_service/dom_service.dart';
2929
templateUrl: 'material_slider.html',
3030
styleUrls: ['material_slider.scss.css'],
3131
changeDetection: ChangeDetectionStrategy.OnPush,
32+
directives: [
33+
NgIf,
34+
],
3235
// TODO(google): Change to `Visibility.local` to reduce code size.
3336
visibility: Visibility.all,
3437
)
@@ -48,18 +51,53 @@ class MaterialSliderComponent implements AfterChanges, HasDisabled {
4851
@Input()
4952
bool disabled = false;
5053

51-
/// The current value of the input element.
54+
bool _isTwoSided = false;
55+
56+
/// True if the slider is 2 sided.
57+
bool get isTwoSided => _isTwoSided;
58+
@Input()
59+
set isTwoSided(bool isTwoSided) {
60+
_isTwoSided = isTwoSided;
61+
}
62+
63+
/// The current value of the input [value] element.
5264
///
53-
/// Must be between [min] and [max], inclusive, and a multiple of [step].
65+
/// When [isTwoSided] is true, then this represents the current value of the
66+
/// right slider knob. Must be between [min] and [max], inclusive, a multiple
67+
/// of [step], and greater than [leftValue].
5468
@Input()
5569
num value = 0;
5670

5771
final _changeController = StreamController<num>.broadcast(sync: true);
5872

59-
/// Publishes events when the value of the input is changed by the user.
73+
/// Publishes events when the value of the [value] input is changed by the
74+
/// user.
6075
@Output()
6176
Stream<num> get valueChange => _changeController.stream;
6277

78+
num _leftValue = 0;
79+
80+
/// The current value of the [leftValue] input in a 2 sided slider, defaults
81+
/// to 0.
82+
///
83+
/// When [isTwoSided] is true, then this represents the current value of the
84+
/// left slider knob. Must be between [min] and [max], inclusive, a multiple
85+
/// of [step] and less than or equal to [value].
86+
num get leftValue => isTwoSided ? _leftValue : min;
87+
@Input()
88+
set leftValue(int val) {
89+
if (isTwoSided) {
90+
_leftValue = val;
91+
}
92+
}
93+
94+
final _leftChangeController = StreamController<num>.broadcast(sync: true);
95+
96+
/// Publishes events when the value of the [leftValue] input is changed
97+
/// by the user in a 2 sided slider.
98+
@Output()
99+
Stream<num> get leftValueChange => _leftChangeController.stream;
100+
63101
/// The minimum progress value.
64102
///
65103
/// Defaults to 0, must be strictly smaller than max.
@@ -78,9 +116,13 @@ class MaterialSliderComponent implements AfterChanges, HasDisabled {
78116
@Input()
79117
num step = 1;
80118

81-
/// The current progress of the input in percent.
119+
/// The current progress of the [value] input in percent.
82120
double get progressPercent => (100.0 * (value - min) / (max - min));
83121

122+
/// The current progress of the [leftValue] input in percent.
123+
double get leftProgressPercent =>
124+
isTwoSided ? (100.0 * (leftValue - min) / (max - min)) : 0;
125+
84126
/// Verifies that the input values of this control are consistent.
85127
@override
86128
void ngAfterChanges() {
@@ -95,6 +137,19 @@ class MaterialSliderComponent implements AfterChanges, HasDisabled {
95137
message: 'Failed assertion: ${value} <= ${max}');
96138
checkArgument(_divisible(value - min, step),
97139
message: 'Failed assertion: (${value} - ${min}) % ${step} ~ 0.');
140+
141+
if (isTwoSided) {
142+
checkArgument(leftValue <= value,
143+
message: 'Failed assertion: ${leftValue} <= ${value}');
144+
checkArgument(leftValue >= min,
145+
message: 'Failed assertion: ${leftValue} >= ${min}');
146+
// Redundant check but done for consistency.
147+
checkArgument(leftValue <= max,
148+
message: 'Failed assertion: ${leftValue} <= ${max}');
149+
checkArgument(_divisible(leftValue - min, step),
150+
message:
151+
'Failed assertion: (${leftValue} - ${min}) % ${step} ~ 0.');
152+
}
98153
return true;
99154
}());
100155
}
@@ -117,6 +172,12 @@ class MaterialSliderComponent implements AfterChanges, HasDisabled {
117172
/// Whether the current user locale is RTL.
118173
bool get isRtl => Bidi.isRtlLanguage(Intl.defaultLocale ?? '');
119174

175+
/// True if mouse click event on left knob of a 2 sided slider.
176+
bool isLeftKnobSelected = false;
177+
178+
/// True if mouse click event on right knob.
179+
bool isRightKnobSelected = false;
180+
120181
/// Updates the current value to reflect the given slider position, if needed.
121182
void _setValueToMousePosition(int position) {
122183
_domService.scheduleRead(() {
@@ -127,25 +188,36 @@ class MaterialSliderComponent implements AfterChanges, HasDisabled {
127188
final fractionOfTrackLtr = (position - containerLeft) / containerWidth;
128189
final fractionOfTrack =
129190
isRtl ? 1.0 - fractionOfTrackLtr : fractionOfTrackLtr;
130-
131191
final scaledValue = (fractionOfTrack * (max - min));
132192
final halfStep = step / 2;
133193
// Clamp to the closest step value.
134194
final unboundedValue = min +
135195
(scaledValue ~/ step) * step +
136196
(scaledValue.remainder(step) > halfStep ? step : 0);
137197
final newValue = math.max(min, math.min(max, unboundedValue));
138-
if (newValue != value) {
139-
value = newValue;
140-
_changeController.add(value);
198+
// Adjust left knob in 2 sided slider
199+
if (isLeftKnobSelected ||
200+
(newValue < leftValue && !isRightKnobSelected)) {
201+
if (newValue != leftValue) {
202+
// Prevent left knob value from being greater than right knob value
203+
leftValue = _getValidLeftValue(value, newValue);
204+
_leftChangeController.add(leftValue);
205+
}
206+
} else {
207+
// Adjust right knob in 1 or 2 sided slider.
208+
if (newValue != value) {
209+
// Prevent right knob value from being less than left knob value
210+
value = _getValidRightValue(leftValue, newValue);
211+
_changeController.add(value);
212+
}
141213
}
142214
});
143215
}
144216

145-
/// Whether the user is currently dragging the slider knob.
217+
/// Whether the user is currently dragging either slider knob.
146218
bool isDragging = false;
147219

148-
/// Handles mouse down events on the slider knob or the slider track.
220+
/// Handles mouse down events on either slider knob or the slider track.
149221
void mouseDown(MouseEvent event) {
150222
if (disabled) return;
151223
if (event.button != 0) return;
@@ -160,12 +232,14 @@ class MaterialSliderComponent implements AfterChanges, HasDisabled {
160232
document.onMouseUp.take(1).listen((event) {
161233
event.preventDefault();
162234
mouseMoveSubscription.cancel();
235+
isLeftKnobSelected = false;
236+
isRightKnobSelected = false;
163237
isDragging = false;
164238
_changeDetector.markForCheck();
165239
});
166240
}
167241

168-
/// Handles touch start events on the slider knob.
242+
/// Handles touch start events on either slider knob.
169243
void touchStart(TouchEvent event) {
170244
if (disabled) return;
171245
event.preventDefault();
@@ -181,36 +255,56 @@ class MaterialSliderComponent implements AfterChanges, HasDisabled {
181255
document.onTouchEnd.take(1).listen((event) {
182256
event.preventDefault();
183257
touchMoveSubscription.cancel();
258+
isLeftKnobSelected = false;
259+
isRightKnobSelected = false;
184260
isDragging = false;
185261
_changeDetector.markForCheck();
186262
});
187263
}
188264

189-
/// Handles key press events on the slider knob.
190-
void knobKeyDown(KeyboardEvent event) {
265+
/// Handles key press events on either slider knob.
266+
///
267+
/// [isLeftKnob] true indicates that the event ocurred on the left knob.
268+
void knobKeyDown(KeyboardEvent event, {bool isLeftKnobPressed = false}) {
191269
if (disabled) return;
192-
var newValue = value;
270+
var currValue = isLeftKnobPressed ? leftValue : value;
271+
var newValue = currValue;
193272
final bigStepSize = ((max - min) / 10.0).ceil();
194273
final sign = isRtl ? -1 : 1;
195274
switch (event.keyCode) {
196275
case KeyCode.DOWN:
197276
case KeyCode.LEFT:
198-
newValue = math.max(min, math.min(max, value - step * sign));
277+
newValue = math.max(min, math.min(max, currValue - step * sign));
199278
break;
200279
case KeyCode.UP:
201280
case KeyCode.RIGHT:
202-
newValue = math.max(min, math.min(max, value + step * sign));
281+
newValue = math.max(min, math.min(max, currValue + step * sign));
203282
break;
204283
case KeyCode.PAGE_UP:
205-
newValue = math.max(min, math.min(max, value + step * bigStepSize));
284+
newValue = math.max(min, math.min(max, currValue + step * bigStepSize));
206285
break;
207286
case KeyCode.PAGE_DOWN:
208-
newValue = math.max(min, math.min(max, value - step * bigStepSize));
287+
newValue = math.max(min, math.min(max, currValue - step * bigStepSize));
209288
break;
210289
}
211-
if (newValue != value) {
212-
value = newValue;
290+
if (isLeftKnobPressed) {
291+
if (newValue != leftValue) {
292+
leftValue = _getValidLeftValue(value, newValue);
293+
_leftChangeController.add(leftValue);
294+
}
295+
} else if (newValue != value) {
296+
value = _getValidRightValue(leftValue, newValue);
213297
_changeController.add(value);
214298
}
215299
}
300+
301+
/// Returns a value that is valid for right knob depending on language
302+
/// direction.
303+
num _getValidRightValue(double valA, double valB, {isRtl = false}) =>
304+
isRtl ? math.min(valA, valB) : math.max(valA, valB);
305+
306+
/// Returns a value that is valid for left knob depending on language
307+
/// direction.
308+
num _getValidLeftValue(double valA, double valB, {isRtl = false}) =>
309+
isRtl ? math.max(valA, valB) : math.min(valA, valB);
216310
}

angular_components/lib/material_slider/material_slider.html

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,34 @@
55
-->
66
<div class="container" [class.is-disabled]="disabled" (mousedown)="mouseDown($event)"
77
(touchstart)="touchStart($event)" #container>
8+
<ng-container *ngIf="isTwoSided">
9+
<div class="track-container double-sided-left-track-container"
10+
style.width="calc({{leftProgressPercent}}%)">
11+
<div class="track"></div>
12+
</div>
13+
<div class="left-knob knob" role="slider"
14+
[attr.tabindex]="disabled ? -1 : 0"
15+
(mousedown)="isLeftKnobSelected = true"
16+
(touchstart)="isLeftKnobSelected = true"
17+
(keydown)="knobKeyDown($event, isLeftKnobPressed: true)"
18+
[style.left.px]="isRtl ? 0 : -8"
19+
[style.right.px]="isRtl ? -8 : 0"
20+
[attr.aria-valuemin]="min"
21+
[attr.aria-valuemax]="max"
22+
[attr.aria-valuenow]="leftValue">
23+
<div class="knob-real"></div>
24+
<div class="knob-hover-shadow"></div>
25+
<div class="knob-drag-shadow" [class.is-dragging]="isDragging"></div>
26+
</div>
27+
</ng-container>
828
<div class="track-container left-track-container"
9-
[style.width.%]="progressPercent">
29+
[style.width.%]="progressPercent - leftProgressPercent">
1030
<div class="track"></div>
1131
</div>
12-
<div class="knob" role="slider"
32+
<div class="right-knob knob" role="slider"
1333
[attr.tabindex]="disabled ? -1 : 0"
34+
(mousedown)="isRightKnobSelected = true"
35+
(touchstart)="isRightKnobSelected = true"
1436
(keydown)="knobKeyDown($event)"
1537
[style.left.px]="isRtl ? 0 : -8"
1638
[style.right.px]="isRtl ? -8 : 0"

angular_components/lib/material_slider/material_slider.scss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@ $track-height: 2px;
5151
width: 100%;
5252
}
5353

54+
.double-sided-left-track-container > .track {
55+
background-color: $mat-grey-500;
56+
}
57+
5458
.left-track-container > .track {
5559
background-color: $mat-blue-500;
5660
}

examples/material_slider_example/lib/material_slider_example.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ class MaterialSliderGalleryConfig {}
2727
class MaterialSliderExample {
2828
int value = 60;
2929
double value2 = 0.5;
30+
int leftValue = 60;
31+
int rightValue = 80;
3032
bool disabled = false;
3133
bool disabled2 = false;
34+
bool disabled3 = false;
3235
}

examples/material_slider_example/lib/material_slider_example.html

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,17 @@ <h2>Custom Colors and Double Values</h2>
2828
[disabled]="disabled2"></material-slider>
2929
<p>Value: {{value2}}</p>
3030
</section>
31+
32+
<section>
33+
<h2>Two Sided</h2>
34+
<material-toggle
35+
[(checked)]="disabled3"
36+
label="Tap to {{disabled3 ? 'enable' : 'disable'}}">
37+
</material-toggle><br/>
38+
<material-slider [isTwoSided]="true"
39+
[(leftValue)]="leftValue"
40+
[(value)]="rightValue"
41+
[disabled]="disabled3"></material-slider>
42+
<p>Left value: {{leftValue}}</p>
43+
<p>Value: {{rightValue}}</p>
44+
</section>

0 commit comments

Comments
 (0)