Skip to content

Commit

Permalink
feat: Created stepper input (#67)
Browse files Browse the repository at this point in the history
* created stepper

* implementing min and max values

* widgetbook

* pr comments
  • Loading branch information
mikecoomber committed May 9, 2024
1 parent 8507830 commit ca51b1f
Show file tree
Hide file tree
Showing 7 changed files with 263 additions and 2 deletions.
2 changes: 2 additions & 0 deletions example/lib/home.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import 'package:zeta_example/pages/components/select_input_example.dart';
import 'package:zeta_example/pages/components/search_bar_example.dart';
import 'package:zeta_example/pages/components/segmented_control_example.dart';
import 'package:zeta_example/pages/components/stepper_example.dart';
import 'package:zeta_example/pages/components/stepper_input_example.dart';
import 'package:zeta_example/pages/components/switch_example.dart';
import 'package:zeta_example/pages/components/snackbar_example.dart';
import 'package:zeta_example/pages/components/tabs_example.dart';
Expand Down Expand Up @@ -84,6 +85,7 @@ final List<Component> components = [
Component(SelectInputExample.name, (context) => const SelectInputExample()),
Component(ScreenHeaderBarExample.name, (context) => const ScreenHeaderBarExample()),
Component(FilterSelectionExample.name, (context) => const FilterSelectionExample()),
Component(StepperInputExample.name, (context) => const StepperInputExample()),
];

final List<Component> theme = [
Expand Down
39 changes: 39 additions & 0 deletions example/lib/pages/components/stepper_input_example.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import 'package:flutter/material.dart';
import 'package:zeta_example/widgets.dart';
import 'package:zeta_flutter/zeta_flutter.dart';

class StepperInputExample extends StatefulWidget {
static const name = 'StepperInput';

const StepperInputExample({super.key});

@override
State<StepperInputExample> createState() => _StepperInputExampleState();
}

class _StepperInputExampleState extends State<StepperInputExample> {
@override
Widget build(BuildContext context) {
return ExampleScaffold(
name: StepperInputExample.name,
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ZetaStepperInput(
min: 0,
max: 10,
initialValue: 5,
onChange: (_) {},
),
ZetaStepperInput(rounded: false),
ZetaStepperInput(
size: ZetaStepperInputSize.large,
onChange: (_) {},
),
].divide(const SizedBox(height: 16)).toList(),
),
),
);
}
}
5 changes: 5 additions & 0 deletions example/widgetbook/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import 'pages/components/screen_header_bar_widgetbook.dart';
import 'pages/components/search_bar_widgetbook.dart';
import 'pages/components/segmented_control_widgetbook.dart';
import 'pages/components/select_input_widgetbook.dart';
import 'pages/components/stepper_input_widgetbook.dart';
import 'pages/components/stepper_widgetbook.dart';
import 'pages/components/switch_widgetbook.dart';
import 'pages/components/snack_bar_widgetbook.dart';
Expand Down Expand Up @@ -141,6 +142,10 @@ class HotReload extends StatelessWidget {
name: 'Stepper',
builder: (context) => stepperUseCase(context),
),
WidgetbookUseCase(
name: 'Stepper Input',
builder: (context) => stepperInputUseCase(context),
),
WidgetbookUseCase(name: 'Dialog', builder: (context) => dialogUseCase(context)),
WidgetbookUseCase(name: 'Search Bar', builder: (context) => searchBarUseCase(context)),
WidgetbookUseCase(name: 'Navigation Rail', builder: (context) => navigationRailUseCase(context)),
Expand Down
23 changes: 23 additions & 0 deletions example/widgetbook/pages/components/stepper_input_widgetbook.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import 'package:flutter/material.dart';
import 'package:widgetbook/widgetbook.dart';
import 'package:zeta_flutter/zeta_flutter.dart';

import '../../test/test_components.dart';
import '../../utils/utils.dart';

Widget stepperInputUseCase(BuildContext context) {
return WidgetbookTestWidget(
widget: ZetaStepperInput(
initialValue: context.knobs.int.input(label: 'Initial value'),
min: context.knobs.int.input(label: 'Minimum value'),
max: context.knobs.int.input(label: 'Maximum value'),
size: context.knobs.list(
label: 'Size',
options: ZetaStepperInputSize.values,
labelBuilder: enumLabelBuilder,
),
rounded: context.knobs.boolean(label: 'Rounded', initialValue: true),
onChange: context.knobs.boolean(label: 'Disabled', initialValue: false) ? null : (_) {},
),
);
}
4 changes: 2 additions & 2 deletions lib/src/components/buttons/button_style.dart
Original file line number Diff line number Diff line change
Expand Up @@ -134,15 +134,15 @@ ButtonStyle buttonStyle(
}),
side: MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (type.border && states.contains(MaterialState.disabled)) {
return BorderSide(color: colors.cool.shade40);
return BorderSide(color: colors.borderDisabled);
}
// TODO(thelukewalton): This removes a defualt border when focused, rather than adding a second border when focused.
if (states.contains(MaterialState.focused)) {
return BorderSide(color: colors.blue, width: ZetaSpacing.x0_5);
}
if (type.border) {
return BorderSide(
color: type == ZetaButtonType.outline ? colors.primary.border : colors.borderDefault,
color: type == ZetaButtonType.outline ? colors.primary.border : colors.borderSubtle,
);
}

Expand Down
191 changes: 191 additions & 0 deletions lib/src/components/stepper_input/stepper_input.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

import '../../../zeta_flutter.dart';

/// Sizes for [ZetaStepperInput]
enum ZetaStepperInputSize {
/// Medium
medium,

/// Large
large,
}

/// A stepper input, also called numeric stepper, is a common UI element that allows uers to input a number or value simply by clicking the plus and minus buttons.
class ZetaStepperInput extends StatefulWidget {
/// Creates a new [ZetaStepperInput]
const ZetaStepperInput({
this.rounded = true,
this.size = ZetaStepperInputSize.medium,
this.initialValue,
this.min,
this.max,
this.onChange,
super.key,
}) : assert(
(min == null || (initialValue ?? 0) >= min) && (max == null || (initialValue ?? 0) <= max),
'Initial value must be inside given min and max values',
);

/// {@macro zeta-component-rounded}
final bool rounded;

/// The size of the stepper input.
final ZetaStepperInputSize size;

/// The initial value of the stepper input.
///
/// Must be in the bounds of [min] and [max] (if given).
final int? initialValue;

/// The minimum value of the stepper input.
final int? min;

/// The maximum value of the stepper input.
final int? max;

/// Called with the value of the stepper whenever it is changed.
final ValueChanged<int>? onChange;

@override
State<ZetaStepperInput> createState() => _ZetaStepperInputState();
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties
..add(DiagnosticsProperty<bool>('rounded', rounded))
..add(EnumProperty<ZetaStepperInputSize>('size', size))
..add(IntProperty('initialValue', initialValue))
..add(IntProperty('min', min))
..add(IntProperty('max', max))
..add(ObjectFlagProperty<ValueChanged<int>?>.has('onChange', onChange));
}
}

class _ZetaStepperInputState extends State<ZetaStepperInput> {
final TextEditingController _controller = TextEditingController();
int _value = 0;
late final bool _disabled;

@override
void initState() {
super.initState();
_disabled = widget.onChange == null;
if (widget.initialValue != null) {
_value = widget.initialValue!;
}
_controller.text = _value.toString();
}

@override
void dispose() {
super.dispose();
_controller.dispose();
}

InputBorder get _border {
final colors = Zeta.of(context).colors;

return OutlineInputBorder(
borderSide: BorderSide(
color: !_disabled ? colors.borderSubtle : colors.borderDisabled,
),
borderRadius: widget.rounded ? ZetaRadius.minimal : ZetaRadius.none,
);
}

double get _height {
if (widget.size != ZetaStepperInputSize.large) {
return ZetaSpacing.x10;
} else {
return ZetaSpacing.x12;
}
}

void _onTextChange(String value) {
int? val = int.tryParse(value);
if (val != null) {
if (widget.max != null && val > widget.max!) {
val = widget.max;
}
if (widget.min != null && val! < widget.min!) {
val = widget.min;
}
_onChange(val!);
}
}

void _onChange(int value) {
if (!(widget.max != null && value > widget.max! || widget.min != null && value < widget.min!)) {
setState(() {
_value = value;
});
_controller.text = value.toString();
widget.onChange?.call(value);
}
}

ZetaIconButton _getButton({bool increase = false}) {
return ZetaIconButton(
icon: increase
? widget.rounded
? ZetaIcons.add_round
: ZetaIcons.add_sharp
: widget.rounded
? ZetaIcons.remove_round
: ZetaIcons.remove_sharp,
type: ZetaButtonType.outlineSubtle,
size: widget.size == ZetaStepperInputSize.medium ? ZetaWidgetSize.medium : ZetaWidgetSize.large,
borderType: widget.rounded ? ZetaWidgetBorder.rounded : ZetaWidgetBorder.sharp,
onPressed: !_disabled
? () => _onChange(
_value + (increase ? 1 : -1),
)
: null,
);
}

@override
Widget build(BuildContext context) {
final colors = Zeta.of(context).colors;

return Row(
mainAxisSize: MainAxisSize.min,
children: [
_getButton(),
SizedBox(
width: ZetaSpacing.xl,
child: TextFormField(
keyboardType: TextInputType.number,
enabled: !_disabled,
controller: _controller,
onChanged: _onTextChange,
textAlign: TextAlign.center,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: _disabled ? colors.textDisabled : null,
),
onTapOutside: (_) {
if (_controller.text.isEmpty) {
_controller.text = _value.toString();
}
},
decoration: InputDecoration(
filled: true,
fillColor: _disabled ? colors.surfaceDisabled : null,
contentPadding: EdgeInsets.zero,
constraints: BoxConstraints(maxHeight: _height),
border: _border,
focusedBorder: _border,
enabledBorder: _border,
disabledBorder: _border,
),
),
),
_getButton(increase: true),
].divide(const SizedBox(width: ZetaSpacing.x2)).toList(),
);
}
}
1 change: 1 addition & 0 deletions lib/zeta_flutter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export 'src/components/segmented_control/segmented_control.dart';
export 'src/components/select_input/select_input.dart';
export 'src/components/snack_bar/snack_bar.dart';
export 'src/components/stepper/stepper.dart';
export 'src/components/stepper_input/stepper_input.dart';
export 'src/components/switch/zeta_switch.dart';
export 'src/components/tabs/tab.dart';
export 'src/components/tabs/tab_bar.dart';
Expand Down

0 comments on commit ca51b1f

Please sign in to comment.