diff --git a/app/CHANGELOG.md b/app/CHANGELOG.md index 9ce6c5558..f950f685d 100644 --- a/app/CHANGELOG.md +++ b/app/CHANGELOG.md @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://github.com/Orange-OpenSource/ouds-flutter/compare/1.3.1...develop) ### Added ### Changed +- [DemoApp][Library] Update `ToolBar Top`, with Badge in Trailing Actions ([#642](https://github.com/Orange-OpenSource/ouds-flutter/issues/642)) - [DemoApp][Library] update tokens 2.5.0 ([#778](https://github.com/Orange-OpenSource/ouds-flutter/issues/778)) - [DemoApp][Library] update tokens 2.4.0 ([#726](https://github.com/Orange-OpenSource/ouds-flutter/issues/726)) diff --git a/app/lib/ui/components/top_bar/toolbar_top_customization_utils.dart b/app/lib/ui/components/top_bar/toolbar_top_customization_utils.dart index 2d0ddf4ca..59f829655 100644 --- a/app/lib/ui/components/top_bar/toolbar_top_customization_utils.dart +++ b/app/lib/ui/components/top_bar/toolbar_top_customization_utils.dart @@ -1,4 +1,3 @@ - // // Software Name: OUDS Flutter // SPDX-FileCopyrightText: Copyright (c) Orange SA @@ -15,11 +14,11 @@ import 'package:flutter/cupertino.dart'; import 'package:ouds_core/components/top_bar/ouds_top_bar_action_config.dart'; import 'package:ouds_flutter_demo/l10n/app_localizations.dart'; import 'package:ouds_flutter_demo/ui/components/top_bar/top_bar_customization.dart'; +import 'package:ouds_flutter_demo/ui/components/top_bar/top_bar_customization_utils.dart'; import 'package:ouds_flutter_demo/ui/components/top_bar/top_bar_enum.dart'; import 'package:ouds_flutter_demo/ui/theme/theme_controller.dart'; import 'package:ouds_flutter_demo/ui/utilities/app_assets.dart'; - /// Utility class to map tag customization options to corresponding OudsToolbarTop attributes. /// /// This class provides static methods to convert customization enums into the appropriate @@ -28,9 +27,9 @@ import 'package:ouds_flutter_demo/ui/utilities/app_assets.dart'; /// user-selected options into code that is used for TopAppBar customization and rendering. class ToolbarTopCustomizationUtils { - /// Minimum number of actions allowed for the Cupertino style of OudsTopBar component. static const int minActionCount = 1; + /// Maximum number of actions allowed for the Cupertino style of OudsTopBar component. static const int maxActionCount = 3; @@ -38,38 +37,41 @@ class ToolbarTopCustomizationUtils { /// to [maxActionCount] (inclusive). static final cupertinoActionCountOptions = List.generate( maxActionCount - (minActionCount - 1), - (index) => minActionCount + index, + (index) => minActionCount + index, ); - static List getLimitedActionsCount(BuildContext context){ + static List getLimitedActionsCount(BuildContext context) { int maxActionsIndex = maxActionCount + 1; return cupertinoActionCountOptions.take(maxActionsIndex).toList(); } static List? getDisabledLeadingActionCountOptions( - TopBarCustomizationState? customizationState) { + TopBarCustomizationState? customizationState, + ) { //disable all options if selected type is none if (customizationState?.selectedLeadingActionType == ToolbarTopActionTypeEnum.none) { - return List.generate(maxActionCount+1, (i) => i); + return List.generate(maxActionCount + 1, (i) => i); } //if selected type is different to back no option to disable if (customizationState?.selectedLeadingActionType != - ToolbarTopActionTypeEnum.back ) { + ToolbarTopActionTypeEnum.back) { return null; } //disable all options > 1 if selected type is back - return List.generate(maxActionCount+1, (i) => i) - .where((value) => value > 1) - .toList(); + return List.generate( + maxActionCount + 1, + (i) => i, + ).where((value) => value > 1).toList(); } static List? getDisabledTrailingActionCountOptions( - TopBarCustomizationState? customizationState) { + TopBarCustomizationState? customizationState, + ) { //disable all options if selected type is none if (customizationState?.selectedTrailingActionType == ToolbarTopActionTypeEnum.none) { - return List.generate(maxActionCount+1, (i) => i); + return List.generate(maxActionCount + 1, (i) => i); } return null; } @@ -86,26 +88,39 @@ class ToolbarTopCustomizationUtils { ThemeController? themeController, required TopBarCustomizationState customizationState, required bool isLeadingActions, - }){ + }) { final safeActionCount = isLeadingActions - ? customizationState.selectedLeadingActionCount.clamp(minActionCount, maxActionCount) - : customizationState.selectedTrailingActionCount.clamp(minActionCount, maxActionCount); - - return List.generate( - safeActionCount, - (index) { - return _getCupertinoActionConfig(context, customizationState,themeController,isLeadingActions); - }, - ); + ? customizationState.selectedLeadingActionCount.clamp( + minActionCount, + maxActionCount, + ) + : customizationState.selectedTrailingActionCount.clamp( + minActionCount, + maxActionCount, + ); + + return List.generate(safeActionCount, (index) { + final isLastItem = index == safeActionCount - 1; + + return _getCupertinoActionConfig( + context, + customizationState, + themeController, + isLeadingActions, + isLastItem, + safeActionCount, + ); + }); } static OudsTopBarActionConfig _getCupertinoActionConfig( - BuildContext context, - TopBarCustomizationState? customizationState, - ThemeController? themeController, - bool isLeadingAction - ){ - + BuildContext context, + TopBarCustomizationState? customizationState, + ThemeController? themeController, + bool isLeadingAction, + bool isLastItem, + int actionCount, + ) { // Handle null state gracefully to avoid runtime errors. if (customizationState == null || themeController == null) { return OudsTopBarActionConfig.none(); @@ -120,19 +135,25 @@ class ToolbarTopCustomizationUtils { switch (actionType) { case ToolbarTopActionTypeEnum.text: return OudsTopBarActionConfig.text( - actionLabel: context.l10n.app_components_common_label_label, // Provide only the relevant parameter. + actionLabel: context + .l10n + .app_components_common_label_label, // Provide only the relevant parameter. onActionPressed: customizationState.hasEnabled == true ? () {} : null, ); case ToolbarTopActionTypeEnum.icon: + final isBadgeEligible = actionCount == 1 || isLastItem; return OudsTopBarActionConfig.icon( // Provide a relevant icon. icon: AppAssets.icons.assistanceTipsAndTricks(themeController), contentDescription: context.l10n.app_components_common_action_a11y, onActionPressed: customizationState.hasEnabled == true ? () {} : null, + badge: !isLeadingAction && isBadgeEligible + ? TopBarCustomizationUtils.getActionBadge(customizationState) + : null, ); case ToolbarTopActionTypeEnum.back: - // The .back() factory handles its own icon and behavior. + // The .back() factory handles its own icon and behavior. return OudsTopBarActionConfig.back( previousPageTitle: customizationState.previousPageTitleText, onActionPressed: () { @@ -146,4 +167,4 @@ class ToolbarTopCustomizationUtils { return OudsTopBarActionConfig.none(); } } -} \ No newline at end of file +} diff --git a/app/lib/ui/components/top_bar/top_appbar_customization_utils.dart b/app/lib/ui/components/top_bar/top_appbar_customization_utils.dart index ac7aa9acb..ac2de0ccf 100644 --- a/app/lib/ui/components/top_bar/top_appbar_customization_utils.dart +++ b/app/lib/ui/components/top_bar/top_appbar_customization_utils.dart @@ -1,4 +1,3 @@ - // // Software Name: OUDS Flutter // SPDX-FileCopyrightText: Copyright (c) Orange SA @@ -12,11 +11,12 @@ // import 'package:flutter/material.dart'; -import 'package:ouds_core/components/top_bar/ouds_top_bar.dart'; import 'package:ouds_core/components/top_bar/ouds_top_appbar.dart'; +import 'package:ouds_core/components/top_bar/ouds_top_bar.dart'; import 'package:ouds_core/components/top_bar/ouds_top_bar_action_config.dart'; import 'package:ouds_flutter_demo/l10n/app_localizations.dart'; import 'package:ouds_flutter_demo/ui/components/top_bar/top_bar_customization.dart'; +import 'package:ouds_flutter_demo/ui/components/top_bar/top_bar_customization_utils.dart'; import 'package:ouds_flutter_demo/ui/components/top_bar/top_bar_enum.dart'; import 'package:ouds_flutter_demo/ui/theme/theme_controller.dart'; import 'package:ouds_flutter_demo/ui/utilities/app_assets.dart'; @@ -29,11 +29,12 @@ import 'package:ouds_flutter_demo/ui/utilities/app_assets.dart'; /// user-selected options into code that is used for top bar customization and rendering. class TopAppBarCustomizationUtils { - /// Minimum number of actions allowed for the Material style TopBar component. static const int minActionCount = 0; + /// Maximum number of actions allowed for the Material style TopBar component. static const int maxActionCount = 3; + /// Maximum title line count for Material style TopBar in medium and large sizes. static const int maxLinesCount = 4; @@ -43,7 +44,6 @@ class TopAppBarCustomizationUtils { /// is displayed with a single line of content. static const int minHeightMediumSize = 112; - /// The default height when the medium variant is displayed with two lines of content. static const int minHeightMediumSizeTwoLines = 128; @@ -56,23 +56,21 @@ class TopAppBarCustomizationUtils { /// to [maxActionCount] (inclusive). static final actionCountOptions = List.generate( maxActionCount - (minActionCount - 1), - (index) => minActionCount + index, + (index) => minActionCount + index, ); /// Generates a list of consecutive item count values from one to [maxLinesCount] . static final maxLinesOptions = List.generate( maxLinesCount, - (index) => index + 1, + (index) => index + 1, ); - - /// Determines the icon to display based on the selected layout. static OudsTopBarActionConfig getNavigationIcon( - BuildContext context, - ThemeController themeController, - NavigationIconTypeEnum icon) { - + BuildContext context, + ThemeController themeController, + NavigationIconTypeEnum icon, + ) { // A switch statement directly maps the enum to the correct factory constructor. switch (icon) { case NavigationIconTypeEnum.back: @@ -101,7 +99,7 @@ class TopAppBarCustomizationUtils { }, ); case NavigationIconTypeEnum.custom: - // The .custom() factory is used for developer-defined icons. + // The .custom() factory is used for developer-defined icons. return OudsTopBarActionConfig.custom( // The asset path is now passed directly to the factory. icon: AppAssets.icons.assistanceTipsAndTricks(themeController), @@ -109,7 +107,7 @@ class TopAppBarCustomizationUtils { onActionPressed: () {}, ); case NavigationIconTypeEnum.none: - // The .none() factory creates a configuration for no action. + // The .none() factory creates a configuration for no action. return OudsTopBarActionConfig.none(); } } @@ -119,19 +117,22 @@ class TopAppBarCustomizationUtils { required TopBarCustomizationState customizationState, required ThemeController themeController, int actionCount = minActionCount, - }){ - final safeActionCount = actionCount.clamp(minActionCount,maxActionCount); + }) { + final safeActionCount = actionCount.clamp(minActionCount, maxActionCount); List actions = []; - actions = List.generate( - safeActionCount, - (index) { - final isLastItem = index == safeActionCount - 1; - return _getActionConfig(context,themeController, customizationState, isLastItem); - }, - ); + actions = List.generate(safeActionCount, (index) { + final isLastItem = index == safeActionCount - 1; + return _getActionConfig( + context, + themeController, + customizationState, + isLastItem, + ); + }); actions.add(_getAvatarActionConfig(context, customizationState)); return actions; } + /// Maps the action avatar type enum to `OudsTopAppBarActionAvatar`. static OudsTopAppBarActionAvatar getActionAvatar(Object actionAvatar) { return actionAvatar == ActionAvatarEnum.monogram @@ -140,27 +141,20 @@ class TopAppBarCustomizationUtils { } /// Retrieves the char to display based on the current customization state. - static String? getMonogramText( - TopBarCustomizationState customizationState) { + static String? getMonogramText(TopBarCustomizationState customizationState) { return customizationState.selectedActionAvatar == ActionAvatarEnum.monogram ? customizationState.actionAvatarMonogramText : "A"; } - /// Retrieves the count to display based on the current customization state. - static OudsTopAppBarActionBadge? getActionBadge(TopBarCustomizationState customizationState) { - return customizationState.selectedIconBadge == ActionIconBadgeEnum.count - ? OudsTopAppBarActionBadge(count: "1", contentDescription: 'one unread notification') - : customizationState.selectedIconBadge == ActionIconBadgeEnum.dot - ? OudsTopAppBarActionBadge(contentDescription: 'Notification') - : null; - } - /// Calculates the expanded header height based on the customization state. - static double getExpandedHeaderValue(TopBarCustomizationState customizationState) { + static double getExpandedHeaderValue( + TopBarCustomizationState customizationState, + ) { // Determine the default header height based on the selected size - double headerValue = customizationState.selectedSize == TopBarSizeEnum.medium - ? OudsTopAppBar.getPreferredSize(size:OudsTopBarSize.medium).height + double headerValue = + customizationState.selectedSize == TopBarSizeEnum.medium + ? OudsTopAppBar.getPreferredSize(size: OudsTopBarSize.medium).height : customizationState.selectedSize == TopBarSizeEnum.large ? OudsTopAppBar.getPreferredSize(size: OudsTopBarSize.large).height : OudsTopAppBar.getPreferredSize().height; @@ -168,23 +162,26 @@ class TopAppBarCustomizationUtils { // Initialize cleanedInput with a default value String cleanedInput = minHeightMediumSize.toString(); // If the expandedHeightText is not empty, clean it by removing non-numeric characters - if(customizationState.expandedHeightText.isNotEmpty){ - cleanedInput = customizationState.expandedHeightText.replaceAll(RegExp(r'[^0-9.]'), ''); + if (customizationState.expandedHeightText.isNotEmpty) { + cleanedInput = customizationState.expandedHeightText.replaceAll( + RegExp(r'[^0-9.]'), + '', + ); } // If the selected size is small, return the default header height - if(customizationState.selectedSize == TopBarSizeEnum.small){ + if (customizationState.selectedSize == TopBarSizeEnum.small) { return headerValue; } // If size is medium and expandedHeightText is provided and greater than default, return it - else if(customizationState.selectedSize == TopBarSizeEnum.medium - && customizationState.expandedHeightText.isNotEmpty - && double.parse(cleanedInput) > headerValue){ + else if (customizationState.selectedSize == TopBarSizeEnum.medium && + customizationState.expandedHeightText.isNotEmpty && + double.parse(cleanedInput) > headerValue) { return double.parse(cleanedInput); } // If size is large and expandedHeightText is provided and greater than default, return it - else if(customizationState.selectedSize == TopBarSizeEnum.large - && customizationState.expandedHeightText.isNotEmpty - && double.parse(cleanedInput) > headerValue){ + else if (customizationState.selectedSize == TopBarSizeEnum.large && + customizationState.expandedHeightText.isNotEmpty && + double.parse(cleanedInput) > headerValue) { return double.parse(cleanedInput); } // Otherwise, return the default header height @@ -194,18 +191,19 @@ class TopAppBarCustomizationUtils { } /// Retrieves the title line count of TopAppBar. - static int getTitleLineCountValue(TopBarCustomizationState customizationState) { + static int getTitleLineCountValue( + TopBarCustomizationState customizationState, + ) { return customizationState.maxLinesSelected; } /// Retrieves the configuration for a simple icon action . static OudsTopBarActionConfig _getActionConfig( - BuildContext context, - ThemeController themeController, - TopBarCustomizationState? customizationState, - bool isLastItem - ){ - + BuildContext context, + ThemeController themeController, + TopBarCustomizationState? customizationState, + bool isLastItem, + ) { // Use the .icon() factory for clarity and type-safety. return OudsTopBarActionConfig.icon( // The factory requires an icon. Provide a default for the demo. @@ -214,7 +212,7 @@ class TopAppBarCustomizationUtils { onActionPressed: () {}, // The badge logic remains the same. badge: (customizationState?.actionSelected == 1 || isLastItem) - ? TopAppBarCustomizationUtils.getActionBadge(customizationState!) + ? TopBarCustomizationUtils.getActionBadge(customizationState!) : null, ); } @@ -222,18 +220,21 @@ class TopAppBarCustomizationUtils { /// Retrieves an avatar action configuration for the TopAppBar. /// The content of the avatar can either be an image or a single letter monogram. static OudsTopBarActionConfig _getAvatarActionConfig( - BuildContext context, - TopBarCustomizationState customizationState){ - + BuildContext context, + TopBarCustomizationState customizationState, + ) { return OudsTopBarActionConfig.avatar( - avatarConfig: OudsTopAppBarAvatarConfig( - image: customizationState.selectedActionAvatar == ActionAvatarEnum.image - ? AppAssets.images.ilTopAppBarAvatar : null, - monogram : customizationState.selectedActionAvatar == ActionAvatarEnum.monogram - ? customizationState.actionAvatarMonogramText : null, - ), - contentDescription: context.l10n.app_components_common_action_a11y, - onActionPressed: () {} + avatarConfig: OudsTopAppBarAvatarConfig( + image: customizationState.selectedActionAvatar == ActionAvatarEnum.image + ? AppAssets.images.ilTopAppBarAvatar + : null, + monogram: + customizationState.selectedActionAvatar == ActionAvatarEnum.monogram + ? customizationState.actionAvatarMonogramText + : null, + ), + contentDescription: context.l10n.app_components_common_action_a11y, + onActionPressed: () {}, ); } @@ -245,19 +246,16 @@ class TopAppBarCustomizationUtils { /// For any other size, it returns an empty string as this value is not needed /// and the corresponding text field in the customization panel will be disabled. static String getExpandedHeightHelperText( - BuildContext context, - TopBarCustomizationState state){ - - if(state.selectedSize == TopBarSizeEnum.medium){ + BuildContext context, + TopBarCustomizationState state, + ) { + if (state.selectedSize == TopBarSizeEnum.medium) { return context.l10n.app_components_topAppBar_mediumHelperTextHeight_label; - } - else if(state.selectedSize == TopBarSizeEnum.large){ + } else if (state.selectedSize == TopBarSizeEnum.large) { return context.l10n.app_components_topAppBar_largeHelperTextHeight_label; - } - else{ + } else { return ""; } - } /// Validates the expanded height input based on the selected [TopBarSizeEnum]. @@ -268,29 +266,28 @@ class TopAppBarCustomizationUtils { /// - At least [minHeightLargeSize] for [TopBarSizeEnum.large] /// Returns null if the input is valid or empty. static String? getExpandedHeightErrorText( - BuildContext context, - TopBarCustomizationState state, - ){ - - if(state.expandedHeightText.isNotEmpty){ - int height = int.parse(state.expandedHeightText.replaceAll(RegExp(r'[^0-9]'), '')); - - if(state.selectedSize == TopBarSizeEnum.medium - && (height < minHeightMediumSize)){ + BuildContext context, + TopBarCustomizationState state, + ) { + if (state.expandedHeightText.isNotEmpty) { + int height = int.parse( + state.expandedHeightText.replaceAll(RegExp(r'[^0-9]'), ''), + ); + + if (state.selectedSize == TopBarSizeEnum.medium && + (height < minHeightMediumSize)) { return context.l10n.app_components_topAppBar_mediumErrorMessage_label; - } - - else if( state.selectedSize == TopBarSizeEnum.large && (height < minHeightLargeSize)){ + } else if (state.selectedSize == TopBarSizeEnum.large && + (height < minHeightLargeSize)) { return context.l10n.app_components_topAppBar_largeErrorMessage_label; } } return null; - } /// Returns the list of max lines options to disable based on the selected TopAppBar size - static List? getMaxLiensDisabledOptions(TopBarCustomizationState state){ + static List? getMaxLiensDisabledOptions(TopBarCustomizationState state) { final list = TopAppBarCustomizationUtils.maxLinesOptions.toList(); final lastTwoValues = list.sublist(list.length - 2); @@ -301,7 +298,6 @@ class TopAppBarCustomizationUtils { : null; } - /// Returns the expanded height of the Top Bar as a string /// based on the current customization state. /// @@ -314,23 +310,23 @@ class TopAppBarCustomizationUtils { /// - Medium + 2 lines → [minHeightMediumSizeTwoLines] /// - Large → [minHeightLargeSize] /// - Otherwise → empty string - static String setExpandedHeight(TopBarCustomizationState state){ - return state.selectedSize == TopBarSizeEnum.medium - && state.maxLinesSelected == 2 + static String setExpandedHeight(TopBarCustomizationState state) { + return state.selectedSize == TopBarSizeEnum.medium && + state.maxLinesSelected == 2 ? minHeightMediumSizeTwoLines.toString() - : state.selectedSize == TopBarSizeEnum.medium - && state.maxLinesSelected == 1 + : state.selectedSize == TopBarSizeEnum.medium && + state.maxLinesSelected == 1 ? minHeightMediumSize.toString() : state.selectedSize == TopBarSizeEnum.large - ? minHeightLargeSize.toString() + ? minHeightLargeSize.toString() : ""; } - static OudsTopAppBarConfig getMaterialConfig(TopBarCustomizationState state){ + static OudsTopAppBarConfig getMaterialConfig(TopBarCustomizationState state) { return OudsTopAppBarConfig( - centerTitle: state.hasCentredAligned, + centerTitle: state.hasCentredAligned, expandedHeight: getExpandedHeaderValue(state), - titleMaxLines : getTitleLineCountValue(state), + titleMaxLines: getTitleLineCountValue(state), showAvatar: state.showAvatar, ); } diff --git a/app/lib/ui/components/top_bar/top_bar_code_generator.dart b/app/lib/ui/components/top_bar/top_bar_code_generator.dart index 909c80b14..c82797325 100644 --- a/app/lib/ui/components/top_bar/top_bar_code_generator.dart +++ b/app/lib/ui/components/top_bar/top_bar_code_generator.dart @@ -28,6 +28,7 @@ class TopBarCodeGenerator { static String title(TopBarCustomizationState customizationState) { return '''title: "${customizationState.titleText}"'''; } + /// Generates the code for the previous title property based on the customization state static String previousPageTitle(TopBarCustomizationState customizationState) { return '''previousPageTitle: "${customizationState.previousPageTitleText}"'''; @@ -65,17 +66,18 @@ class TopBarCodeGenerator { actionConfigCode = 'OudsTopBarActionConfig.back(onActionPressed: (){})'; break; case NavigationIconTypeEnum.close: - actionConfigCode = 'OudsTopBarActionConfig.close(onActionPressed: (){})'; + actionConfigCode = + 'OudsTopBarActionConfig.close(onActionPressed: (){})'; break; case NavigationIconTypeEnum.menu: actionConfigCode = 'OudsTopBarActionConfig.menu(onActionPressed: (){})'; break; case NavigationIconTypeEnum.custom: actionConfigCode = - '''OudsTopBarActionConfig.custom(icon: "assets/tips-and-tricks.svg", onActionPressed: (){})'''; + '''OudsTopBarActionConfig.custom(icon: "assets/tips-and-tricks.svg", onActionPressed: (){})'''; break; case NavigationIconTypeEnum.none: - actionConfigCode = '''OudsTopBarActionConfig.none()'''; + actionConfigCode = '''OudsTopBarActionConfig.none()'''; break; } return '''leadingActions: [ @@ -84,7 +86,9 @@ class TopBarCodeGenerator { } /// Generates the avatar configuration code, combining avatar icon and monogram - static String getAvatarConfigCode(TopBarCustomizationState customizationState) { + static String getAvatarConfigCode( + TopBarCustomizationState customizationState, + ) { return '''avatarConfig: OudsTopAppBarAvatarConfig( ${avatarIconCode(customizationState)}, ${monogramText(customizationState)}, @@ -93,15 +97,15 @@ class TopBarCodeGenerator { /// Generates the code for app bar actions, including avatar and other actions static String? getAppBarActionsCode( - TopBarCustomizationState customizationState) { + TopBarCustomizationState customizationState, + ) { final List configs = []; // Generate code for standard icon actions final int actionCount = customizationState.actionSelected; if (actionCount > 0) { - configs.addAll(List.generate( - actionCount, - (index) { + configs.addAll( + List.generate(actionCount, (index) { final isBadgeEligible = (actionCount == 1) || (index == actionCount - 1); return '''OudsTopBarActionConfig.icon( @@ -109,13 +113,14 @@ class TopBarCodeGenerator { badge: ${isBadgeEligible ? getActionBadgeCode(customizationState) : null}, onActionPressed: (){} )'''; - }, - )); + }), + ); } // Generate code for the avatar action if enabled if (customizationState.showAvatar) { - final avatarCode = '''OudsTopBarActionConfig.avatar( + final avatarCode = + '''OudsTopBarActionConfig.avatar( ${getAvatarConfigCode(customizationState)}, onActionPressed: (){} )'''; @@ -133,14 +138,15 @@ class TopBarCodeGenerator { /// Returns the badge code based on the selected icon badge type static String? getActionBadgeCode( - TopBarCustomizationState customizationState) { + TopBarCustomizationState customizationState, + ) { return customizationState.selectedIconBadge == ActionIconBadgeEnum.count - ? '''OudsTopAppBarActionBadge( + ? '''OudsTopBarActionBadge( count: "1", contentDescription: 'one unread notification' )''' : customizationState.selectedIconBadge == ActionIconBadgeEnum.dot - ? '''OudsTopAppBarActionBadge( + ? '''OudsTopBarActionBadge( contentDescription: 'Notification' )''' : null; @@ -163,8 +169,9 @@ class TopBarCodeGenerator { // Main method to generate the full code for the TopAppBar based on the customization state static String updateCode(BuildContext context) { - final TopBarCustomizationState? customizationState = - TopBarCustomization.of(context); + final TopBarCustomizationState? customizationState = TopBarCustomization.of( + context, + ); if (customizationState == null) { return '// Waiting for customization state...'; @@ -185,15 +192,17 @@ class TopBarCodeGenerator { // else material code generator final materialConfigCode = _generateMaterialConfigCode(customizationState); - final List params = [ - getSize(customizationState), - title(customizationState), - leadingActions(customizationState), - getAppBarActionsCode(customizationState), - materialConfigCode, - ].where((e) => e != null && e.toString().trim().isNotEmpty) - .cast() - .toList(); + final List params = + [ + getSize(customizationState), + title(customizationState), + leadingActions(customizationState), + getAppBarActionsCode(customizationState), + materialConfigCode, + ] + .where((e) => e != null && e.toString().trim().isNotEmpty) + .cast() + .toList(); return """OudsTopBar( ${params.join(',\n ')} @@ -202,7 +211,8 @@ class TopBarCodeGenerator { /// Generates the code for the materialConfig parameter. static String? _generateMaterialConfigCode( - TopBarCustomizationState customizationState) { + TopBarCustomizationState customizationState, + ) { final List configLines = [ titleMaxLines(customizationState), expandedHeight(customizationState), @@ -222,7 +232,9 @@ class TopBarCodeGenerator { /// Generates the code for toolbar top actions static String? actionsCode( - TopBarCustomizationState customizationState, bool isLeadingActions) { + TopBarCustomizationState customizationState, + bool isLeadingActions, + ) { final actionType = isLeadingActions ? customizationState.selectedLeadingActionType : customizationState.selectedTrailingActionType; @@ -235,33 +247,28 @@ class TopBarCodeGenerator { return null; } - String actionConfigCode; - switch (actionType) { - case ToolbarTopActionTypeEnum.text: - actionConfigCode = - '''OudsTopBarActionConfig.text(actionLabel: "Label", onActionPressed: (){})'''; - break; - case ToolbarTopActionTypeEnum.icon: - actionConfigCode = - '''OudsTopBarActionConfig.icon( + final List configs = List.generate(safeActionCount, (index) { + switch (actionType) { + case ToolbarTopActionTypeEnum.text: + return '''OudsTopBarActionConfig.text(actionLabel: "Label", onActionPressed: (){})'''; + case ToolbarTopActionTypeEnum.icon: + final isBadgeEligible = + (safeActionCount == 1) || (index == safeActionCount - 1); + return '''OudsTopBarActionConfig.icon( icon: "assets/functional-social-and-engagement-heart-empty.svg", + badge: ${!isLeadingActions && isBadgeEligible ? getActionBadgeCode(customizationState) : null}, onActionPressed: (){})'''; - break; - case ToolbarTopActionTypeEnum.back: - actionConfigCode = """OudsTopBarActionConfig.back( + case ToolbarTopActionTypeEnum.back: + return """OudsTopBarActionConfig.back( ${previousPageTitle(customizationState)}, onActionPressed: (){})"""; - break; - case ToolbarTopActionTypeEnum.none: - actionConfigCode = ''; - break; - } - - final List configs = - List.generate(safeActionCount, (index) => actionConfigCode); + case ToolbarTopActionTypeEnum.none: + return ''; + } + }); return '''${isLeadingActions ? 'leadingActions' : 'trailingActions'}: [ ${configs.join(',\n ')} ]'''; } -} \ No newline at end of file +} diff --git a/app/lib/ui/components/top_bar/top_bar_customization_utils.dart b/app/lib/ui/components/top_bar/top_bar_customization_utils.dart index d777e6247..cefe4d1be 100644 --- a/app/lib/ui/components/top_bar/top_bar_customization_utils.dart +++ b/app/lib/ui/components/top_bar/top_bar_customization_utils.dart @@ -1,4 +1,3 @@ - // // Software Name: OUDS Flutter // SPDX-FileCopyrightText: Copyright (c) Orange SA @@ -22,7 +21,6 @@ import 'package:ouds_flutter_demo/ui/theme/theme_controller.dart'; /// Utility class for building and managing OudsTopBar actions and sizes. class TopBarCustomizationUtils { - /// Builds a list of actions for the top bar based on the provided context, /// customization state, and desired action count. /// @@ -35,9 +33,8 @@ class TopBarCustomizationUtils { required TopBarCustomizationState customizationState, required bool isLeadingActions, ThemeController? themeController, - //int actionCount = TopAppBarCustomizationUtils.minActionCount, }) { - if(Theme.of(context).platform == TargetPlatform.iOS){ + if (Theme.of(context).platform == TargetPlatform.iOS) { return ToolbarTopCustomizationUtils.buildCupertinoActionsList( context: context, themeController: themeController, @@ -46,20 +43,22 @@ class TopBarCustomizationUtils { ); } //android leading action support only one action (icon) - if(isLeadingActions){ + if (isLeadingActions) { return [ - TopAppBarCustomizationUtils - .getNavigationIcon(context, - themeController!, - customizationState.selectedIconType) + TopAppBarCustomizationUtils.getNavigationIcon( + context, + themeController!, + customizationState.selectedIconType, + ), ]; } return TopAppBarCustomizationUtils.getMaterialActions( - context: context, - themeController: themeController!, - customizationState: customizationState, - actionCount: customizationState.actionSelected); + context: context, + themeController: themeController!, + customizationState: customizationState, + actionCount: customizationState.actionSelected, + ); } /// Maps the top app bar size type enum to `OudsBarTopSize`. @@ -73,4 +72,18 @@ class TopBarCustomizationUtils { return OudsTopBarSize.small; } } -} \ No newline at end of file + + /// Retrieves the count to display based on the current customization state. + static OudsTopBarActionBadge? getActionBadge( + TopBarCustomizationState customizationState, + ) { + return customizationState.selectedIconBadge == ActionIconBadgeEnum.count + ? OudsTopBarActionBadge( + count: "1", + contentDescription: 'one unread notification', + ) + : customizationState.selectedIconBadge == ActionIconBadgeEnum.dot + ? OudsTopBarActionBadge(contentDescription: 'Notification') + : null; + } +} diff --git a/app/lib/ui/components/top_bar/top_bar_demo_screen.dart b/app/lib/ui/components/top_bar/top_bar_demo_screen.dart index cc5697222..b2c6268a9 100644 --- a/app/lib/ui/components/top_bar/top_bar_demo_screen.dart +++ b/app/lib/ui/components/top_bar/top_bar_demo_screen.dart @@ -1,4 +1,3 @@ - /* * // Software Name: OUDS Flutter * // SPDX-FileCopyrightText: Copyright (c) Orange SA @@ -14,8 +13,8 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:ouds_core/components/top_bar/ouds_top_bar.dart'; import 'package:ouds_core/components/top_bar/ouds_top_appbar.dart'; +import 'package:ouds_core/components/top_bar/ouds_top_bar.dart'; import 'package:ouds_core/components/top_bar/ouds_top_bar_action_config.dart'; import 'package:ouds_flutter_demo/l10n/app_localizations.dart'; import 'package:ouds_flutter_demo/main_app_bar.dart'; @@ -26,8 +25,10 @@ import 'package:ouds_flutter_demo/ui/components/top_bar/top_bar_customization.da import 'package:ouds_flutter_demo/ui/components/top_bar/top_bar_customization_utils.dart'; import 'package:ouds_flutter_demo/ui/components/top_bar/top_bar_enum.dart'; import 'package:ouds_flutter_demo/ui/theme/theme_controller.dart'; +import 'package:ouds_flutter_demo/ui/utilities/code.dart'; import 'package:ouds_flutter_demo/ui/utilities/customizable/customizable_chips.dart'; import 'package:ouds_flutter_demo/ui/utilities/customizable/customizable_section.dart'; +import 'package:ouds_flutter_demo/ui/utilities/customizable/customizable_switch.dart'; import 'package:ouds_flutter_demo/ui/utilities/customizable/customizable_textfield.dart'; import 'package:ouds_flutter_demo/ui/utilities/detail_screen_header.dart'; import 'package:ouds_flutter_demo/ui/utilities/dismiss_keyboard.dart'; @@ -37,13 +38,14 @@ import 'package:ouds_flutter_demo/ui/utilities/sheets_bottom/ouds_sheets_bottom. import 'package:ouds_theme_contract/ouds_component_version.dart'; import 'package:ouds_theme_contract/ouds_theme.dart'; import 'package:provider/provider.dart'; -import 'package:ouds_flutter_demo/ui/utilities/customizable/customizable_switch.dart'; -import 'package:ouds_flutter_demo/ui/utilities/code.dart'; /// This screen displays an appBar demo and allows customization of appBar properties class TopBarDemoScreen extends StatefulWidget { final String? previousPageTitle; - const TopBarDemoScreen({super.key,this.previousPageTitle}); // Default value set to false + const TopBarDemoScreen({ + super.key, + this.previousPageTitle, + }); // Default value set to false @override State createState() => _TopBarDemoScreenState(); @@ -61,19 +63,16 @@ class _TopBarDemoScreenState extends State { @override Widget build(BuildContext context) { - return DismissKeyboard( - child: TopBarCustomization( - child: child(context) - ), - ); + return DismissKeyboard(child: TopBarCustomization(child: child(context))); } - Widget child(BuildContext context){ - + Widget child(BuildContext context) { return Padding( - padding: EdgeInsets.only(bottom: defaultTargetPlatform == TargetPlatform.android - ? MediaQuery.of(context).viewPadding.bottom - : OudsTheme.of(context).spaceScheme(context).paddingBlockNone), + padding: EdgeInsets.only( + bottom: defaultTargetPlatform == TargetPlatform.android + ? MediaQuery.of(context).viewPadding.bottom + : OudsTheme.of(context).spaceScheme(context).paddingBlockNone, + ), child: Scaffold( bottomSheet: OudsSheetsBottom( onExpansionChanged: _onExpansionChanged, @@ -107,19 +106,22 @@ class _Body extends StatefulWidget { class _BodyState extends State<_Body> { @override Widget build(BuildContext context) { - ThemeController? themeController = Provider.of(context, listen: false); + ThemeController? themeController = Provider.of( + context, + listen: false, + ); return DetailScreenDescription( description: context.l10n.app_components_topAppBar_description_text, widget: Column( children: [ _TopBarDemo(), - SizedBox(height: themeController.currentTheme.spaceScheme(context).fixedMedium), - Code( - code: TopBarCodeGenerator.updateCode(context), + SizedBox( + height: themeController.currentTheme + .spaceScheme(context) + .fixedMedium, ), - ReferenceDesignVersionComponent( - version: OudsComponentVersion.appBar, - ) + Code(code: TopBarCodeGenerator.updateCode(context)), + ReferenceDesignVersionComponent(version: OudsComponentVersion.appBar), ], ), ); @@ -149,22 +151,22 @@ class _TopBarDemoState extends State<_TopBarDemo> { final barTop = OudsTopBar( size: TopBarCustomizationUtils.getSize(customizationState!.selectedSize), - leadingActions: TopBarCustomizationUtils - .buildActions( + leadingActions: TopBarCustomizationUtils.buildActions( context: context, customizationState: customizationState!, themeController: themeController, isLeadingActions: true, ), title: customizationState?.titleText, - trailingActions: TopBarCustomizationUtils - .buildActions( + trailingActions: TopBarCustomizationUtils.buildActions( context: context, customizationState: customizationState!, themeController: themeController, isLeadingActions: false, ), - materialConfig: TopAppBarCustomizationUtils.getMaterialConfig(customizationState!) + materialConfig: TopAppBarCustomizationUtils.getMaterialConfig( + customizationState!, + ), ); return LightDarkBox( @@ -177,11 +179,7 @@ class _TopBarDemoState extends State<_TopBarDemo> { } // Helper method to reduce duplication - Widget _wrapWithSizedBox( - BuildContext context, - Widget child, - double height, - ) { + Widget _wrapWithSizedBox(BuildContext context, Widget child, double height) { return Theme.of(context).platform == TargetPlatform.android ? SizedBox(height: height, child: child) : child; @@ -193,11 +191,13 @@ class _TopAppBarCustomizationContent extends StatefulWidget { const _TopAppBarCustomizationContent(); @override - State<_TopAppBarCustomizationContent> createState() => _TopAppBarCustomizationContentState(); + State<_TopAppBarCustomizationContent> createState() => + _TopAppBarCustomizationContentState(); } /// This state class handles the customization options for the TopAppBar -class _TopAppBarCustomizationContentState extends State<_TopAppBarCustomizationContent> { +class _TopAppBarCustomizationContentState + extends State<_TopAppBarCustomizationContent> { late final FocusNode titleFocus; late final FocusNode headerFocus; late final FocusNode lineCountFocus; @@ -223,7 +223,9 @@ class _TopAppBarCustomizationContentState extends State<_TopAppBarCustomizationC @override Widget build(BuildContext context) { - final TopBarCustomizationState? customizationState = TopBarCustomization.of(context); + final TopBarCustomizationState? customizationState = TopBarCustomization.of( + context, + ); var navigationIconType = customizationState!.iconTypeState.list; var size = customizationState.sizeState.list; var actionIconBadgeType = customizationState.actionIconBadgeState.list; @@ -240,8 +242,10 @@ class _TopAppBarCustomizationContentState extends State<_TopAppBarCustomizationC setState(() { customizationState.selectedSize = selectedOption; customizationState.maxLinesSelected = 1; - customizationState.expandedHeightText = TopAppBarCustomizationUtils - .setExpandedHeight(customizationState); + customizationState.expandedHeightText = + TopAppBarCustomizationUtils.setExpandedHeight( + customizationState, + ); }); }, ), @@ -250,10 +254,11 @@ class _TopAppBarCustomizationContentState extends State<_TopAppBarCustomizationC value: customizationState.hasCentredAligned, onChanged: customizationState.selectedSize == TopBarSizeEnum.small ? (value) { - setState(() { - customizationState.hasCentredAligned = value; - }); - } : null, + setState(() { + customizationState.hasCentredAligned = value; + }); + } + : null, ), CustomizableChips( title: NavigationIconTypeEnum.enumName(context), @@ -280,22 +285,32 @@ class _TopAppBarCustomizationContentState extends State<_TopAppBarCustomizationC onSelected: (selectedOption) { setState(() { customizationState.maxLinesSelected = selectedOption; - customizationState.expandedHeightText = TopAppBarCustomizationUtils - .setExpandedHeight(customizationState); + customizationState.expandedHeightText = + TopAppBarCustomizationUtils.setExpandedHeight( + customizationState, + ); }); }, - disabledOptions: TopAppBarCustomizationUtils.getMaxLiensDisabledOptions(customizationState) + disabledOptions: + TopAppBarCustomizationUtils.getMaxLiensDisabledOptions( + customizationState, + ), ), CustomizableTextField( title: context.l10n.app_components_topAppBar_expandedHeight_label, text: customizationState.expandedHeightText, - helperText: TopAppBarCustomizationUtils - .getExpandedHeightHelperText(context, customizationState), + helperText: TopAppBarCustomizationUtils.getExpandedHeightHelperText( + context, + customizationState, + ), focusNode: headerFocus, fieldType: FieldType.customHeight, keyboardType: TextInputType.number, fieldEnable: customizationState.selectedSize != TopBarSizeEnum.small, - errorText: TopAppBarCustomizationUtils.getExpandedHeightErrorText(context,customizationState), + errorText: TopAppBarCustomizationUtils.getExpandedHeightErrorText( + context, + customizationState, + ), ), CustomizableChips( title: context.l10n.app_components_common_trailingActionCount_label, @@ -316,15 +331,15 @@ class _TopAppBarCustomizationContentState extends State<_TopAppBarCustomizationC onSelected: customizationState.actionSelected == 0 ? null : (selectedOption) { - setState(() { - customizationState.selectedIconBadge = selectedOption; - }); - }, + setState(() { + customizationState.selectedIconBadge = selectedOption; + }); + }, ), CustomizableSwitch( title: context.l10n.app_components_topAppBar_showAvatar_label, value: customizationState.showAvatar, - onChanged: (value) { + onChanged: (value) { setState(() { customizationState.showAvatar = value; }); @@ -338,16 +353,23 @@ class _TopAppBarCustomizationContentState extends State<_TopAppBarCustomizationC onSelected: !customizationState.showAvatar ? null : (selectedOption) { - setState(() { - customizationState.selectedActionAvatar = selectedOption; - customizationState.actionAvatarMonogramText = TopAppBarCustomizationUtils.getMonogramText(customizationState); - }); - } , + setState(() { + customizationState.selectedActionAvatar = selectedOption; + customizationState.actionAvatarMonogramText = + TopAppBarCustomizationUtils.getMonogramText( + customizationState, + ); + }); + }, ), CustomizableTextField( - fieldEnable: TopAppBarCustomizationUtils - .getActionAvatar(customizationState.selectedActionAvatar) == OudsTopAppBarActionAvatar.monogram, - title: context.l10n.app_components_topAppBar_actionAvatarMonogram_label, + fieldEnable: + TopAppBarCustomizationUtils.getActionAvatar( + customizationState.selectedActionAvatar, + ) == + OudsTopAppBarActionAvatar.monogram, + title: + context.l10n.app_components_topAppBar_actionAvatarMonogram_label, text: customizationState.actionAvatarMonogramText ?? "A", focusNode: monogramFocus, fieldType: FieldType.monogram, @@ -363,11 +385,13 @@ class _ToolBarTopCustomizationContent extends StatefulWidget { const _ToolBarTopCustomizationContent(); @override - State<_ToolBarTopCustomizationContent> createState() => _ToolbarTopCustomizationContentState(); + State<_ToolBarTopCustomizationContent> createState() => + _ToolbarTopCustomizationContentState(); } /// This state class handles the customization options for the TopNavigationBar -class _ToolbarTopCustomizationContentState extends State<_ToolBarTopCustomizationContent> { +class _ToolbarTopCustomizationContentState + extends State<_ToolBarTopCustomizationContent> { late final FocusNode titleFocus; late final FocusNode leadingFocus; late final FocusNode trailingFocus; @@ -393,7 +417,9 @@ class _ToolbarTopCustomizationContentState extends State<_ToolBarTopCustomizatio @override Widget build(BuildContext context) { - final TopBarCustomizationState? customizationState = TopBarCustomization.of(context); + final TopBarCustomizationState? customizationState = TopBarCustomization.of( + context, + ); var navigationActionType = customizationState!.leadingActionTypeState.list; var trailingActionType = customizationState.trailingActionTypeState.list; final sizeOptions = customizationState.sizeState.list @@ -413,9 +439,9 @@ class _ToolbarTopCustomizationContentState extends State<_ToolBarTopCustomizatio }, ), CustomizableSwitch( - title: context.l10n.app_components_toolbarTop_actionEnabled_label, - value: customizationState.hasEnabled, - onChanged: (value) => customizationState.hasEnabled = value + title: context.l10n.app_components_toolbarTop_actionEnabled_label, + value: customizationState.hasEnabled, + onChanged: (value) => customizationState.hasEnabled = value, ), CustomizableChips( title: ToolbarTopActionTypeEnum.enumName(context), @@ -425,18 +451,23 @@ class _ToolbarTopCustomizationContentState extends State<_ToolBarTopCustomizatio onSelected: (selectedOption) { setState(() { customizationState.selectedLeadingActionType = selectedOption; - if(selectedOption == ToolbarTopActionTypeEnum.back){ - customizationState.selectedLeadingActionCount = ToolbarTopCustomizationUtils.minActionCount; + if (selectedOption == ToolbarTopActionTypeEnum.back) { + customizationState.selectedLeadingActionCount = + ToolbarTopCustomizationUtils.minActionCount; } }); }, ), CustomizableChips( - title: context.l10n.app_components_toolbarTop_leadingActionCount_label, + title: + context.l10n.app_components_toolbarTop_leadingActionCount_label, options: ToolbarTopCustomizationUtils.getLimitedActionsCount(context), selectedOption: customizationState.selectedLeadingActionCount, getText: (option) => option.toString(), - disabledOptions: ToolbarTopCustomizationUtils.getDisabledLeadingActionCountOptions(customizationState), + disabledOptions: + ToolbarTopCustomizationUtils.getDisabledLeadingActionCountOptions( + customizationState, + ), onSelected: (selectedOption) { setState(() { customizationState.selectedLeadingActionCount = selectedOption; @@ -444,7 +475,8 @@ class _ToolbarTopCustomizationContentState extends State<_ToolBarTopCustomizatio }, ), CustomizableChips( - title: context.l10n.app_components_toolbarTop_trailingActionType_label, + title: + context.l10n.app_components_toolbarTop_trailingActionType_label, options: trailingActionType, selectedOption: customizationState.selectedTrailingActionType, getText: (option) => option.stringValue(context), @@ -459,14 +491,29 @@ class _ToolbarTopCustomizationContentState extends State<_ToolBarTopCustomizatio options: ToolbarTopCustomizationUtils.getLimitedActionsCount(context), selectedOption: customizationState.selectedTrailingActionCount, getText: (option) => option.toString(), - disabledOptions: ToolbarTopCustomizationUtils - .getDisabledTrailingActionCountOptions(customizationState), + disabledOptions: + ToolbarTopCustomizationUtils.getDisabledTrailingActionCountOptions( + customizationState, + ), onSelected: (selectedOption) { setState(() { customizationState.selectedTrailingActionCount = selectedOption; }); }, ), + CustomizableChips( + title: ActionIconBadgeEnum.enumName(context), + options: customizationState.actionIconBadgeState.list, + selectedOption: customizationState.selectedIconBadge, + getText: (option) => option.stringValue(context), + onSelected: customizationState.actionSelected == 0 + ? null + : (selectedOption) { + setState(() { + customizationState.selectedIconBadge = selectedOption; + }); + }, + ), CustomizableTextField( title: context.l10n.app_components_common_title_label, text: customizationState.titleText, diff --git a/ouds_core/CHANGELOG.md b/ouds_core/CHANGELOG.md index 97bae6427..827501b6c 100644 --- a/ouds_core/CHANGELOG.md +++ b/ouds_core/CHANGELOG.md @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://github.com/Orange-OpenSource/ouds-flutter/compare/1.3.1...develop) ### Added ### Changed +- [DemoApp] Update `ToolBar Top`, with Badge in Trailing Actions ([#642](https://github.com/Orange-OpenSource/ouds-flutter/issues/642)) - [Library] update tokens 2.5.0 ([#778](https://github.com/Orange-OpenSource/ouds-flutter/issues/778)) - [Library] update tokens 2.4.0 ([#726](https://github.com/Orange-OpenSource/ouds-flutter/issues/726)) diff --git a/ouds_core/lib/components/button/internal/ouds_button_utils.dart b/ouds_core/lib/components/button/internal/ouds_button_utils.dart index 448c0ea50..a00c0bd4b 100644 --- a/ouds_core/lib/components/button/internal/ouds_button_utils.dart +++ b/ouds_core/lib/components/button/internal/ouds_button_utils.dart @@ -12,83 +12,99 @@ /// @nodoc library; + import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; import 'package:ouds_core/components/badge/ouds_badge.dart'; -import 'package:ouds_core/components/button/ouds_button.dart'; import 'package:ouds_core/components/button/internal/ouds_button_control_state.dart'; import 'package:ouds_core/components/button/internal/ouds_button_icon_modifier.dart'; import 'package:ouds_core/components/button/internal/ouds_button_style_modifier.dart'; -import 'package:ouds_core/components/top_bar/ouds_top_appbar.dart'; +import 'package:ouds_core/components/button/ouds_button.dart'; +import 'package:ouds_core/components/common/ouds_icon_status.dart'; +import 'package:ouds_core/components/top_bar/ouds_top_bar.dart'; /// Builds an icon button with optional badge, wrapped with semantics for accessibility. /// /// This widget creates an IconButton styled according to the provided layout, appearance, /// and control state. It also integrates badge display if provided. /// - Widget buildIconBadgeButton( - BuildContext context, - OudsButtonLayout layout, - OudsButtonAppearance appearance, - OudsButtonControlState buttonState, - Function()? onPressed, - String? icon, - OudsTopAppBarActionBadge? badge, - String? package - ){ - - return MergeSemantics( - child: Semantics( - child: IconButton( - style: OudsButtonStyleModifier.buildButtonStyle( - context, appearance: appearance, layout: layout, buttonState: buttonState), - onPressed: onPressed , - icon: _buildIconWithBadge( - context, icon!, appearance, layout, - buttonState,badge, package), +Widget buildIconBadgeButton( + BuildContext context, + OudsButtonLayout layout, + OudsButtonAppearance appearance, + OudsButtonControlState buttonState, + Function()? onPressed, + String? icon, + OudsTopBarActionBadge? badge, + String? package, +) { + return MergeSemantics( + child: Semantics( + child: IconButton( + style: OudsButtonStyleModifier.buildButtonStyle( + context, + appearance: appearance, + layout: layout, + buttonState: buttonState, + ), + onPressed: onPressed, + icon: _buildIconWithBadge( + context, + icon!, + appearance, + layout, + buttonState, + badge, + package, ), ), - ); - } + ), + ); +} /// Builds an icon widget with an optional badge overlay. /// /// This function creates an SVG icon with specified size and color based on the control state /// and appearance. If a badge is provided, it overlays the icon with the badge. /// - Widget _buildIconWithBadge( - BuildContext context, - String assetName, - final OudsButtonAppearance appearance, - final OudsButtonLayout layout, - final OudsButtonControlState buttonState, - OudsTopAppBarActionBadge? badge, - String? package - ) { - - final widgetIcon = SvgPicture.asset( - excludeFromSemantics: true, - package: package, - assetName, - fit: BoxFit.contain, - matchTextDirection: true, - width: OudsButtonIconModifier.getIconSize(context, layout), - height: OudsButtonIconModifier.getIconSize(context, layout), - colorFilter: ColorFilter.mode( - OudsButtonIconModifier.getIconColor(context, buttonState, appearance), - BlendMode.srcIn, - ), - ); - - // Wrap icon with badge if provided - return badge != null - ? OudsBadge( - semanticsLabel: badge.contentDescription, - label: badge.count.toString(), - status: OudsBadgeStatus.negative, - size: badge.hasCount ? OudsBadgeSize.medium : OudsBadgeSize.xsmall, - child: widgetIcon - ) - : widgetIcon; +Widget _buildIconWithBadge( + BuildContext context, + String assetName, + final OudsButtonAppearance appearance, + final OudsButtonLayout layout, + final OudsButtonControlState buttonState, + OudsTopBarActionBadge? badge, + String? package, +) { + final widgetIcon = SvgPicture.asset( + excludeFromSemantics: true, + package: package, + assetName, + fit: BoxFit.contain, + matchTextDirection: true, + width: OudsButtonIconModifier.getIconSize(context, layout), + height: OudsButtonIconModifier.getIconSize(context, layout), + colorFilter: ColorFilter.mode( + OudsButtonIconModifier.getIconColor(context, buttonState, appearance), + BlendMode.srcIn, + ), + ); - } \ No newline at end of file + // Wrap icon with badge if provided + return badge != null + ? (badge.hasCount + ? OudsBadge.count( + semanticsLabel: badge.contentDescription, + label: badge.count.toString(), + status: Negative(), + size: OudsBadgeSize.medium, + child: widgetIcon, + ) + : OudsBadge.standard( + semanticsLabel: badge.contentDescription, + status: Negative(), + size: OudsBadgeSize.xsmall, + child: widgetIcon, + )) + : widgetIcon; +} diff --git a/ouds_core/lib/components/button/ouds_button.dart b/ouds_core/lib/components/button/ouds_button.dart index 419e96a9c..3685c7a86 100644 --- a/ouds_core/lib/components/button/ouds_button.dart +++ b/ouds_core/lib/components/button/ouds_button.dart @@ -23,7 +23,7 @@ import 'package:ouds_core/components/button/internal/ouds_button_loading_modifie import 'package:ouds_core/components/button/internal/ouds_button_style_modifier.dart'; import 'package:ouds_core/components/button/internal/ouds_button_utils.dart'; import 'package:ouds_core/components/common/OudsBorder.dart'; -import 'package:ouds_core/components/top_bar/ouds_top_appbar.dart'; +import 'package:ouds_core/components/top_bar/ouds_top_bar.dart'; import 'package:ouds_core/l10n/gen/ouds_localizations.dart'; import 'package:ouds_theme_contract/ouds_theme.dart'; @@ -156,7 +156,7 @@ class OudsButton extends StatefulWidget { @internal Widget buildIconButtonWithBadge( BuildContext context, - OudsTopAppBarActionBadge? badge, + OudsTopBarActionBadge? badge, OudsButtonControlState buttonState, ) { return buildIconBadgeButton( diff --git a/ouds_core/lib/components/top_bar/ouds_top_appbar.dart b/ouds_core/lib/components/top_bar/ouds_top_appbar.dart index 10eea0923..9b549bd13 100644 --- a/ouds_core/lib/components/top_bar/ouds_top_appbar.dart +++ b/ouds_core/lib/components/top_bar/ouds_top_appbar.dart @@ -1,4 +1,3 @@ - /* * // Software Name: OUDS Flutter * // SPDX-FileCopyrightText: Copyright (c) Orange SA @@ -17,19 +16,16 @@ library; import 'package:flutter/material.dart'; +import 'package:ouds_core/components/button/internal/ouds_button_control_state.dart'; +import 'package:ouds_core/components/button/ouds_button.dart'; import 'package:ouds_core/components/top_bar/internal/ouds_top_bar_style_modifier.dart'; import 'package:ouds_core/components/top_bar/internal/ouds_topappbar_actions_modifier.dart'; import 'package:ouds_core/components/top_bar/ouds_top_bar.dart'; import 'package:ouds_core/components/top_bar/ouds_top_bar_action_config.dart'; import 'package:ouds_theme_contract/ouds_theme.dart'; -import 'package:ouds_core/components/badge/ouds_badge.dart'; -import 'package:ouds_core/components/button/ouds_button.dart'; -import 'package:ouds_core/components/button/internal/ouds_button_control_state.dart'; /// Defines the avatar type for an avatar action. -enum OudsTopAppBarActionAvatar { - image, monogram -} +enum OudsTopAppBarActionAvatar { image, monogram } /// The [SliverAppBar.toolbarHeight] value defined in [SliverAppBar.medium] for medium app bar const double _mediumHeight = 112; @@ -113,7 +109,7 @@ const double _largeHeight = 152; /// ) /// ``` -class OudsTopAppBar extends StatefulWidget implements PreferredSizeWidget{ +class OudsTopAppBar extends StatefulWidget implements PreferredSizeWidget { final OudsTopBarSize size; final String? title; final List? leadingActions; @@ -125,7 +121,8 @@ class OudsTopAppBar extends StatefulWidget implements PreferredSizeWidget{ final bool showAvatar; final String? icon; - const OudsTopAppBar({super.key, + const OudsTopAppBar({ + super.key, this.size = OudsTopBarSize.small, this.leadingActions, this.title, @@ -135,13 +132,13 @@ class OudsTopAppBar extends StatefulWidget implements PreferredSizeWidget{ this.expandedHeight, this.titleMaxLines = 1, this.showAvatar = false, - this.icon - }) ; + this.icon, + }); @override - State createState() =>_OudsTopAppBarState(); + State createState() => _OudsTopAppBarState(); -// Helper method to calculate the preferred height based on the AppBar size + // Helper method to calculate the preferred height based on the AppBar size static double getHeight(OudsTopBarSize size, double? expandedHeight) { if (size == OudsTopBarSize.medium) { // Use expandedHeight if provided and greater than or equal to _mediumHeight; @@ -170,35 +167,39 @@ class OudsTopAppBar extends StatefulWidget implements PreferredSizeWidget{ } } -class _OudsTopAppBarState extends State{ +class _OudsTopAppBarState extends State { @override PreferredSizeWidget build(BuildContext context) { final topAppBarActionsModifier = OudsTopAppBarActionsModifier(); final topAppBarBackgroundColorModifier = OudsTopBarStyleModifier(context); final trailingActions = widget.trailingActions != null - ? List.generate( - widget.trailingActions!.length, - (index) => widget.trailingActions![index] - .buildTopAppbarTrailingAction(context,widget.showAvatar) - ) : null; + ? List.generate( + widget.trailingActions!.length, + (index) => widget.trailingActions![index] + .buildTopAppbarTrailingAction(context, widget.showAvatar), + ) + : null; - switch (widget.size){ + switch (widget.size) { case OudsTopBarSize.small: return _buildSmallTopAppBar( - topAppBarActionsModifier, - topAppBarBackgroundColorModifier, - trailingActions); + topAppBarActionsModifier, + topAppBarBackgroundColorModifier, + trailingActions, + ); case OudsTopBarSize.medium: return _buildMediumTopAppBar( - topAppBarActionsModifier, - topAppBarBackgroundColorModifier, - trailingActions); + topAppBarActionsModifier, + topAppBarBackgroundColorModifier, + trailingActions, + ); case OudsTopBarSize.large: return _buildLargeTopAppBar( - topAppBarActionsModifier, - topAppBarBackgroundColorModifier, - trailingActions); + topAppBarActionsModifier, + topAppBarBackgroundColorModifier, + trailingActions, + ); } } @@ -207,38 +208,43 @@ class _OudsTopAppBarState extends State{ /// This method creates a [PreferredSize] widget that contains a styled [AppBar] /// with optional leading and action widgets, background blur effect, and border decoration. PreferredSize _buildSmallTopAppBar( - OudsTopAppBarActionsModifier actionsModifier, - OudsTopBarStyleModifier styleModifier, - List? trailingActions - ){ - final backgroundColor = styleModifier.getBackgroundColor(widget.translucent); + OudsTopAppBarActionsModifier actionsModifier, + OudsTopBarStyleModifier styleModifier, + List? trailingActions, + ) { + final backgroundColor = styleModifier.getBackgroundColor( + widget.translucent, + ); return PreferredSize( preferredSize: Size.fromHeight(widget.preferredSize.height), child: ClipRect( - child: BackdropFilter(filter: styleModifier.getBlurEffect(), + child: BackdropFilter( + filter: styleModifier.getBlurEffect(), child: Container( - decoration: BoxDecoration( - border: styleModifier.getBorder(), - ), + decoration: BoxDecoration(border: styleModifier.getBorder()), child: AppBar( centerTitle: widget.centerTitle, title: Text( - widget.title ?? "", - maxLines: 1, - style: TextStyle( - color: OudsTheme.of(context).colorScheme(context).contentDefault, - overflow: TextOverflow.ellipsis, - fontFamily: OudsTheme.of(context).fontFamily, - ) + widget.title ?? "", + maxLines: 1, + style: TextStyle( + color: OudsTheme.of( + context, + ).colorScheme(context).contentDefault, + overflow: TextOverflow.ellipsis, + fontFamily: OudsTheme.of(context).fontFamily, + ), ), automaticallyImplyLeading: false, leading: actionsModifier.getLeadingIconButton( - context, widget.leadingActions?.first), + context, + widget.leadingActions?.first, + ), actions: trailingActions, backgroundColor: backgroundColor, ), ), - ) , + ), ), ); } @@ -249,25 +255,27 @@ class _OudsTopAppBarState extends State{ /// with a [SliverAppBar] that supports expanded height, custom title, leading widget, /// actions, and styling options such as background blur and border decoration. PreferredSize _buildMediumTopAppBar( - OudsTopAppBarActionsModifier actionsModifier, - OudsTopBarStyleModifier styleModifier, - List? trailingActions - ) { + OudsTopAppBarActionsModifier actionsModifier, + OudsTopBarStyleModifier styleModifier, + List? trailingActions, + ) { final backgroundColor = styleModifier.getBackgroundColor( - widget.translucent); + widget.translucent, + ); // Determine the height: if expandedHeight is null or less than _mediumHeight, // use _mediumHeight; otherwise, use expandedHeight entered by user - final height = (widget.expandedHeight == null || widget.expandedHeight! < _mediumHeight) + final height = + (widget.expandedHeight == null || + widget.expandedHeight! < _mediumHeight) ? _mediumHeight : widget.expandedHeight!; return PreferredSize( preferredSize: Size.fromHeight(height), child: ClipRect( - child: BackdropFilter(filter: styleModifier.getBlurEffect(), + child: BackdropFilter( + filter: styleModifier.getBlurEffect(), child: Container( - decoration: BoxDecoration( - border: styleModifier.getBorder(), - ), + decoration: BoxDecoration(border: styleModifier.getBorder()), child: CustomScrollView( slivers: [ SliverAppBar.medium( @@ -275,15 +283,12 @@ class _OudsTopAppBarState extends State{ title: Text( widget.title ?? "", style: TextStyle( - color: OudsTheme - .of(context) - .colorScheme(context) - .contentDefault, - fontFamily: Theme - .of(context) - .appBarTheme - .titleTextStyle - ?.fontFamily, + color: OudsTheme.of( + context, + ).colorScheme(context).contentDefault, + fontFamily: Theme.of( + context, + ).appBarTheme.titleTextStyle?.fontFamily, ), maxLines: widget.titleMaxLines, softWrap: true, @@ -292,15 +297,22 @@ class _OudsTopAppBarState extends State{ automaticallyImplyLeading: false, leading: widget.leadingActions?.isNotEmpty == true ? actionsModifier.getLeadingIconButton( - context, widget.leadingActions!.first) + context, + widget.leadingActions!.first, + ) : null, actions: trailingActions != null ? actionsModifier - .getTrailingActionList(context, widget.trailingActions,trailingActions) - ?.map((action) => Center(child: action)).toList() + .getTrailingActionList( + context, + widget.trailingActions, + trailingActions, + ) + ?.map((action) => Center(child: action)) + .toList() : null, backgroundColor: backgroundColor, - ) + ), ], ), ), @@ -315,58 +327,70 @@ class _OudsTopAppBarState extends State{ /// with a [SliverAppBar] configured for large display, supporting expanded height, /// custom title, leading widget, actions, and styling options such as background blur and border decoration. PreferredSize _buildLargeTopAppBar( - OudsTopAppBarActionsModifier actionsModifier, - OudsTopBarStyleModifier styleModifier, - List? trailingActions - ) { - final backgroundColor = styleModifier.getBackgroundColor(widget.translucent); + OudsTopAppBarActionsModifier actionsModifier, + OudsTopBarStyleModifier styleModifier, + List? trailingActions, + ) { + final backgroundColor = styleModifier.getBackgroundColor( + widget.translucent, + ); // Determine the height: if expandedHeight is null or less than _largeHeight, // use _largeHeight; otherwise, use expandedHeight entered by user - final height = (widget.expandedHeight == null || widget.expandedHeight! < _largeHeight) + final height = + (widget.expandedHeight == null || widget.expandedHeight! < _largeHeight) ? _largeHeight : widget.expandedHeight!; - return PreferredSize( - preferredSize: Size.fromHeight(height), - child: ClipRect( - child: BackdropFilter(filter: styleModifier.getBlurEffect(), - child: Container( - decoration: BoxDecoration( - border: styleModifier.getBorder(), - ), - child: CustomScrollView( - slivers: [ - SliverAppBar.large( - expandedHeight: height, - title: Text( - widget.title ?? "", - style: TextStyle( - color: OudsTheme.of(context).colorScheme(context).contentDefault, - fontFamily: Theme.of(context).appBarTheme.titleTextStyle?.fontFamily, - ), - maxLines: widget.titleMaxLines, - softWrap: true, - overflow: TextOverflow.ellipsis, + return PreferredSize( + preferredSize: Size.fromHeight(height), + child: ClipRect( + child: BackdropFilter( + filter: styleModifier.getBlurEffect(), + child: Container( + decoration: BoxDecoration(border: styleModifier.getBorder()), + child: CustomScrollView( + slivers: [ + SliverAppBar.large( + expandedHeight: height, + title: Text( + widget.title ?? "", + style: TextStyle( + color: OudsTheme.of( + context, + ).colorScheme(context).contentDefault, + fontFamily: Theme.of( + context, + ).appBarTheme.titleTextStyle?.fontFamily, ), - automaticallyImplyLeading: false, - leading: actionsModifier.getLeadingIconButton( - context, widget.leadingActions!.first,), - actions: trailingActions != null - ? actionsModifier - .getTrailingActionList(context,widget.trailingActions,trailingActions) - ?.map((action) => Center(child: action)).toList() - : null, - backgroundColor: backgroundColor, + maxLines: widget.titleMaxLines, + softWrap: true, + overflow: TextOverflow.ellipsis, ), - ], - ), + automaticallyImplyLeading: false, + leading: actionsModifier.getLeadingIconButton( + context, + widget.leadingActions!.first, + ), + actions: trailingActions != null + ? actionsModifier + .getTrailingActionList( + context, + widget.trailingActions, + trailingActions, + ) + ?.map((action) => Center(child: action)) + .toList() + : null, + backgroundColor: backgroundColor, + ), + ], ), ), - ) + ), + ), ); } } - /// Configuration class for the Avatar component within the [OudsTopAppBar]. /// /// This class defines the visual and accessibility properties of an avatar, @@ -393,7 +417,7 @@ class OudsTopAppBarAvatarConfig { this.monogram, this.contentDescription, this.monogramBackgroundColor, - this.monogramColor + this.monogramColor, }); } @@ -410,15 +434,10 @@ class OudsTopAppBarAvatarConfig { /// - The widget handles tap events and updates its visual state accordingly. class BadgeIconButton extends StatefulWidget { final String? icon; - final OudsTopAppBarActionBadge? badge; + final OudsTopBarActionBadge? badge; final VoidCallback? onPressed; - const BadgeIconButton({ - super.key, - this.icon, - this.badge, - this.onPressed, - }); + const BadgeIconButton({super.key, this.icon, this.badge, this.onPressed}); @override State createState() => _BadgeIconButtonState(); @@ -430,7 +449,6 @@ class _BadgeIconButtonState extends State { @override Widget build(BuildContext context) { - final buttonStateDeterminer = OudsButtonControlStateDeterminer( enabled: widget.onPressed != null, isPressed: _isPressed, @@ -451,40 +469,9 @@ class _BadgeIconButtonState extends State { appearance: OudsButtonAppearance.minimal, icon: widget.icon, onPressed: widget.onPressed, - ).buildIconButtonWithBadge( - context, - widget.badge, - buttonState, - ), + ).buildIconButtonWithBadge(context, widget.badge, buttonState), ), ), ); } } - -/// A badge in top app bar action -/// -/// * See [OudsBadge] -/// -/// - [contentDescription]: Content description of the badge, needed for accessibility support. -/// - [count]: Optional integer to display as badge count. -/// -/// Example usage: -/// ```dart -/// OudsTopAppBarActionBadge( -/// contentDescription: 'Unread messages', -/// count: 5, -/// ); -/// ``` -/// -class OudsTopAppBarActionBadge { - final String contentDescription; - final String? count; - - const OudsTopAppBarActionBadge({ - required this.contentDescription, - this.count, - }); - - bool get hasCount => count != null; -} diff --git a/ouds_core/lib/components/top_bar/ouds_top_bar.dart b/ouds_core/lib/components/top_bar/ouds_top_bar.dart index a10eb0dd7..5aacab344 100644 --- a/ouds_core/lib/components/top_bar/ouds_top_bar.dart +++ b/ouds_core/lib/components/top_bar/ouds_top_bar.dart @@ -1,4 +1,3 @@ - /* * // Software Name: OUDS Flutter * // SPDX-FileCopyrightText: Copyright (c) Orange SA @@ -36,10 +35,12 @@ typedef OudsAppBar = OudsTopBar; enum OudsTopBarSize { /// A standard-height top bar. small, + /// An expanded-height top bar.(Material only). medium, + /// A larger expanded-height top bar, often used for prominent titles or imagery. - large + large, } /// Defines the type of action to be displayed in an [OudsTopBar]. @@ -171,8 +172,7 @@ enum OudsTopBarActionType { /// * [OudsToolbarTop], the Cupertino (iOS) implementation. /// * [OudsTopBarActionConfig], the configuration object for actions. /// -class OudsTopBar extends StatelessWidget implements PreferredSizeWidget{ - +class OudsTopBar extends StatelessWidget implements PreferredSizeWidget { /// Common parameters final String? title; final bool translucent; @@ -205,12 +205,12 @@ class OudsTopBar extends StatelessWidget implements PreferredSizeWidget{ leadingActions: leadingActions, trailingActions: trailingActions, ); - }else{ + } else { // Material only supports single leading action assert( - leadingActions == null || leadingActions!.length <= 1, - 'Material variant only supports a single leading action. ' - 'Received ${leadingActions?.length} actions.', + leadingActions == null || leadingActions!.length <= 1, + 'Material variant only supports a single leading action. ' + 'Received ${leadingActions?.length} actions.', ); return OudsTopAppBar( title: title, @@ -231,7 +231,10 @@ class OudsTopBar extends StatelessWidget implements PreferredSizeWidget{ @override Size get preferredSize => defaultTargetPlatform == TargetPlatform.iOS ? OudsToolbarTop.getPreferredSize - : OudsTopAppBar.getPreferredSize(size: size,expandedHeight: materialConfig?.expandedHeight); + : OudsTopAppBar.getPreferredSize( + size: size, + expandedHeight: materialConfig?.expandedHeight, + ); /// Returns the preferred size based on the platform: OudsToolbarTop size for iOS, /// OudsTopAppBar size for Android and other platforms. @@ -245,12 +248,16 @@ class OudsTopBar extends StatelessWidget implements PreferredSizeWidget{ /// /// Returns: /// - A [Size] object representing the preferred size. - static Size getPreferredSize({OudsTopBarSize? size, double? expandedHeight}){ - return defaultTargetPlatform == TargetPlatform.iOS + static Size getPreferredSize({OudsTopBarSize? size, double? expandedHeight}) { + return defaultTargetPlatform == TargetPlatform.iOS ? OudsToolbarTop.getPreferredSize - : OudsTopAppBar.getPreferredSize(size: size!,expandedHeight: expandedHeight); + : OudsTopAppBar.getPreferredSize( + size: size!, + expandedHeight: expandedHeight, + ); } } + /// A configuration object for Material-specific properties. /// /// - [centerTitle]: Whether to center the title. Defaults to false. @@ -288,4 +295,28 @@ class OudsTopAppBarConfig { this.titleMaxLines = 1, this.showAvatar = false, }); -} \ No newline at end of file +} + +/// A badge in top bar action +/// +/// * See [OudsBadge] +/// +/// - [contentDescription]: Content description of the badge, needed for accessibility support. +/// - [count]: Optional integer to display as badge count. +/// +/// Example usage: +/// ```dart +/// OudsTopBarActionBadge( +/// contentDescription: 'Unread messages', +/// count: 5, +/// ); +/// ``` +/// +class OudsTopBarActionBadge { + final String contentDescription; + final String? count; + + const OudsTopBarActionBadge({required this.contentDescription, this.count}); + + bool get hasCount => count != null; +} diff --git a/ouds_core/lib/components/top_bar/ouds_top_bar_action_config.dart b/ouds_core/lib/components/top_bar/ouds_top_bar_action_config.dart index 262db08ef..f1f369f00 100644 --- a/ouds_core/lib/components/top_bar/ouds_top_bar_action_config.dart +++ b/ouds_core/lib/components/top_bar/ouds_top_bar_action_config.dart @@ -1,4 +1,3 @@ - /* * // Software Name: OUDS Flutter * // SPDX-FileCopyrightText: Copyright (c) Orange SA @@ -13,8 +12,11 @@ */ /// {@category TopBar} library; + import 'package:flutter/cupertino.dart'; import 'package:ouds_core/components/avatar/ouds_avatar.dart'; +import 'package:ouds_core/components/badge/ouds_badge.dart'; +import 'package:ouds_core/components/common/ouds_icon_status.dart'; import 'package:ouds_core/components/top_bar/internal/ouds_toolbar_top_action_modifier.dart'; import 'package:ouds_core/components/top_bar/internal/ouds_toolbar_top_text_style_modifier.dart'; import 'package:ouds_core/components/top_bar/ouds_top_appbar.dart'; @@ -36,6 +38,7 @@ import 'package:ouds_theme_contract/ouds_theme_contract.dart'; /// - [contentDescription]: Accessibility description for the action. /// - [widget]: A custom widget to display as the action. This is only used when [type] is [OudsTopBarActionType.widget]. /// - [icon]: Path to custom icon asset. This is used when [type] is [OudsTopBarActionType.icon] or [OudsTopBarActionType.custom]. +/// - [badge]: Configuration for a notification badge. This is used when [type] is [OudsTopBarActionType.icon]. /// /// ### iOS/Cupertino-Only Parameters /// @@ -50,7 +53,6 @@ import 'package:ouds_theme_contract/ouds_theme_contract.dart'; /// /// ### Android/Material-Only Parameters /// -/// - [badge]: Configuration for a notification badge. This is only used on Material when [type] is [OudsTopBarActionType.icon]. /// - [avatarConfig]: Configuration for an avatar. This is only used on Material when [type] is [OudsTopBarActionType.avatar]. /// /// ### Example of usage @@ -96,11 +98,12 @@ import 'package:ouds_theme_contract/ouds_theme_contract.dart'; /// class OudsTopBarActionConfig { ///Common parameters - final OudsTopBarActionType type ; + final OudsTopBarActionType type; final VoidCallback? onActionPressed; final String? contentDescription; final Widget? widget; final String? icon; + final OudsTopBarActionBadge? badge; ///Cupertino-Only Parameters final String? actionLabel; @@ -108,7 +111,6 @@ class OudsTopBarActionConfig { ///Material-Only Parameters final OudsTopAppBarAvatarConfig? avatarConfig; - final OudsTopAppBarActionBadge? badge; /// Private base constructor. const OudsTopBarActionConfig._({ @@ -120,17 +122,15 @@ class OudsTopBarActionConfig { this.avatarConfig, this.actionLabel, this.icon, - this.previousPageTitle + this.previousPageTitle, }); /// Creates a configuration for an icon-based action. - /// - /// The [badge] parameter is only applied on Material and will be ignored on iOS. factory OudsTopBarActionConfig.icon({ required String icon, // Make this non-nullable for clarity VoidCallback? onActionPressed, String? contentDescription, - OudsTopAppBarActionBadge? badge, // Material-only + OudsTopBarActionBadge? badge, }) { return OudsTopBarActionConfig._( type: OudsTopBarActionType.icon, @@ -244,9 +244,7 @@ class OudsTopBarActionConfig { /// Creates a configuration for an empty action, resulting in no visible output. factory OudsTopBarActionConfig.none() { - return OudsTopBarActionConfig._( - type: OudsTopBarActionType.none, - ); + return OudsTopBarActionConfig._(type: OudsTopBarActionType.none); } /// An internal method that builds the widget for this action on the iOS platform. @@ -265,13 +263,11 @@ class OudsTopBarActionConfig { /// **Throws:** /// - [UnimplementedError] if the action [type] is not supported for the iOS platform. - Widget buildToolbarTopAction( - BuildContext context, - bool isLeadingAction) { + Widget buildToolbarTopAction(BuildContext context, bool isLeadingAction) { final ModalRoute? currentRoute = ModalRoute.of(context); switch (type) { - // TEXT ACTION + // TEXT ACTION case OudsTopBarActionType.text: return _CustomCupertinoButton( type: type, @@ -279,7 +275,7 @@ class OudsTopBarActionConfig { actionLabel: actionLabel, isLeadingAction: isLeadingAction, ); - // BACK ACTION (icon + optional label) + // BACK ACTION (icon + optional label) case OudsTopBarActionType.back: return _CustomCupertinoButton( contentDescription: contentDescription, @@ -288,21 +284,40 @@ class OudsTopBarActionConfig { route: currentRoute, type: type, ); - // NO ACTION + // NO ACTION case OudsTopBarActionType.none: return SizedBox.shrink(); - // ICON ACTION + // ICON ACTION case OudsTopBarActionType.icon: + final customCupertinoButton = _CustomCupertinoButton( + contentDescription: contentDescription, + type: type, + onActionPressed: onActionPressed, + icon: icon, + ); return Padding( - padding: EdgeInsetsDirectional.only(start : isLeadingAction ? 16 : 0, end: isLeadingAction ? 0 : 16), - child: _CustomCupertinoButton( - contentDescription: contentDescription, - type: type, - onActionPressed: onActionPressed, - icon: icon - ), + padding: EdgeInsetsDirectional.only( + start: isLeadingAction ? 16 : 0, + end: isLeadingAction ? 0 : 16, + ), + child: badge != null + ? (badge!.hasCount + ? OudsBadge.count( + semanticsLabel: badge?.contentDescription, + label: badge?.count.toString(), + status: Negative(), + size: OudsBadgeSize.medium, + child: customCupertinoButton, + ) + : OudsBadge.standard( + semanticsLabel: badge?.contentDescription, + status: Negative(), + size: OudsBadgeSize.xsmall, + child: customCupertinoButton, + )) + : customCupertinoButton, ); - // CUSTOM ACTION (fully custom widget) + // CUSTOM ACTION (fully custom widget) case OudsTopBarActionType.widget: return widget ?? SizedBox.shrink(); default: @@ -310,7 +325,6 @@ class OudsTopBarActionConfig { } } - /// An internal method that builds the widget for this action on the Android platform. /// /// This method is called by [OudsTopAppBar] to render the appropriate @@ -330,11 +344,9 @@ class OudsTopBarActionConfig { /// **Throws:** /// - [UnimplementedError] if the action [type] is not supported for the Material platform. /// - Widget buildTopAppbarTrailingAction(BuildContext context,bool showAvatar) { - + Widget buildTopAppbarTrailingAction(BuildContext context, bool showAvatar) { final theme = OudsTheme.of(context); - final iconButtonWithBadge = - MergeSemantics( + final iconButtonWithBadge = MergeSemantics( child: Semantics( label: contentDescription, child: BadgeIconButton( @@ -350,11 +362,10 @@ class OudsTopBarActionConfig { switch (type) { case OudsTopBarActionType.icon: return iconButtonWithBadge; - case OudsTopBarActionType.avatar :{ - return showAvatar - ? _buildAvatar(context,theme) - : SizedBox.shrink(); - } + case OudsTopBarActionType.avatar: + { + return showAvatar ? _buildAvatar(context, theme) : SizedBox.shrink(); + } case OudsTopBarActionType.widget: return widget!; default: @@ -374,11 +385,11 @@ class OudsTopBarActionConfig { child: OudsAvatar( image: avatarConfig?.image, monogramBackgroundColor: - avatarConfig?.monogramBackgroundColor ?? + avatarConfig?.monogramBackgroundColor ?? theme.colorScheme(context).surfaceInverseHigh, monogram: avatarConfig?.monogram, monogramColor: - avatarConfig?.monogramColor ?? + avatarConfig?.monogramColor ?? theme.colorScheme(context).contentOnActionEnabled, onClick: onActionPressed, ), @@ -412,7 +423,11 @@ class _ToolbarTopBackChevron extends StatelessWidget { final VoidCallback? onActionPressed; final bool isPressed; - const _ToolbarTopBackChevron(this.contentDescription, this.onActionPressed,this.isPressed); + const _ToolbarTopBackChevron( + this.contentDescription, + this.onActionPressed, + this.isPressed, + ); @override Widget build(BuildContext context) { @@ -421,21 +436,30 @@ class _ToolbarTopBackChevron extends StatelessWidget { // Builds the back icon using a modifier class, which centralizes icon creation. // The icon's appearance changes based on whether it's enabled or pressed. Widget iconWidget = Semantics( - label: contentDescription - ?? OudsLocalizations.of(context)?.core_topAppBar_backNavigationIcon_a11y, - child: actionModifier.buildBackIcon(onActionPressed != null,isPressed), + label: + contentDescription ?? + OudsLocalizations.of(context)?.core_topAppBar_backNavigationIcon_a11y, + child: actionModifier.buildBackIcon(onActionPressed != null, isPressed), ); // KeyedSubtree gives this part of the widget tree a stable identity, // which can be useful for testing or advanced framework features. - return KeyedSubtree(key: StandardComponentType.backButton.key, child: iconWidget); + return KeyedSubtree( + key: StandardComponentType.backButton.key, + child: iconWidget, + ); } } /// A private widget that displays the title of the previous page next to the /// back chevron, a common pattern in iOS navigation. class _ToolbarTopBackLabel extends StatelessWidget { - const _ToolbarTopBackLabel({required this.specifiedPreviousTitle, required this.route, this.onActionPressed, required this.isPressed}); + const _ToolbarTopBackLabel({ + required this.specifiedPreviousTitle, + required this.route, + this.onActionPressed, + required this.isPressed, + }); final String? specifiedPreviousTitle; final ModalRoute? route; @@ -443,7 +467,11 @@ class _ToolbarTopBackLabel extends StatelessWidget { final bool isPressed; // Builds the Text widget for the previous page's title. - Widget _buildPreviousTitleWidget(BuildContext context, String? previousTitle, Widget? child) { + Widget _buildPreviousTitleWidget( + BuildContext context, + String? previousTitle, + Widget? child, + ) { if (previousTitle == null) { return const SizedBox.shrink(); } @@ -454,7 +482,7 @@ class _ToolbarTopBackLabel extends StatelessWidget { previousTitle, maxLines: 1, overflow: TextOverflow.ellipsis, - style: textStyleModifier.getTextActionStyle(onActionPressed,isPressed), + style: textStyleModifier.getTextActionStyle(onActionPressed, isPressed), ); // If the title is too long, it defaults to the standard "Back" label @@ -463,7 +491,11 @@ class _ToolbarTopBackLabel extends StatelessWidget { textWidget = Text(CupertinoLocalizations.of(context).backButtonLabel); } - return Align(alignment: AlignmentDirectional.centerStart, widthFactor: 1.0, child: textWidget); + return Align( + alignment: AlignmentDirectional.centerStart, + widthFactor: 1.0, + child: textWidget, + ); } @override @@ -474,9 +506,10 @@ class _ToolbarTopBackLabel extends StatelessWidget { } // Otherwise, try to automatically get the title from the previous route // using CupertinoRouteTransitionMixin. - else if (route is CupertinoRouteTransitionMixin && !route!.isFirst) { + else if (route is CupertinoRouteTransitionMixin && + !route!.isFirst) { final CupertinoRouteTransitionMixin cupertinoRoute = - route! as CupertinoRouteTransitionMixin; + route! as CupertinoRouteTransitionMixin; // ValueListenableBuilder ensures the widget rebuilds if the previous // route's title changes. return ValueListenableBuilder( @@ -502,16 +535,16 @@ class _CustomCupertinoButton extends StatefulWidget { final String? actionLabel; final String? icon; - const _CustomCupertinoButton( - { - this.contentDescription, - this.onActionPressed, - this.previousPageTitle, - this.route, - required this.type, - this.isLeadingAction, - this.actionLabel, - this.icon}); + const _CustomCupertinoButton({ + this.contentDescription, + this.onActionPressed, + this.previousPageTitle, + this.route, + required this.type, + this.isLeadingAction, + this.actionLabel, + this.icon, + }); @override State<_CustomCupertinoButton> createState() => _CustomCupertinoButtonState(); @@ -542,34 +575,47 @@ class _CustomCupertinoButtonState extends State<_CustomCupertinoButton> { } /// Builds the appropriate CupertinoButton based on the action type. - Widget _buildButtonByType(BuildContext context, OudsToolbarTopTextStyleModifier textStyleModifier, OudsToolbarTopActionModifier actionModifier) { + Widget _buildButtonByType( + BuildContext context, + OudsToolbarTopTextStyleModifier textStyleModifier, + OudsToolbarTopActionModifier actionModifier, + ) { switch (widget.type) { case OudsTopBarActionType.back: return CupertinoButton( pressedOpacity: 1, // Disable default opacity feedback. - padding: EdgeInsetsDirectional.only(top: 5.0,start: 8), + padding: EdgeInsetsDirectional.only(top: 5.0, start: 8), child: DefaultTextStyle( - // The text style is passed the `_isPressed` state for custom feedback. - style: textStyleModifier.getTextActionStyle(widget.onActionPressed, _isPressed), - child: ConstrainedBox( - constraints: BoxConstraints(minWidth: _kNavBarBackButtonTapWidth), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - _ToolbarTopBackChevron(widget.contentDescription, widget.onActionPressed, _isPressed), - const Padding(padding: EdgeInsetsDirectional.only(start: 2.0)), - Flexible( - child: _ToolbarTopBackLabel( - specifiedPreviousTitle: widget.previousPageTitle, - route: widget.route, - onActionPressed: widget.onActionPressed, - isPressed: _isPressed, - ), + // The text style is passed the `_isPressed` state for custom feedback. + style: textStyleModifier.getTextActionStyle( + widget.onActionPressed, + _isPressed, + ), + child: ConstrainedBox( + constraints: BoxConstraints(minWidth: _kNavBarBackButtonTapWidth), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + _ToolbarTopBackChevron( + widget.contentDescription, + widget.onActionPressed, + _isPressed, + ), + const Padding( + padding: EdgeInsetsDirectional.only(start: 2.0), + ), + Flexible( + child: _ToolbarTopBackLabel( + specifiedPreviousTitle: widget.previousPageTitle, + route: widget.route, + onActionPressed: widget.onActionPressed, + isPressed: _isPressed, ), - ], - ), + ), + ], ), ), + ), onPressed: () { if (widget.onActionPressed != null) { widget.onActionPressed!(); @@ -594,7 +640,10 @@ class _CustomCupertinoButtonState extends State<_CustomCupertinoButton> { maxLines: 1, overflow: TextOverflow.ellipsis, // Style changes based on pressed state. - style: textStyleModifier.getTextActionStyle(widget.onActionPressed, _isPressed), + style: textStyleModifier.getTextActionStyle( + widget.onActionPressed, + _isPressed, + ), ), ); case OudsTopBarActionType.icon: @@ -609,7 +658,11 @@ class _CustomCupertinoButtonState extends State<_CustomCupertinoButton> { padding: EdgeInsetsDirectional.only(top: 5), onPressed: widget.onActionPressed, // The icon's appearance changes based on the pressed state. - child: actionModifier.buildActionIcon(widget.icon, widget.onActionPressed != null, _isPressed), + child: actionModifier.buildActionIcon( + widget.icon, + widget.onActionPressed != null, + _isPressed, + ), ), ), ); @@ -617,4 +670,4 @@ class _CustomCupertinoButtonState extends State<_CustomCupertinoButton> { return SizedBox.shrink(); } } -} \ No newline at end of file +} diff --git a/ouds_global_raw_tokens/CHANGELOG.md b/ouds_global_raw_tokens/CHANGELOG.md index a82803f6c..6e35b8922 100644 --- a/ouds_global_raw_tokens/CHANGELOG.md +++ b/ouds_global_raw_tokens/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://github.com/Orange-OpenSource/ouds-flutter/compare/1.3.1...develop) ### Added ### Changed +- [Library] update tokens 2.4.0 ([#726](https://github.com/Orange-OpenSource/ouds-flutter/issues/726)) + ### Fixed ## [1.3.1](https://github.com/Orange-OpenSource/ouds-flutter/compare/1.3.0...1.3.1) - 2026-05-14