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