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(cat-voices): Yes/No choice component #1508

Merged
merged 10 commits into from
Jan 14, 2025
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import 'package:catalyst_voices/common/ext/document_property_ext.dart';
import 'package:catalyst_voices/widgets/widgets.dart';
import 'package:catalyst_voices_localization/catalyst_voices_localization.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 YesNoChoiceWidget extends StatefulWidget {
final DocumentProperty<bool> property;
final ValueChanged<DocumentChange> onChanged;
final bool isEditMode;
final bool isRequired;

const YesNoChoiceWidget({
super.key,
required this.property,
required this.onChanged,
required this.isEditMode,
required this.isRequired,
});

@override
State<YesNoChoiceWidget> createState() => _YesNoChoiceWidgetState();
}

class _YesNoChoiceWidgetState extends State<YesNoChoiceWidget> {
late bool? selectedValue;

String get _description => widget.property.formattedDescription;

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

_handleInitialValue();
}

@override
void didUpdateWidget(covariant YesNoChoiceWidget 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(
crossAxisAlignment: CrossAxisAlignment.start,
LynxLynxx marked this conversation as resolved.
Show resolved Hide resolved
mainAxisSize: MainAxisSize.min,
children: [
if (_description.isNotEmpty) ...[
Text(
_description,
style: Theme.of(context).textTheme.titleSmall,
),
const SizedBox(height: 8),
],
_YesNoChoiceSegmentButton(
context,
value: selectedValue,
enabled: widget.isEditMode,
onChanged: _handleValueChanged,
validator: (value) {
// TODO(dtscalac): add validation
final result = widget.property.schema.validatePropertyValue(value);

return LocalizedDocumentValidationResult.from(result)
.message(context);
},
),
],
);
}

void _handleInitialValue() {
selectedValue = widget.property.value;
}

void _handleValueChanged(bool? value) {
setState(() {
selectedValue = value;
});
if (value == null && widget.property.value != value) {
_notifyChangeListener(value);
}
}

void _notifyChangeListener(bool? value) {
widget.onChanged(
DocumentChange(
nodeId: widget.property.schema.nodeId,
value: value,
),
);
}
}

class _YesNoChoiceSegmentButton extends FormField<bool?> {
final bool? value;
final ValueChanged<bool?>? onChanged;

_YesNoChoiceSegmentButton(
BuildContext context, {
super.key,
required this.value,
required this.onChanged,
super.validator,
super.enabled,
AutovalidateMode autovalidateMode = AutovalidateMode.onUserInteraction,
}) : super(
initialValue: value,
autovalidateMode: autovalidateMode,
builder: (field) {
void onChangedHandler(Set<bool> selected) {
final newValue = selected.isEmpty ? null : selected.first;
field.didChange(newValue);
onChanged?.call(newValue);
}

return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
IgnorePointer(
ignoring: !enabled,
child: VoicesSegmentedButton<bool>(
key: key,
segments: [
ButtonSegment(
value: true,
label: Text(context.l10n.yes),
),
ButtonSegment(
value: false,
label: Text(context.l10n.no),
),
],
selected: value != null ? {value} : {},
onChanged: onChangedHandler,
emptySelectionAllowed: true,
style: _getButtonStyle(field),
),
),
if (field.hasError)
Text(
field.errorText ?? context.l10n.snackbarErrorLabelText,
style: Theme.of(context)
.textTheme
.bodySmall
?.copyWith(color: Theme.of(context).colorScheme.error),
),
],
);
},
);

static ButtonStyle? _getButtonStyle(FormFieldState<bool?> field) {
if (field.errorText == null) return null;

return ButtonStyle(
side: WidgetStatePropertyAll(
BorderSide(
color: Theme.of(field.context).colorScheme.error,
),
),
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'package:catalyst_voices/widgets/document_builder/document_token_value_wi
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';
import 'package:catalyst_voices/widgets/document_builder/single_line_https_url_widget.dart.dart';
import 'package:catalyst_voices/widgets/document_builder/yes_no_choice_widget.dart';
import 'package:catalyst_voices/widgets/widgets.dart';
import 'package:catalyst_voices_localization/catalyst_voices_localization.dart';
import 'package:catalyst_voices_models/catalyst_voices_models.dart';
Expand Down Expand Up @@ -113,7 +114,11 @@ class _DocumentBuilderSectionTileState
void _toggleEditMode() {
setState(() {
_isEditMode = !_isEditMode;
_pendingChanges.clear();
if (!_isEditMode) {
_pendingChanges.clear();
_editedSection = widget.section;
_builder = _editedSection.toBuilder();
}
});
}

Expand Down Expand Up @@ -218,7 +223,6 @@ class _PropertyBuilder extends StatelessWidget {
case TagGroupDefinition():
case TagSelectionDefinition():
case DurationInMonthsDefinition():
case YesNoChoiceDefinition():
case SPDXLicenceOrUrlDefinition():
case LanguageCodeDefinition():
throw UnimplementedError('${definition.type} not implemented');
Expand Down Expand Up @@ -269,6 +273,14 @@ class _PropertyBuilder extends StatelessWidget {
isEditMode: isEditMode,
onChanged: onChanged,
);
case YesNoChoiceDefinition():
final castProperty = definition.castProperty(property);
return YesNoChoiceWidget(
property: castProperty,
onChanged: onChanged,
isEditMode: isEditMode,
isRequired: castProperty.schema.isRequired,
);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ final class YesNoChoiceDefinition extends BaseDocumentDefinition<bool> {
DocumentSchemaProperty<bool> schema,
bool? value,
) {
// TODO(dtscalac): validate yes no choice
return DocumentValidator.validateBool(schema, value);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,16 @@ final class DocumentValidator {
// ignore: avoid_positional_boolean_parameters
bool? value,
) {
return validateBasic(schema, value);
final result = validateBasic(schema, value);

if (result.isInvalid) {
return result;
}

if (value == null) {
return MissingRequiredDocumentValue(invalidNodeId: schema.nodeId);
}
return const SuccessfulDocumentValidation();
}

static DocumentValidationResult validatePattern(
Expand Down
Loading