Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Created stepper input #67

Merged
merged 4 commits into from
May 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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) {
thelukewalton marked this conversation as resolved.
Show resolved Hide resolved
_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
Loading