11import 'package:flutter/foundation.dart' ;
22import 'package:flutter/material.dart' ;
3+ import 'package:flutter/services.dart' ;
34
45import '../../../zeta_flutter.dart' ;
56
6- // TODO(UX-1355): Create slider input field.
7-
87/// Sliders allow users to make selections from a range of values.
98///
109/// Figma: https://www.figma.com/design/JesXQFLaPJLc1BdBM4sisI/%F0%9F%A6%93-ZDS---Components?node-id=875-11860&node-type=canvas&m=dev
@@ -20,12 +19,13 @@ class ZetaSlider extends ZetaStatefulWidget {
2019 this .divisions,
2120 this .semanticLabel,
2221 this .min = 0.0 ,
23- this .max = 1.0 ,
22+ this .max = 100.0 ,
23+ this .inputField = false ,
2424 });
2525
2626 /// Double value to represent slider percentage.
2727 ///
28- /// Default [min] / [max] are 0.0 and 1 .0 respectively; this value should be between [min] and [max] .
28+ /// Default [min] / [max] are 0.0 and 100 .0 respectively; this value should be between [min] and [max] .
2929 final double value;
3030
3131 /// Callback to handle changing of slider
@@ -45,6 +45,13 @@ class ZetaSlider extends ZetaStatefulWidget {
4545 /// Maximum value of the slider.
4646 final double max;
4747
48+ /// Whether to show an input field to the right of the slider.
49+ /// The input field will change the slider value when updated.
50+ ///
51+ /// This also adds the min and max values below the slider.
52+ /// This will default to 0 - 100 if min and max are not changed.
53+ final bool inputField;
54+
4855 @override
4956 State <ZetaSlider > createState () => _ZetaSliderState ();
5057 @override
@@ -57,24 +64,81 @@ class ZetaSlider extends ZetaStatefulWidget {
5764 ..add (IntProperty ('divisions' , divisions))
5865 ..add (StringProperty ('semanticLabel' , semanticLabel))
5966 ..add (DoubleProperty ('max' , max))
60- ..add (DoubleProperty ('min' , min));
67+ ..add (DoubleProperty ('min' , min))
68+ ..add (DiagnosticsProperty <bool >('inputField' , inputField));
6169 }
6270}
6371
6472class _ZetaSliderState extends State <ZetaSlider > {
6573 bool _selected = false ;
74+ final _inputController = TextEditingController ();
75+ Debounce ? _debounce;
76+
77+ @override
78+ void initState () {
79+ super .initState ();
80+ _inputController.text = widget.value.toString ();
81+ _inputController.addListener (_updateTextField);
82+ }
83+
84+ // Called when slider is updated, changes text field value.
85+ void _updateTextFieldFromSlider (double sliderValue) {
86+ _inputController.text = sliderValue.toInt ().toString ();
87+ }
88+
89+ // Called when text field is updated, changes slider value.
90+ void _updateTextField () {
91+ _debounce? .cancel ();
92+ _debounce = Debounce (
93+ () {
94+ final number = int .tryParse (_inputController.text);
95+ if (number != null ) {
96+ final num newValue;
97+ if (widget.divisions != null ) {
98+ final divisionSize = (widget.max - widget.min) / widget.divisions! ;
99+ final snappedValue = ((number - widget.min) / divisionSize).round () * divisionSize + widget.min;
100+ newValue = snappedValue.round ().clamp (widget.min, widget.max);
101+ if (number != newValue) {
102+ _inputController.text = newValue.toString ();
103+ }
104+ } else {
105+ newValue = number.clamp (widget.min, widget.max).toInt ();
106+ if (number != newValue) {
107+ _inputController.text = newValue.toString ();
108+ }
109+ }
110+ widget.onChange? .call (newValue.toDouble ());
111+ }
112+ },
113+ duration: const Duration (milliseconds: 200 ),
114+ );
115+ }
116+
117+ @override
118+ void dispose () {
119+ _debounce? .cancel ();
120+ _inputController.dispose ();
121+ super .dispose ();
122+ }
66123
67124 @override
68125 Widget build (BuildContext context) {
69- final colors = Zeta .of (context).colors;
126+ final zeta = Zeta .of (context);
127+ final colors = zeta.colors;
128+
129+ final activeColor = widget.onChange == null
130+ ? colors.mainDisabled
131+ : _selected
132+ ? colors.mainPrimary
133+ : colors.mainDefault;
70134
71135 return MergeSemantics (
72136 child: Semantics (
73137 label: widget.semanticLabel,
74138 child: SliderTheme (
75139 data: SliderThemeData (
76140 /// Active Track
77- activeTrackColor: _activeColor ,
141+ activeTrackColor: activeColor ,
78142 disabledActiveTrackColor: colors.mainDisabled,
79143
80144 /// Inactive Track
@@ -90,45 +154,98 @@ class _ZetaSliderState extends State<ZetaSlider> {
90154 thumbColor: colors.mainDefault,
91155 disabledThumbColor: colors.mainDisabled,
92156 overlayShape: _SliderThumb (
93- size: Zeta . of (context) .spacing.xl / 2 ,
157+ size: zeta .spacing.xl / 2 ,
94158 rounded: context.rounded,
95- color: _activeColor ,
159+ color: activeColor ,
96160 ),
97161 thumbShape: _SliderThumb (
98- size: Zeta . of (context) .spacing.large / 2 ,
162+ size: zeta .spacing.large / 2 ,
99163 rounded: context.rounded,
100- color: _activeColor ,
164+ color: activeColor ,
101165 ),
102166 trackShape: context.rounded ? _RoundedRectangleTrackShape () : const RectangularSliderTrackShape (),
103167 ),
104- child: Slider (
105- value: widget.value,
106- onChanged: widget.onChange,
107- divisions: widget.divisions,
108- onChangeStart: (_) {
109- setState (() {
110- _selected = true ;
111- });
112- },
113- onChangeEnd: (_) {
114- setState (() {
115- _selected = false ;
116- });
117- },
118- min: widget.min,
119- max: widget.max,
120- ),
168+ child: _generateSlider (),
121169 ),
122170 ),
123171 );
124172 }
125173
126- Color get _activeColor {
127- final colors = Zeta .of (context).colors;
128- if (widget.onChange == null ) {
129- return colors.mainDisabled;
174+ Widget _generateCoreSlider () {
175+ return Slider (
176+ value: widget.value,
177+ onChanged: widget.inputField
178+ ? widget.onChange != null
179+ ? (value) {
180+ // Update text field when slider changes
181+ _updateTextFieldFromSlider (value);
182+ widget.onChange? .call (value);
183+ }
184+ : null
185+ : widget.onChange,
186+ divisions: widget.divisions,
187+ onChangeStart: (_) => setState (() => _selected = true ),
188+ onChangeEnd: (_) => setState (() => _selected = false ),
189+ min: widget.min,
190+ max: widget.max,
191+ );
192+ }
193+
194+ // Private function to generate slider depending on input field boolean.
195+ Widget _generateSlider () {
196+ final zeta = Zeta .of (context);
197+ final colors = zeta.colors;
198+
199+ // If input field is true, return slider with input field.
200+ if (widget.inputField) {
201+ return Row (
202+ spacing: zeta.spacing.large,
203+ children: [
204+ Expanded (
205+ child: Column (
206+ spacing: zeta.spacing.small,
207+ children: [
208+ //Slider
209+ _generateCoreSlider (),
210+ //Numbers
211+ Row (
212+ mainAxisAlignment: MainAxisAlignment .spaceBetween,
213+ children: [
214+ Text (
215+ widget.min.toInt ().toString (),
216+ style: zeta.textStyles.bodyMedium.apply (
217+ color: widget.onChange == null ? colors.mainDisabled : colors.mainDefault,
218+ ),
219+ ),
220+ Text (
221+ widget.max.toInt ().toString (),
222+ style: zeta.textStyles.bodyMedium.apply (
223+ color: widget.onChange == null ? colors.mainDisabled : colors.mainDefault,
224+ ),
225+ ),
226+ ],
227+ ).paddingHorizontal (zeta.spacing.minimum),
228+ ],
229+ ),
230+ ),
231+ //Text Input
232+ SizedBox (
233+ width: zeta.spacing.xl_8 + zeta.spacing.small,
234+ child: ZetaTextInput (
235+ keyboardType: TextInputType .number,
236+ disabled: widget.onChange == null ,
237+ size: ZetaWidgetSize .large,
238+ textAlign: TextAlign .center,
239+ inputFormatters: < TextInputFormatter > [FilteringTextInputFormatter .digitsOnly],
240+ controller: _inputController,
241+ ),
242+ ),
243+ ],
244+ );
130245 }
131- return _selected ? colors.mainPrimary : colors.mainDefault;
246+
247+ // If input field is false, return just the slider.
248+ return _generateCoreSlider ();
132249 }
133250}
134251
0 commit comments