Skip to content

Commit

Permalink
Merge branch 'main' into feat/milestone-list-header
Browse files Browse the repository at this point in the history
  • Loading branch information
dtscalac authored Jan 30, 2025
2 parents 419d1f6 + e54098a commit 2f21e9c
Show file tree
Hide file tree
Showing 6 changed files with 411 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import 'package:catalyst_voices/common/ext/document_property_schema_ext.dart';
import 'package:catalyst_voices/widgets/toggles/voices_radio_button_form.dart';
import 'package:catalyst_voices_models/catalyst_voices_models.dart';
import 'package:catalyst_voices_view_models/catalyst_voices_view_models.dart';
import 'package:flutter/material.dart';

class RadioButtonSelectWidget extends StatefulWidget {
final DocumentValueProperty<String> property;
final DocumentRadioButtonSelect schema;
final bool isEditMode;
final ValueChanged<List<DocumentChange>> onChanged;

const RadioButtonSelectWidget({
super.key,
required this.property,
required this.schema,
required this.isEditMode,
required this.onChanged,
});

@override
State<RadioButtonSelectWidget> createState() =>
_RadioButtonSelectionWidgetState();
}

class _RadioButtonSelectionWidgetState extends State<RadioButtonSelectWidget> {
late String _selectedValue;

List<String> get _items => widget.schema.enumValues ?? <String>[];

String get _title => widget.schema.formattedTitle;

@override
void initState() {
super.initState();
_handleInitialValue();
}

@override
void didUpdateWidget(RadioButtonSelectWidget oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.property.value != widget.property.value) {
_handleInitialValue();
}

if (oldWidget.isEditMode != widget.isEditMode &&
widget.isEditMode == false) {
_handleInitialValue();
}
}

@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (_title.isNotEmpty) ...[
Text(
_title,
style: Theme.of(context).textTheme.titleSmall,
),
const SizedBox(height: 8),
VoicesRadioButtonFormField(
selectedValue: _selectedValue,
items: _items,
enabled: widget.isEditMode,
onChanged: _handleValueChanged,
validator: _validate,
),
],
],
);
}

void _handleInitialValue() {
_selectedValue = widget.property.value ?? '';
}

void _handleValueChanged(String? value) {
setState(() {
_selectedValue = value ?? '';
});

if (widget.property.value != value) {
_notifyChangeListener(value);
}
}

void _notifyChangeListener(String? value) {
final change = DocumentValueChange(
nodeId: widget.schema.nodeId,
value: value,
);
widget.onChanged([change]);
}

String? _validate(String? value) {
final result = widget.schema.validate(value);
return LocalizedDocumentValidationResult.from(result).message(context);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import 'package:catalyst_voices/widgets/document_builder/duration_in_months_widg
import 'package:catalyst_voices/widgets/document_builder/language_code_widget.dart';
import 'package:catalyst_voices/widgets/document_builder/list_length_picker_widget.dart';
import 'package:catalyst_voices/widgets/document_builder/multiline_text_entry_markdown_widget.dart';
import 'package:catalyst_voices/widgets/document_builder/radio_button_selection_widget.dart';
import 'package:catalyst_voices/widgets/document_builder/simple_text_entry_widget.dart';
import 'package:catalyst_voices/widgets/document_builder/single_dropdown_selection_widget.dart';
import 'package:catalyst_voices/widgets/document_builder/single_grouped_tag_selector_widget.dart';
Expand Down Expand Up @@ -387,6 +388,15 @@ class _PropertyValueBuilder extends StatelessWidget {
isEditMode: isEditMode,
onChanged: onChanged,
);

case DocumentRadioButtonSelect():
return RadioButtonSelectWidget(
property: schema.castProperty(property),
schema: schema,
isEditMode: isEditMode,
onChanged: onChanged,
);

case DocumentDurationInMonthsSchema():
return DurationInMonthsWidget(
property: schema.castProperty(property),
Expand All @@ -401,7 +411,6 @@ class _PropertyValueBuilder extends StatelessWidget {
isEditMode: isEditMode,
onChanged: onChanged,
);

case DocumentSpdxLicenseOrUrlSchema():
case DocumentGenericIntegerSchema():
case DocumentGenericNumberSchema():
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import 'package:catalyst_voices/common/ext/ext.dart';
import 'package:catalyst_voices/widgets/toggles/voices_radio.dart';
import 'package:catalyst_voices_localization/catalyst_voices_localization.dart';
import 'package:catalyst_voices_shared/catalyst_voices_shared.dart';
import 'package:flutter/material.dart';

class VoicesRadioButtonFormField extends FormField<String> {
final String? selectedValue;
final ValueChanged<String?>? onChanged;
final List<String> items;

VoicesRadioButtonFormField({
super.key,
required this.items,
required this.onChanged,
required this.selectedValue,
super.enabled,
super.validator,
AutovalidateMode autovalidateMode = AutovalidateMode.always,
}) : super(
initialValue: selectedValue,
autovalidateMode: autovalidateMode,
builder: (field) {
final state = field as _RadioButtonFormState;
void onChangedHandler(String? selected) {
field.didChange(selected);
onChanged?.call(selected);
}

return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_RadioButtonList(
enabled: enabled,
items: items,
onChanged: onChangedHandler,
value: state._internalValue,
),
if (field.hasError)
_ErrorText(
errorText: field.errorText,
),
],
);
},
);

@override
FormFieldState<String> createState() => _RadioButtonFormState();
}

class _RadioButtonFormState extends FormFieldState<String> {
String? _internalValue;

@override
void initState() {
super.initState();
_internalValue = widget.initialValue;
}

@override
void didUpdateWidget(VoicesRadioButtonFormField oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.initialValue != _internalValue) {
_internalValue = widget.initialValue;
}
if (_internalValue != value) {
setValue(_internalValue);
validate();
}
}
}

class _RadioButtonList extends StatelessWidget {
final bool enabled;
final List<String> items;
final ValueChanged<String?>? onChanged;
final String? value;

const _RadioButtonList({
required this.enabled,
required this.items,
this.onChanged,
required this.value,
});

@override
Widget build(BuildContext context) {
return IgnorePointer(
ignoring: !enabled,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: items
.map<Widget>(
(e) => VoicesRadio<String>(
key: ValueKey(e),
value: e,
label: Text(
e,
style: getStyle(context, e),
),
groupValue: value,
onChanged: onChanged,
),
)
.separatedBy(const SizedBox(height: 10))
.toList(),
),
);
}

TextStyle? getStyle(BuildContext context, String? itemValue) {
final textStyle = context.textTheme.bodyLarge;
if (enabled || value == itemValue) {
return textStyle;
}
return textStyle?.copyWith(
color: context.colors.textDisabled,
);
}
}

class _ErrorText extends StatelessWidget {
final String? errorText;

const _ErrorText({
this.errorText,
});

@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(top: 4),
child: Text(
errorText ?? context.l10n.snackbarErrorLabelText,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.error,
),
),
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import 'package:catalyst_voices/widgets/toggles/voices_radio.dart';
import 'package:catalyst_voices/widgets/toggles/voices_radio_button_form.dart';
import 'package:catalyst_voices_brands/catalyst_voices_brands.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';

import '../../helpers/helpers.dart';

void main() {
group(VoicesRadioButtonFormField, () {
late List<String> items;
late VoicesColorScheme voicesColors;

setUpAll(() {
items = ['Item 1', 'Item 2', 'Item 3'];

voicesColors = const VoicesColorScheme.optional(
textDisabled: Colors.grey,
outlineBorderVariant: Colors.blueGrey,
);
});

testWidgets('initially displays with no selection', (tester) async {
await tester.pumpApp(
Scaffold(
body: Center(
child: VoicesRadioButtonFormField(
items: items,
selectedValue: null,
onChanged: (_) {},
),
),
),
voicesColors: voicesColors,
);
await tester.pumpAndSettle();

final option1Finder = find.text('Item 1');
final option2Finder = find.text('Item 2');
final option3Finder = find.text('Item 3');

expect(option1Finder, findsOneWidget);
expect(option2Finder, findsOneWidget);
expect(option3Finder, findsOneWidget);

final radioButtons = find.byType(VoicesRadio<String>);
expect(radioButtons, findsNWidgets(3));
});

testWidgets('updates selection when an item is tapped', (tester) async {
String? selectedValue;
await tester.pumpApp(
Scaffold(
body: Center(
child: VoicesRadioButtonFormField(
items: items,
selectedValue: 'Item 1',
onChanged: (value) {
selectedValue = value;
},
),
),
),
voicesColors: voicesColors,
);
await tester.pumpAndSettle();

await tester.tap(find.text('Item 2'));
await tester.pump();

expect(selectedValue, 'Item 2');
});

testWidgets('displays error message when in error state',
(WidgetTester tester) async {
await tester.pumpApp(
Scaffold(
body: Center(
child: VoicesRadioButtonFormField(
items: items,
selectedValue: null,
onChanged: (_) {},
validator: (value) =>
value == null ? 'Selection is required' : null,
autovalidateMode: AutovalidateMode.always,
),
),
),
voicesColors: voicesColors,
);
await tester.pumpAndSettle();

final errorTextFinder = find.text('Selection is required');
expect(errorTextFinder, findsOneWidget);
});
});
}
Loading

0 comments on commit 2f21e9c

Please sign in to comment.