@@ -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