-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(cat-voices): Add link(s) component (#1492)
* feat: creating ui layout * feat: adding validation for text field * feat: adding localization string for validation * chore: adding ui styling * feat: changing logic of suffix icon in voices_textfield * fix: remove unecessary check * fix: relisten to controller if widget changes * chore: conditional logic, making variables private * chore: changing widget from stateful to stateless * chore: adding extension to format description
- Loading branch information
Showing
10 changed files
with
423 additions
and
6 deletions.
There are no files selected for viewing
8 changes: 8 additions & 0 deletions
8
catalyst_voices/apps/voices/lib/common/ext/document_property_ext.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,8 @@ | ||
import 'package:catalyst_voices/common/ext/string_ext.dart'; | ||
import 'package:catalyst_voices_models/catalyst_voices_models.dart'; | ||
|
||
extension DocumentPropertyExt on DocumentProperty { | ||
String get formattedDescription { | ||
return (schema.description ?? '').starred(isEnabled: schema.isRequired); | ||
} | ||
} |
123 changes: 123 additions & 0 deletions
123
...st_voices/apps/voices/lib/widgets/document_builder/single_line_https_url_widget.dart.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,123 @@ | ||
import 'package:catalyst_voices/common/ext/document_property_ext.dart'; | ||
import 'package:catalyst_voices/widgets/text_field/voices_https_text_field.dart'; | ||
import 'package:catalyst_voices/widgets/widgets.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 SingleLineHttpsUrlWidget extends StatefulWidget { | ||
final DocumentProperty<String> property; | ||
final bool isEditMode; | ||
final ValueChanged<DocumentChange> onChanged; | ||
|
||
const SingleLineHttpsUrlWidget({ | ||
super.key, | ||
required this.property, | ||
required this.isEditMode, | ||
required this.onChanged, | ||
}); | ||
|
||
@override | ||
State<SingleLineHttpsUrlWidget> createState() => | ||
_SingleLineHttpsUrlWidgetState(); | ||
} | ||
|
||
class _SingleLineHttpsUrlWidgetState extends State<SingleLineHttpsUrlWidget> { | ||
late final TextEditingController _textEditingController; | ||
late final FocusNode _focusNode; | ||
|
||
String get _description => widget.property.formattedDescription; | ||
|
||
@override | ||
void initState() { | ||
super.initState(); | ||
_textEditingController = TextEditingController(text: widget.property.value); | ||
_textEditingController.addListener(_handleControllerChange); | ||
_focusNode = FocusNode(canRequestFocus: widget.isEditMode); | ||
} | ||
|
||
@override | ||
void didUpdateWidget(covariant SingleLineHttpsUrlWidget oldWidget) { | ||
super.didUpdateWidget(oldWidget); | ||
|
||
if (oldWidget.isEditMode != widget.isEditMode && | ||
widget.isEditMode == false) { | ||
_textEditingController.text = widget.property.value ?? ''; | ||
} | ||
|
||
if (widget.isEditMode != oldWidget.isEditMode) { | ||
_handleEditModeChanged(); | ||
} | ||
} | ||
|
||
@override | ||
void dispose() { | ||
_textEditingController.dispose(); | ||
super.dispose(); | ||
} | ||
|
||
@override | ||
Widget build(BuildContext context) { | ||
return Column( | ||
crossAxisAlignment: CrossAxisAlignment.start, | ||
mainAxisSize: MainAxisSize.min, | ||
children: [ | ||
if (_description.isNotEmpty) ...[ | ||
Text( | ||
_description, | ||
style: Theme.of(context).textTheme.titleSmall, | ||
), | ||
const SizedBox(height: 8), | ||
], | ||
VoicesHttpsTextField( | ||
controller: _textEditingController, | ||
focusNode: _focusNode, | ||
onFieldSubmitted: _notifyChangeListener, | ||
validator: _validate, | ||
enabled: widget.isEditMode, | ||
), | ||
], | ||
); | ||
} | ||
|
||
void _handleControllerChange() { | ||
final controllerValue = _textEditingController.text; | ||
if (widget.property.value != controllerValue && | ||
controllerValue.isNotEmpty) { | ||
_notifyChangeListener(controllerValue); | ||
} | ||
} | ||
|
||
void _notifyChangeListener(String? value) { | ||
final change = DocumentChange( | ||
nodeId: widget.property.schema.nodeId, | ||
value: value, | ||
); | ||
|
||
widget.onChanged(change); | ||
} | ||
|
||
VoicesTextFieldValidationResult _validate(String? value) { | ||
if (value == null || value.isEmpty) { | ||
return const VoicesTextFieldValidationResult.none(); | ||
} | ||
final schema = widget.property.schema; | ||
final result = schema.validatePropertyValue(value); | ||
if (result.isValid) { | ||
return const VoicesTextFieldValidationResult.success(); | ||
} else { | ||
final localized = LocalizedDocumentValidationResult.from(result); | ||
return VoicesTextFieldValidationResult.error(localized.message(context)); | ||
} | ||
} | ||
|
||
void _handleEditModeChanged() { | ||
_focusNode.canRequestFocus = widget.isEditMode; | ||
|
||
if (widget.isEditMode) { | ||
_focusNode.requestFocus(); | ||
} else { | ||
_focusNode.unfocus(); | ||
} | ||
} | ||
} |
147 changes: 147 additions & 0 deletions
147
catalyst_voices/apps/voices/lib/widgets/text_field/voices_https_text_field.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,147 @@ | ||
import 'package:catalyst_voices/widgets/widgets.dart'; | ||
import 'package:catalyst_voices_assets/catalyst_voices_assets.dart'; | ||
import 'package:catalyst_voices_brands/catalyst_voices_brands.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 VoicesHttpsTextField extends StatefulWidget { | ||
final ValueChanged<String?>? onFieldSubmitted; | ||
final TextEditingController? controller; | ||
final FocusNode? focusNode; | ||
final VoicesTextFieldValidator? validator; | ||
final bool enabled; | ||
|
||
const VoicesHttpsTextField({ | ||
super.key, | ||
this.onFieldSubmitted, | ||
this.focusNode, | ||
this.enabled = false, | ||
this.controller, | ||
this.validator, | ||
}); | ||
|
||
@override | ||
State<VoicesHttpsTextField> createState() => _VoicesHttpsTextFieldState(); | ||
} | ||
|
||
class _VoicesHttpsTextFieldState extends State<VoicesHttpsTextField> | ||
with LaunchUrlMixin { | ||
TextEditingController? _controller; | ||
TextEditingController get _effectiveController { | ||
return widget.controller ?? (_controller ??= TextEditingController()); | ||
} | ||
|
||
@override | ||
void dispose() { | ||
_controller?.dispose(); | ||
_controller = null; | ||
|
||
super.dispose(); | ||
} | ||
|
||
@override | ||
Widget build(BuildContext context) { | ||
return GestureDetector( | ||
onTap: widget.enabled ? null : _launchUrl, | ||
child: VoicesTextField( | ||
controller: _effectiveController, | ||
focusNode: widget.focusNode, | ||
onFieldSubmitted: widget.onFieldSubmitted, | ||
validator: widget.validator, | ||
decoration: VoicesTextFieldDecoration( | ||
hintText: | ||
widget.enabled ? context.l10n.noUrlAdded : context.l10n.addUrl, | ||
prefixIcon: VoicesAssets.icons.link.buildIcon(), | ||
showStatusSuffixIcon: widget.enabled, | ||
suffixIcon: ValueListenableBuilder( | ||
valueListenable: _effectiveController, | ||
builder: (context, value, child) { | ||
return _AdditionalSuffixIcons( | ||
enabled: widget.enabled, | ||
canClear: value.text.isNotEmpty, | ||
onLinkTap: _launchUrl, | ||
onClearTap: _onClearTap, | ||
); | ||
}, | ||
), | ||
enabledBorder: OutlineInputBorder( | ||
borderSide: BorderSide( | ||
color: Theme.of(context).colorScheme.outlineVariant, | ||
), | ||
), | ||
disabledBorder: OutlineInputBorder( | ||
borderSide: BorderSide( | ||
color: Theme.of(context).colorScheme.outlineVariant, | ||
), | ||
), | ||
fillColor: Theme.of(context).colors.onSurfaceNeutralOpaqueLv1, | ||
filled: true, | ||
), | ||
style: _getTextStyle(context), | ||
enabled: widget.enabled, | ||
readOnly: !widget.enabled, | ||
autovalidateMode: AutovalidateMode.onUserInteraction, | ||
), | ||
); | ||
} | ||
|
||
TextStyle? _getTextStyle(BuildContext context) { | ||
final theme = Theme.of(context); | ||
final textTheme = theme.textTheme; | ||
if (widget.enabled) { | ||
return textTheme.bodyLarge; | ||
} | ||
return textTheme.bodyLarge?.copyWith( | ||
color: theme.colorScheme.primary, | ||
decoration: TextDecoration.underline, | ||
decorationColor: theme.colorScheme.primary, | ||
); | ||
} | ||
|
||
Future<void> _launchUrl() async { | ||
if (_effectiveController.text.isEmpty) return; | ||
|
||
final url = Uri.tryParse(_effectiveController.text); | ||
if (url != null) { | ||
await launchHrefUrl(url); | ||
} | ||
} | ||
|
||
void _onClearTap() { | ||
_effectiveController.clear(); | ||
} | ||
} | ||
|
||
class _AdditionalSuffixIcons extends StatelessWidget { | ||
final bool enabled; | ||
final bool canClear; | ||
final VoidCallback onLinkTap; | ||
final VoidCallback onClearTap; | ||
|
||
const _AdditionalSuffixIcons({ | ||
required this.enabled, | ||
required this.canClear, | ||
required this.onLinkTap, | ||
required this.onClearTap, | ||
}); | ||
|
||
@override | ||
Widget build(BuildContext context) { | ||
if (!enabled) { | ||
return VoicesIconButton( | ||
onTap: onLinkTap, | ||
child: VoicesAssets.icons.externalLink.buildIcon( | ||
color: Theme.of(context).colorScheme.primary, | ||
), | ||
); | ||
} | ||
return Offstage( | ||
offstage: !canClear, | ||
child: TextButton( | ||
onPressed: onClearTap, | ||
child: Text(context.l10n.clear), | ||
), | ||
); | ||
} | ||
} |
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
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
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
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.