-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: VoicesCheckbox, VoicesCheckboxGroup, VoicesRadio (#663)
* feat: Basic VoicesRadio and theming * feat: VoicesRadio now accepts Label and Note widgets + update example page * Themed VoicesCheckbox + example * feat: Checkbox label + note. Update docs plus examples * doc: LabelDecorator docs * doc: Missing docs dots * fix: add toggleable to project.dlc * fix: remove unnecessary null unwrapping * test: AffixDecorator tests * LabelDecorator tests * feat: CheckboxGroup initial draft * feat: finish VoicesCheckboxGroup widget * fix: static analyze warning + some VoicesCheckboxGroupElement tests * test: VoicesCheckboxGroup and VoicesCheckbox tests * fix: test name typo
- Loading branch information
1 parent
178813f
commit c420b00
Showing
17 changed files
with
1,331 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -245,3 +245,4 @@ xcworkspace | |
yoroi | ||
unchunk | ||
EUTXO | ||
toggleable |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
import 'package:catalyst_voices_brands/catalyst_voices_brands.dart'; | ||
import 'package:collection/collection.dart'; | ||
import 'package:flutter/material.dart'; | ||
|
||
class LabelDecorator extends StatelessWidget { | ||
/// An optional widget to display the label text next to the child. | ||
final Widget? label; | ||
|
||
/// An optional widget to display the note text next to the child. | ||
final Widget? note; | ||
|
||
/// The main content of the decorator. | ||
final Widget child; | ||
|
||
const LabelDecorator({ | ||
super.key, | ||
this.label, | ||
this.note, | ||
required this.child, | ||
}); | ||
|
||
@override | ||
Widget build(BuildContext context) { | ||
final label = this.label; | ||
final note = this.note; | ||
final theme = Theme.of(context); | ||
|
||
return Row( | ||
mainAxisSize: MainAxisSize.min, | ||
children: [ | ||
Flexible(child: child), | ||
if (label != null) | ||
DefaultTextStyle( | ||
style: (theme.textTheme.bodyLarge ?? const TextStyle()) | ||
.copyWith(color: theme.colors.textPrimary), | ||
child: label, | ||
), | ||
if (note != null) | ||
DefaultTextStyle( | ||
style: (theme.textTheme.bodySmall ?? const TextStyle()) | ||
.copyWith(color: theme.colors.textOnPrimary), | ||
child: note, | ||
), | ||
].expandIndexed( | ||
(index, element) { | ||
return [ | ||
if (index == 1) const SizedBox(width: 4), | ||
if (index == 2) const SizedBox(width: 8), | ||
element, | ||
]; | ||
}, | ||
).toList(), | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
import 'package:catalyst_voices/widgets/common/label_decorator.dart'; | ||
import 'package:flutter/material.dart'; | ||
|
||
/// A checkbox widget with optional label, note, and error state. | ||
/// | ||
/// This widget provides a visual representation of a boolean value and allows | ||
/// users to toggle its state. It can display an optional label and note | ||
/// for context, as well as indicate an error state through visual styling. | ||
class VoicesCheckbox extends StatelessWidget { | ||
/// The current value of the checkbox. | ||
final bool value; | ||
|
||
/// Callback function invoked when the checkbox value changes. | ||
/// | ||
/// The function receives the new value as a parameter. | ||
/// | ||
/// When null widget is disabled. | ||
final ValueChanged<bool>? onChanged; | ||
|
||
/// Indicates whether the checkbox is in an error state. | ||
final bool isError; | ||
|
||
/// An optional widget to display the label text. | ||
final Widget? label; | ||
|
||
/// An optional widget to display the note text. | ||
final Widget? note; | ||
|
||
const VoicesCheckbox({ | ||
super.key, | ||
required this.value, | ||
required this.onChanged, | ||
this.isError = false, | ||
this.label, | ||
this.note, | ||
}); | ||
|
||
@override | ||
Widget build(BuildContext context) { | ||
final onChanged = this.onChanged; | ||
|
||
return GestureDetector( | ||
onTap: onChanged != null ? () => onChanged(!value) : null, | ||
behavior: HitTestBehavior.opaque, | ||
child: LabelDecorator( | ||
label: label, | ||
note: note, | ||
child: Checkbox( | ||
value: value, | ||
// forcing null unwrapping because we're not allowing null value | ||
onChanged: onChanged != null ? (value) => onChanged(value!) : null, | ||
isError: isError, | ||
), | ||
), | ||
); | ||
} | ||
} |
152 changes: 152 additions & 0 deletions
152
catalyst_voices/lib/widgets/toggles/voices_checkbox_group.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,152 @@ | ||
import 'package:catalyst_voices/widgets/toggles/voices_checkbox.dart'; | ||
import 'package:flutter/material.dart'; | ||
|
||
/// Data describing a single checkbox element within a [VoicesCheckboxGroup]. | ||
/// | ||
/// This class holds information for a single checkbox element displayed | ||
/// within a [VoicesCheckboxGroup]. | ||
/// | ||
/// Type Parameters: | ||
/// | ||
/// * T: The type of the value associated with the checkbox element. | ||
final class VoicesCheckboxGroupElement<T> { | ||
/// The value associated with the checkbox element. | ||
final T value; | ||
|
||
/// A widget to display the text label for the checkbox. | ||
final Widget? label; | ||
|
||
/// An optional widget to display additional information below the label. | ||
final Widget? note; | ||
|
||
/// A flag indicating if the checkbox represents an error state. | ||
final bool isError; | ||
|
||
/// Default constructor for [VoicesCheckboxGroupElement]. | ||
/// | ||
/// Should have at least [label] or [note]. | ||
const VoicesCheckboxGroupElement({ | ||
required this.value, | ||
this.label, | ||
this.note, | ||
this.isError = false, | ||
}) : assert( | ||
label != null || note != null, | ||
'Should have at least label or note', | ||
); | ||
} | ||
|
||
/// A widget that groups a list of checkboxes in a column with a name | ||
/// and the ability to toggle all elements at once. | ||
/// | ||
/// This widget displays a group of checkboxes with a shared name. It allows | ||
/// users to select individual checkboxes and provides a master checkbox to | ||
/// toggle all elements on/off simultaneously. You can also optionally specify | ||
/// a callback function to be notified when the selection changes. | ||
/// | ||
/// Type Parameters: | ||
/// | ||
/// * T: The type of the value associated with each checkbox element. | ||
class VoicesCheckboxGroup<T extends Object> extends StatelessWidget { | ||
/// The name displayed for the checkbox group. | ||
final Widget name; | ||
|
||
/// A list of [VoicesCheckboxGroupElement] objects defining each checkbox. | ||
final List<VoicesCheckboxGroupElement<T>> elements; | ||
|
||
/// A set of currently selected values within the group. | ||
final Set<T> selected; | ||
|
||
/// An optional callback function to be called when the selection changes. | ||
final ValueChanged<Set<T>>? onChanged; | ||
|
||
/// How the checkboxes should be aligned horizontally within the group. | ||
/// | ||
/// Defaults to [CrossAxisAlignment.start]. | ||
final CrossAxisAlignment crossAxisAlignment; | ||
|
||
bool get _isGroupEnabled => onChanged != null; | ||
|
||
bool get _isGroupSelected => | ||
elements.every((element) => selected.contains(element.value)); | ||
|
||
const VoicesCheckboxGroup({ | ||
super.key, | ||
required this.name, | ||
required this.elements, | ||
required this.selected, | ||
this.onChanged, | ||
this.crossAxisAlignment = CrossAxisAlignment.start, | ||
}) : assert(elements.length > 0, 'Elements have to be non empty'); | ||
|
||
@override | ||
Widget build(BuildContext context) { | ||
return Column( | ||
mainAxisSize: MainAxisSize.min, | ||
crossAxisAlignment: crossAxisAlignment, | ||
children: [ | ||
VoicesCheckbox( | ||
value: _isGroupSelected, | ||
label: name, | ||
onChanged: _isGroupEnabled ? _toggleAll : null, | ||
), | ||
Padding( | ||
padding: const EdgeInsetsDirectional.only(start: 24), | ||
child: Column( | ||
mainAxisSize: MainAxisSize.min, | ||
crossAxisAlignment: crossAxisAlignment, | ||
children: elements | ||
.map( | ||
(element) { | ||
return VoicesCheckbox( | ||
value: selected.contains(element.value), | ||
label: element.label, | ||
note: element.note, | ||
onChanged: _isGroupEnabled | ||
? (value) => _updateElement(element.value, value) | ||
: null, | ||
isError: element.isError, | ||
); | ||
}, | ||
) | ||
.expand( | ||
(element) => [ | ||
const SizedBox(height: 16), | ||
element, | ||
], | ||
) | ||
.toList(), | ||
), | ||
), | ||
], | ||
); | ||
} | ||
|
||
void _toggleAll(bool isChecked) { | ||
assert( | ||
onChanged != null, | ||
'Toggled group status but change callback is null', | ||
); | ||
|
||
final updatedSelection = | ||
isChecked ? elements.map((e) => e.value).toSet() : <T>{}; | ||
|
||
onChanged!(updatedSelection); | ||
} | ||
|
||
void _updateElement(T value, bool isChecked) { | ||
assert( | ||
onChanged != null, | ||
'Toggled group status but change callback is null', | ||
); | ||
|
||
final updatedSelection = <T>{...selected}; | ||
if (isChecked) { | ||
updatedSelection.add(value); | ||
} else { | ||
updatedSelection.remove(value); | ||
} | ||
|
||
onChanged!(updatedSelection); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
import 'package:catalyst_voices/widgets/common/label_decorator.dart'; | ||
import 'package:flutter/material.dart'; | ||
|
||
/// A Voices Radio widget that combines a Radio button with optional | ||
/// label and note text elements. | ||
/// | ||
/// Provides a convenient way to create radio buttons with associated | ||
/// information in a visually appealing manner. | ||
class VoicesRadio<T extends Object> extends StatelessWidget { | ||
/// The value associated with this radio button. | ||
final T value; | ||
|
||
/// An optional widget to display the label text. | ||
final Widget? label; | ||
|
||
/// An optional widget to display the note text. | ||
final Widget? note; | ||
|
||
/// The current selected value in the radio group. | ||
final T? groupValue; | ||
|
||
/// Whether the radio button value can be changed by widget itself. | ||
final bool toggleable; | ||
|
||
/// A callback function that is called when the radio button value changes. | ||
/// | ||
/// When null widget is disabled. | ||
final ValueChanged<T?>? onChanged; | ||
|
||
const VoicesRadio({ | ||
required this.value, | ||
this.label, | ||
this.note, | ||
required this.groupValue, | ||
this.toggleable = false, | ||
required this.onChanged, | ||
super.key, | ||
}); | ||
|
||
@override | ||
Widget build(BuildContext context) { | ||
return GestureDetector( | ||
onTap: onChanged != null ? _changeGroupValue : null, | ||
behavior: HitTestBehavior.opaque, | ||
child: LabelDecorator( | ||
label: label, | ||
note: note, | ||
child: Radio( | ||
value: value, | ||
groupValue: groupValue, | ||
onChanged: onChanged, | ||
toggleable: toggleable, | ||
), | ||
), | ||
); | ||
} | ||
|
||
void _changeGroupValue() { | ||
final onChanged = this.onChanged; | ||
assert( | ||
onChanged != null, | ||
'Make sure onChange is not null when using _changeGroupValue', | ||
); | ||
|
||
if (groupValue == value && toggleable) { | ||
onChanged!(null); | ||
} | ||
|
||
if (groupValue != value) { | ||
onChanged!(value); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.