From 0721c648594e19367e1a8532a288b15eebb771e0 Mon Sep 17 00:00:00 2001 From: Freek van de Ven Date: Wed, 29 Jan 2025 15:14:41 +0100 Subject: [PATCH] feat: add Semantics widget to inputs and dynamic texts This will add accessibility id and id to the inputfields, buttons and dynamic texts so they can be accessed in appium for automated tests --- CHANGELOG.md | 6 +- .../availability_accessibility_ids.dart | 202 ++++++++++++++++++ .../lib/src/config/availability_options.dart | 6 + .../ui/screens/availability_modification.dart | 13 +- .../src/ui/screens/availability_overview.dart | 24 ++- .../ui/screens/template_day_modification.dart | 24 ++- .../lib/src/ui/screens/template_overview.dart | 104 +++++---- .../screens/template_week_modification.dart | 46 ++-- .../src/ui/widgets/availability_clear.dart | 33 +-- .../availability_template_selection.dart | 48 +++-- .../lib/src/ui/widgets/calendar.dart | 51 +++-- .../lib/src/ui/widgets/calendar_grid.dart | 55 +++-- .../lib/src/ui/widgets/color_selection.dart | 40 ++-- .../ui/widgets/generic_time_selection.dart | 3 + .../lib/src/ui/widgets/input_fields.dart | 68 +++--- .../lib/src/ui/widgets/pause_selection.dart | 77 ++++--- .../lib/src/ui/widgets/template_legend.dart | 99 +++++---- .../src/ui/widgets/template_name_input.dart | 28 ++- .../widgets/template_week_day_selection.dart | 41 ++-- .../ui/widgets/template_week_overview.dart | 74 +++++-- packages/flutter_availability/pubspec.yaml | 5 +- .../pubspec.yaml | 2 +- 22 files changed, 755 insertions(+), 294 deletions(-) create mode 100644 packages/flutter_availability/lib/src/config/availability_accessibility_ids.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ded310..4788ec1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.1.0 + +* Added CustomSemantics widget that is used to wrap all the buttons, textfields and dynamic texts to make the userstory accessible for e2e testing. + ## 1.0.0 -- T.B.D \ No newline at end of file +* First release of flutter_availability userstory \ No newline at end of file diff --git a/packages/flutter_availability/lib/src/config/availability_accessibility_ids.dart b/packages/flutter_availability/lib/src/config/availability_accessibility_ids.dart new file mode 100644 index 0000000..646a1b7 --- /dev/null +++ b/packages/flutter_availability/lib/src/config/availability_accessibility_ids.dart @@ -0,0 +1,202 @@ +/// Accessibility identifiers for all widgets in the availability userstory that +/// need to be interacted with by e2e tests. This includes buttons, textfields, +/// and dynamic texts. +class AvailabilityAccessibilityIds { + /// default [AvailabilityAccessibilityIds] constructor where all the + /// identifiers are required. This is to ensure that apps automatically break + /// when new identifiers are added. + const AvailabilityAccessibilityIds({ + required this.monthNameTextIdentifier, + required this.previousMonthButtonIdentifier, + required this.nextMonthButtonIdentifier, + required this.availabilityDateButtonIdentifier, + required this.createNewTemplateButtonIdentifier, + required this.viewAvailabilitiesButtonIdentifier, + required this.clearAvailabilitiesButtonIdentifier, + required this.toggleTemplateDrawerButtonIdentifier, + required this.availabilitiesPeriodTextIdentifier, + required this.selectUnavailableForPeriodButtonIdentifier, + required this.addTemplateToAvailabilitiesButtonIdentifier, + required this.removeTemplatesFromAvailabilitiesButtonIdentifier, + required this.createNewDayTemplateButtonIdentifier, + required this.createNewWeekTemplateButtonIdentifier, + required this.dayTemplateEditButtonIdentifier, + required this.weekTemplateEditButtonIdentifier, + required this.templateNameTextFieldIdentifier, + required this.startTimeTextFieldIdentifier, + required this.endTimeTextFieldIdentifier, + required this.durationTextFieldIdentifier, + required this.addBreaksButtonIdentifier, + required this.editBreakButtonIdentifier, + required this.deleteBreakButtonIdentifier, + required this.colorSelectionButtonIdentifier, + required this.colorSelectedButtonIdentifier, + required this.weekDayButtonIdentifier, + required this.weekDayTimeIdentifier, + required this.weekDayBreakIdentifier, + required this.templateNameIdentifier, + required this.deleteTemplateButtonIdentifier, + required this.saveButtonIdentifier, + required this.addButtonIdentifier, + required this.nextButtonIdentifier, + required this.closeButtonIdentifier, + }); + + /// Empty [AvailabilityAccessibilityIds] constructor where all the identifiers + /// are already set to their default values. You can override all or some of + /// the default values. + const AvailabilityAccessibilityIds.empty({ + this.monthNameTextIdentifier = "text_month_name", + this.previousMonthButtonIdentifier = "button_previous_month", + this.nextMonthButtonIdentifier = "button_next_month", + this.availabilityDateButtonIdentifier = "button_availability_date", + this.createNewTemplateButtonIdentifier = "button_create_template", + this.viewAvailabilitiesButtonIdentifier = "button_view_availabilities", + this.clearAvailabilitiesButtonIdentifier = "button_clear_availabilities", + this.toggleTemplateDrawerButtonIdentifier = "button_toggle_template_drawer", + this.availabilitiesPeriodTextIdentifier = "text_availabilities_period", + this.selectUnavailableForPeriodButtonIdentifier = + "button_select_unavailable_for_period", + this.addTemplateToAvailabilitiesButtonIdentifier = + "button_add_template_to_availabilities", + this.removeTemplatesFromAvailabilitiesButtonIdentifier = + "button_remove_templates_from_availabilities", + this.createNewDayTemplateButtonIdentifier = "button_create_template_day", + this.createNewWeekTemplateButtonIdentifier = "button_create_template_week", + this.dayTemplateEditButtonIdentifier = "button_edit_template_day", + this.weekTemplateEditButtonIdentifier = "button_edit_template_week", + this.templateNameTextFieldIdentifier = "textfield_template_name", + this.startTimeTextFieldIdentifier = "textfield_start_time", + this.endTimeTextFieldIdentifier = "textfield_end_time", + this.durationTextFieldIdentifier = "textfield_duration", + this.addBreaksButtonIdentifier = "button_add_breaks", + this.editBreakButtonIdentifier = "button_edit_break", + this.deleteBreakButtonIdentifier = "button_delete_break", + this.colorSelectionButtonIdentifier = "button_select_color", + this.colorSelectedButtonIdentifier = "button_selected_color", + this.weekDayButtonIdentifier = "button_select_week_day", + this.weekDayTimeIdentifier = "text_week_day_time", + this.weekDayBreakIdentifier = "text_week_day_break", + this.templateNameIdentifier = "text_template_name", + this.deleteTemplateButtonIdentifier = "button_delete_template", + this.saveButtonIdentifier = "button_save", + this.addButtonIdentifier = "button_add", + this.nextButtonIdentifier = "button_next", + this.closeButtonIdentifier = "button_close", + }); + + /// The identifier for the text that displays the month that is being viewed + final String monthNameTextIdentifier; + + /// The identifier for the button to navigate to the previous month + final String previousMonthButtonIdentifier; + + /// The identifier for the button to navigate to the next month + final String nextMonthButtonIdentifier; + + /// The identifier for the button to select a date in the availability view + /// The month and day are appended to this identifier + final String availabilityDateButtonIdentifier; + + /// The identifier for the button to go template overview screen + final String createNewTemplateButtonIdentifier; + + /// The identifier for the button to view availabilities + final String viewAvailabilitiesButtonIdentifier; + + /// The identifier for the button to clear availabilities; + final String clearAvailabilitiesButtonIdentifier; + + /// The identifier for the button to toggle the template drawer + final String toggleTemplateDrawerButtonIdentifier; + + /// The identifier for the text that displays the period of availabilities + /// that are being viewed + final String availabilitiesPeriodTextIdentifier; + + /// The identifier for the checkbox to clear all availabilities for a period + final String selectUnavailableForPeriodButtonIdentifier; + + /// The identifier for the button to add a template to a selection of + /// availabilities + final String addTemplateToAvailabilitiesButtonIdentifier; + + /// The identifier for the button to remove all templates from a selection of + /// availabilities + final String removeTemplatesFromAvailabilitiesButtonIdentifier; + + /// The identifier for the button to create a new day template + final String createNewDayTemplateButtonIdentifier; + + /// The identifier for the button to create a new week template + final String createNewWeekTemplateButtonIdentifier; + + /// The identifier for the button to edit a specific day template, the index + /// of the item in the list is appended to this identifier + final String dayTemplateEditButtonIdentifier; + + /// The identifier for the button to edit a specific week template, the index + /// of the item in the list is appended to this identifier + final String weekTemplateEditButtonIdentifier; + + /// The identifier for the textfield to edit the name of a template + final String templateNameTextFieldIdentifier; + + /// The identifier for the textfield to edit a start time + final String startTimeTextFieldIdentifier; + + /// The identifier for the textfield to edit an end time + final String endTimeTextFieldIdentifier; + + /// The identifier for the textfield to edit a duration + final String durationTextFieldIdentifier; + + /// The identifier for the button to add new breaks + final String addBreaksButtonIdentifier; + + /// The identifier for the break edit button to edit a specific break, the + /// index of the item in the list is appended to this identifier + final String editBreakButtonIdentifier; + + /// The identifier for the break delete button to delete a specific break, the + /// index of the item in the list is appended to this identifier + final String deleteBreakButtonIdentifier; + + /// The identifier for the button to select a color from the list of colors, + /// the index of the item in the list is appended to this identifier + final String colorSelectionButtonIdentifier; + + /// The identifier for the button for the currently selected color from the + /// list of colors. This overrides [colorSelectionButtonIdentifier] + final String colorSelectedButtonIdentifier; + + /// The identifier for the button to select a day of the week in the template + /// modification screen. The index of the day is appended to this identifier + final String weekDayButtonIdentifier; + + /// The identifier for the time of a day in the template view + /// The index of the day is appended to this identifier + final String weekDayTimeIdentifier; + + /// The identifier for the time of a break in the template view + /// The index of the day and time is appended to this identifier + final String weekDayBreakIdentifier; + + /// The identifier for the name of a template + final String templateNameIdentifier; + + /// The identifier for the button to delete a template + final String deleteTemplateButtonIdentifier; + + /// The identifier for the button to save (templates or availabilities) + final String saveButtonIdentifier; + + /// The identifier for the button to save breaks + final String addButtonIdentifier; + + /// The identifier for the button to navigate to next step for week templates + final String nextButtonIdentifier; + + /// The identifier for the button to close a dialog + final String closeButtonIdentifier; +} diff --git a/packages/flutter_availability/lib/src/config/availability_options.dart b/packages/flutter_availability/lib/src/config/availability_options.dart index 1861a36..c3f4d5d 100644 --- a/packages/flutter_availability/lib/src/config/availability_options.dart +++ b/packages/flutter_availability/lib/src/config/availability_options.dart @@ -1,6 +1,7 @@ import "dart:async"; import "package:flutter/material.dart"; +import "package:flutter_availability/src/config/availability_accessibility_ids.dart"; import "package:flutter_availability/src/config/availability_translations.dart"; import "package:flutter_availability/src/service/errors.dart"; import "package:flutter_availability/src/ui/widgets/defaults/default_base_screen.dart"; @@ -15,6 +16,7 @@ class AvailabilityOptions { /// AvailabilityOptions constructor where everything is optional. AvailabilityOptions({ this.translations = const AvailabilityTranslations.empty(), + this.accessibilityIds = const AvailabilityAccessibilityIds.empty(), this.baseScreenBuilder = DefaultBaseScreen.builder, this.primaryButtonBuilder = DefaultPrimaryButton.builder, this.secondaryButtonBuilder = DefaultSecondaryButton.builder, @@ -39,6 +41,10 @@ class AvailabilityOptions { /// The translations for the availability userstory final AvailabilityTranslations translations; + /// All the accessibility ids for the availability userstory + /// These are used to add identifiers to the elements for testing + final AvailabilityAccessibilityIds accessibilityIds; + /// The implementation for communicating with the persistance layer final AvailabilityDataInterface dataInterface; diff --git a/packages/flutter_availability/lib/src/ui/screens/availability_modification.dart b/packages/flutter_availability/lib/src/ui/screens/availability_modification.dart index ece6482..5a2e9a3 100644 --- a/packages/flutter_availability/lib/src/ui/screens/availability_modification.dart +++ b/packages/flutter_availability/lib/src/ui/screens/availability_modification.dart @@ -7,6 +7,7 @@ import "package:flutter_availability/src/ui/widgets/availability_template_select import "package:flutter_availability/src/ui/widgets/availabillity_time_selection.dart"; import "package:flutter_availability/src/ui/widgets/base_page.dart"; import "package:flutter_availability/src/ui/widgets/pause_selection.dart"; +import "package:flutter_availability/src/ui/widgets/semantic_widget.dart"; import "package:flutter_availability/src/util/scope.dart"; import "package:flutter_availability/src/util/utils.dart"; import "package:flutter_hooks/flutter_hooks.dart"; @@ -62,6 +63,7 @@ class _AvailabilitiesModificationScreenState var options = availabilityScope.options; var spacing = options.spacing; var translations = options.translations; + var identifiers = options.accessibilityIds; useEffect(() { availabilityScope.popHandler.add(widget.onExit); @@ -126,10 +128,13 @@ class _AvailabilitiesModificationScreenState } var canSave = _availabilityViewModel.canSave; - var saveButton = options.primaryButtonBuilder( - context, - canSave ? onClickSave : null, - Text(translations.saveButton), + var saveButton = CustomSemantics( + identifier: identifiers.saveButtonIdentifier, + child: options.primaryButtonBuilder( + context, + canSave ? onClickSave : null, + Text(translations.saveButton), + ), ); // ignore: avoid_positional_boolean_parameters diff --git a/packages/flutter_availability/lib/src/ui/screens/availability_overview.dart b/packages/flutter_availability/lib/src/ui/screens/availability_overview.dart index b24fafe..6e60561 100644 --- a/packages/flutter_availability/lib/src/ui/screens/availability_overview.dart +++ b/packages/flutter_availability/lib/src/ui/screens/availability_overview.dart @@ -1,6 +1,7 @@ import "package:flutter/material.dart"; import "package:flutter_availability/src/ui/widgets/base_page.dart"; import "package:flutter_availability/src/ui/widgets/calendar.dart"; +import "package:flutter_availability/src/ui/widgets/semantic_widget.dart"; import "package:flutter_availability/src/ui/widgets/template_legend.dart"; import "package:flutter_availability/src/util/scope.dart"; import "package:flutter_availability_data_interface/flutter_availability_data_interface.dart"; @@ -43,6 +44,7 @@ class _AvailabilityOverviewState extends State { var service = availabilityScope.service; var options = availabilityScope.options; var translations = options.translations; + var identifiers = options.accessibilityIds; var availabilityStream = useMemoized( () => service.getOverviewDataForMonth(_selectedDate), @@ -127,16 +129,22 @@ class _AvailabilityOverviewState extends State { } } - var clearSelectedButton = options.bigTextButtonBuilder( - context, - onClearButtonClicked, - Text(translations.clearAvailabilityButton), + var clearSelectedButton = CustomSemantics( + identifier: identifiers.clearAvailabilitiesButtonIdentifier, + child: options.bigTextButtonBuilder( + context, + onClearButtonClicked, + Text(translations.clearAvailabilityButton), + ), ); - var startEditButton = options.primaryButtonBuilder( - context, - onButtonPress, - Text(translations.editAvailabilityButton), + var startEditButton = CustomSemantics( + identifier: identifiers.viewAvailabilitiesButtonIdentifier, + child: options.primaryButtonBuilder( + context, + onButtonPress, + Text(translations.editAvailabilityButton), + ), ); return options.baseScreenBuilder( diff --git a/packages/flutter_availability/lib/src/ui/screens/template_day_modification.dart b/packages/flutter_availability/lib/src/ui/screens/template_day_modification.dart index 3839513..634375c 100644 --- a/packages/flutter_availability/lib/src/ui/screens/template_day_modification.dart +++ b/packages/flutter_availability/lib/src/ui/screens/template_day_modification.dart @@ -4,6 +4,7 @@ import "package:flutter_availability/src/ui/view_models/day_template_view_model. import "package:flutter_availability/src/ui/view_models/template_daydata_view_model.dart"; import "package:flutter_availability/src/ui/widgets/base_page.dart"; import "package:flutter_availability/src/ui/widgets/color_selection.dart"; +import "package:flutter_availability/src/ui/widgets/semantic_widget.dart"; import "package:flutter_availability/src/ui/widgets/template_name_input.dart"; import "package:flutter_availability/src/ui/widgets/template_time_break.dart"; import "package:flutter_availability/src/util/scope.dart"; @@ -51,6 +52,7 @@ class _DayTemplateModificationScreenState var service = availabilityScope.service; var options = availabilityScope.options; var translations = options.translations; + var identifiers = options.accessibilityIds; useEffect(() { availabilityScope.popHandler.add(widget.onExit); @@ -101,10 +103,13 @@ class _DayTemplateModificationScreenState var canSave = _viewModel.canSave; - var deleteButton = options.bigTextButtonBuilder( - context, - onDeletePressed, - Text(translations.deleteTemplateButton), + var deleteButton = CustomSemantics( + identifier: identifiers.deleteTemplateButtonIdentifier, + child: options.bigTextButtonBuilder( + context, + onDeletePressed, + Text(translations.deleteTemplateButton), + ), ); void onNameChanged(String name) { @@ -153,10 +158,13 @@ class _DayTemplateModificationScreenState ), ], buttons: [ - options.primaryButtonBuilder( - context, - canSave ? onSavePressed : null, - Text(translations.saveButton), + CustomSemantics( + identifier: identifiers.saveButtonIdentifier, + child: options.primaryButtonBuilder( + context, + canSave ? onSavePressed : null, + Text(translations.saveButton), + ), ), if (widget.template != null) ...[ const SizedBox(height: 8), diff --git a/packages/flutter_availability/lib/src/ui/screens/template_overview.dart b/packages/flutter_availability/lib/src/ui/screens/template_overview.dart index f93ec25..d20bcf2 100644 --- a/packages/flutter_availability/lib/src/ui/screens/template_overview.dart +++ b/packages/flutter_availability/lib/src/ui/screens/template_overview.dart @@ -1,5 +1,6 @@ import "package:flutter/material.dart"; import "package:flutter_availability/src/ui/widgets/base_page.dart"; +import "package:flutter_availability/src/ui/widgets/semantic_widget.dart"; import "package:flutter_availability/src/util/scope.dart"; import "package:flutter_availability_data_interface/flutter_availability_data_interface.dart"; import "package:flutter_hooks/flutter_hooks.dart"; @@ -34,6 +35,7 @@ class AvailabilityTemplateOverview extends HookWidget { var service = availabilityScope.service; var options = availabilityScope.options; var translations = options.translations; + var identifiers = options.accessibilityIds; var dayTemplateStream = useMemoized(() => service.getDayTemplates()); var weekTemplateStream = useMemoized(() => service.getWeekTemplates()); @@ -61,6 +63,7 @@ class AvailabilityTemplateOverview extends HookWidget { var dayTemplateSection = _TemplateListSection( sectionTitle: translations.dayTemplates, createButtonText: translations.createDayTemplate, + createButtonIdentifier: identifiers.createNewDayTemplateButtonIdentifier, onEditTemplate: onEditTemplate, onSelectTemplate: onSelectTemplate, onAddTemplate: () => onAddTemplate(AvailabilityTemplateType.day), @@ -72,6 +75,7 @@ class AvailabilityTemplateOverview extends HookWidget { var weekTemplateSection = _TemplateListSection( sectionTitle: translations.weekTemplates, createButtonText: translations.createWeekTemplate, + createButtonIdentifier: identifiers.createNewWeekTemplateButtonIdentifier, templates: weekTemplates, isLoading: weekTemplatesSnapshot.connectionState == ConnectionState.waiting, @@ -99,6 +103,7 @@ class _TemplateListSection extends StatelessWidget { const _TemplateListSection({ required this.sectionTitle, required this.createButtonText, + required this.createButtonIdentifier, required this.templates, required this.isLoading, required this.onEditTemplate, @@ -108,6 +113,10 @@ class _TemplateListSection extends StatelessWidget { final String sectionTitle; final String createButtonText; + + /// The accessibility identifier for the create button + final String createButtonIdentifier; + // transform the stream to a snapshot as low as possible to reduce rebuilds final List templates; final bool isLoading; @@ -140,11 +149,14 @@ class _TemplateListSection extends StatelessWidget { children: [ const Icon(Icons.add), const SizedBox(width: 8), - options.smallTextButtonBuilder( - context, - onAddTemplate, - Text( - createButtonText, + CustomSemantics( + identifier: createButtonIdentifier, + child: options.smallTextButtonBuilder( + context, + onAddTemplate, + Text( + createButtonText, + ), ), ), ], @@ -157,9 +169,10 @@ class _TemplateListSection extends StatelessWidget { Text(sectionTitle, style: textTheme.titleMedium), const SizedBox(height: 8), const Divider(height: 1), - for (var template in templates) ...[ + for (var (index, template) in templates.indexed) ...[ _TemplateListSectionItem( template: template, + index: index, onTemplateClicked: onClickTemplate, onEditTemplate: onEditTemplate, ), @@ -177,12 +190,16 @@ class _TemplateListSection extends StatelessWidget { class _TemplateListSectionItem extends StatelessWidget { const _TemplateListSectionItem({ required this.template, + required this.index, required this.onTemplateClicked, required this.onEditTemplate, }); final AvailabilityTemplateModel template; + /// The index of the template in the list + final int index; + final void Function(AvailabilityTemplateModel template) onTemplateClicked; final void Function(AvailabilityTemplateModel template) onEditTemplate; @@ -191,43 +208,52 @@ class _TemplateListSectionItem extends StatelessWidget { var theme = Theme.of(context); var availabilityScope = AvailabilityScope.of(context); var options = availabilityScope.options; + var identifiers = options.accessibilityIds; + var templateTypeIdentifer = + template.templateType == AvailabilityTemplateType.day + ? identifiers.dayTemplateEditButtonIdentifier + : identifiers.weekTemplateEditButtonIdentifier; + var templateIdentifier = "${templateTypeIdentifer}_$index"; - return InkWell( - onTap: () => onTemplateClicked(template), - child: Container( - padding: const EdgeInsets.all(12), - margin: const EdgeInsets.only(top: 8), - decoration: BoxDecoration( - border: Border.all( - color: theme.dividerColor, - width: 1, + return CustomSemantics( + identifier: templateIdentifier, + child: InkWell( + onTap: () => onTemplateClicked(template), + child: Container( + padding: const EdgeInsets.all(12), + margin: const EdgeInsets.only(top: 8), + decoration: BoxDecoration( + border: Border.all( + color: theme.dividerColor, + width: 1, + ), + borderRadius: options.borderRadius, ), - borderRadius: options.borderRadius, - ), - child: Row( - children: [ - Container( - decoration: BoxDecoration( - color: Color(template.color), - borderRadius: options.borderRadius, + child: Row( + children: [ + Container( + decoration: BoxDecoration( + color: Color(template.color), + borderRadius: options.borderRadius, + ), + height: 20, + width: 20, ), - height: 20, - width: 20, - ), - const SizedBox(width: 8), - Expanded( - child: Text( - template.name, - style: theme.textTheme.bodyLarge, - overflow: TextOverflow.ellipsis, + const SizedBox(width: 8), + Expanded( + child: Text( + template.name, + style: theme.textTheme.bodyLarge, + overflow: TextOverflow.ellipsis, + ), ), - ), - const SizedBox(width: 4), - InkWell( - onTap: () => onEditTemplate(template), - child: const Icon(Icons.edit), - ), - ], + const SizedBox(width: 4), + InkWell( + onTap: () => onEditTemplate(template), + child: const Icon(Icons.edit), + ), + ], + ), ), ), ); diff --git a/packages/flutter_availability/lib/src/ui/screens/template_week_modification.dart b/packages/flutter_availability/lib/src/ui/screens/template_week_modification.dart index b1ff595..fa36a7b 100644 --- a/packages/flutter_availability/lib/src/ui/screens/template_week_modification.dart +++ b/packages/flutter_availability/lib/src/ui/screens/template_week_modification.dart @@ -3,6 +3,7 @@ import "package:flutter_availability/src/service/errors.dart"; import "package:flutter_availability/src/ui/view_models/template_daydata_view_model.dart"; import "package:flutter_availability/src/ui/view_models/week_template_view_models.dart"; import "package:flutter_availability/src/ui/widgets/color_selection.dart"; +import "package:flutter_availability/src/ui/widgets/semantic_widget.dart"; import "package:flutter_availability/src/ui/widgets/template_name_input.dart"; import "package:flutter_availability/src/ui/widgets/template_time_break.dart"; import "package:flutter_availability/src/ui/widgets/template_week_day_selection.dart"; @@ -53,6 +54,7 @@ class _WeekTemplateModificationScreenState var service = availabilityScope.service; var options = availabilityScope.options; var translations = options.translations; + var identifiers = options.accessibilityIds; var spacing = options.spacing; var weekTemplateDate = _viewModel.data; @@ -134,22 +136,31 @@ class _WeekTemplateModificationScreenState }); var canSave = _viewModel.canSave; - var nextButton = options.primaryButtonBuilder( - context, - canSave ? onNextPressed : null, - Text(translations.nextButton), + var nextButton = CustomSemantics( + identifier: identifiers.nextButtonIdentifier, + child: options.primaryButtonBuilder( + context, + canSave ? onNextPressed : null, + Text(translations.nextButton), + ), ); - var saveButton = options.primaryButtonBuilder( - context, - canSave ? onSavePressed : null, - Text(translations.saveButton), + var saveButton = CustomSemantics( + identifier: identifiers.saveButtonIdentifier, + child: options.primaryButtonBuilder( + context, + canSave ? onSavePressed : null, + Text(translations.saveButton), + ), ); - var deleteButton = options.bigTextButtonBuilder( - context, - onDeletePressed, - Text(translations.deleteTemplateButton), + var deleteButton = CustomSemantics( + identifier: identifiers.deleteTemplateButtonIdentifier, + child: options.bigTextButtonBuilder( + context, + onDeletePressed, + Text(translations.deleteTemplateButton), + ), ); var title = Center( @@ -230,10 +241,13 @@ class _WeekTemplateModificationScreenState ), const SizedBox(width: 12), Expanded( - child: Text( - _viewModel.name ?? "", - style: textTheme.bodyLarge, - overflow: TextOverflow.ellipsis, + child: CustomSemantics( + identifier: identifiers.templateNameIdentifier, + child: Text( + _viewModel.name ?? "", + style: textTheme.bodyLarge, + overflow: TextOverflow.ellipsis, + ), ), ), ], diff --git a/packages/flutter_availability/lib/src/ui/widgets/availability_clear.dart b/packages/flutter_availability/lib/src/ui/widgets/availability_clear.dart index 61f3e52..4829ac4 100644 --- a/packages/flutter_availability/lib/src/ui/widgets/availability_clear.dart +++ b/packages/flutter_availability/lib/src/ui/widgets/availability_clear.dart @@ -1,4 +1,5 @@ import "package:flutter/material.dart"; +import "package:flutter_availability/src/ui/widgets/semantic_widget.dart"; import "package:flutter_availability/src/util/scope.dart"; /// @@ -30,6 +31,7 @@ class AvailabilityClearSection extends StatelessWidget { var availabilityScope = AvailabilityScope.of(context); var options = availabilityScope.options; var translations = options.translations; + var identifiers = options.accessibilityIds; var isSingleDay = range.start.isAtSameMomentAs(range.end); @@ -43,22 +45,29 @@ class AvailabilityClearSection extends StatelessWidget { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - titleText, - style: textTheme.titleMedium, + CustomSemantics( + identifier: identifiers.availabilitiesPeriodTextIdentifier, + child: Text( + titleText, + style: textTheme.titleMedium, + ), ), const SizedBox(height: 8), Row( children: [ - Checkbox( - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - visualDensity: VisualDensity.compact, - splashRadius: 0, - value: clearAvailable, - onChanged: (value) { - if (value == null) return; - onChanged(value); - }, + CustomSemantics( + identifier: + identifiers.selectUnavailableForPeriodButtonIdentifier, + child: Checkbox( + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + visualDensity: VisualDensity.compact, + splashRadius: 0, + value: clearAvailable, + onChanged: (value) { + if (value == null) return; + onChanged(value); + }, + ), ), const SizedBox(width: 8), Text( diff --git a/packages/flutter_availability/lib/src/ui/widgets/availability_template_selection.dart b/packages/flutter_availability/lib/src/ui/widgets/availability_template_selection.dart index 51b6a12..961d9f8 100644 --- a/packages/flutter_availability/lib/src/ui/widgets/availability_template_selection.dart +++ b/packages/flutter_availability/lib/src/ui/widgets/availability_template_selection.dart @@ -1,4 +1,5 @@ import "package:flutter/material.dart"; +import "package:flutter_availability/src/ui/widgets/semantic_widget.dart"; import "package:flutter_availability/src/util/scope.dart"; import "package:flutter_availability_data_interface/flutter_availability_data_interface.dart"; @@ -34,6 +35,7 @@ class AvailabilityTemplateSelection extends StatelessWidget { var availabilityScope = AvailabilityScope.of(context); var options = availabilityScope.options; var translations = options.translations; + var identifiers = options.accessibilityIds; var titleText = translations.availabilityAddTemplateTitle; if (selectedTemplates.isNotEmpty) { @@ -47,10 +49,13 @@ class AvailabilityTemplateSelection extends StatelessWidget { var addButton = options.bigTextButtonWrapperBuilder( context, onTemplateAdd, - options.bigTextButtonBuilder( - context, - onTemplateAdd, - Text(translations.addButton), + CustomSemantics( + identifier: identifiers.addTemplateToAvailabilitiesButtonIdentifier, + child: options.bigTextButtonBuilder( + context, + onTemplateAdd, + Text(translations.addButton), + ), ), ); @@ -89,6 +94,7 @@ class _TemplateList extends StatelessWidget { var theme = Theme.of(context); var availabilityScope = AvailabilityScope.of(context); var options = availabilityScope.options; + var identifiers = options.accessibilityIds; return Container( padding: const EdgeInsets.all(12), @@ -107,8 +113,8 @@ class _TemplateList extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - for (var template in selectedTemplates) ...[ - _TemplateListItem(template: template), + for (var (index, template) in selectedTemplates.indexed) ...[ + _TemplateListItem(template: template, index: index), if (template != selectedTemplates.last) ...[ const SizedBox(height: 12), ], @@ -117,9 +123,13 @@ class _TemplateList extends StatelessWidget { ), ), const SizedBox(width: 8), - InkWell( - onTap: onTemplatesRemoved, - child: const Icon(Icons.remove), + CustomSemantics( + identifier: + identifiers.removeTemplatesFromAvailabilitiesButtonIdentifier, + child: InkWell( + onTap: onTemplatesRemoved, + child: const Icon(Icons.remove), + ), ), ], ), @@ -128,15 +138,22 @@ class _TemplateList extends StatelessWidget { } class _TemplateListItem extends StatelessWidget { - const _TemplateListItem({required this.template}); + const _TemplateListItem({ + required this.template, + required this.index, + }); final AvailabilityTemplateModel template; + /// The index of the template in the list of selected templates + final int index; + @override Widget build(BuildContext context) { var theme = Theme.of(context); var availabilityScope = AvailabilityScope.of(context); var options = availabilityScope.options; + var identifiers = options.accessibilityIds; return Row( children: [ @@ -150,10 +167,13 @@ class _TemplateListItem extends StatelessWidget { ), const SizedBox(width: 12), Expanded( - child: Text( - template.name, - style: theme.textTheme.bodyLarge, - overflow: TextOverflow.ellipsis, + child: CustomSemantics( + identifier: "${identifiers.templateNameIdentifier}_$index", + child: Text( + template.name, + style: theme.textTheme.bodyLarge, + overflow: TextOverflow.ellipsis, + ), ), ), ], diff --git a/packages/flutter_availability/lib/src/ui/widgets/calendar.dart b/packages/flutter_availability/lib/src/ui/widgets/calendar.dart index b0f2635..146a633 100644 --- a/packages/flutter_availability/lib/src/ui/widgets/calendar.dart +++ b/packages/flutter_availability/lib/src/ui/widgets/calendar.dart @@ -1,6 +1,7 @@ import "package:flutter/material.dart"; import "package:flutter_availability/flutter_availability.dart"; import "package:flutter_availability/src/ui/widgets/calendar_grid.dart"; +import "package:flutter_availability/src/ui/widgets/semantic_widget.dart"; import "package:flutter_availability/src/util/scope.dart"; /// @@ -65,6 +66,7 @@ class CalendarView extends StatelessWidget { var availabilityScope = AvailabilityScope.of(context); var options = availabilityScope.options; var translations = options.translations; + var identifiers = options.accessibilityIds; var mappedCalendarDays = _mapAvailabilitiesToCalendarDays(availabilities); var existsTemplateDeviations = mappedCalendarDays.any( @@ -74,33 +76,42 @@ class CalendarView extends StatelessWidget { var monthDateSelector = Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - IconButton( - padding: EdgeInsets.zero, - icon: const Icon(Icons.chevron_left), - onPressed: () { - onMonthChanged( - DateTime(month.year, month.month - 1), - ); - }, + CustomSemantics( + identifier: identifiers.previousMonthButtonIdentifier, + child: IconButton( + padding: EdgeInsets.zero, + icon: const Icon(Icons.chevron_left), + onPressed: () { + onMonthChanged( + DateTime(month.year, month.month - 1), + ); + }, + ), ), const SizedBox(width: 44), SizedBox( width: _calculateTextWidthOfLongestMonth(context, translations), - child: Text( - translations.monthYearFormatter(context, month), - style: textTheme.titleMedium, - textAlign: TextAlign.center, + child: CustomSemantics( + identifier: identifiers.monthNameTextIdentifier, + child: Text( + translations.monthYearFormatter(context, month), + style: textTheme.titleMedium, + textAlign: TextAlign.center, + ), ), ), const SizedBox(width: 44), - IconButton( - padding: EdgeInsets.zero, - icon: const Icon(Icons.chevron_right), - onPressed: () { - onMonthChanged( - DateTime(month.year, month.month + 1), - ); - }, + CustomSemantics( + identifier: identifiers.nextMonthButtonIdentifier, + child: IconButton( + padding: EdgeInsets.zero, + icon: const Icon(Icons.chevron_right), + onPressed: () { + onMonthChanged( + DateTime(month.year, month.month + 1), + ); + }, + ), ), ], ); diff --git a/packages/flutter_availability/lib/src/ui/widgets/calendar_grid.dart b/packages/flutter_availability/lib/src/ui/widgets/calendar_grid.dart index 2d10c7c..7dad0d0 100644 --- a/packages/flutter_availability/lib/src/ui/widgets/calendar_grid.dart +++ b/packages/flutter_availability/lib/src/ui/widgets/calendar_grid.dart @@ -1,5 +1,6 @@ import "package:flutter/material.dart"; import "package:flutter_availability/flutter_availability.dart"; +import "package:flutter_availability/src/ui/widgets/semantic_widget.dart"; import "package:flutter_availability/src/util/scope.dart"; /// Returns the days of the week as abbreviated strings @@ -119,6 +120,7 @@ class _CalendarDay extends StatelessWidget { var colorScheme = theme.colorScheme; var availabilityScope = AvailabilityScope.of(context); var options = availabilityScope.options; + var identifiers = options.accessibilityIds; var colors = options.colors; var dayColor = day.color ?? @@ -134,6 +136,10 @@ class _CalendarDay extends StatelessWidget { textStyle = textTheme.titleMedium?.copyWith(color: textColor); } + var dayIdentifier = + "${identifiers.availabilityDateButtonIdentifier}_${day.date.year}_" + "${day.date.month}_${day.date.day}"; + var decoration = day.outsideMonth ? null : BoxDecoration( @@ -145,32 +151,35 @@ class _CalendarDay extends StatelessWidget { ), ); - return InkWell( - onTap: () => onDayTap(day.date), - child: DecoratedBox( - decoration: BoxDecoration( - borderRadius: options.borderRadius, - border: Border.all( - color: day.isSelected ? theme.dividerColor : Colors.transparent, - width: 1.5, - ), - ), - child: Stack( - children: [ - Center( - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 4), - decoration: decoration, - child: Text(day.date.day.toString(), style: textStyle), - ), + return CustomSemantics( + identifier: dayIdentifier, + child: InkWell( + onTap: () => onDayTap(day.date), + child: DecoratedBox( + decoration: BoxDecoration( + borderRadius: options.borderRadius, + border: Border.all( + color: day.isSelected ? theme.dividerColor : Colors.transparent, + width: 1.5, ), - if (day.templateDeviation) ...[ - Positioned( - right: 4, - child: Text("*", style: textStyle), + ), + child: Stack( + children: [ + Center( + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 4), + decoration: decoration, + child: Text(day.date.day.toString(), style: textStyle), + ), ), + if (day.templateDeviation) ...[ + Positioned( + right: 4, + child: Text("*", style: textStyle), + ), + ], ], - ], + ), ), ), ); diff --git a/packages/flutter_availability/lib/src/ui/widgets/color_selection.dart b/packages/flutter_availability/lib/src/ui/widgets/color_selection.dart index 0be6254..b86ba9a 100644 --- a/packages/flutter_availability/lib/src/ui/widgets/color_selection.dart +++ b/packages/flutter_availability/lib/src/ui/widgets/color_selection.dart @@ -1,6 +1,7 @@ import "dart:math"; import "package:flutter/material.dart"; +import "package:flutter_availability/src/ui/widgets/semantic_widget.dart"; import "package:flutter_availability/src/util/scope.dart"; /// Widget for selecting a color for a template @@ -40,9 +41,10 @@ class TemplateColorSelection extends StatelessWidget { spacing: 8, runSpacing: 8, children: [ - for (var color in colors.templateColors) ...[ + for (var (index, color) in colors.templateColors.indexed) ...[ _TemplateColorItem( color: color, + index: index, selectedColor: selectedColor, onColorSelected: onColorSelected, ), @@ -57,6 +59,7 @@ class TemplateColorSelection extends StatelessWidget { class _TemplateColorItem extends StatelessWidget { const _TemplateColorItem({ required this.color, + required this.index, required this.selectedColor, required this.onColorSelected, }); @@ -66,14 +69,20 @@ class _TemplateColorItem extends StatelessWidget { final Color color; + /// The index of the color in the list of colors + final int index; + final int? selectedColor; @override Widget build(BuildContext context) { var availabilityScope = AvailabilityScope.of(context); var options = availabilityScope.options; + var identifiers = options.accessibilityIds; var colors = options.colors; + var isSelected = selectedColor == color.value; + /// If the color is selected, deselect it, otherwise select it void onColorClick(Color color) => onColorSelected( color.value == selectedColor ? null : color.value, @@ -83,20 +92,25 @@ class _TemplateColorItem extends StatelessWidget { ? colors.templateColorLightCheckmarkColor : colors.templateColorDarkCheckmarkColor; - var icon = selectedColor == color.value - ? Icon(Icons.check, color: checkMarkColor) - : null; + var icon = isSelected ? Icon(Icons.check, color: checkMarkColor) : null; - return GestureDetector( - onTap: () => onColorClick(color), - child: Container( - width: 40, - height: 40, - decoration: BoxDecoration( - color: color, - borderRadius: options.borderRadius, + var colorIdentifier = isSelected + ? identifiers.colorSelectedButtonIdentifier + : "${identifiers.colorSelectionButtonIdentifier}_$index"; + + return CustomSemantics( + identifier: colorIdentifier, + child: GestureDetector( + onTap: () => onColorClick(color), + child: Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: color, + borderRadius: options.borderRadius, + ), + child: icon, ), - child: icon, ), ); } diff --git a/packages/flutter_availability/lib/src/ui/widgets/generic_time_selection.dart b/packages/flutter_availability/lib/src/ui/widgets/generic_time_selection.dart index cafe460..a44b498 100644 --- a/packages/flutter_availability/lib/src/ui/widgets/generic_time_selection.dart +++ b/packages/flutter_availability/lib/src/ui/widgets/generic_time_selection.dart @@ -44,6 +44,7 @@ class TimeSelection extends StatelessWidget { var availabilityScope = AvailabilityScope.of(context); var options = availabilityScope.options; var translations = options.translations; + var identifiers = options.accessibilityIds; return Column( crossAxisAlignment: crossAxisAlignment, @@ -63,6 +64,7 @@ class TimeSelection extends StatelessWidget { Expanded( flex: 2, child: TimeInputField( + identifier: identifiers.startTimeTextFieldIdentifier, initialValue: startTime, onTimeChanged: onStartChanged, ), @@ -78,6 +80,7 @@ class TimeSelection extends StatelessWidget { Expanded( flex: 2, child: TimeInputField( + identifier: identifiers.endTimeTextFieldIdentifier, initialValue: endTime, onTimeChanged: onEndChanged, ), diff --git a/packages/flutter_availability/lib/src/ui/widgets/input_fields.dart b/packages/flutter_availability/lib/src/ui/widgets/input_fields.dart index 6edb344..6147db2 100644 --- a/packages/flutter_availability/lib/src/ui/widgets/input_fields.dart +++ b/packages/flutter_availability/lib/src/ui/widgets/input_fields.dart @@ -1,5 +1,6 @@ import "package:flutter/material.dart"; import "package:flutter/services.dart"; +import "package:flutter_availability/src/ui/widgets/semantic_widget.dart"; import "package:flutter_availability/src/util/scope.dart"; /// An input field for time selection @@ -8,12 +9,16 @@ class TimeInputField extends StatelessWidget { const TimeInputField({ required this.initialValue, required this.onTimeChanged, + required this.identifier, super.key, }); /// final TimeOfDay? initialValue; + /// The accessibility identifier for this input field + final String identifier; + /// final void Function(TimeOfDay) onTimeChanged; @@ -37,21 +42,25 @@ class TimeInputField extends StatelessWidget { } } - return TextFormField( - decoration: InputDecoration( - suffixIcon: const Icon(Icons.access_time), - hintText: translations.time, - hintStyle: theme.inputDecorationTheme.hintStyle, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), + return CustomSemantics( + identifier: identifier, + isTextField: true, + child: TextFormField( + decoration: InputDecoration( + suffixIcon: const Icon(Icons.access_time), + hintText: translations.time, + hintStyle: theme.inputDecorationTheme.hintStyle, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + ), ), + initialValue: initialValue != null + ? translations.timeFormatter(context, initialValue!) + : null, + readOnly: true, + style: options.textStyles.inputFieldTextStyle, + onTap: onFieldtap, ), - initialValue: initialValue != null - ? translations.timeFormatter(context, initialValue!) - : null, - readOnly: true, - style: options.textStyles.inputFieldTextStyle, - onTap: onFieldtap, ); } } @@ -122,6 +131,7 @@ class _DurationInputFieldState extends State { var availabilityScope = AvailabilityScope.of(context); var options = availabilityScope.options; var translations = options.translations; + var identifiers = options.accessibilityIds; return Focus( onFocusChange: (hasFocus) { @@ -131,22 +141,26 @@ class _DurationInputFieldState extends State { _removeOverlay(); } }, - child: TextFormField( - decoration: InputDecoration( - labelText: translations.time, - labelStyle: theme.inputDecorationTheme.hintStyle, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), + child: CustomSemantics( + identifier: identifiers.durationTextFieldIdentifier, + isTextField: true, + child: TextFormField( + decoration: InputDecoration( + labelText: translations.time, + labelStyle: theme.inputDecorationTheme.hintStyle, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + ), + suffixIcon: const Icon(Icons.access_time), ), - suffixIcon: const Icon(Icons.access_time), + initialValue: widget.initialValue?.inMinutes.toString(), + keyboardType: TextInputType.number, + style: options.textStyles.inputFieldTextStyle, + onChanged: _onFieldChanged, + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + ], ), - initialValue: widget.initialValue?.inMinutes.toString(), - keyboardType: TextInputType.number, - style: options.textStyles.inputFieldTextStyle, - onChanged: _onFieldChanged, - inputFormatters: [ - FilteringTextInputFormatter.digitsOnly, - ], ), ); } diff --git a/packages/flutter_availability/lib/src/ui/widgets/pause_selection.dart b/packages/flutter_availability/lib/src/ui/widgets/pause_selection.dart index 238a914..e9e6eff 100644 --- a/packages/flutter_availability/lib/src/ui/widgets/pause_selection.dart +++ b/packages/flutter_availability/lib/src/ui/widgets/pause_selection.dart @@ -4,6 +4,7 @@ import "package:flutter_availability/src/service/pop_handler.dart"; import "package:flutter_availability/src/ui/view_models/break_view_model.dart"; import "package:flutter_availability/src/ui/widgets/generic_time_selection.dart"; import "package:flutter_availability/src/ui/widgets/input_fields.dart"; +import "package:flutter_availability/src/ui/widgets/semantic_widget.dart"; import "package:flutter_availability/src/util/scope.dart"; /// @@ -32,6 +33,7 @@ class PauseSelection extends StatelessWidget { var availabilityScope = AvailabilityScope.of(context); var options = availabilityScope.options; var translations = options.translations; + var identifiers = options.accessibilityIds; var popHandler = availabilityScope.popHandler; Future openBreakDialog( @@ -73,13 +75,16 @@ class PauseSelection extends StatelessWidget { var sortedBreaks = breaks.toList()..sort((a, b) => a.compareTo(b)); - var addButton = options.bigTextButtonWrapperBuilder( - context, - onClickAddBreak, - options.bigTextButtonBuilder( + var addButton = CustomSemantics( + identifier: identifiers.addBreaksButtonIdentifier, + child: options.bigTextButtonWrapperBuilder( context, onClickAddBreak, - Text(translations.addButton), + options.bigTextButtonBuilder( + context, + onClickAddBreak, + Text(translations.addButton), + ), ), ); @@ -99,10 +104,11 @@ class PauseSelection extends StatelessWidget { ), ], ), - for (var breakModel in sortedBreaks) ...[ + for (var (index, breakModel) in sortedBreaks.indexed) ...[ const SizedBox(height: 8), BreakDisplay( breakModel: breakModel, + index: index, onRemove: () => onDeleteBreak(breakModel), onClick: () async => onEditBreak(breakModel), ), @@ -119,6 +125,7 @@ class BreakDisplay extends StatelessWidget { /// Creates a new break display const BreakDisplay({ required this.breakModel, + required this.index, required this.onRemove, required this.onClick, super.key, @@ -127,6 +134,9 @@ class BreakDisplay extends StatelessWidget { /// The break to display final BreakViewModel breakModel; + /// The index of the break in the list + final int index; + /// Callback for when the minus button is clicked final VoidCallback onRemove; @@ -140,6 +150,7 @@ class BreakDisplay extends StatelessWidget { var options = availabilityScope.options; var colors = options.colors; var translations = options.translations; + var identifiers = options.accessibilityIds; var starTime = translations.timeFormatter( context, @@ -151,6 +162,9 @@ class BreakDisplay extends StatelessWidget { ); var breakDuration = breakModel.duration.inMinutes; + var editBreakIdentifier = "${identifiers.editBreakButtonIdentifier}_$index"; + var deleteBreakIdentifier = + "${identifiers.deleteBreakButtonIdentifier}_$index"; return InkWell( onTap: onClick, @@ -163,16 +177,22 @@ class BreakDisplay extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), child: Row( children: [ - Text( - "$breakDuration " - "${translations.timeMinutes} | " - "$starTime - " - "$endTime", + CustomSemantics( + identifier: editBreakIdentifier, + child: Text( + "$breakDuration " + "${translations.timeMinutes} | " + "$starTime - " + "$endTime", + ), ), const Spacer(), - InkWell( - onTap: onRemove, - child: const Icon(Icons.remove), + CustomSemantics( + identifier: deleteBreakIdentifier, + child: InkWell( + onTap: onRemove, + child: const Icon(Icons.remove), + ), ), ], ), @@ -251,6 +271,7 @@ class _AvailabilityBreakSelectionDialogState var availabilityScope = AvailabilityScope.of(context); var options = availabilityScope.options; var translations = options.translations; + var identifiers = options.accessibilityIds; var spacing = options.spacing; void onUpdateDuration(Duration? duration) { @@ -299,13 +320,16 @@ class _AvailabilityBreakSelectionDialogState var onSaveButtonPress = canSave ? onSave : null; - var saveButton = options.primaryButtonBuilder( - context, - onSaveButtonPress, - Text( - widget.initialBreak == null - ? translations.addButton - : translations.saveButton, + var saveButton = CustomSemantics( + identifier: identifiers.addButtonIdentifier, + child: options.primaryButtonBuilder( + context, + onSaveButtonPress, + Text( + widget.initialBreak == null + ? translations.addButton + : translations.saveButton, + ), ), ); @@ -381,10 +405,13 @@ class _AvailabilityBreakSelectionDialogState Positioned( right: 0, top: 0, - child: IconButton( - padding: const EdgeInsets.all(16), - icon: const Icon(Icons.close), - onPressed: () => Navigator.of(context).pop(), + child: CustomSemantics( + identifier: identifiers.closeButtonIdentifier, + child: IconButton( + padding: const EdgeInsets.all(16), + icon: const Icon(Icons.close), + onPressed: () => Navigator.of(context).pop(), + ), ), ), ], diff --git a/packages/flutter_availability/lib/src/ui/widgets/template_legend.dart b/packages/flutter_availability/lib/src/ui/widgets/template_legend.dart index e4e5dfe..6065aa8 100644 --- a/packages/flutter_availability/lib/src/ui/widgets/template_legend.dart +++ b/packages/flutter_availability/lib/src/ui/widgets/template_legend.dart @@ -1,5 +1,6 @@ import "package:flutter/material.dart"; import "package:flutter_availability/src/config/availability_options.dart"; +import "package:flutter_availability/src/ui/widgets/semantic_widget.dart"; import "package:flutter_availability/src/util/scope.dart"; import "package:flutter_availability_data_interface/flutter_availability_data_interface.dart"; @@ -36,6 +37,7 @@ class _TemplateLegendState extends State { var options = availabilityScope.options; var colors = options.colors; var translations = options.translations; + var identifiers = options.accessibilityIds; var featureSet = options.featureSet; var templatesLoading = @@ -63,20 +65,23 @@ class _TemplateLegendState extends State { }); } - var createNewTemplateButton = GestureDetector( - onTap: () => widget.onViewTemplates(), - child: ColoredBox( - color: Colors.transparent, - child: Row( - children: [ - const SizedBox(width: 12), - const Icon(Icons.add, size: 20), - const SizedBox(width: 6), - Text( - translations.createTemplateButton, - style: textTheme.bodyLarge, - ), - ], + var createNewTemplateButton = CustomSemantics( + identifier: identifiers.createNewTemplateButtonIdentifier, + child: GestureDetector( + onTap: () => widget.onViewTemplates(), + child: ColoredBox( + color: Colors.transparent, + child: Row( + children: [ + const SizedBox(width: 12), + const Icon(Icons.add, size: 20), + const SizedBox(width: 6), + Text( + translations.createTemplateButton, + style: textTheme.bodyLarge, + ), + ], + ), ), ), ); @@ -104,6 +109,7 @@ class _TemplateLegendState extends State { left: 12, ), child: _TemplateLegendItem( + index: 0, name: translations.templateSelectionLabel, backgroundColor: Colors.white, borderColor: colorScheme.primary, @@ -116,6 +122,7 @@ class _TemplateLegendState extends State { left: 12, ), child: _TemplateLegendItem( + index: 1, name: translations.availabilityWithoutTemplateLabel, backgroundColor: colors.customAvailabilityColor ?? colorScheme.secondary, @@ -123,13 +130,14 @@ class _TemplateLegendState extends State { ), ], if (featureSet.require(AvailabilityFeature.templates)) ...[ - for (var template in templates) ...[ + for (var (index, template) in templates.indexed) ...[ Padding( padding: const EdgeInsets.only( top: 10, left: 12, ), child: _TemplateLegendItem( + index: index + 2, name: template.name, backgroundColor: Color(template.color), ), @@ -149,27 +157,30 @@ class _TemplateLegendState extends State { return Column( children: [ // a button to open/close a drawer with all the templates - GestureDetector( - onTap: onDrawerHeaderClick, - child: ColoredBox( - color: Colors.transparent, - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - translations.templateLegendTitle, - style: textTheme.titleMedium, - ), - if (templatesAvailable || - (_templateDrawerOpen && templatesLoading)) ...[ - Icon( - _templateDrawerOpen - ? Icons.arrow_drop_up - : Icons.arrow_drop_down, + CustomSemantics( + identifier: identifiers.toggleTemplateDrawerButtonIdentifier, + child: GestureDetector( + onTap: onDrawerHeaderClick, + child: ColoredBox( + color: Colors.transparent, + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + translations.templateLegendTitle, + style: textTheme.titleMedium, ), + if (templatesAvailable || + (_templateDrawerOpen && templatesLoading)) ...[ + Icon( + _templateDrawerOpen + ? Icons.arrow_drop_up + : Icons.arrow_drop_down, + ), + ], ], - ], + ), ), ), ), @@ -208,11 +219,17 @@ class _TemplateLegendItem extends StatelessWidget { const _TemplateLegendItem({ required this.name, required this.backgroundColor, + required this.index, this.borderColor, }); final String name; + /// The index of the color in the list of colors (index 0 is the selected + /// color template, index 1 is the color for availabilities without a + /// template) + final int index; + final Color backgroundColor; final Color? borderColor; @@ -222,6 +239,9 @@ class _TemplateLegendItem extends StatelessWidget { var theme = Theme.of(context); var availabilityScope = AvailabilityScope.of(context); var options = availabilityScope.options; + var identifiers = options.accessibilityIds; + + var templateIdentifier = "${identifiers.templateNameIdentifier}_$index"; return Row( children: [ @@ -238,10 +258,13 @@ class _TemplateLegendItem extends StatelessWidget { ), const SizedBox(width: 8), Expanded( - child: Text( - name, - style: theme.textTheme.bodyLarge, - overflow: TextOverflow.ellipsis, + child: CustomSemantics( + identifier: templateIdentifier, + child: Text( + name, + style: theme.textTheme.bodyLarge, + overflow: TextOverflow.ellipsis, + ), ), ), ], diff --git a/packages/flutter_availability/lib/src/ui/widgets/template_name_input.dart b/packages/flutter_availability/lib/src/ui/widgets/template_name_input.dart index 9cdf508..89ec4e2 100644 --- a/packages/flutter_availability/lib/src/ui/widgets/template_name_input.dart +++ b/packages/flutter_availability/lib/src/ui/widgets/template_name_input.dart @@ -1,4 +1,5 @@ import "package:flutter/material.dart"; +import "package:flutter_availability/src/ui/widgets/semantic_widget.dart"; import "package:flutter_availability/src/util/scope.dart"; /// Input section for the template name @@ -23,6 +24,7 @@ class TemplateNameInput extends StatelessWidget { var availabilityScope = AvailabilityScope.of(context); var options = availabilityScope.options; var translations = options.translations; + var identifiers = options.accessibilityIds; return Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -32,19 +34,23 @@ class TemplateNameInput extends StatelessWidget { style: textTheme.titleMedium, ), const SizedBox(height: 8), - TextFormField( - decoration: InputDecoration( - hintText: translations.templateTitleHintText, - hintStyle: theme.inputDecorationTheme.hintStyle, - counterText: "", - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), + CustomSemantics( + identifier: identifiers.templateNameTextFieldIdentifier, + isTextField: true, + child: TextFormField( + decoration: InputDecoration( + hintText: translations.templateTitleHintText, + hintStyle: theme.inputDecorationTheme.hintStyle, + counterText: "", + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + ), ), + maxLength: 100, + initialValue: initialValue, + style: options.textStyles.inputFieldTextStyle, + onChanged: onNameChanged, ), - maxLength: 100, - initialValue: initialValue, - style: options.textStyles.inputFieldTextStyle, - onChanged: onNameChanged, ), ], ); diff --git a/packages/flutter_availability/lib/src/ui/widgets/template_week_day_selection.dart b/packages/flutter_availability/lib/src/ui/widgets/template_week_day_selection.dart index dcbaca7..5aab16d 100644 --- a/packages/flutter_availability/lib/src/ui/widgets/template_week_day_selection.dart +++ b/packages/flutter_availability/lib/src/ui/widgets/template_week_day_selection.dart @@ -2,6 +2,7 @@ import "package:flutter/material.dart"; import "package:flutter_availability/src/ui/widgets/calendar_grid.dart"; +import "package:flutter_availability/src/ui/widgets/semantic_widget.dart"; import "package:flutter_availability/src/util/scope.dart"; /// A widget for selecting a day of the week @@ -58,10 +59,10 @@ class _TemplateWeekDaySelectionState extends State { child: Row( crossAxisAlignment: CrossAxisAlignment.end, children: [ - for (var day in days) ...[ + for (var (index, day) in days.indexed) ...[ _DaySelectionCard( day: day, - days: days, + index: index, selectedDayIndex: _selectedDayIndex, onDaySelected: (selected) => onDaySelected(selected, days.indexOf(day)), @@ -81,12 +82,12 @@ class _DaySelectionCard extends StatelessWidget { const _DaySelectionCard({ required this.selectedDayIndex, required this.day, - required this.days, + required this.index, required this.onDaySelected, }); final String day; - final List days; + final int index; final int selectedDayIndex; @@ -94,11 +95,11 @@ class _DaySelectionCard extends StatelessWidget { @override Widget build(BuildContext context) { - var index = days.indexOf(day); var isSelected = index == selectedDayIndex; return _DaySelectionCardLayout( day: day, + index: index, isSelected: isSelected, onDaySelected: onDaySelected, ); @@ -108,6 +109,7 @@ class _DaySelectionCard extends StatelessWidget { class _DaySelectionCardLayout extends StatelessWidget { const _DaySelectionCardLayout({ required this.day, + required this.index, required this.isSelected, required this.onDaySelected, }); @@ -115,6 +117,9 @@ class _DaySelectionCardLayout extends StatelessWidget { final String day; final bool isSelected; + /// The index of the day in the list of days + final int index; + final void Function(bool) onDaySelected; @override @@ -124,6 +129,7 @@ class _DaySelectionCardLayout extends StatelessWidget { var abbreviationTextStyle = textTheme.headlineMedium; var availabilityScope = AvailabilityScope.of(context); var options = availabilityScope.options; + var identifiers = options.accessibilityIds; abbreviationTextStyle = isSelected ? abbreviationTextStyle?.copyWith( @@ -131,22 +137,27 @@ class _DaySelectionCardLayout extends StatelessWidget { ) : abbreviationTextStyle; + var identifier = "${identifiers.weekDayButtonIdentifier}_$index"; + return AnimatedContainer( duration: const Duration(milliseconds: 300), height: isSelected ? 72 : 64, width: isSelected ? 72 : 64, - child: ChoiceChip( - shape: RoundedRectangleBorder(borderRadius: options.borderRadius), - padding: EdgeInsets.zero, - label: Center( - child: Text( - day.toUpperCase(), - style: abbreviationTextStyle, + child: CustomSemantics( + identifier: identifier, + child: ChoiceChip( + shape: RoundedRectangleBorder(borderRadius: options.borderRadius), + padding: EdgeInsets.zero, + label: Center( + child: Text( + day.toUpperCase(), + style: abbreviationTextStyle, + ), ), + selected: isSelected, + showCheckmark: theme.chipTheme.showCheckmark ?? false, + onSelected: onDaySelected, ), - selected: isSelected, - showCheckmark: theme.chipTheme.showCheckmark ?? false, - onSelected: onDaySelected, ), ); } diff --git a/packages/flutter_availability/lib/src/ui/widgets/template_week_overview.dart b/packages/flutter_availability/lib/src/ui/widgets/template_week_overview.dart index 9a3540b..cd89183 100644 --- a/packages/flutter_availability/lib/src/ui/widgets/template_week_overview.dart +++ b/packages/flutter_availability/lib/src/ui/widgets/template_week_overview.dart @@ -3,6 +3,7 @@ import "package:flutter_availability/src/ui/view_models/break_view_model.dart"; import "package:flutter_availability/src/ui/view_models/template_daydata_view_model.dart"; import "package:flutter_availability/src/ui/view_models/week_template_view_models.dart"; import "package:flutter_availability/src/ui/widgets/calendar_grid.dart"; +import "package:flutter_availability/src/ui/widgets/semantic_widget.dart"; import "package:flutter_availability/src/util/scope.dart"; import "package:flutter_availability_data_interface/flutter_availability_data_interface.dart"; @@ -28,12 +29,24 @@ class TemplateWeekOverview extends StatelessWidget { var availabilityScope = AvailabilityScope.of(context); var options = availabilityScope.options; var translations = options.translations; + var identifiers = options.accessibilityIds; var colors = options.colors; var dayNames = getDaysOfTheWeekAsStrings(translations, context); var templateData = template.data; + var editButton = CustomSemantics( + identifier: identifiers.weekTemplateEditButtonIdentifier, + child: options.smallTextButtonBuilder( + context, + onClickEdit, + Text( + translations.editTemplateButton, + ), + ), + ); + return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -46,13 +59,7 @@ class TemplateWeekOverview extends StatelessWidget { overflow: TextOverflow.ellipsis, ), ), - options.smallTextButtonBuilder( - context, - onClickEdit, - Text( - translations.editTemplateButton, - ), - ), + editButton, ], ), const SizedBox(height: 8), @@ -67,12 +74,13 @@ class TemplateWeekOverview extends StatelessWidget { ), child: Column( children: [ - for (var day in WeekDay.values) ...[ + for (var (index, day) in WeekDay.values.indexed) ...[ _TemplateDayDetailRow( - dayName: dayNames[day.index], + dayName: dayNames[index], dayData: templateData.containsKey(day) ? templateData[day] : null, - isOdd: day.index.isOdd, + index: index, + isOdd: index.isOdd, ), ], ], @@ -88,6 +96,7 @@ class _TemplateDayDetailRow extends StatelessWidget { required this.dayName, required this.dayData, required this.isOdd, + required this.index, }); /// The name of the day @@ -97,6 +106,9 @@ class _TemplateDayDetailRow extends StatelessWidget { /// This causes a layered effect final bool isOdd; + /// The index of the day + final int index; + /// The data of the day final DayTemplateDataViewModel? dayData; @@ -107,6 +119,7 @@ class _TemplateDayDetailRow extends StatelessWidget { var availabilityScope = AvailabilityScope.of(context); var options = availabilityScope.options; var translations = options.translations; + var identifiers = options.accessibilityIds; var startTime = dayData?.startTime; var endTime = dayData?.endTime; @@ -119,6 +132,8 @@ class _TemplateDayDetailRow extends StatelessWidget { dayPeriod = translations.unavailable; } + var dayPeriodIdentifier = "${identifiers.weekDayTimeIdentifier}_$index"; + var breaks = dayData?.breaks ?? []; BoxDecoration? boxDecoration; @@ -145,13 +160,23 @@ class _TemplateDayDetailRow extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(dayName, style: textTheme.bodyLarge), - Text(dayPeriod, style: textTheme.bodyLarge), + CustomSemantics( + identifier: dayPeriodIdentifier, + child: Text( + dayPeriod, + style: textTheme.bodyLarge, + ), + ), ], ), // for each break add a line - for (var dayBreak in breaks) ...[ + for (var (breakIndex, dayBreak) in breaks.indexed) ...[ const SizedBox(height: 4), - _TemplateDayDetailPauseRow(dayBreakViewModel: dayBreak), + _TemplateDayDetailPauseRow( + dayBreakViewModel: dayBreak, + dayIndex: index, + breakIndex: breakIndex, + ), ], ], ), @@ -162,10 +187,20 @@ class _TemplateDayDetailRow extends StatelessWidget { class _TemplateDayDetailPauseRow extends StatelessWidget { const _TemplateDayDetailPauseRow({ required this.dayBreakViewModel, + required this.dayIndex, + required this.breakIndex, }); final BreakViewModel dayBreakViewModel; + /// The index of the day in the list of days + /// This is used to create unique identifiers when there are multiple days + /// with breaks + final int dayIndex; + + /// The index of the break in the list of breaks + final int breakIndex; + @override Widget build(BuildContext context) { var theme = Theme.of(context); @@ -173,6 +208,7 @@ class _TemplateDayDetailPauseRow extends StatelessWidget { var availabilityScope = AvailabilityScope.of(context); var options = availabilityScope.options; var translations = options.translations; + var identifiers = options.accessibilityIds; var dayBreak = dayBreakViewModel.toBreak(); var startTime = TimeOfDay.fromDateTime(dayBreak.startTime); @@ -186,6 +222,9 @@ class _TemplateDayDetailPauseRow extends StatelessWidget { fontStyle: FontStyle.italic, ); + var breakIdentifier = + "${identifiers.weekDayBreakIdentifier}_${dayIndex}_$breakIndex"; + return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -196,9 +235,12 @@ class _TemplateDayDetailPauseRow extends StatelessWidget { style: pauseTextStyle, ), ), - Text( - pausePeriod, - style: pauseTextStyle, + CustomSemantics( + identifier: breakIdentifier, + child: Text( + pausePeriod, + style: pauseTextStyle, + ), ), ], ); diff --git a/packages/flutter_availability/pubspec.yaml b/packages/flutter_availability/pubspec.yaml index 8af4a4c..5eeff44 100644 --- a/packages/flutter_availability/pubspec.yaml +++ b/packages/flutter_availability/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_availability description: "Flutter availability userstory package" -version: 1.0.0 +version: 1.1.0 publish_to: https://forgejo.internal.iconica.nl/api/packages/internal/pub @@ -14,7 +14,7 @@ dependencies: flutter_hooks: ^0.20.5 flutter_availability_data_interface: hosted: https://forgejo.internal.iconica.nl/api/packages/internal/pub - version: ^1.0.0 + version: ^1.1.0 dev_dependencies: flutter_test: @@ -25,4 +25,3 @@ dev_dependencies: url: https://github.com/Iconica-Development/flutter_iconica_analysis ref: 7.0.0 -flutter: diff --git a/packages/flutter_availability_data_interface/pubspec.yaml b/packages/flutter_availability_data_interface/pubspec.yaml index a6429f0..d82c9d1 100644 --- a/packages/flutter_availability_data_interface/pubspec.yaml +++ b/packages/flutter_availability_data_interface/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_availability_data_interface description: "The data interface for the flutter_availability component" -version: 1.0.0 +version: 1.1.0 publish_to: https://forgejo.internal.iconica.nl/api/packages/internal/pub