From 22e280c8f5686ac2e9058956c596bfddc119e228 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 22 Nov 2024 10:26:21 +0000 Subject: [PATCH 1/5] fix(TM-47414): Setting the value of the text box on submit and closing the dialog box --- .../components/organisms/file_picker/file_picker.dart | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/src/components/organisms/file_picker/file_picker.dart b/lib/src/components/organisms/file_picker/file_picker.dart index e685473..694fb63 100644 --- a/lib/src/components/organisms/file_picker/file_picker.dart +++ b/lib/src/components/organisms/file_picker/file_picker.dart @@ -1056,7 +1056,14 @@ class _MultiInputDialogState extends State<_MultiInputDialog> { widget.textFields[index].value = value; }); }, - onFieldSubmitted: (String value) => widget.textFields[index].value = value, + onFieldSubmitted: (String value) { + setState(() { + widget.textFields[index].value = value; + }); + if (isValid) { + unawaited(Navigator.maybePop(context, widget.textFields)); + } + }, validator: (String? value) => widget.onValidate?.call(widget.textFields[index]), decoration: ZdsInputDecoration( hintText: widget.textFields[index].hint, From 0b39e4ed883138be18fc4ce335d4b88b220cf176 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 22 Nov 2024 10:28:55 +0000 Subject: [PATCH 2/5] feat(TM-48504): Creating custom widget having copying capability --- .../pages/components/selectable_widget.dart | 56 ++++++++++ example/lib/routes.dart | 5 + lib/src/components/organisms.dart | 1 + .../organisms/selectable_widget.dart | 102 ++++++++++++++++++ 4 files changed, 164 insertions(+) create mode 100644 example/lib/pages/components/selectable_widget.dart create mode 100644 lib/src/components/organisms/selectable_widget.dart diff --git a/example/lib/pages/components/selectable_widget.dart b/example/lib/pages/components/selectable_widget.dart new file mode 100644 index 0000000..1d8e7a9 --- /dev/null +++ b/example/lib/pages/components/selectable_widget.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; +import 'package:zds_flutter/zds_flutter.dart'; + +/// Contains a demonstration of the ZdsSelectableWidget class. +/// This demo showcases how to use the ZdsSelectableWidget with both HTML and plain text content within a Flutter application. +class SelectableWidgetDemo extends StatelessWidget { + const SelectableWidgetDemo({super.key}); + @override + Widget build(BuildContext context) { + String htmlContent = + '''


Birds are a group of

warm-blooded +This paragraph contains a lot of lines in the source code, but the browser ignores it. +This paragraph contains a lot of spaces in the source code, but the browser ignores it. +The number of lines in a paragraph depends on the size of the browser window. If you resize the browser window, the number of lines in this paragraph will change.

+'''; + String plainTextContent = + 'The standard chunk of Lorem Ipsum used since the 1500s is reproduced below for those interested. Sections 1.10.32 and 1.10.33 from "de Finibus Bonorum et Malorum" by Cicero are also reproduced in their exact original form, accompanied by English versions from the 1914 translation by H. Rackham.'; + return Scaffold( + backgroundColor: Theme.of(context).colorScheme.surface, + body: Column( + children: [ + ZdsListGroup( + padding: EdgeInsets.all(10.0), + headerLabel: Text('For Html Content'), + items: [ + ZdsSelectableWidget( + copyable: true, + textToCopy: htmlContent, + isHtmlData: true, + child: ZdsHtmlContainer( + htmlContent, + showReadMore: false, + onLinkTap: (_, __, ___) { + print('Link tapped'); + }, + ), + ) + ], + ), + ZdsListGroup( + padding: EdgeInsets.all(10.0), + headerLabel: Text('For Plain Text Content'), + items: [ + ZdsSelectableWidget( + copyable: true, + textToCopy: plainTextContent, + isHtmlData: true, + child: Text('$plainTextContent'), + ) + ], + ), + ], + ), + ); + } +} diff --git a/example/lib/routes.dart b/example/lib/routes.dart index 010f29e..1a2aae1 100644 --- a/example/lib/routes.dart +++ b/example/lib/routes.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:zds_flutter_example/pages/components/chat.dart'; import 'package:zds_flutter_example/pages/components/comment.dart'; +import 'package:zds_flutter_example/pages/components/selectable_widget.dart'; import 'home.dart'; import 'pages/assets/animations.dart'; @@ -318,6 +319,10 @@ final kRoutes = { title: 'Nested Flow', child: NestedFlowDemo(), ), + const DemoRoute( + title: 'SelectableWidget', + child: SelectableWidgetDemo(), + ), ], 'Assets': [ const DemoRoute( diff --git a/lib/src/components/organisms.dart b/lib/src/components/organisms.dart index db7b594..9762fcf 100644 --- a/lib/src/components/organisms.dart +++ b/lib/src/components/organisms.dart @@ -24,6 +24,7 @@ export 'organisms/properties_list.dart'; export 'organisms/quill_editor/quill.dart'; export 'organisms/radio_list.dart'; export 'organisms/search_app_bar.dart'; +export 'organisms/selectable_widget.dart'; export 'organisms/tab_scaffold.dart'; export 'organisms/tags_list.dart'; export 'organisms/temp_directory/resolver.dart' show clearUiTempDirectory; diff --git a/lib/src/components/organisms/selectable_widget.dart b/lib/src/components/organisms/selectable_widget.dart new file mode 100644 index 0000000..8d5300f --- /dev/null +++ b/lib/src/components/organisms/selectable_widget.dart @@ -0,0 +1,102 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:html/dom.dart' as dom; +import 'package:html/parser.dart' as html_parser; + +import '../../../zds_flutter.dart'; + +/// A selectable widget that can be used to select the text from the child content on long press. +/// +/// Contains the implementation of the ZdsSelectableWidget class. +/// This widget allows users to select and copy text from its child content on a long press. +/// It supports both plain text and HTML content, converting HTML to plain text if necessary +class ZdsSelectableWidget extends StatefulWidget { + /// Constructor + const ZdsSelectableWidget({super.key, required this.child, required this.textToCopy, this.isHtmlData, this.copyable}); + + /// Child widget + final Widget child; + + /// text to be copied + final String textToCopy; + + /// Whether the copied text is in HTML format (if it is we will convert it to plain text) + final bool? isHtmlData; + + /// Whether the copied text is copyable + final bool? copyable; + + @override + State createState() => _ZdsSelectableWidgetState(); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('isHtmlData', isHtmlData)) + ..add(DiagnosticsProperty('copyable', copyable)) + ..add(StringProperty('textToCopy', textToCopy)); + } +} + +class _ZdsSelectableWidgetState extends State { + bool isSelected = false; + + void toggleSelection() { + setState(() { + isSelected = !isSelected; + }); + } + + String htmlToPlainText(String htmlString) { + final dom.Document document = html_parser.parse(htmlString); + return document.body?.text ?? ''; + } + + @override + Widget build(BuildContext context) { + if (!(widget.copyable ?? false)) return widget.child; + final zeta = Zeta.of(context).colors; + return GestureDetector( + child: ColoredBox(color: isSelected ? zeta.primary.surface : Colors.transparent, child: widget.child), + onLongPress: () async { + toggleSelection(); + if (isSelected) { + var copiedText = widget.textToCopy; + if (widget.isHtmlData ?? false) { + try { + copiedText = htmlToPlainText(widget.textToCopy); + } catch (e) { + copiedText = widget.textToCopy; + } + } + await Clipboard.setData(ClipboardData(text: copiedText)); + ScaffoldMessenger.of(context).showZdsToast( + ZdsToast( + rounded: false, + title: Padding( + padding: const EdgeInsets.all(10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + ComponentStrings.of(context).get('COPIED_TO_CLIPBOARD', 'Copied to Clipboard'), + ), + ], + ), + ), + ), + ); + } + Future.delayed(const Duration(seconds: 4), toggleSelection); + }, + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('isSelected', isSelected)); + } +} From 4fba33748649cf8e3c9c344535511a167ba3db72 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 22 Nov 2024 10:29:55 +0000 Subject: [PATCH 3/5] feat(TM-48272): Widget locator updated for quill editor --- lib/src/components/organisms/quill_editor/quill_editor.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/src/components/organisms/quill_editor/quill_editor.dart b/lib/src/components/organisms/quill_editor/quill_editor.dart index 798c82b..4209ee0 100644 --- a/lib/src/components/organisms/quill_editor/quill_editor.dart +++ b/lib/src/components/organisms/quill_editor/quill_editor.dart @@ -1,6 +1,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_quill/flutter_quill.dart'; +import '../../../../zds_flutter.dart'; import 'quill_toolbar.dart'; /// A custom widget for the Quill editor. @@ -91,7 +92,7 @@ class ZdsQuillEditor extends StatelessWidget { placeholder: placeholder, editorKey: editorKey, ), - ); + ).semantics(identifier: 'TEXT_EDITOR'); // If readOnly, return just editor if (readOnly) return editor; From e8420e9924d5051191d2be0472edd1d3c2ae70e9 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 22 Nov 2024 10:34:35 +0000 Subject: [PATCH 4/5] test(TM-48505): Widget Test case for custom widget having copying capability --- .../organisms/selectable_widget.dart | 12 +- test/fixtures/test_app.dart | 31 ++++++ .../organisms/selectable_widget_test.dart | 105 ++++++++++++++++++ 3 files changed, 147 insertions(+), 1 deletion(-) create mode 100644 test/fixtures/test_app.dart create mode 100644 test/lib/src/components/organisms/selectable_widget_test.dart diff --git a/lib/src/components/organisms/selectable_widget.dart b/lib/src/components/organisms/selectable_widget.dart index 8d5300f..9e6c5a5 100644 --- a/lib/src/components/organisms/selectable_widget.dart +++ b/lib/src/components/organisms/selectable_widget.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -42,6 +44,7 @@ class ZdsSelectableWidget extends StatefulWidget { class _ZdsSelectableWidgetState extends State { bool isSelected = false; + Timer? _selectionTimer; void toggleSelection() { setState(() { @@ -54,6 +57,12 @@ class _ZdsSelectableWidgetState extends State { return document.body?.text ?? ''; } + @override + void dispose() { + _selectionTimer?.cancel(); + super.dispose(); + } + @override Widget build(BuildContext context) { if (!(widget.copyable ?? false)) return widget.child; @@ -61,6 +70,7 @@ class _ZdsSelectableWidgetState extends State { return GestureDetector( child: ColoredBox(color: isSelected ? zeta.primary.surface : Colors.transparent, child: widget.child), onLongPress: () async { + if (isSelected) return; toggleSelection(); if (isSelected) { var copiedText = widget.textToCopy; @@ -88,8 +98,8 @@ class _ZdsSelectableWidgetState extends State { ), ), ); + _selectionTimer = Timer(const Duration(seconds: 4), toggleSelection); } - Future.delayed(const Duration(seconds: 4), toggleSelection); }, ); } diff --git a/test/fixtures/test_app.dart b/test/fixtures/test_app.dart new file mode 100644 index 0000000..ef72249 --- /dev/null +++ b/test/fixtures/test_app.dart @@ -0,0 +1,31 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:zds_flutter/zds_flutter.dart'; + +class TestApp extends StatelessWidget { + const TestApp({super.key, required this.builder}); + final WidgetBuilder builder; + @override + Widget build(BuildContext context) { + return MaterialApp( + localizationsDelegates: >[ + GlobalMaterialLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + ComponentDelegate(testing: true), + ], + home: ZetaProvider( + builder: (context, themeData, themeMode) { + return Scaffold(body: builder.call(context)); + }, + ), + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(ObjectFlagProperty.has('builder', builder)); + } +} diff --git a/test/lib/src/components/organisms/selectable_widget_test.dart b/test/lib/src/components/organisms/selectable_widget_test.dart new file mode 100644 index 0000000..4d28a8a --- /dev/null +++ b/test/lib/src/components/organisms/selectable_widget_test.dart @@ -0,0 +1,105 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:zds_flutter/zds_flutter.dart'; +import '../../../../fixtures/test_app.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + var clipboardContent = 'Selectable Text'; + setUp(() { + // Mock the clipboard data + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, + (MethodCall methodCall) async { + if (methodCall.method == 'Clipboard.getData') { + return {'text': clipboardContent}; + } + return null; + }); + }); + tearDown(() { + // Remove the mock handler + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(SystemChannels.platform, null); + }); + group('ZdsSelectableWidget Tests', () { + testWidgets('Displays child widget', (WidgetTester tester) async { + const childWidget = Text('Selectable Text'); + clipboardContent = 'Selectable Text'; + await tester.pumpWidget( + TestApp( + builder: (BuildContext context) { + return const ZdsSelectableWidget( + textToCopy: 'Selectable Text', + child: childWidget, + ); + }, + ), + ); + await tester.pump(const Duration(milliseconds: 100)); + expect(find.byWidget(childWidget), findsOneWidget); + }); + testWidgets('Copies plain text to clipboard on long press', (WidgetTester tester) async { + const childWidget = Text('Selectable Text'); + clipboardContent = 'Selectable Text'; + await tester.pumpWidget( + TestApp( + builder: (BuildContext context) { + return const ZdsSelectableWidget( + textToCopy: 'Selectable Text', + copyable: true, + child: childWidget, + ); + }, + ), + ); + await tester.pump(const Duration(milliseconds: 100)); + await tester.longPress(find.byWidget(childWidget)); + await tester.pumpAndSettle(); + final clipboardData = await Clipboard.getData('text/plain'); + expect(clipboardData?.text, clipboardContent); + }); + testWidgets('Copies HTML text to clipboard as plain text on long press', (WidgetTester tester) async { + const childWidget = Text('Selectable HTML Text'); + const htmlText = '

Selectable HTML Text

'; + clipboardContent = 'Selectable HTML Text'; + await tester.pumpWidget( + TestApp( + builder: (BuildContext context) { + return const ZdsSelectableWidget( + textToCopy: htmlText, + isHtmlData: true, + copyable: true, + child: childWidget, + ); + }, + ), + ); + await tester.pump(const Duration(milliseconds: 100)); + await tester.longPress(find.byWidget(childWidget)); + await tester.pumpAndSettle(); + final clipboardData = await Clipboard.getData('text/plain'); + expect(clipboardData?.text, 'Selectable HTML Text'); + }); + testWidgets('Does not copy text if copyable is false', (WidgetTester tester) async { + const childWidget = Text('Non-copyable Text'); + clipboardContent = ''; + await tester.pumpWidget( + TestApp( + builder: (BuildContext context) { + return const ZdsSelectableWidget( + textToCopy: 'Non-copyable Text', + copyable: false, + child: childWidget, + ); + }, + ), + ); + await tester.pump(const Duration(milliseconds: 100)); + await tester.longPress(find.byWidget(childWidget)); + await tester.pumpAndSettle(); + final clipboardData = await Clipboard.getData('text/plain'); + expect(clipboardData?.text, ''); + }); + }); +} From 734f021c13eca86755b0c57593573e6183e4c74e Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 22 Nov 2024 10:37:25 +0000 Subject: [PATCH 5/5] ci: Update PR action --- .github/workflows/pull-request.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index fc19734..a3ece78 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -22,7 +22,6 @@ jobs: - uses: ZebraDevs/flutter-code-quality@main with: token: ${{secrets.GITHUB_TOKEN}} - run-tests: false run-coverage: false check-secret: @@ -34,7 +33,7 @@ jobs: id: check run: | echo "defined=true" >> $GITHUB_OUTPUT; - if [ "${{ secrets.FIREBASE_SERVICE_ACCOUNT_ZETA_DS }}" == '' ]; then + if [ "${{ secrets.FIREBASE_SERVICE_ACCOUNT_ZDS_FLUTTER }}" == '' ]; then echo "defined=false" >> $GITHUB_OUTPUT; fi