From 5b99e1583da5a996f9625aeb6f945855f807f849 Mon Sep 17 00:00:00 2001 From: Lucas SAUDON Date: Tue, 17 Dec 2024 17:41:31 +0100 Subject: [PATCH] fix(a11y): Correction du focus, description image et email MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Accordéon - Bouton radio - Interrupteur - Bouton - Lien --- app/lib/core/assets/images.dart | 2 + app/lib/core/assets/svgs.dart | 4 + .../presentation/pages/creer_compte_view.dart | 1 + .../presentation/widgets/jaccepte.dart | 65 +++++----- .../pages/mot_de_passe_oublie_view.dart | 1 + .../presentation/pages/se_connecter_view.dart | 1 + .../presentation/pages/bibliotheque_view.dart | 2 +- .../pages/pre_onboarding_page.dart | 14 ++- .../widgets/mes_informations_annee.dart | 2 +- .../widgets/mes_informations_nom.dart | 2 +- ...informations_nombre_de_parts_fiscales.dart | 2 +- .../widgets/mes_informations_prenom.dart | 2 +- .../pages/aide_simulateur_velo_page.dart | 2 +- app/lib/l10n/l10n.dart | 1 + packages/agir_lints/lib/analysis_options.yaml | 1 + .../dsfr.dart/example/lib/toggles_page.dart | 4 +- packages/dsfr.dart/lib/dsfr.dart | 4 +- .../dsfr.dart/lib/src/atoms/focus_widget.dart | 36 ++++++ .../lib/src/composants/accordions.dart | 74 +++++++----- .../lib/src/composants/buttons/button.dart | 4 +- .../src/composants/buttons/raw_button.dart | 112 ++++++++---------- .../lib/src/composants/checkbox.dart | 29 +---- .../dsfr.dart/lib/src/composants/input.dart | 9 +- .../lib/src/composants/input_headless.dart | 8 +- .../src/composants/{links.dart => link.dart} | 42 ++----- .../lib/src/composants/radios/radio.dart | 78 +++++++----- .../{toggle.dart => toggle_switch.dart} | 46 +++++-- packages/dsfr.dart/test/input_test.dart | 8 +- packages/dsfr.dart/test/toggle_test.dart | 4 +- 29 files changed, 316 insertions(+), 244 deletions(-) create mode 100644 packages/dsfr.dart/lib/src/atoms/focus_widget.dart rename packages/dsfr.dart/lib/src/composants/{links.dart => link.dart} (79%) rename packages/dsfr.dart/lib/src/composants/{toggle.dart => toggle_switch.dart} (59%) diff --git a/app/lib/core/assets/images.dart b/app/lib/core/assets/images.dart index c4e46bbe..dd5981e3 100644 --- a/app/lib/core/assets/images.dart +++ b/app/lib/core/assets/images.dart @@ -2,5 +2,7 @@ abstract final class AssetsImages { const AssetsImages._(); static const franceNationVerte = 'assets/logos/logo_fnv.png'; + static const franceNationVerteSemantic = 'Logo de France Nation Verte'; + static const nosGestesClimat = 'assets/logos/nos_gestes_climat.png'; } diff --git a/app/lib/core/assets/svgs.dart b/app/lib/core/assets/svgs.dart index d4c01a33..4cf4202c 100644 --- a/app/lib/core/assets/svgs.dart +++ b/app/lib/core/assets/svgs.dart @@ -2,7 +2,11 @@ abstract final class AssetsSvgs { const AssetsSvgs._(); static const republiqueFrancaise = 'assets/logos/logo_rf.svg'; + static const republiqueFrancaiseSemantic = + 'Logo officiel de la République Française'; static const ademe = 'assets/logos/logo_ademe.svg'; + static const ademeSemantic = + 'Logo de l’ADEME: Agence de la transition écologique'; static const mesAidesVeloLogo = 'assets/logos/mesaidesvelo_logo.svg'; static const mesAidesVeloTexte = 'assets/logos/mesaidesvelo_texte.svg'; static const bibliothequeEmpty = 'assets/svgs/bibliotheque_illustration.svg'; diff --git a/app/lib/features/authentification/creer_compte/presentation/pages/creer_compte_view.dart b/app/lib/features/authentification/creer_compte/presentation/pages/creer_compte_view.dart index 75efdf5f..b5cda585 100644 --- a/app/lib/features/authentification/creer_compte/presentation/pages/creer_compte_view.dart +++ b/app/lib/features/authentification/creer_compte/presentation/pages/creer_compte_view.dart @@ -44,6 +44,7 @@ class CreerCompteView extends StatelessWidget { const SizedBox(height: DsfrSpacings.s3w), DsfrInput( label: Localisation.adresseEmail, + hintText: Localisation.adresseEmailHint, onChanged: (final value) => context .read() .add(CreerCompteAdresseMailAChangee(value)), diff --git a/app/lib/features/authentification/creer_compte/presentation/widgets/jaccepte.dart b/app/lib/features/authentification/creer_compte/presentation/widgets/jaccepte.dart index d439ffe2..0787e07f 100644 --- a/app/lib/features/authentification/creer_compte/presentation/widgets/jaccepte.dart +++ b/app/lib/features/authentification/creer_compte/presentation/widgets/jaccepte.dart @@ -45,42 +45,41 @@ class _JaccepteState extends State { return GestureDetector( onTap: () => widget.onChanged(!widget.value), behavior: HitTestBehavior.opaque, - child: Row( - children: [ - Semantics( - checked: widget.value, - label: '$jaccepte${widget.label}', - onTap: () => widget.onChanged(!widget.value), - child: DsfrCheckboxIcon(value: widget.value), - ), - const SizedBox(width: DsfrSpacings.s1w), - Expanded( - child: Text.rich( - TextSpan( - text: jaccepte, - children: [ - TextSpan( - text: '${widget.label} ', - style: style.copyWith( - decoration: TextDecoration.underline, - ), - recognizer: _recognizer, - children: const [ - WidgetSpan( - alignment: PlaceholderAlignment.middle, - child: Icon( - DsfrIcons.systemExternalLinkFill, - size: 16, - ), + child: Semantics( + checked: widget.value, + label: '$jaccepte${widget.label}', + child: Row( + children: [ + DsfrCheckboxIcon(value: widget.value), + const SizedBox(width: DsfrSpacings.s1w), + Expanded( + child: Text.rich( + TextSpan( + text: jaccepte, + children: [ + TextSpan( + text: '${widget.label} ', + style: style.copyWith( + decoration: TextDecoration.underline, ), - ], - ), - ], + recognizer: _recognizer, + children: const [ + WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: Icon( + DsfrIcons.systemExternalLinkFill, + size: 16, + ), + ), + ], + ), + ], + ), + style: style, ), - style: style, ), - ), - ], + ], + ), ), ); } diff --git a/app/lib/features/authentification/mot_de_passe_oublie/pages/mot_de_passe_oublie_view.dart b/app/lib/features/authentification/mot_de_passe_oublie/pages/mot_de_passe_oublie_view.dart index 82e18bfe..92ce0151 100644 --- a/app/lib/features/authentification/mot_de_passe_oublie/pages/mot_de_passe_oublie_view.dart +++ b/app/lib/features/authentification/mot_de_passe_oublie/pages/mot_de_passe_oublie_view.dart @@ -34,6 +34,7 @@ class MotDePasseOublieView extends StatelessWidget { const SizedBox(height: DsfrSpacings.s3w), DsfrInput( label: Localisation.adresseEmail, + hintText: Localisation.adresseEmailHint, onChanged: (final value) => context.read().add( MotDePasseOublieEmailChange(value), diff --git a/app/lib/features/authentification/se_connecter/presentation/pages/se_connecter_view.dart b/app/lib/features/authentification/se_connecter/presentation/pages/se_connecter_view.dart index dc6baa25..0c26bcc7 100644 --- a/app/lib/features/authentification/se_connecter/presentation/pages/se_connecter_view.dart +++ b/app/lib/features/authentification/se_connecter/presentation/pages/se_connecter_view.dart @@ -44,6 +44,7 @@ class SeConnecterView extends StatelessWidget { const SizedBox(height: DsfrSpacings.s3w), DsfrInput( label: Localisation.adresseEmail, + hintText: Localisation.adresseEmailHint, onChanged: (final value) => context .read() .add(SeConnecterAdresseMailAChange(value)), diff --git a/app/lib/features/bibliotheque/presentation/pages/bibliotheque_view.dart b/app/lib/features/bibliotheque/presentation/pages/bibliotheque_view.dart index 3b212dc6..e822563b 100644 --- a/app/lib/features/bibliotheque/presentation/pages/bibliotheque_view.dart +++ b/app/lib/features/bibliotheque/presentation/pages/bibliotheque_view.dart @@ -61,7 +61,7 @@ class _Favorites extends StatelessWidget { const _Favorites(); @override - Widget build(final context) => DsfrToggle( + Widget build(final context) => DsfrToggleSwitch( label: Localisation.mesFavoris, value: context.select( (final value) => value.state.isFavorites, diff --git a/app/lib/features/pre_onboarding/presentation/pages/pre_onboarding_page.dart b/app/lib/features/pre_onboarding/presentation/pages/pre_onboarding_page.dart index 9fa8b355..2b0f472f 100644 --- a/app/lib/features/pre_onboarding/presentation/pages/pre_onboarding_page.dart +++ b/app/lib/features/pre_onboarding/presentation/pages/pre_onboarding_page.dart @@ -45,11 +45,21 @@ class PreOnboardingPage extends StatelessWidget { SvgPicture.asset( AssetsSvgs.republiqueFrancaise, height: 69, + semanticsLabel: + AssetsSvgs.republiqueFrancaiseSemantic, ), const SizedBox(width: DsfrSpacings.s3w), - Image.asset(AssetsImages.franceNationVerte, height: 46), + Image.asset( + AssetsImages.franceNationVerte, + semanticLabel: AssetsImages.franceNationVerteSemantic, + height: 46, + ), const SizedBox(width: DsfrSpacings.s3w), - SvgPicture.asset(AssetsSvgs.ademe, height: 55), + SvgPicture.asset( + AssetsSvgs.ademe, + height: 55, + semanticsLabel: AssetsSvgs.ademeSemantic, + ), ], ), ], diff --git a/app/lib/features/profil/informations/presentation/widgets/mes_informations_annee.dart b/app/lib/features/profil/informations/presentation/widgets/mes_informations_annee.dart index a58566f4..809fdee4 100644 --- a/app/lib/features/profil/informations/presentation/widgets/mes_informations_annee.dart +++ b/app/lib/features/profil/informations/presentation/widgets/mes_informations_annee.dart @@ -17,7 +17,7 @@ class MesInformationsAnnee extends StatelessWidget { return DsfrInput( label: Localisation.anneeDeNaissance, - hint: Localisation.facultatif, + hintText: Localisation.facultatif, initialValue: anneeDeNaissance?.toString(), onChanged: (final value) { final parsedValue = int.tryParse(value); diff --git a/app/lib/features/profil/informations/presentation/widgets/mes_informations_nom.dart b/app/lib/features/profil/informations/presentation/widgets/mes_informations_nom.dart index 173a0fac..85f74d98 100644 --- a/app/lib/features/profil/informations/presentation/widgets/mes_informations_nom.dart +++ b/app/lib/features/profil/informations/presentation/widgets/mes_informations_nom.dart @@ -15,7 +15,7 @@ class MesInformationsNom extends StatelessWidget { return DsfrInput( label: Localisation.nom, - hint: Localisation.facultatif, + hintText: Localisation.facultatif, initialValue: nom, onChanged: (final value) => context .read() diff --git a/app/lib/features/profil/informations/presentation/widgets/mes_informations_nombre_de_parts_fiscales.dart b/app/lib/features/profil/informations/presentation/widgets/mes_informations_nombre_de_parts_fiscales.dart index 1703c83a..2bc50a1e 100644 --- a/app/lib/features/profil/informations/presentation/widgets/mes_informations_nombre_de_parts_fiscales.dart +++ b/app/lib/features/profil/informations/presentation/widgets/mes_informations_nombre_de_parts_fiscales.dart @@ -18,7 +18,7 @@ class MesInformationsNombreDePartsFiscales extends StatelessWidget { return DsfrInput( label: Localisation.nombreDePartsFiscales, - hint: Localisation.nombreDePartsFiscalesDescription, + hintText: Localisation.nombreDePartsFiscalesDescription, initialValue: FnvNumberFormat.formatNumber(nombreDePartsFiscales), onChanged: (final value) { final parse = double.tryParse(value.replaceFirst(',', '.')); diff --git a/app/lib/features/profil/informations/presentation/widgets/mes_informations_prenom.dart b/app/lib/features/profil/informations/presentation/widgets/mes_informations_prenom.dart index 08116282..50f22049 100644 --- a/app/lib/features/profil/informations/presentation/widgets/mes_informations_prenom.dart +++ b/app/lib/features/profil/informations/presentation/widgets/mes_informations_prenom.dart @@ -16,7 +16,7 @@ class MesInformationsPrenom extends StatelessWidget { return DsfrInput( label: Localisation.prenom, - hint: Localisation.obligatoire, + hintText: Localisation.obligatoire, initialValue: prenom, onChanged: (final value) => context .read() diff --git a/app/lib/features/simulateur_velo/presentation/pages/aide_simulateur_velo_page.dart b/app/lib/features/simulateur_velo/presentation/pages/aide_simulateur_velo_page.dart index 01a80808..054ca9c6 100644 --- a/app/lib/features/simulateur_velo/presentation/pages/aide_simulateur_velo_page.dart +++ b/app/lib/features/simulateur_velo/presentation/pages/aide_simulateur_velo_page.dart @@ -340,7 +340,7 @@ class _NombreDePartsFiscales extends StatelessWidget { return DsfrInput( label: Localisation.nombreDePartsFiscales, - hint: Localisation.nombreDePartsFiscalesDescription, + hintText: Localisation.nombreDePartsFiscalesDescription, initialValue: FnvNumberFormat.formatNumber(nombreDePartsFiscales), onChanged: (final value) { final parse = double.tryParse(value.replaceFirst(',', '.')); diff --git a/app/lib/l10n/l10n.dart b/app/lib/l10n/l10n.dart index e4ed9857..90576c4b 100644 --- a/app/lib/l10n/l10n.dart +++ b/app/lib/l10n/l10n.dart @@ -12,6 +12,7 @@ abstract final class Localisation { static const acheterUnVelo = 'Acheter un vélo'; static const actionRealisee = 'Action réalisée'; static const adresseEmail = 'Mon adresse email'; + static const adresseEmailHint = 'Format attendu : nom@domaine.fr'; static const adultes = 'Adulte(s)'; static const aideVeloAvertissement = 'Veuillez compléter ces informations afin de débuter l’estimation'; diff --git a/packages/agir_lints/lib/analysis_options.yaml b/packages/agir_lints/lib/analysis_options.yaml index aa066724..25734ca9 100644 --- a/packages/agir_lints/lib/analysis_options.yaml +++ b/packages/agir_lints/lib/analysis_options.yaml @@ -109,6 +109,7 @@ dart_code_metrics: - prefer-addition-subtraction-assignments: false - prefer-boolean-prefixes: false - prefer-commenting-analyzer-ignores: false + - prefer-container: false - prefer-correct-identifier-length: false - prefer-correct-handler-name: false - prefer-correct-switch-length: false diff --git a/packages/dsfr.dart/example/lib/toggles_page.dart b/packages/dsfr.dart/example/lib/toggles_page.dart index 52693d0a..6628f68d 100644 --- a/packages/dsfr.dart/example/lib/toggles_page.dart +++ b/packages/dsfr.dart/example/lib/toggles_page.dart @@ -23,7 +23,7 @@ class _TogglesPageState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - DsfrToggle( + DsfrToggleSwitch( label: 'Vos favoris', value: _toggle, onChanged: (final value) { @@ -33,7 +33,7 @@ class _TogglesPageState extends State { }, ), const SizedBox(height: 16), - DsfrToggle( + DsfrToggleSwitch( label: 'Vos favoris', value: !_toggle, onChanged: (final value) { diff --git a/packages/dsfr.dart/lib/dsfr.dart b/packages/dsfr.dart/lib/dsfr.dart index 19399ff4..ef8e11f6 100755 --- a/packages/dsfr.dart/lib/dsfr.dart +++ b/packages/dsfr.dart/lib/dsfr.dart @@ -9,8 +9,8 @@ export 'src/composants/divider.dart'; export 'src/composants/form_message.dart'; export 'src/composants/input.dart'; export 'src/composants/input_headless.dart'; +export 'src/composants/link.dart'; export 'src/composants/link_icon_position.dart'; -export 'src/composants/links.dart'; export 'src/composants/modal.dart'; export 'src/composants/notice.dart'; export 'src/composants/radios/radio.dart'; @@ -19,7 +19,7 @@ export 'src/composants/radios/radio_set.dart'; export 'src/composants/radios/radio_set_headless.dart'; export 'src/composants/select.dart'; export 'src/composants/tags.dart'; -export 'src/composants/toggle.dart'; +export 'src/composants/toggle_switch.dart'; export 'src/fondamentaux/colors.g.dart'; export 'src/fondamentaux/fonts.dart'; export 'src/fondamentaux/icons.g.dart'; diff --git a/packages/dsfr.dart/lib/src/atoms/focus_widget.dart b/packages/dsfr.dart/lib/src/atoms/focus_widget.dart new file mode 100644 index 00000000..6728661b --- /dev/null +++ b/packages/dsfr.dart/lib/src/atoms/focus_widget.dart @@ -0,0 +1,36 @@ +import 'package:dsfr/src/fondamentaux/colors.g.dart'; +import 'package:dsfr/src/fondamentaux/spacing.g.dart'; +import 'package:flutter/material.dart'; + +class DsfrFocusWidget extends StatelessWidget { + const DsfrFocusWidget({ + super.key, + required this.isFocused, + this.borderRadius, + required this.child, + }); + + final bool isFocused; + final BorderRadiusGeometry? borderRadius; + final Widget child; + + @override + Widget build(final BuildContext context) => DecoratedBox( + decoration: BoxDecoration( + border: isFocused + ? const Border.fromBorderSide( + BorderSide( + color: DsfrColors.focus525, + width: DsfrSpacings.s0v5, + strokeAlign: BorderSide.strokeAlignOutside, + ), + ) + : null, + borderRadius: borderRadius, + ), + child: Padding( + padding: const EdgeInsets.all(DsfrSpacings.s0v5), + child: child, + ), + ); +} diff --git a/packages/dsfr.dart/lib/src/composants/accordions.dart b/packages/dsfr.dart/lib/src/composants/accordions.dart index ed202bc3..f18558d1 100644 --- a/packages/dsfr.dart/lib/src/composants/accordions.dart +++ b/packages/dsfr.dart/lib/src/composants/accordions.dart @@ -1,3 +1,4 @@ +import 'package:dsfr/src/atoms/focus_widget.dart'; import 'package:dsfr/src/composants/divider.dart'; import 'package:dsfr/src/fondamentaux/colors.g.dart'; import 'package:dsfr/src/fondamentaux/icons.g.dart'; @@ -62,7 +63,7 @@ class _DsfrAccordionsGroupState extends State { } } -class _DsfrAccordion extends StatelessWidget { +class _DsfrAccordion extends StatefulWidget { const _DsfrAccordion({ required this.index, required this.item, @@ -75,37 +76,54 @@ class _DsfrAccordion extends StatelessWidget { final bool isExpanded; final DsfrAccordionCallback onAccordionCallback; - void _handleTap() => onAccordionCallback(index, !isExpanded); + @override + State<_DsfrAccordion> createState() => _DsfrAccordionState(); +} + +class _DsfrAccordionState extends State<_DsfrAccordion> + with MaterialStateMixin<_DsfrAccordion> { + void _handleTap() => + widget.onAccordionCallback(widget.index, !widget.isExpanded); @override Widget build(final context) => Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - ColoredBox( - color: isExpanded ? DsfrColors.blueFrance925 : Colors.transparent, - child: GestureDetector( - onTap: item.isEnable ? _handleTap : null, - behavior: HitTestBehavior.opaque, - child: ConstrainedBox( - constraints: const BoxConstraints(minHeight: 48), - child: Padding( - padding: - const EdgeInsets.symmetric(vertical: DsfrSpacings.s3v), - child: Row( - children: [ - Expanded(child: item.headerBuilder(isExpanded)), - if (item.isEnable) - AnimatedRotation( - turns: isExpanded ? -0.5 : 0, - duration: Durations.short4, - child: const Icon( - DsfrIcons.systemArrowDownSLine, - size: DsfrSpacings.s2w, - color: DsfrColors.blueFranceSun113, - ), + InkWell( + onTap: widget.item.isEnable ? _handleTap : null, + onHighlightChanged: updateMaterialState(WidgetState.pressed), + onHover: updateMaterialState(WidgetState.hovered), + focusColor: Colors.transparent, + onFocusChange: updateMaterialState(WidgetState.focused), + child: DsfrFocusWidget( + isFocused: isFocused, + child: ColoredBox( + color: widget.isExpanded + ? DsfrColors.blueFrance925 + : Colors.transparent, + child: ConstrainedBox( + constraints: const BoxConstraints(minHeight: 48), + child: Padding( + padding: + const EdgeInsets.symmetric(vertical: DsfrSpacings.s3v), + child: Row( + children: [ + Expanded( + child: widget.item.headerBuilder(widget.isExpanded), ), - const SizedBox(width: DsfrSpacings.s2w), - ], + if (widget.item.isEnable) + AnimatedRotation( + turns: widget.isExpanded ? -0.5 : 0, + duration: Durations.short4, + child: const Icon( + DsfrIcons.systemArrowDownSLine, + size: DsfrSpacings.s2w, + color: DsfrColors.blueFranceSun113, + ), + ), + const SizedBox(width: DsfrSpacings.s2w), + ], + ), ), ), ), @@ -118,9 +136,9 @@ class _DsfrAccordion extends StatelessWidget { top: DsfrSpacings.s2w, bottom: DsfrSpacings.s3w, ), - child: item.body, + child: widget.item.body, ), - crossFadeState: isExpanded + crossFadeState: widget.isExpanded ? CrossFadeState.showSecond : CrossFadeState.showFirst, duration: Durations.short4, diff --git a/packages/dsfr.dart/lib/src/composants/buttons/button.dart b/packages/dsfr.dart/lib/src/composants/buttons/button.dart index 7c50dede..979ca669 100755 --- a/packages/dsfr.dart/lib/src/composants/buttons/button.dart +++ b/packages/dsfr.dart/lib/src/composants/buttons/button.dart @@ -62,9 +62,9 @@ class DsfrButton extends StatelessWidget { return DsfrRawButton( variant: variant, - foregroundColor: foregroundColor, size: size, - onTap: onPressed, + foregroundColor: foregroundColor, + onPressed: onPressed, child: Center(child: child), ); } diff --git a/packages/dsfr.dart/lib/src/composants/buttons/raw_button.dart b/packages/dsfr.dart/lib/src/composants/buttons/raw_button.dart index be1ba23d..fe75589a 100644 --- a/packages/dsfr.dart/lib/src/composants/buttons/raw_button.dart +++ b/packages/dsfr.dart/lib/src/composants/buttons/raw_button.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:dsfr/src/atoms/focus_widget.dart'; import 'package:dsfr/src/composants/buttons/button.dart'; import 'package:dsfr/src/composants/buttons/button_background_color.dart'; import 'package:dsfr/src/composants/buttons/button_border.dart'; @@ -12,12 +13,12 @@ import 'package:flutter/material.dart'; class DsfrRawButton extends StatefulWidget { const DsfrRawButton({ super.key, - required this.child, required this.variant, - this.foregroundColor, required this.size, + this.foregroundColor, this.borderRadius, - this.onTap, + this.onPressed, + required this.child, }); final Widget child; @@ -25,7 +26,7 @@ class DsfrRawButton extends StatefulWidget { final Color? foregroundColor; final DsfrButtonSize size; final BorderRadius? borderRadius; - final VoidCallback? onTap; + final VoidCallback? onPressed; @override State createState() => _DsfrRawButtonState(); @@ -33,8 +34,6 @@ class DsfrRawButton extends StatefulWidget { class _DsfrRawButtonState extends State with MaterialStateMixin { - static const _focusBorderWidth = 2.0; - static const _focusPadding = EdgeInsets.all(4); late final double _minHeight; late final EdgeInsetsGeometry _padding; late final TextStyle _textStyle; @@ -66,7 +65,7 @@ class _DsfrRawButtonState extends State _padding = _getPadding(widget.size); _textStyle = _getTextStyle(widget.size); _minHeight = _getMinHeight(widget.size); - setMaterialState(WidgetState.disabled, widget.onTap == null); + setMaterialState(WidgetState.disabled, widget.onPressed == null); } EdgeInsetsGeometry _getPadding(final DsfrButtonSize size) => switch (size) { @@ -93,7 +92,7 @@ class _DsfrRawButtonState extends State @override void didUpdateWidget(final DsfrRawButton oldWidget) { super.didUpdateWidget(oldWidget); - setMaterialState(WidgetState.disabled, widget.onTap == null); + setMaterialState(WidgetState.disabled, widget.onPressed == null); } @override @@ -106,46 +105,49 @@ class _DsfrRawButtonState extends State @override Widget build(final context) { final textColor = _foregroundColor.resolve(materialStates); - final button = Semantics( - enabled: widget.onTap != null, - button: true, - child: ConstrainedBox( - constraints: BoxConstraints(minHeight: _minHeight), - child: DecoratedBox( - decoration: BoxDecoration( - color: _backgroundColor.resolve(materialStates), - border: _border.resolve(materialStates), - borderRadius: widget.borderRadius, - ), - child: Material( - color: Colors.transparent, - child: InkWell( - onTap: widget.onTap == null - ? null - : () { - if (_timer?.isActive ?? false) { - return; - } - widget.onTap!(); - _timer = Timer( - const Duration(milliseconds: 500), - () {}, - ); - }, - onHighlightChanged: updateMaterialState(WidgetState.pressed), - onHover: updateMaterialState(WidgetState.hovered), - canRequestFocus: widget.onTap != null, - onFocusChange: updateMaterialState(WidgetState.focused), - child: Padding( - padding: _padding, - child: Center( - widthFactor: 1, - heightFactor: 1, - child: IconTheme( - data: IconThemeData(color: textColor), - child: DefaultTextStyle( - style: _textStyle.copyWith(color: textColor), - child: widget.child, + + return DsfrFocusWidget( + isFocused: isFocused, + child: Semantics( + enabled: widget.onPressed != null, + button: true, + child: ConstrainedBox( + constraints: BoxConstraints(minHeight: _minHeight), + child: DecoratedBox( + decoration: BoxDecoration( + color: _backgroundColor.resolve(materialStates), + border: _border.resolve(materialStates), + borderRadius: widget.borderRadius, + ), + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: widget.onPressed == null + ? null + : () { + if (_timer?.isActive ?? false) { + return; + } + widget.onPressed!(); + _timer = + Timer(const Duration(milliseconds: 500), () {}); + }, + onHighlightChanged: updateMaterialState(WidgetState.pressed), + onHover: updateMaterialState(WidgetState.hovered), + focusColor: Colors.transparent, + canRequestFocus: widget.onPressed != null, + onFocusChange: updateMaterialState(WidgetState.focused), + child: Padding( + padding: _padding, + child: Center( + widthFactor: 1, + heightFactor: 1, + child: IconTheme( + data: IconThemeData(color: textColor), + child: DefaultTextStyle( + style: _textStyle.copyWith(color: textColor), + child: widget.child, + ), ), ), ), @@ -155,19 +157,5 @@ class _DsfrRawButtonState extends State ), ), ); - - return isFocused - ? DecoratedBox( - decoration: const BoxDecoration( - border: Border.fromBorderSide( - BorderSide( - color: DsfrColors.focus525, - width: _focusBorderWidth, - ), - ), - ), - child: Padding(padding: _focusPadding, child: button), - ) - : button; } } diff --git a/packages/dsfr.dart/lib/src/composants/checkbox.dart b/packages/dsfr.dart/lib/src/composants/checkbox.dart index db03ebb5..d34b866c 100644 --- a/packages/dsfr.dart/lib/src/composants/checkbox.dart +++ b/packages/dsfr.dart/lib/src/composants/checkbox.dart @@ -1,5 +1,5 @@ +import 'package:dsfr/src/atoms/focus_widget.dart'; import 'package:dsfr/src/composants/checkbox_icon.dart'; -import 'package:dsfr/src/fondamentaux/colors.g.dart'; import 'package:dsfr/src/fondamentaux/fonts.dart'; import 'package:dsfr/src/fondamentaux/spacing.g.dart'; import 'package:flutter/material.dart'; @@ -81,28 +81,11 @@ class DsfrCheckbox extends StatelessWidget { builder: (final context) { final hasFocus = Focus.of(context).hasFocus; - return DecoratedBox( - decoration: BoxDecoration( - border: hasFocus - ? Border.fromBorderSide( - BorderSide( - color: hasFocus - ? DsfrColors.focus525 - : Colors.transparent, - width: 2, - strokeAlign: 1, - ), - ) - : null, - borderRadius: hasFocus - ? const BorderRadius.all(Radius.circular(2 + 2)) - : null, - ), - child: Padding( - padding: const EdgeInsets.all(2), - child: - DsfrCheckboxIcon(value: value, padding: padding), - ), + return DsfrFocusWidget( + isFocused: hasFocus, + borderRadius: + const BorderRadius.all(Radius.circular(4)), + child: DsfrCheckboxIcon(value: value, padding: padding), ); }, ), diff --git a/packages/dsfr.dart/lib/src/composants/input.dart b/packages/dsfr.dart/lib/src/composants/input.dart index 7def5292..e9ff7823 100644 --- a/packages/dsfr.dart/lib/src/composants/input.dart +++ b/packages/dsfr.dart/lib/src/composants/input.dart @@ -12,7 +12,7 @@ class DsfrInput extends StatefulWidget { const DsfrInput({ super.key, required this.label, - this.hint, + this.hintText, this.suffixText, this.controller, this.initialValue, @@ -37,7 +37,7 @@ class DsfrInput extends StatefulWidget { }); final String label; - final String? hint; + final String? hintText; final String? suffixText; final TextEditingController? controller; final String? initialValue; @@ -107,13 +107,14 @@ class _DsfrInputState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ label, - if (widget.hint != null) ...[ + if (widget.hintText != null) ...[ const SizedBox(height: DsfrSpacings.s1v), Text( - widget.hint!, + widget.hintText!, style: widget.hintStyle.copyWith(color: widget.hintColor), ), ], + const SizedBox(height: DsfrSpacings.s1w), FocusTraversalOrder( order: const NumericFocusOrder(1), child: DsfrInputHeadless( diff --git a/packages/dsfr.dart/lib/src/composants/input_headless.dart b/packages/dsfr.dart/lib/src/composants/input_headless.dart index 7cfb6edc..10b94a11 100644 --- a/packages/dsfr.dart/lib/src/composants/input_headless.dart +++ b/packages/dsfr.dart/lib/src/composants/input_headless.dart @@ -39,9 +39,9 @@ class DsfrInputHeadless extends StatefulWidget { this.autofillHints, }); - final String? suffixText; - final TextEditingController? controller; final String? initialValue; + final TextEditingController? controller; + final String? suffixText; final ValueChanged onChanged; final ValueChanged? onFieldSubmitted; final FormFieldValidator? validator; @@ -101,7 +101,7 @@ class _DsfrInputHeadlessState extends State { borderSide: BorderSide( color: widget.inputBorderColor, width: widget.inputBorderWidth, - strokeAlign: 1, + strokeAlign: BorderSide.strokeAlignOutside, ), borderRadius: BorderRadius.vertical(top: Radius.circular(widget.radius)), ); @@ -112,7 +112,7 @@ class _DsfrInputHeadlessState extends State { BorderSide( color: _isFocused ? widget.focusColor : Colors.transparent, width: widget.focusThickness, - strokeAlign: 1, + strokeAlign: BorderSide.strokeAlignOutside, ), ), borderRadius: BorderRadius.vertical( diff --git a/packages/dsfr.dart/lib/src/composants/links.dart b/packages/dsfr.dart/lib/src/composants/link.dart similarity index 79% rename from packages/dsfr.dart/lib/src/composants/links.dart rename to packages/dsfr.dart/lib/src/composants/link.dart index ea95cae0..efbb5d07 100644 --- a/packages/dsfr.dart/lib/src/composants/links.dart +++ b/packages/dsfr.dart/lib/src/composants/link.dart @@ -1,3 +1,4 @@ +import 'package:dsfr/src/atoms/focus_widget.dart'; import 'package:dsfr/src/composants/link_icon_position.dart'; import 'package:dsfr/src/fondamentaux/colors.g.dart'; import 'package:dsfr/src/fondamentaux/fonts.dart'; @@ -10,8 +11,6 @@ class DsfrLink extends StatefulWidget { required this.label, required this.textStyle, required this.underlineThickness, - required this.focusBorderWidth, - required this.focusPadding, required this.iconSize, required this.iconPosition, this.icon, @@ -29,8 +28,6 @@ class DsfrLink extends StatefulWidget { label: label, textStyle: const DsfrTextStyle.bodySm(), underlineThickness: 1.75, - focusBorderWidth: 2, - focusPadding: const EdgeInsets.all(4), iconSize: 16, iconPosition: iconPosition, icon: icon, @@ -48,8 +45,6 @@ class DsfrLink extends StatefulWidget { label: label, textStyle: const DsfrTextStyle.bodyMd(), underlineThickness: 2, - focusBorderWidth: 2, - focusPadding: const EdgeInsets.all(4), iconSize: 16, iconPosition: iconPosition, icon: icon, @@ -63,8 +58,6 @@ class DsfrLink extends StatefulWidget { final VoidCallback? onTap; final TextStyle textStyle; final double underlineThickness; - final double focusBorderWidth; - final EdgeInsetsGeometry focusPadding; @override State createState() => _DsfrLinkState(); @@ -115,7 +108,7 @@ class _DsfrLinkState extends State with MaterialStateMixin { TextSpan(text: widget.label), ]; - final link = Semantics( + return Semantics( enabled: widget.onTap != null, link: true, child: Material( @@ -124,8 +117,8 @@ class _DsfrLinkState extends State with MaterialStateMixin { onTap: widget.onTap, onHighlightChanged: updateMaterialState(WidgetState.pressed), onHover: updateMaterialState(WidgetState.hovered), + focusColor: Colors.transparent, highlightColor: const Color(0x21000000), - splashFactory: NoSplash.splashFactory, canRequestFocus: widget.onTap != null, onFocusChange: updateMaterialState(WidgetState.focused), child: DecoratedBox( @@ -141,31 +134,20 @@ class _DsfrLinkState extends State with MaterialStateMixin { ) : null, ), - child: Text.rich( - TextSpan( - children: widget.iconPosition == DsfrLinkIconPosition.start - ? list - : list.reversed.toList(), + child: DsfrFocusWidget( + isFocused: isFocused, + child: Text.rich( + TextSpan( + children: widget.iconPosition == DsfrLinkIconPosition.start + ? list + : list.reversed.toList(), + ), + style: widget.textStyle.copyWith(color: resolveForegroundColor), ), - style: widget.textStyle.copyWith(color: resolveForegroundColor), ), ), ), ), ); - - return isFocused - ? DecoratedBox( - decoration: BoxDecoration( - border: Border.fromBorderSide( - BorderSide( - color: DsfrColors.focus525, - width: widget.focusBorderWidth, - ), - ), - ), - child: Padding(padding: widget.focusPadding, child: link), - ) - : link; } } diff --git a/packages/dsfr.dart/lib/src/composants/radios/radio.dart b/packages/dsfr.dart/lib/src/composants/radios/radio.dart index fc108283..6fe9fcfc 100644 --- a/packages/dsfr.dart/lib/src/composants/radios/radio.dart +++ b/packages/dsfr.dart/lib/src/composants/radios/radio.dart @@ -1,10 +1,11 @@ +import 'package:dsfr/src/atoms/focus_widget.dart'; import 'package:dsfr/src/composants/radios/radio_icon.dart'; import 'package:dsfr/src/fondamentaux/colors.g.dart'; import 'package:dsfr/src/fondamentaux/fonts.dart'; import 'package:dsfr/src/fondamentaux/spacing.g.dart'; import 'package:flutter/material.dart'; -class DsfrRadioButton extends StatelessWidget { +class DsfrRadioButton extends StatefulWidget { const DsfrRadioButton({ super.key, required this.title, @@ -21,35 +22,56 @@ class DsfrRadioButton extends StatelessWidget { final Color? backgroundColor; @override - Widget build(final context) => GestureDetector( - onTap: onChanged == null ? null : () => onChanged!(value), - behavior: HitTestBehavior.opaque, - child: DecoratedBox( - decoration: BoxDecoration( - color: backgroundColor, - border: Border.fromBorderSide( - BorderSide( - color: groupValue == value - ? DsfrColors.blueFranceSun113 - : DsfrColors.grey900, - ), - ), - ), - child: Padding( - padding: const EdgeInsets.all(DsfrSpacings.s2w), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - RadioIcon( - key: ValueKey(title), - value: value, - groupValue: groupValue, + State> createState() => _DsfrRadioButtonState(); +} + +class _DsfrRadioButtonState extends State> + with MaterialStateMixin> { + @override + Widget build(final context) => DsfrFocusWidget( + isFocused: isFocused, + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: widget.onChanged == null + ? null + : () => widget.onChanged!(widget.value), + onHighlightChanged: updateMaterialState(WidgetState.pressed), + onHover: updateMaterialState(WidgetState.hovered), + focusColor: Colors.transparent, + canRequestFocus: widget.onChanged != null, + onFocusChange: updateMaterialState(WidgetState.focused), + child: DecoratedBox( + decoration: BoxDecoration( + color: widget.backgroundColor, + border: Border.fromBorderSide( + BorderSide( + color: widget.groupValue == widget.value + ? DsfrColors.blueFranceSun113 + : DsfrColors.grey900, + ), ), - const SizedBox(width: DsfrSpacings.s1w), - Flexible( - child: Text(title, style: const DsfrTextStyle.bodyMd()), + ), + child: Padding( + padding: const EdgeInsets.all(DsfrSpacings.s2w), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + RadioIcon( + key: ValueKey(widget.title), + value: widget.value, + groupValue: widget.groupValue, + ), + const SizedBox(width: DsfrSpacings.s1w), + Flexible( + child: Text( + widget.title, + style: const DsfrTextStyle.bodyMd(), + ), + ), + ], ), - ], + ), ), ), ), diff --git a/packages/dsfr.dart/lib/src/composants/toggle.dart b/packages/dsfr.dart/lib/src/composants/toggle_switch.dart similarity index 59% rename from packages/dsfr.dart/lib/src/composants/toggle.dart rename to packages/dsfr.dart/lib/src/composants/toggle_switch.dart index 5654fc51..a0eddc39 100644 --- a/packages/dsfr.dart/lib/src/composants/toggle.dart +++ b/packages/dsfr.dart/lib/src/composants/toggle_switch.dart @@ -1,11 +1,12 @@ +import 'package:dsfr/src/atoms/focus_widget.dart'; import 'package:dsfr/src/fondamentaux/colors.g.dart'; import 'package:dsfr/src/fondamentaux/fonts.dart'; import 'package:dsfr/src/fondamentaux/icons.g.dart'; import 'package:dsfr/src/fondamentaux/spacing.g.dart'; import 'package:flutter/material.dart'; -class DsfrToggle extends StatelessWidget { - const DsfrToggle({ +class DsfrToggleSwitch extends StatefulWidget { + const DsfrToggleSwitch({ super.key, required this.label, required this.value, @@ -17,15 +18,36 @@ class DsfrToggle extends StatelessWidget { final String label; @override - Widget build(final context) => GestureDetector( - onTap: () => onChanged(!value), - behavior: HitTestBehavior.opaque, - child: Row( - children: [ - _Switch(value: value), - const SizedBox(width: DsfrSpacings.s2w), - Flexible(child: Text(label, style: const DsfrTextStyle.bodyMd())), - ], + State createState() => _DsfrToggleSwitchState(); +} + +class _DsfrToggleSwitchState extends State + with MaterialStateMixin { + @override + Widget build(final context) => Semantics( + toggled: widget.value, + child: InkWell( + onTap: () => widget.onChanged(!widget.value), + onHighlightChanged: updateMaterialState(WidgetState.pressed), + onHover: updateMaterialState(WidgetState.hovered), + focusColor: Colors.transparent, + highlightColor: Colors.transparent, + splashFactory: NoSplash.splashFactory, + excludeFromSemantics: true, + onFocusChange: updateMaterialState(WidgetState.focused), + child: Row( + children: [ + DsfrFocusWidget( + isFocused: isFocused, + borderRadius: const BorderRadius.all(Radius.circular(24 + 2)), + child: _Switch(value: widget.value), + ), + const SizedBox(width: DsfrSpacings.s2w), + Flexible( + child: Text(widget.label, style: const DsfrTextStyle.bodyMd()), + ), + ], + ), ), ); } @@ -42,7 +64,7 @@ class _Switch extends StatelessWidget { const offset = width - height; const primary = DsfrColors.blueFranceSun113; const border = Border.fromBorderSide(BorderSide(color: primary)); - const borderRadius = BorderRadius.all(Radius.circular(width)); + const borderRadius = BorderRadius.all(Radius.circular(height)); return SizedBox( width: width, diff --git a/packages/dsfr.dart/test/input_test.dart b/packages/dsfr.dart/test/input_test.dart index 6b2cfa96..e97f8312 100644 --- a/packages/dsfr.dart/test/input_test.dart +++ b/packages/dsfr.dart/test/input_test.dart @@ -15,17 +15,17 @@ void main() { }); testWidgets('Voir la description', (final tester) async { - const hint = 'Indice'; + const hintText = 'Indice'; await tester.pumpWidget( App( child: DsfrInput( label: 'Label', - hint: hint, + hintText: hintText, onChanged: (final value) {}, ), ), ); - expect(find.text(hint), findsOneWidget); + expect(find.text(hintText), findsOneWidget); }); testWidgets('Voir la valeur par défaut', (final tester) async { @@ -35,7 +35,7 @@ void main() { App( child: DsfrInput( label: 'Label', - hint: 'Indice', + hintText: 'Indice', controller: controller, onChanged: (final value) {}, ), diff --git a/packages/dsfr.dart/test/toggle_test.dart b/packages/dsfr.dart/test/toggle_test.dart index 35213852..b70c0aa7 100755 --- a/packages/dsfr.dart/test/toggle_test.dart +++ b/packages/dsfr.dart/test/toggle_test.dart @@ -11,7 +11,7 @@ void main() { const label = 'Label'; await tester.pumpWidget( App( - child: DsfrToggle( + child: DsfrToggleSwitch( label: label, value: false, onChanged: (final value) {}, @@ -26,7 +26,7 @@ void main() { final completer = Completer(); await tester.pumpWidget( App( - child: DsfrToggle( + child: DsfrToggleSwitch( label: label, value: false, onChanged: completer.complete,