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

Overhaul of SBBCheckbox #258

Merged
merged 15 commits into from
Jan 11, 2025
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
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,20 @@ It is expected that you keep this format strictly, since we depend on it in our

## [Unreleased]

### Changed

- corrected layout of `SBBCheckbox`
- changed the behavior of the `SBBCheckboxListItem` trailing widget and icon

### Added

- added `isLoading` to `SBBCheckboxListItem` for an animated bottom loading indicator
- added `boxed` variant of the `SBBCheckboxListItem` via redirecting constructor
- added `semanticLabel` and `checkboxSemanticLabel` to the `SBBCheckbox` and `SBBCheckboxListItem` respectively

### Fixed

- correct color usage of the `SBBCheckboxListItem`
- simplified `SBBPagination` and added animation

## [2.1.1] - 2024-12-14
Expand Down
163 changes: 132 additions & 31 deletions example/lib/pages/checkbox_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ import 'package:flutter/material.dart';
import '../native_app.dart';

class CheckboxPage extends StatefulWidget {
const CheckboxPage({super.key});

@override
_CheckboxPageState createState() => _CheckboxPageState();
CheckboxPageState createState() => CheckboxPageState();
}

class _CheckboxPageState extends State<CheckboxPage> {
class CheckboxPageState extends State<CheckboxPage> {
bool? _value1 = false;
bool? _value2 = false;
bool? _listItemValue1 = false;
Expand All @@ -18,6 +20,11 @@ class _CheckboxPageState extends State<CheckboxPage> {
bool? _listItemValue5 = false;
bool? _listItemValue6 = false;
bool? _listItemValue7 = false;
bool _listItemValue8 = false;

int _enabledIndex = 0;

bool get _isEnabled => _enabledIndex == 0;

@override
Widget build(BuildContext context) {
Expand All @@ -28,8 +35,8 @@ class _CheckboxPageState extends State<CheckboxPage> {
const ThemeModeSegmentedButton(),
const SizedBox(height: sbbDefaultSpacing),
const SBBListHeader('Checkbox'),
SBBGroup(
padding: const EdgeInsets.all(sbbDefaultSpacing / 2),
Padding(
padding: const EdgeInsets.all(sbbDefaultSpacing * .5),
child: Row(
children: [
SBBCheckbox(
Expand All @@ -53,58 +60,54 @@ class _CheckboxPageState extends State<CheckboxPage> {
],
),
),
const SizedBox(height: sbbDefaultSpacing),
const SBBListHeader('CheckboxListItem'),
const SizedBox(height: sbbDefaultSpacing * 2),
SBBSegmentedButton(
values: ['All Enabled', 'All Disabled'],
selectedStateIndex: _enabledIndex,
selectedIndexChanged: (i) => setState(() => _enabledIndex = i),
),
const SBBListHeader('Checkbox Item - List'),
SBBGroup(
child: Column(
children: [
SBBCheckboxListItem(
value: _listItemValue1,
label: 'Default',
label: 'Label',
allowMultilineLabel: true,
onChanged: (value) => setState(() => _listItemValue1 = value),
onChanged: _isEnabled ? (value) => setState(() => _listItemValue1 = value) : null,
),
SBBCheckboxListItem(
value: _listItemValue2,
label: 'Tristate',
tristate: true,
onChanged: (value) => setState(() => _listItemValue2 = value),
),
SBBCheckboxListItem(
value: _listItemValue3,
label: 'Call to Action',
onChanged: (value) => setState(() => _listItemValue3 = value),
trailingIcon: SBBIcons.circle_information_small_small,
onCallToAction: () => sbbToast.show(message: 'Call to Action'),
onChanged: _isEnabled ? (value) => setState(() => _listItemValue2 = value) : null,
),
SBBCheckboxListItem(
value: _listItemValue4,
label: 'Icon',
onChanged: (value) => setState(() => _listItemValue4 = value),
label: 'Leading Icon',
onChanged: _isEnabled ? (value) => setState(() => _listItemValue4 = value) : null,
leadingIcon: SBBIcons.alarm_clock_small,
),
SBBCheckboxListItem(
value: _listItemValue5,
label: 'Icon, Call to Action',
onChanged: (value) => setState(() => _listItemValue5 = value),
label: 'Leading and Trailing Icon',
onChanged: _isEnabled ? (value) => setState(() => _listItemValue5 = value) : null,
leadingIcon: SBBIcons.alarm_clock_small,
trailingIcon: SBBIcons.circle_information_small_small,
onCallToAction: () => sbbToast.show(message: 'Call to Action'),
trailingIcon: SBBIcons.dog_small,
),
SBBCheckboxListItem(
value: _listItemValue5,
label: 'Disabled, Icon, Call to Action',
onChanged: null,
leadingIcon: SBBIcons.alarm_clock_small,
value: _listItemValue3,
label: 'Button',
onChanged: _isEnabled ? (value) => setState(() => _listItemValue3 = value) : null,
trailingIcon: SBBIcons.circle_information_small_small,
onCallToAction: () => sbbToast.show(message: 'Call to Action'),
onCallToAction: () => sbbToast.show(message: 'Button pressed'),
),
SBBCheckboxListItem.custom(
value: _listItemValue6,
label: 'Custom trailing Widget',
onChanged: (value) => setState(() => _listItemValue6 = value),
trailingWidget: const Padding(
padding: const EdgeInsetsDirectional.only(
onChanged: _isEnabled ? (value) => setState(() => _listItemValue6 = value) : null,
trailingWidget: Padding(
padding: EdgeInsetsDirectional.only(
top: sbbDefaultSpacing / 4 * 3,
end: sbbDefaultSpacing,
),
Expand All @@ -117,12 +120,110 @@ class _CheckboxPageState extends State<CheckboxPage> {
allowMultilineLabel: true,
secondaryLabel:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut convallis leo et metus semper hendrerit. Duis nec nunc a ligula cursus vulputate. Donec sed elit ultricies, euismod erat et, eleifend augue.',
onChanged: _isEnabled ? (value) => setState(() => _listItemValue7 = value) : null,
),
SBBCheckboxListItem(
value: _listItemValue8,
label: 'Loading',
secondaryLabel: 'This will stop loading if selected.',
onChanged: _isEnabled ? (value) => setState(() => _listItemValue8 = value!) : null,
isLoading: !_listItemValue8,
isLastElement: true,
onChanged: (value) => setState(() => _listItemValue7 = value),
),
],
),
),
const SizedBox(height: sbbDefaultSpacing),
const SBBListHeader('Checkbox Item - Boxed'),
Column(
// spacing: sbbDefaultSpacing * 0.5, add once support for Flutter SDK 3.24.5 removed
// and remove SizedBoxes below
children: [
SBBGroup(
child: SBBCheckboxListItem.boxed(
value: _listItemValue1,
label: 'Label',
allowMultilineLabel: true,
onChanged: _isEnabled ? (value) => setState(() => _listItemValue1 = value) : null,
),
),
SizedBox(height: sbbDefaultSpacing * .5),
SBBGroup(
child: SBBCheckboxListItem.boxed(
value: _listItemValue2,
label: 'Tristate',
tristate: true,
onChanged: _isEnabled ? (value) => setState(() => _listItemValue2 = value) : null,
),
),
SizedBox(height: sbbDefaultSpacing * .5),
SBBGroup(
child: SBBCheckboxListItem.boxed(
value: _listItemValue4,
label: 'Leading Icon',
onChanged: _isEnabled ? (value) => setState(() => _listItemValue4 = value) : null,
leadingIcon: SBBIcons.alarm_clock_small,
),
),
SizedBox(height: sbbDefaultSpacing * .5),
SBBGroup(
child: SBBCheckboxListItem.boxed(
value: _listItemValue5,
label: 'Leading and Trailing Icon',
onChanged: _isEnabled ? (value) => setState(() => _listItemValue5 = value) : null,
leadingIcon: SBBIcons.alarm_clock_small,
trailingIcon: SBBIcons.dog_small,
),
),
SizedBox(height: sbbDefaultSpacing * .5),
SBBGroup(
child: SBBCheckboxListItem.boxed(
value: _listItemValue3,
label: 'Button',
onChanged: _isEnabled ? (value) => setState(() => _listItemValue3 = value) : null,
trailingIcon: SBBIcons.circle_information_small_small,
onCallToAction: () => sbbToast.show(message: 'Button pressed'),
),
),
SizedBox(height: sbbDefaultSpacing * .5),
SBBGroup(
child: SBBCheckboxListItem.custom(
value: _listItemValue6,
label: 'Custom trailing Widget',
onChanged: _isEnabled ? (value) => setState(() => _listItemValue6 = value) : null,
trailingWidget: Padding(
padding: EdgeInsetsDirectional.only(
top: sbbDefaultSpacing / 4 * 3,
end: sbbDefaultSpacing,
),
child: Text('CHF 0.99'),
),
),
),
SizedBox(height: sbbDefaultSpacing * .5),
SBBGroup(
child: SBBCheckboxListItem.boxed(
value: _listItemValue7,
label: 'Multiline Label with\nSecondary Label',
allowMultilineLabel: true,
secondaryLabel:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut convallis leo et metus semper hendrerit. '
'Duis nec nunc a ligula cursus vulputate. Donec sed elit ultricies, euismod erat et, eleifend augue.',
onChanged: _isEnabled ? (value) => setState(() => _listItemValue7 = value) : null,
),
),
SizedBox(height: sbbDefaultSpacing * .5),
SBBGroup(
child: SBBCheckboxListItem.boxed(
value: _listItemValue8,
label: 'Loading',
secondaryLabel: 'This will not stop.',
onChanged: _isEnabled ? (value) => setState(() => _listItemValue8 = value!) : null,
isLoading: true,
),
),
],
),
],
);
}
Expand Down
3 changes: 1 addition & 2 deletions lib/sbb_design_system_mobile.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@ export 'src/button/sbb_icon_form_button.dart';
export 'src/button/sbb_primary_button.dart';
export 'src/button/sbb_secondary_button.dart';
export 'src/button/sbb_tertiary_button.dart';
export 'src/checkbox/sbb_checkbox.dart';
export 'src/checkbox/sbb_checkbox_list_item.dart';
export 'src/checkbox/checkbox.dart';
export 'src/chip/sbb_chip.dart';
export 'src/group/sbb_group.dart';
export 'src/header/sbb_header.dart';
Expand Down
109 changes: 109 additions & 0 deletions lib/src/checkbox/bottom_loading_indicator.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import 'package:flutter/material.dart';

import '../../sbb_design_system_mobile.dart';

class BottomLoadingIndicator extends StatefulWidget {
const BottomLoadingIndicator({
super.key,
this.circularBorderRadius = 0.0,
this.height = 3.0,
this.widthRatio = 0.3,
this.duration = const Duration(seconds: 3),
}) : assert(0.0 <= widthRatio && widthRatio <= 1.0);

/// The BorderRadius to correct the clipping of the loading bar.
///
/// If you use this [BottomLoadingIndicator] on a widget with rounded borders,
/// make sure to set the [circularBorderRadius] equal to that rounding.
///
/// This will round the bottomLeft and bottomRight corners of the [ClipRRect]
/// to correctly clip the loading bar.
///
/// Defaults to 0.
final double circularBorderRadius;

/// The height of the [BottomLoadingIndicator] in absolute pixels.
///
/// Defaults to 3.
final double height;

/// The relative width of the [BottomLoadingIndicator] to its parent widget. Must be between 0.0 and 1.0.
///
/// If the parent is 100px wide and the widthRatio is 0.2, the effective
/// width of the [BottomLoadingIndicator] will be 20px.
///
/// Defaults to 0.3.
final double widthRatio;

/// The duration of the animation of the [BottomLoadingIndicator].
///
/// Defaults to 3 seconds.
final Duration duration;

@override
State<BottomLoadingIndicator> createState() => _BottomLoadingIndicatorState();
}

class _BottomLoadingIndicatorState extends State<BottomLoadingIndicator> with SingleTickerProviderStateMixin {
late final AnimationController _controller = AnimationController(
duration: widget.duration,
vsync: this,
)..repeat();
late final Animation<Offset> _offsetAnimation = Tween<Offset>(
begin: const Offset(-1, 0.0),
end: const Offset(1, 0.0),
).animate(CurvedAnimation(
parent: _controller,
curve: Curves.linear,
));

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

@override
Widget build(BuildContext context) {
final color = SBBBaseStyle.of(context).primaryColor!;

return ClipRRect(
borderRadius: _resolveBorderRadius(),
child: SlideTransition(
key: widget.key,
transformHitTests: false,
position: _offsetAnimation,
// add a SizedBox with the height of the borderRadius to stop the ClipRRect from
// clamping the values in borderRadius
child: SizedBox(
width: double.infinity,
height: widget.circularBorderRadius > 0 ? widget.circularBorderRadius : widget.height,
child: Align(
alignment: Alignment.bottomCenter,
child: SizedBox(
width: double.infinity,
height: widget.height,
child: DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [SBBColors.white.withOpacity(0.0), color],
stops: [1.0 - widget.widthRatio, 1.0],
),
),
),
),
),
),
),
);
}

BorderRadius _resolveBorderRadius() {
return widget.circularBorderRadius > 0
? BorderRadius.only(
bottomLeft: Radius.circular(widget.circularBorderRadius),
bottomRight: Radius.circular(widget.circularBorderRadius),
)
: BorderRadius.zero;
}
}
2 changes: 2 additions & 0 deletions lib/src/checkbox/checkbox.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export 'sbb_checkbox_list_item.dart';
export 'sbb_checkbox.dart';
Loading
Loading