Skip to content

Commit

Permalink
feat: rework SBBCheckbox and align to design specs (#258)
Browse files Browse the repository at this point in the history
* feat: paint checkbox instead of stacking boxes

* feat: align colors of design specs to checkbox

* feat: add boxed CheckboxListItem

* feat: fix trailing icon weirdness

* test: add tests for boxed listItem

* feat: add isLoading to SBBCheckboxListItem

* docs: add documentation to SBBCheckboxListItem

* feat: add semantics to SBBCheckbox and SBBCheckboxListItem

* chore: final cleanup and refactorings

* chore: update changelog with checkbox changes

* fix: fix support for FlutterSDK < 3.27.x in example app

* fix: fix mouse courser for Flutter SDK < 3.27.x

* fix: correct right padding on SBBIconButtonSmall

* feat: remove isBoxed variant and replace with SBBGroup
  • Loading branch information
smallTrogdor authored Jan 11, 2025
1 parent ab2e699 commit 15c8cbd
Show file tree
Hide file tree
Showing 29 changed files with 929 additions and 365 deletions.
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

0 comments on commit 15c8cbd

Please sign in to comment.