Skip to content

Fix time picker period selector a11y touch targets #170060

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from

Conversation

bleroux
Copy link
Contributor

@bleroux bleroux commented Jun 5, 2025

Description

This PR fixes the TimePicker day period selector touch targets.

Before

Flutter Google Agenda Compose
Image Image Image Image

After

Flutter Google Agenda Compose
image Image Image Image

Implementation choice

This PR implements two main changes:

  • it expands the _DayPeriodControl bounds by reducing some existing spacing. Doing so the touch area of the AM/PM buttons can grow outside the day period container and respect the minimum interactive height.
  • it expands the bounds of the AM/PM buttons semantics. The solution here is somewhat tricky/hacky:

Because the semantics bounds are clipped by their ancestor, the PR changes the widget order:

Before, the order was _DayPeriodControl > _DayPeriodInputPadding > Material (with shape and clip) > Row (or Column depending on the orientation) > children [ Semantics > AM InkWell, Semantics > PM InkWell ]
(Which leads to the Semantics being clipped by the Material shape).

After, the order is _DayPeriodControl > _DayPeriodInputPadding > Row (or Column depending on the orientation) > children [ Semantics > Material (with shape and clip) > AM InkWell, Semantics > Material (with shape and clip) > PM InkWell ]

The difficulty here is that the TimePickerThemeData.dayPeriodShape is meant to be the shape for the whole day period container. In order to change the order, this PR has to set separetly the shapes of the AM and PM buttons. To do so it adds some logic to 'split' the shape in two parts. This is ok for the default shape but not possible for any shape, in that case the original shape will be use for both buttons which gives a different result than before this PR. This annoyance seems less a problem than having the default rendering not respecting a11y touch targets.

I'm not aware of a way to specify that the semantics bounds should not be clipped by a parent. If a solution exists for this it would be better to use it instead of splitting the shape.

Related Issue

Fixes touch target size not up to a11y standards for DatePicker day period selector.

Tests

Adds 2 tests.
Updates 5 tests.

@github-actions github-actions bot added framework flutter/packages/flutter repository. See also f: labels. f: material design flutter/packages/flutter/material repository. labels Jun 5, 2025
@bleroux bleroux force-pushed the fix_time_picker_day_selector_tap_targets branch 2 times, most recently from 1f7e605 to 7c5332f Compare June 5, 2025 17:42
@bleroux bleroux requested review from Piinks and chunhtai June 5, 2025 17:42
bottom: _TimePickerModel.useMaterial3Of(context) ? 20 : 24,
bottom:
(_TimePickerModel.useMaterial3Of(context) ? 20 : 24) -
minInteractiveVerticalPadding / 2,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIUC, the additional space we save here will be shared by both bottom, why do we divide it by 2?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because this space is the one between the header and the period selector. Half of the whole vertical padding applies to it.

image

@@ -306,7 +314,7 @@ class _TimePickerHeader extends StatelessWidget {
: VerticalDirection.down,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 12,
spacing: math.max(0, 16 - minInteractiveVerticalPadding / 2),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same

child: Column(
children: <Widget>[
Expanded(child: amButton),
if (showSeparator)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why we conditionally show separator now?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because with this fix two shapes are drawn. The bottom border of the AM button and the top border of the PM button replace the divider.

@@ -650,50 +646,126 @@ class _DayPeriodControl extends StatelessWidget {
}

final Widget result;
bool showSeparator = true;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
bool showSeparator = true;
final bool hasRoundedBorder = resolvedShape is RoundedRectangleBorder && resolvedShape.borderRadius is BorderRadius
bool showSeparator = !hasRoundedBorder;

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Though I don't quite get why we only show separator when this condition is met

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Though I don't quite get why we only show separator when this condition is met

The goal was to fallback to the previous rendering when it was not possible to cut the shape.
It's true that it might be better to always remove the separator and assume that the bottom of tha AM shape and the top of the PM shape will look as a Divider.

Comment on lines 663 to 664
if (resolvedShape is RoundedRectangleBorder && resolvedShape.borderRadius is BorderRadius) {
showSeparator = false;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (resolvedShape is RoundedRectangleBorder && resolvedShape.borderRadius is BorderRadius) {
showSeparator = false;
if (hasRoundedBorder) {

Comment on lines 718 to 719
if (resolvedShape is RoundedRectangleBorder && resolvedShape.borderRadius is BorderRadius) {
showSeparator = false;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (resolvedShape is RoundedRectangleBorder && resolvedShape.borderRadius is BorderRadius) {
showSeparator = false;
if (hasRoundedBorder) {

minSemanticHeight: kMinInteractiveDimension,
checked: selected,
alignment: semanticAlignment,
child: Material(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explain a bit why this is needed to expand the size of the button? Why would cliping at the parent row/ column prevent the child from expanding the size? Assuming children return big enough space for itself, parent material should not clip the space right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let me try to explain this current solution and why it is not as simple as I would like it to be. I'm very interested in this discussion. It would great to find out a better solution (mainly I would prefer to remove the logic which 'cuts' the shape).

why this is needed to expand the size of the button? Why would cliping at the parent row/ column prevent the child from expanding the size? Assuming children return big enough space for itself, parent material should not clip the space right?

This _AmPmSemantics widget is not used to expand the size of the button, it is used to expand the semantics bounds.
The spec design requires that the visual size for each button is 40 in dial mode and 36 in input mode only.

For instance, see https://m3.material.io/components/time-pickers/specs#1cb56d62-0b4a-4db0-8620-215f2d0cfa69

image

To respect this visual size, the size of the buttons is restricted (line 758 below):

child: SizedBox(
height: dayPeriodSize.height,
child: Row(
children: <Widget>[
Expanded(child: amButton),
if (showSeparator)
Container(
decoration: BoxDecoration(border: Border(left: resolvedSide)),
width: 1,
),
Expanded(child: pmButton),
],
),
),

While writing this, I realize that now that this PR contains logic to cut the shape I can probably remove the _AmPmSemantics widget, remove the SizedBox line 757, and add some logic to add padding inside _AmPmButton. That way a usual Semantics widget could be use.
Let me verify this and push a commit. This will get us some reference to discuss further.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I updated the PR to replace the _AmPmSemantics widget with a normal Semantics and adding some padding to get the inner Material properly sized.

I wonder if it would be possible to use a Stack to get rid of the logic which cuts the shape: one layer would contain the day period container and the InkWells, the second layer the Semantics, I don't know if it is possible. I will experiment and let you know.

orientation == Orientation.portrait
? defaultTheme.dayPeriodPortraitSize.height
: defaultTheme.dayPeriodLandscapeSize.height;
final double minInteractiveVerticalPadding =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are we sure only veritical space will be a problem?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the default day period size (from M3 and M2), the horizontal space is greater than minInteractiveDimension so I focused only on the vertical space.
I'm not sure it would make sense for someone to change this default to something less than minInteractiveDimension and assuming the framework will keep making this accessible.

@bleroux bleroux force-pushed the fix_time_picker_day_selector_tap_targets branch from 7c5332f to 8992206 Compare June 6, 2025 12:01
@bleroux
Copy link
Contributor Author

bleroux commented Jun 12, 2025

Convert to draft, as I will file 2 alternatives PRs for discussion.

@bleroux bleroux marked this pull request as draft June 12, 2025 08:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
f: material design flutter/packages/flutter/material repository. framework flutter/packages/flutter repository. See also f: labels.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

touch target size not up to a11y standards for DatePicker day period selector.
2 participants