Skip to content

Commit 0c8bb7e

Browse files
committed
feat(progress-indicator): add reduce motion and fix accessibility for screen reader, fix stop indicator issue, change gap size for small type to 0
1 parent 5f5c514 commit 0c8bb7e

12 files changed

Lines changed: 334 additions & 159 deletions

app/lib/ui/components/progress_indicator/linear_progress_indicator_demo_screen.dart

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,7 @@ class _CustomizationContentState extends State<_CustomizationContent> {
233233
onSelected: (selectedOption) {
234234
setState(() {
235235
customizationState.selectedType = selectedOption;
236+
customizationState.hasPercentage = false;
236237
});
237238
},
238239
),
@@ -331,9 +332,13 @@ class _CustomizationContentState extends State<_CustomizationContent> {
331332
.l10n
332333
.app_components_progressIndicator_helperTextPercentage_tech,
333334
value: customizationState.hasPercentage,
334-
onChanged: (value) {
335-
customizationState.hasPercentage = value;
336-
},
335+
onChanged:
336+
customizationState.selectedType ==
337+
ProgressIndicatorEnumType.determinate
338+
? (value) {
339+
customizationState.hasPercentage = value;
340+
}
341+
: null,
337342
),
338343
),
339344
Visibility(

ouds_core/lib/components/progress_indicator/internal/ouds_progress_indicator_status_modifier.dart

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
1-
/*
2-
* // Software Name: OUDS Flutter
3-
* // SPDX-FileCopyrightText: Copyright (c) Orange SA
4-
* // SPDX-License-Identifier: MIT
5-
* //
6-
* // This software is distributed under the MIT license,
7-
* // the text of which is available at https://opensource.org/license/MIT/
8-
* // or see the "LICENSE" file for more details.
9-
* //
10-
* // Software description: Flutter library of reusable graphical components
11-
* //
12-
*/
1+
// Software Name: OUDS Flutter
2+
// SPDX-FileCopyrightText: Copyright (c) Orange SA
3+
// SPDX-License-Identifier: MIT
4+
//
5+
// This software is distributed under the MIT license,
6+
// the text of which is available at https://opensource.org/license/MIT/
7+
// or see the "LICENSE" file for more details.
8+
//
9+
// Software description: Flutter library of reusable graphical components
10+
//
1311

1412
/// @nodoc
1513
library;
@@ -18,10 +16,11 @@ import 'package:flutter/material.dart';
1816
import 'package:ouds_core/components/common/ouds_icon_status.dart';
1917
import 'package:ouds_theme_contract/ouds_theme.dart';
2018

21-
/// Modifier class to handle color logic based on progress indicator status.
19+
/// Resolves the indicator color from the active theme based on an [OudsIconStatus].
2220
class OudsProgressIndicatorStatusModifier {
2321
final BuildContext context;
2422

23+
/// Creates a status modifier bound to the given [context].
2524
OudsProgressIndicatorStatusModifier(this.context);
2625

2726
/// Returns the indicator color based on the progress indicator status.

ouds_core/lib/components/progress_indicator/internal/ouds_progress_indicator_style_modifier.dart

Lines changed: 20 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
//
21
// Software Name: OUDS Flutter
32
// SPDX-FileCopyrightText: Copyright (c) Orange SA
43
// SPDX-License-Identifier: MIT
@@ -9,7 +8,6 @@
98
//
109
// Software description: Flutter library of reusable graphical components
1110
//
12-
1311
/// @nodoc
1412
library;
1513

@@ -28,15 +26,11 @@ class OudsProgressIndicatorStyleModifier {
2826
OudsProgressIndicatorStyleModifier(this.context);
2927

3028
/// Gets the border radius for a progress indicator based on the theme configuration.
31-
///
32-
/// Returns a [BorderRadius] object with the appropriate radius value.
3329
BorderRadius getBorderRadius() {
3430
return BorderRadius.circular(getDoubleBorderRadius());
3531
}
3632

3733
/// Retrieves the border radius value depending on whether rounded corners are enabled.
38-
///
39-
/// Checks the theme configuration for the `rounded` property and returns the corresponding radius.
4034
double getDoubleBorderRadius() {
4135
final progressIndicatorTokens = OudsTheme.of(
4236
context,
@@ -53,48 +47,40 @@ class OudsProgressIndicatorStyleModifier {
5347

5448
/// Computes the gap size for the progress indicator based on the specified size type.
5549
///
56-
/// For the default size, it converts a 10-degree angle into a distance on the circle.
57-
/// For small gaps, it returns a fixed value.
50+
/// For the default size with [StrokeCap.butt], it converts a 10-degree angle into a
51+
/// distance on the circle. For [StrokeCap.round], it returns a fixed value per size.
5852
double computeGapSize(OudsProgressIndicatorGapSize gapSizeType) {
59-
final gapStroke = getStrokeCap(gapSizeType);
60-
return gapSizeType == OudsProgressIndicatorGapSize.defaultSize &&
61-
gapStroke == StrokeCap.butt
62-
? 1 / 360 * math.pi
63-
: gapSizeType == OudsProgressIndicatorGapSize.defaultSize &&
64-
gapStroke == StrokeCap.round
65-
? 4
66-
: 1;
53+
final strokeCap = getStrokeCap(gapSizeType);
54+
55+
if (strokeCap == StrokeCap.butt) {
56+
return gapSizeType == OudsProgressIndicatorGapSize.defaultSize
57+
? 1 / 360 * math.pi
58+
: 0;
59+
}
60+
61+
return switch (gapSizeType) {
62+
OudsProgressIndicatorGapSize.defaultSize => 4,
63+
OudsProgressIndicatorGapSize.small => 1,
64+
};
6765
}
6866

69-
/// Returns the gap size used by a linear progress indicator.
67+
/// Returns the gap size for a linear progress indicator.
68+
/// - defaultSize: 4.0 dp (M3 default, matching Android).
69+
/// - small: 1.0 dp (smaller but visible).
7070
double linearGapSize(OudsProgressIndicatorGapSize gapSizeType) {
71-
return gapSizeType == OudsProgressIndicatorGapSize.defaultSize ? 4 : 1;
71+
return gapSizeType == OudsProgressIndicatorGapSize.defaultSize ? 4.0 : 1.0;
7272
}
7373

74-
/// Calculates the stroke width based on the component size.
75-
/// The stroke width is equal to 25% of the radius, 12.5% of the diameter.
74+
/// Calculates the stroke width: 12.5% of the diameter.
7675
double computeStrokeWidth(double componentSize) {
7776
return componentSize * 0.125;
7877
}
7978

8079
/// Determines the stroke cap style based on the border radius and gap size.
81-
///
82-
/// Uses a round stroke cap if the radius is greater than zero.
83-
/// Otherwise, chooses between square or butt caps depending on the gap size.
8480
StrokeCap getStrokeCap(OudsProgressIndicatorGapSize gapSizeType) {
8581
final radius = getDoubleBorderRadius();
8682

87-
return radius > 0.0
88-
? StrokeCap.round
89-
: gapSizeType == OudsProgressIndicatorGapSize.small
90-
? StrokeCap.square
91-
: StrokeCap.butt;
92-
}
93-
94-
/// Returns the stroke cap used by the linear progress indicator.
95-
StrokeCap getLinearStrokeCap(OudsProgressIndicatorGapSize gapSizeType) {
96-
final radius = getDoubleBorderRadius();
97-
return radius > 0.0 ? StrokeCap.round : StrokeCap.square;
83+
return radius > 0.0 ? StrokeCap.round : StrokeCap.butt;
9884
}
9985

10086
/// Gets the track color for the progress indicator.

ouds_core/lib/components/progress_indicator/internal/ouds_progress_indicator_utils.dart

Lines changed: 73 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,13 @@ class OudsProgressIndicatorUtils {
4040
}
4141
}
4242

43-
/// Returns the progress value only when the indicator is determinate.
43+
/// Returns the raw progress value only when the indicator is determinate.
4444
///
45-
/// For indeterminate indicators, this method returns `null` so that the
46-
/// underlying Flutter progress indicator switches to its indeterminate state.
45+
/// For indeterminate indicators, returns `null` so that the underlying
46+
/// Flutter progress widget enters its indeterminate spinning state.
47+
///
48+
/// > Note: the value is **not** clamped here — call [clampedProgressValue]
49+
/// > when a `[0.0, 1.0]` guarantee is required.
4750
static double? getProgressValue(
4851
OudsProgressIndicatorType? progressType,
4952
double? progress,
@@ -55,16 +58,15 @@ class OudsProgressIndicatorUtils {
5558

5659
/// Builds the helper text displayed below a linear progress indicator.
5760
///
58-
/// If [percentage] is `true`, the helper text is generated from [progress]
59-
/// and formatted as a percentage.
60-
///
61-
/// If [percentage] is `false`, the provided [helperText] is returned.
62-
///
63-
/// Returns an empty string when no helper text should be displayed.
61+
/// - When [percentage] is `true`, the text is derived from [progress] and
62+
/// formatted as a percentage (e.g. `75%` or `75 %` when
63+
/// [spaceBeforePercentage] is `true`).
64+
/// - When [percentage] is `false`, [helperText] is returned as-is.
65+
/// - Returns `null` when neither condition produces content.
6466
static String? buildHelperText(
6567
bool percentage,
6668
bool spaceBeforePercentage,
67-
double? progress, // ou double/int selon ton type réel
69+
double? progress,
6870
String? helperText,
6971
) {
7072
final progressValue = progress != null ? (progress * 100).round() : 0;
@@ -96,15 +98,72 @@ class OudsProgressIndicatorUtils {
9698
: null;
9799
}
98100

99-
/// Returns `true` when the progress indicator should animate its value.
101+
/// Returns `true` when the progress indicator should animate its value change.
100102
///
101-
/// Animation is only enabled for determinate indicators when [animated]
102-
/// is set to `true`.
103+
/// Animation is only active when **all three** conditions hold:
104+
/// 1. [progressType] is [OudsProgressIndicatorType.determinate] — indeterminate
105+
/// indicators drive their own spin loop internally.
106+
/// 2. [animated] is `true` — the caller has explicitly requested animation.
107+
/// 3. The OS-level reduced-motion setting is **not** active — see
108+
/// [shouldDisableAnimations] for the cross-platform detection logic.
103109
static bool shouldAnimate(
104110
OudsProgressIndicatorType? progressType,
111+
BuildContext context,
105112
bool animated,
106113
) {
107-
return progressType == OudsProgressIndicatorType.determinate && animated;
114+
final reduceMotion = shouldDisableAnimations(context);
115+
return progressType == OudsProgressIndicatorType.determinate &&
116+
animated &&
117+
!reduceMotion;
118+
}
119+
120+
/// Returns `true` when the OS accessibility settings request that animations
121+
/// be suppressed, using the most reliable source available per platform.
122+
///
123+
/// Two complementary checks are combined:
124+
///
125+
/// 1. **[MediaQuery.disableAnimationsOf]** — covers Android's global
126+
/// "Remove Animations" toggle (`Settings › Accessibility › Visibility
127+
/// enhancements › Remove animations`) and any host app that explicitly
128+
/// injects `disableAnimations: true` into its [MediaQuery].
129+
///
130+
/// 2. **[AccessibilityFeatures.reduceMotion]** read from
131+
/// `View.of(context).platformDispatcher` — covers iOS "Reduce Motion"
132+
/// (`Settings › Accessibility › Motion › Reduce Motion`).
133+
/// [MediaQuery.disableAnimationsOf] does **not** reliably map to
134+
/// `UIAccessibility.isReduceMotionEnabled` in all Flutter versions,
135+
/// so the platform dispatcher is queried directly as a fallback.
136+
static bool shouldDisableAnimations(BuildContext context) {
137+
// Android master toggle and any host-app MediaQuery override.
138+
if (MediaQuery.disableAnimationsOf(context)) return true;
139+
140+
// iOS "Reduce Motion" — read from the platform dispatcher directly because
141+
// MediaQuery.disableAnimationsOf does not reliably reflect this flag on iOS.
142+
final accessibilityFeatures = View.of(
143+
context,
144+
).platformDispatcher.accessibilityFeatures;
145+
return accessibilityFeatures.reduceMotion;
146+
}
147+
148+
/// Builds the accessibility semantics value label for a progress indicator.
149+
///
150+
/// Returns a localized string such as `'75 pour cent'` (FR) or `'75 percent'`
151+
/// (EN) when the indicator is determinate, so that VoiceOver on real iOS
152+
/// devices reads the value as a whole number (*"soixante-quinze pour cent"*)
153+
/// instead of digit-by-digit (*"sept cinq pourcent"*), which happens when the
154+
/// `%` symbol is used directly in the semantics value string.
155+
///
156+
/// Returns `null` for indeterminate indicators (no meaningful value to report).
157+
static String? buildSemanticValueLabel(
158+
OudsProgressIndicatorType? progressType,
159+
double? progress,
160+
OudsLocalizations? localizations,
161+
) {
162+
final clamped = clampedProgressValue(progressType, progress);
163+
if (clamped == null || localizations == null) return null;
164+
return localizations.core_progressIndicator_percentage_a11y(
165+
(clamped * 100).round(),
166+
);
108167
}
109168

110169
/// Returns the normalized progress value used by the indicator.

0 commit comments

Comments
 (0)