diff --git a/MIGRATION.md b/MIGRATION.md index 36c3e04fe..1e2d2b226 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -57,6 +57,55 @@ The OUDS icon library has been updated to version 1.6. Some icon names or asset - Review any icon constants or asset paths you reference directly - Update to the new icon names from v1.6 if you receive compilation errors or missing asset warnings +#### 4. `OudsLink` — Dedicated named constructors for icon and navigation variants + +`OudsLink` now provides dedicated named constructors for every non text-only variant. The generic `OudsLink()` constructor's `layout` parameter is now **deprecated** in favor of these constructors. + +**Impact**: Low (additive — the default constructor keeps working for text-only and legacy usages) + +**Before**: +```dart + +OudsLink( + label: 'Label', + icon: 'assets/ic_heart.svg', +) + +OudsLink( + label: 'Label', + layout: OudsLinkLayout.back, +) + +OudsLink( + label: 'Label', + layout: OudsLinkLayout.next, +) +``` + +**After**: +```dart +OudsLink.icon( + label: 'Label', + icon: 'assets/ic_heart.svg', + tinted: true, +) + +OudsLink.previous(label: 'Label') + +OudsLink.next(label: 'Label') + +OudsLink.external(label: 'Label') +``` + +**Required Action**: +- Replace `OudsLink(icon: ...)` with `OudsLink.icon(icon: ..., tinted: ...)` +- Replace `OudsLink(layout: OudsLinkLayout.back)` with `OudsLink.previous(...)` +- Replace `OudsLink(layout: OudsLinkLayout.next)` with `OudsLink.next(...)` +- Use `OudsLink.external(...)` for links navigating outside the current product, service or application +- Text-only links keep using the default `OudsLink(...)` constructor unchanged + +**Reason for Change**: Improve type safety and API clarity by making each link variant explicit through its own constructor, consistent with `OudsTag` and `OudsBadge` + ### Component Design Version Updates Several components have been updated to align with new design specification versions. These changes are primarily visual and do not require code changes unless you override component tokens in a custom theme. diff --git a/NOTICE.txt b/NOTICE.txt index a229a3d69..1ded6f35f 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -115,6 +115,7 @@ ouds_theme_orange/assets/component/link/expanded-false.svg ouds_theme_orange/assets/component/link/expanded-true.svg ouds_theme_orange/assets/component/link/next.svg ouds_theme_orange/assets/component/link/previous.svg +ouds_theme_orange/assets/component/link/external-link.svg ouds_theme_orange/assets/component/radio-button/radio-button-selected.svg ouds_theme_orange/assets/component/switch/selected-switch.svg ouds_theme_orange/assets/component/tag/close.svg @@ -176,6 +177,7 @@ ouds_theme_orange_compact/assets/component/link/expanded-false.svg ouds_theme_orange_compact/assets/component/link/expanded-true.svg ouds_theme_orange_compact/assets/component/link/next.svg ouds_theme_orange_compact/assets/component/link/previous.svg +ouds_theme_orange_compact/assets/component/link/external-link.svg ouds_theme_orange_compact/assets/component/radio-button/radio-button-selected.svg ouds_theme_orange_compact/assets/component/switch/selected-switch.svg ouds_theme_orange_compact/assets/component/tag/close.svg @@ -238,6 +240,7 @@ ouds_theme_sosh/assets/component/link/expanded-false.svg ouds_theme_sosh/assets/component/link/expanded-true.svg ouds_theme_sosh/assets/component/link/next.svg ouds_theme_sosh/assets/component/link/previous.svg +ouds_theme_sosh/assets/component/link/external-link.svg ouds_theme_sosh/assets/component/radio-button/radio-button-selected.svg ouds_theme_sosh/assets/component/switch/selected-switch.svg ouds_theme_sosh/assets/component/tag/close.svg @@ -295,6 +298,7 @@ ouds_theme_wireframe/assets/component/link/expanded-false.svg ouds_theme_wireframe/assets/component/link/expanded-true.svg ouds_theme_wireframe/assets/component/link/next.svg ouds_theme_wireframe/assets/component/link/previous.svg +ouds_theme_wireframe/assets/component/link/external-link.svg ouds_theme_wireframe/assets/component/radio-button/radio-button-selected.svg ouds_theme_wireframe/assets/component/switch/selected-switch.svg ouds_theme_wireframe/assets/component/tag/close.svg diff --git a/app/CHANGELOG.md b/app/CHANGELOG.md index a36b523ae..684ed8476 100644 --- a/app/CHANGELOG.md +++ b/app/CHANGELOG.md @@ -8,6 +8,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - [DemoApp][Library] Create component - `List item` ([#54](https://github.com/Orange-OpenSource/ouds-flutter/issues/54)) ### Changed +- [DemoApp][Library] In library, update `link` component to v2.4.0 ([#861](https://github.com/Orange-OpenSource/ouds-flutter/issues/861)) +- [DemoApp][Library] In library, update `link` component to v2.3.0 ([#838](https://github.com/Orange-OpenSource/ouds-flutter/issues/838)) - [DemoApp][Library] For `button` component, update to version 3.3.0 ([#832](https://github.com/Orange-OpenSource/ouds-flutter/issues/832)) - [DemoApp][Library] Update `progress-indicator`: add size parameter to OudsCircularProgressIndicator for button integration ([#876](https://github.com/Orange-OpenSource/ouds-flutter/issues/876)) ### Fixed diff --git a/app/lib/l10n/gen/ouds_flutter_app_localizations.dart b/app/lib/l10n/gen/ouds_flutter_app_localizations.dart index 04b55555c..8cde13cf1 100644 --- a/app/lib/l10n/gen/ouds_flutter_app_localizations.dart +++ b/app/lib/l10n/gen/ouds_flutter_app_localizations.dart @@ -481,6 +481,12 @@ abstract class AppLocalizations { /// **'Size'** String get app_components_common_size_label; + /// No description provided for @app_components_common_density_label. + /// + /// In en, this message translates to: + /// **'Density'** + String get app_components_common_density_label; + /// No description provided for @app_components_common_textAndLoaderLayout_label. /// /// In en, this message translates to: @@ -625,6 +631,12 @@ abstract class AppLocalizations { /// **'Type'** String get app_components_common_type_tech; + /// No description provided for @app_components_common_tinted_tech. + /// + /// In en, this message translates to: + /// **'Tinted icon'** + String get app_components_common_tinted_tech; + /// No description provided for @app_components_alert_tech. /// /// In en, this message translates to: @@ -1224,11 +1236,11 @@ abstract class AppLocalizations { /// **'The phone number can\'t be empty.'** String get app_components_phoneNumberInput_error_label; - /// No description provided for @app_components_link_label. + /// No description provided for @app_components_link_tech. /// /// In en, this message translates to: /// **'Link'** - String get app_components_link_label; + String get app_components_link_tech; /// No description provided for @app_components_link_description_text. /// @@ -1236,17 +1248,23 @@ abstract class AppLocalizations { /// **'Link is a UI element that allows to navigate from one location to another, either within the same page or across different pages in the same resource, or to an external resource.'** String get app_components_link_description_text; - /// No description provided for @app_components_link_backLayout_label. + /// No description provided for @app_components_link_previousLayout_tech. /// /// In en, this message translates to: - /// **'Back'** - String get app_components_link_backLayout_label; + /// **'Previous'** + String get app_components_link_previousLayout_tech; - /// No description provided for @app_components_link_nextLayout_label. + /// No description provided for @app_components_link_nextLayout_tech. /// /// In en, this message translates to: /// **'Next'** - String get app_components_link_nextLayout_label; + String get app_components_link_nextLayout_tech; + + /// No description provided for @app_components_link_externalLayout_tech. + /// + /// In en, this message translates to: + /// **'External'** + String get app_components_link_externalLayout_tech; /// No description provided for @app_components_listItem_tech. /// diff --git a/app/lib/l10n/gen/ouds_flutter_app_localizations_ar.dart b/app/lib/l10n/gen/ouds_flutter_app_localizations_ar.dart index c419ca05d..49d23cf85 100644 --- a/app/lib/l10n/gen/ouds_flutter_app_localizations_ar.dart +++ b/app/lib/l10n/gen/ouds_flutter_app_localizations_ar.dart @@ -212,6 +212,9 @@ class AppLocalizationsAr extends AppLocalizations { @override String get app_components_common_size_label => 'Size'; + @override + String get app_components_common_density_label => 'Density'; + @override String get app_components_common_textAndLoaderLayout_label => 'Text + Loader'; @@ -286,6 +289,9 @@ class AppLocalizationsAr extends AppLocalizations { @override String get app_components_common_type_tech => 'Type'; + @override + String get app_components_common_tinted_tech => 'Tinted icon'; + @override String get app_components_alert_tech => 'Alert'; @@ -636,17 +642,20 @@ class AppLocalizationsAr extends AppLocalizations { 'لا يمكن أن يكون رقم الهاتف فارغًا.'; @override - String get app_components_link_label => 'Link'; + String get app_components_link_tech => 'Link'; @override String get app_components_link_description_text => 'Link هو عنصر واجهة مستخدم يسمح بالتنقل من مكان إلى آخر، سواء داخل نفس الصفحة أو عبر صفحات مختلفة في نفس المورد، أو إلى مورد خارجي'; @override - String get app_components_link_backLayout_label => 'Back'; + String get app_components_link_previousLayout_tech => 'Previous'; + + @override + String get app_components_link_nextLayout_tech => 'Next'; @override - String get app_components_link_nextLayout_label => 'Next'; + String get app_components_link_externalLayout_tech => 'External'; @override String get app_components_listItem_tech => 'List item'; diff --git a/app/lib/l10n/gen/ouds_flutter_app_localizations_en.dart b/app/lib/l10n/gen/ouds_flutter_app_localizations_en.dart index 0c8cd3cd7..74db3782a 100644 --- a/app/lib/l10n/gen/ouds_flutter_app_localizations_en.dart +++ b/app/lib/l10n/gen/ouds_flutter_app_localizations_en.dart @@ -212,6 +212,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get app_components_common_size_label => 'Size'; + @override + String get app_components_common_density_label => 'Density'; + @override String get app_components_common_textAndLoaderLayout_label => 'Text + Loader'; @@ -286,6 +289,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get app_components_common_type_tech => 'Type'; + @override + String get app_components_common_tinted_tech => 'Tinted icon'; + @override String get app_components_alert_tech => 'Alert'; @@ -636,17 +642,20 @@ class AppLocalizationsEn extends AppLocalizations { 'The phone number can\'t be empty.'; @override - String get app_components_link_label => 'Link'; + String get app_components_link_tech => 'Link'; @override String get app_components_link_description_text => 'Link is a UI element that allows to navigate from one location to another, either within the same page or across different pages in the same resource, or to an external resource.'; @override - String get app_components_link_backLayout_label => 'Back'; + String get app_components_link_previousLayout_tech => 'Previous'; + + @override + String get app_components_link_nextLayout_tech => 'Next'; @override - String get app_components_link_nextLayout_label => 'Next'; + String get app_components_link_externalLayout_tech => 'External'; @override String get app_components_listItem_tech => 'List item'; diff --git a/app/lib/l10n/gen/ouds_flutter_app_localizations_fr.dart b/app/lib/l10n/gen/ouds_flutter_app_localizations_fr.dart index e8c89d720..792d88730 100644 --- a/app/lib/l10n/gen/ouds_flutter_app_localizations_fr.dart +++ b/app/lib/l10n/gen/ouds_flutter_app_localizations_fr.dart @@ -214,6 +214,9 @@ class AppLocalizationsFr extends AppLocalizations { @override String get app_components_common_size_label => 'Size'; + @override + String get app_components_common_density_label => 'Density'; + @override String get app_components_common_textAndLoaderLayout_label => 'Text + Loader'; @@ -288,6 +291,9 @@ class AppLocalizationsFr extends AppLocalizations { @override String get app_components_common_type_tech => 'Type'; + @override + String get app_components_common_tinted_tech => 'Tinted icon'; + @override String get app_components_alert_tech => 'Alert'; @@ -640,17 +646,20 @@ class AppLocalizationsFr extends AppLocalizations { 'The phone number can\'t be empty.'; @override - String get app_components_link_label => 'Link'; + String get app_components_link_tech => 'Link'; @override String get app_components_link_description_text => 'Un Link (ou lien) est un élément d\'interface utilisateur qui permet de naviguer d\'un emplacement à un autre, que ce soit au sein de la même page, entre différentes pages de la même ressource ou vers une ressource externe.'; @override - String get app_components_link_backLayout_label => 'Back'; + String get app_components_link_previousLayout_tech => 'Previous'; + + @override + String get app_components_link_nextLayout_tech => 'Next'; @override - String get app_components_link_nextLayout_label => 'Next'; + String get app_components_link_externalLayout_tech => 'External'; @override String get app_components_listItem_tech => 'List item'; diff --git a/app/lib/l10n/ouds_flutter_en.arb b/app/lib/l10n/ouds_flutter_en.arb index 314629860..ea1ad5fa6 100644 --- a/app/lib/l10n/ouds_flutter_en.arb +++ b/app/lib/l10n/ouds_flutter_en.arb @@ -113,6 +113,7 @@ "app_components_common_onColoredBackground_label": "On colored background", "app_components_common_status_label": "Status", "app_components_common_size_label": "Size", + "app_components_common_density_label": "Density", "app_components_common_textAndLoaderLayout_label": "Text + Loader", "app_components_common_roundedCorner_label": "Rounded Corner", "app_components_common_loader_label": "Loader", @@ -137,6 +138,7 @@ "app_components_common_description_tech": "Description", "app_components_common_edgeToEdge_tech": "Edge to edge", "app_components_common_type_tech" : "Type", + "app_components_common_tinted_tech": "Tinted icon", "@_components_alert": {}, "app_components_alert_tech": "Alert", @@ -273,10 +275,11 @@ "app_components_phoneNumberInput_error_label": "The phone number can't be empty.", "@_components_link": {}, - "app_components_link_label": "Link", + "app_components_link_tech": "Link", "app_components_link_description_text": "Link is a UI element that allows to navigate from one location to another, either within the same page or across different pages in the same resource, or to an external resource.", - "app_components_link_backLayout_label": "Back", - "app_components_link_nextLayout_label": "Next", + "app_components_link_previousLayout_tech": "Previous", + "app_components_link_nextLayout_tech": "Next", + "app_components_link_externalLayout_tech": "External", "@_components_list_item": {}, "app_components_listItem_tech" : "List item", diff --git a/app/lib/ui/components/components.dart b/app/lib/ui/components/components.dart index 1ef2b454c..ba54540b6 100644 --- a/app/lib/ui/components/components.dart +++ b/app/lib/ui/components/components.dart @@ -316,16 +316,10 @@ List components(BuildContext context) { ], ), Component( - context.l10n.app_components_link_label, + context.l10n.app_components_link_tech, ComponentContainer( child: Column( - children: [ - OudsLink( - label: "Link", - layout: OudsLinkLayout.next, - onPressed: () {}, - ), - ], + children: [OudsLink.next(label: "Link", onPressed: () {})], ), ), context.l10n.app_components_link_description_text, diff --git a/app/lib/ui/components/link/link_code_generator.dart b/app/lib/ui/components/link/link_code_generator.dart index adb0d48b1..99035b243 100644 --- a/app/lib/ui/components/link/link_code_generator.dart +++ b/app/lib/ui/components/link/link_code_generator.dart @@ -14,6 +14,7 @@ import 'package:flutter/material.dart'; import 'package:ouds_core/components/link/ouds_link.dart'; import 'package:ouds_flutter_demo/ui/components/link/link_customization.dart'; import 'package:ouds_flutter_demo/ui/components/link/link_customization_utils.dart'; +import 'package:ouds_flutter_demo/ui/components/link/link_enum.dart'; /// /// The TagCodeGenerator class is responsible for dynamically generating Flutter @@ -26,34 +27,54 @@ class LinkCodeGenerator { // Static method to generate the code based on link customization state static String updateCode(BuildContext context) { // Fetch the current tag customization state from context - final LinkCustomizationState? customizationState = LinkCustomization.of(context); + final LinkCustomizationState? customizationState = LinkCustomization.of( + context, + ); // Get the text value for the tag from customization state String label = customizationState?.labelText ?? "Label"; // Get layout & size from customization state - OudsLinkLayout layout = - LinkCustomizationUtils.getLayout(customizationState?.selectedLayout as Object); - OudsLinkSize size = - LinkCustomizationUtils.getSize(customizationState?.selectedSize as Object); + LinkEnumLayout layout = + customizationState?.selectedLayout ?? LinkEnumLayout.textOnly; + OudsLinkSize size = LinkCustomizationUtils.getSize( + customizationState?.selectedSize as Object, + ); + OudsLinkDensity density = LinkCustomizationUtils.getDensity( + customizationState?.selectedDensity as Object, + ); - String? sizeCode = size == OudsLinkSize.small ? OudsLinkSize.small.toString() : OudsLinkSize.defaultSize.toString(); - String? layoutCode = layout.toString(); - String? iconCode = - layout == OudsLinkLayout.textAndIcon ? "assets/ic_heart.svg" : null; + String? sizeCode = size == OudsLinkSize.small + ? OudsLinkSize.small.toString() + : 'OudsLinkSize.default'; + String? densityCode = density == OudsLinkDensity.compact + ? OudsLinkDensity.compact.toString() + : 'OudsLinkDensity.default'; String? pressedCode = " ${customizationState?.hasEnabled == true ? "() {}" : 'null'}"; + // Determines the dedicated OudsLink constructor name matching the layout. + String constructorName = switch (layout) { + LinkEnumLayout.textAndIcon => "OudsLink.icon", + LinkEnumLayout.next => "OudsLink.next", + LinkEnumLayout.previous => "OudsLink.previous", + LinkEnumLayout.external => "OudsLink.external", + LinkEnumLayout.textOnly => "OudsLink", + }; + List params = [ ' label: "$label",', + if (layout == LinkEnumLayout.textAndIcon) ...[ + ' icon: "assets/ic_heart.svg",', + ' tinted: ${customizationState?.isTinted ?? true},', + ], ' size: $sizeCode,', - ' layout: $layoutCode,', - if (iconCode != null) ' icon: "$iconCode",', + ' density: $densityCode,', ' onPressed:$pressedCode', ]; // Base widget - String code = "OudsLink(\n${params.join('\n')}\n)"; + String code = "$constructorName(\n${params.join('\n')}\n)"; // Apply colored surface wrapper if needed code = coloredSurfaceCodeModifier(context, code); @@ -62,9 +83,13 @@ class LinkCodeGenerator { } // Method to wrap code with colored background or theme if needed - static String coloredSurfaceCodeModifier(BuildContext context, String childCode) { - final LinkCustomizationState? customizationState = LinkCustomization.of(context); - + static String coloredSurfaceCodeModifier( + BuildContext context, + String childCode, + ) { + final LinkCustomizationState? customizationState = LinkCustomization.of( + context, + ); if (customizationState?.hasOnColoredBox == true) { return ''' diff --git a/app/lib/ui/components/link/link_customization.dart b/app/lib/ui/components/link/link_customization.dart index 0f7ce4455..8534b7aad 100644 --- a/app/lib/ui/components/link/link_customization.dart +++ b/app/lib/ui/components/link/link_customization.dart @@ -1,13 +1,10 @@ import 'package:flutter/material.dart'; -import 'package:ouds_flutter_demo/ui/utilities/customizable/customizable_widget_state.dart'; import 'package:ouds_flutter_demo/ui/components/link/link_enum.dart'; +import 'package:ouds_flutter_demo/ui/utilities/customizable/customizable_widget_state.dart'; /// Section for InheritedWidget to pass data down the widget tree class _LinkCustomization extends InheritedWidget { - const _LinkCustomization({ - required super.child, - required this.data, - }); + const _LinkCustomization({required super.child, required this.data}); final LinkCustomizationState data; @@ -17,10 +14,7 @@ class _LinkCustomization extends InheritedWidget { /// Main Widget class for tag customization class LinkCustomization extends StatefulWidget { - const LinkCustomization({ - super.key, - required this.child, - }); + const LinkCustomization({super.key, required this.child}); final Widget child; @@ -28,15 +22,19 @@ class LinkCustomization extends StatefulWidget { LinkCustomizationState createState() => LinkCustomizationState(); static LinkCustomizationState? of(BuildContext context) { - return (context.dependOnInheritedWidgetOfExactType<_LinkCustomization>())?.data; + return (context.dependOnInheritedWidgetOfExactType<_LinkCustomization>()) + ?.data; } } /// tag customization state management -class LinkCustomizationState extends CustomizationWidgetState { +class LinkCustomizationState + extends CustomizationWidgetState { late final LayoutState layoutState; late final LabelTextState labelTextState; late final SizeState sizeState; + late final DensityState densityState; + late final TintedState tintedState; @override void initState() { @@ -44,6 +42,8 @@ class LinkCustomizationState extends CustomizationWidgetState layoutState = LayoutState(setState); labelTextState = LabelTextState(setState); sizeState = SizeState(setState); + densityState = DensityState(setState); + tintedState = TintedState(setState); } LinkEnumLayout get selectedLayout => layoutState.selected; @@ -55,17 +55,18 @@ class LinkCustomizationState extends CustomizationWidgetState LinkEnumSize get selectedSize => sizeState.selected; set selectedSize(LinkEnumSize value) => sizeState.selected = value; + LinkEnumDensity get selectedDensity => densityState.selected; + set selectedDensity(LinkEnumDensity value) => densityState.selected = value; + + bool get isTinted => tintedState.value; + set isTinted(bool value) => tintedState.value = value; @override Widget build(BuildContext context) { - return _LinkCustomization( - data: this, - child: widget.child, - ); + return _LinkCustomization(data: this, child: widget.child); } } - /// Layout State Management class LayoutState { LayoutState(this._setState); @@ -74,8 +75,9 @@ class LayoutState { final List _layout = [ LinkEnumLayout.textOnly, LinkEnumLayout.textAndIcon, - LinkEnumLayout.back, + LinkEnumLayout.previous, LinkEnumLayout.next, + LinkEnumLayout.external, ]; List get list => _layout; @@ -110,10 +112,7 @@ class SizeState { final void Function(void Function()) _setState; - List _sizeList = [ - LinkEnumSize.defaultSize, - LinkEnumSize.small - ]; + List _sizeList = [LinkEnumSize.defaultSize, LinkEnumSize.small]; LinkEnumSize _selectedSize = LinkEnumSize.defaultSize; List get list => _sizeList; @@ -130,3 +129,48 @@ class SizeState { }); } } + +/// Tinted State Management +/// +/// Controls whether the icon displayed by [OudsLink.icon] should be tinted +/// with the theme color, or shown with its original colors. +class TintedState { + TintedState(this._setState); + + final void Function(void Function()) _setState; + bool _tinted = true; + + bool get value => _tinted; + set value(bool newValue) { + _setState(() { + _tinted = newValue; + }); + } +} + +/// Density State Management +class DensityState { + DensityState(this._setState); + + final void Function(void Function()) _setState; + + List _densityList = [ + LinkEnumDensity.defaultDensity, + LinkEnumDensity.compact, + ]; + LinkEnumDensity _selectedDensity = LinkEnumDensity.defaultDensity; + + List get list => _densityList; + set list(List newList) { + _setState(() { + _densityList = newList; + }); + } + + LinkEnumDensity get selected => _selectedDensity; + set selected(LinkEnumDensity newValue) { + _setState(() { + _selectedDensity = newValue; + }); + } +} diff --git a/app/lib/ui/components/link/link_customization_utils.dart b/app/lib/ui/components/link/link_customization_utils.dart index 2e57730f3..c93997355 100644 --- a/app/lib/ui/components/link/link_customization_utils.dart +++ b/app/lib/ui/components/link/link_customization_utils.dart @@ -10,6 +10,7 @@ // Software description: Flutter library of reusable graphical components // +import 'package:flutter/foundation.dart'; import 'package:ouds_core/components/link/ouds_link.dart'; import 'package:ouds_flutter_demo/ui/components/link/link_customization.dart'; import 'package:ouds_flutter_demo/ui/components/link/link_enum.dart'; @@ -19,22 +20,63 @@ import 'package:ouds_flutter_demo/ui/utilities/app_assets.dart'; /// Utility class to map tag customization options to corresponding OudsLink attributes. /// /// This class provides static methods to convert customization enums into the appropriate -/// [OudsLink] properties. It includes methods for determining the link layout based on the input enum values. -/// These methods help in translating +/// [OudsLink] properties. It includes methods for building the dedicated [OudsLink] +/// constructor matching the selected layout. These methods help in translating /// user-selected options into code that is used for link customization and rendering. class LinkCustomizationUtils { - /// Maps the layout enum to `OudsLinkLayout`. - static OudsLinkLayout getLayout(Object layout) { - switch (layout) { + /// Builds the [OudsLink] widget matching the selected layout, using the + /// dedicated constructor for each variant (icon, previous, next, external). + static OudsLink buildLink({ + required LinkCustomizationState customizationState, + required ThemeController themeController, + required VoidCallback? onPressed, + }) { + final label = getText(customizationState); + final size = getSize(customizationState.selectedSize); + final density = getDensity(customizationState.selectedDensity); + final tinted = customizationState.isTinted; + + switch (customizationState.selectedLayout) { case LinkEnumLayout.textAndIcon: - return OudsLinkLayout.textAndIcon; + return OudsLink.icon( + label: label, + icon: tinted + ? AppAssets.icons.assistanceTipsAndTricks(themeController) + : AppAssets.icons.icUntintedSquare, + size: size, + density: density, + onPressed: onPressed, + tinted: tinted, + ); case LinkEnumLayout.next: - return OudsLinkLayout.next; - case LinkEnumLayout.back: - return OudsLinkLayout.back; - default: - return OudsLinkLayout.textOnly; + return OudsLink.next( + label: label, + size: size, + density: density, + onPressed: onPressed, + ); + case LinkEnumLayout.previous: + return OudsLink.previous( + label: label, + size: size, + density: density, + onPressed: onPressed, + ); + case LinkEnumLayout.external: + return OudsLink.external( + label: label, + size: size, + density: density, + onPressed: onPressed, + ); + case LinkEnumLayout.textOnly: + return OudsLink( + label: label, + size: size, + density: density, + onPressed: onPressed, + ); } } @@ -43,14 +85,6 @@ class LinkCustomizationUtils { return customizationState.labelText; } - /// Determines the icon to display based on the selected layout. - static String? getIcon(LinkCustomizationState? customizationState, ThemeController themeController) { - if (customizationState?.selectedLayout == LinkEnumLayout.textAndIcon) { - return AppAssets.icons.functionalSocialAndEngagementHeartEmpty(themeController); - } - return null; - } - /// Maps the enum to `OudsLinkSize`. static OudsLinkSize getSize(Object size) { switch (size) { @@ -60,4 +94,14 @@ class LinkCustomizationUtils { return OudsLinkSize.defaultSize; } } + + /// Maps the enum to `OudsLinkDensity`. + static OudsLinkDensity getDensity(Object density) { + switch (density) { + case LinkEnumDensity.compact: + return OudsLinkDensity.compact; + default: + return OudsLinkDensity.defaultDensity; + } + } } diff --git a/app/lib/ui/components/link/link_demo_screen.dart b/app/lib/ui/components/link/link_demo_screen.dart index a198e7d07..a796d7b6c 100644 --- a/app/lib/ui/components/link/link_demo_screen.dart +++ b/app/lib/ui/components/link/link_demo_screen.dart @@ -12,7 +12,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:ouds_core/components/link/ouds_link.dart'; import 'package:ouds_flutter_demo/l10n/app_localizations.dart'; import 'package:ouds_flutter_demo/main_app_bar.dart'; import 'package:ouds_flutter_demo/ui/components/link/link_code_generator.dart'; @@ -36,7 +35,7 @@ import 'package:provider/provider.dart'; class LinkDemoScreen extends StatefulWidget { final String? previousPageTitle; - const LinkDemoScreen({super.key,this.previousPageTitle}); + const LinkDemoScreen({super.key, this.previousPageTitle}); @override State createState() => _LinkDemoScreenState(); @@ -57,7 +56,11 @@ class _LinkDemoScreenState extends State { return DismissKeyboard( child: LinkCustomization( child: 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, @@ -67,11 +70,14 @@ class _LinkDemoScreenState extends State { key: _scaffoldKey, extendBodyBehindAppBar: true, appBar: MainAppBar( - showBackButton: true, - title: context.l10n.app_components_link_label, - previousPageTitle: widget.previousPageTitle, + showBackButton: true, + title: context.l10n.app_components_link_tech, + previousPageTitle: widget.previousPageTitle, + ), + body: ExcludeSemantics( + excluding: !_isBottomSheetExpanded, + child: _Body(), ), - body: ExcludeSemantics(excluding: !_isBottomSheetExpanded, child: _Body()), ), ), ), @@ -90,19 +96,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_link_description_text, widget: Column( children: [ _LinkDemo(), - SizedBox(height: themeController.currentTheme.spaceScheme(context).fixedMedium), - Code( - code: LinkCodeGenerator.updateCode(context), + SizedBox( + height: themeController.currentTheme + .spaceScheme(context) + .fixedMedium, ), - ReferenceDesignVersionComponent( - version: OudsComponentVersion.link, - ) + Code(code: LinkCodeGenerator.updateCode(context)), + ReferenceDesignVersionComponent(version: OudsComponentVersion.link), ], ), ); @@ -136,23 +145,20 @@ class _LinkDemoState extends State<_LinkDemo> { if (customizationState?.hasOnColoredBox == true) { return ComponentDemoBox( colored: customizationState?.hasOnColoredBox == true, - child: OudsLink( - label: customizationState!.labelText, - icon: LinkCustomizationUtils.getIcon(customizationState, themeController!), - size: LinkCustomizationUtils.getSize(customizationState?.selectedSize as Object), - layout: LinkCustomizationUtils.getLayout(customizationState?.selectedLayout as Object), - onPressed: customizationState?.hasEnabled == true ? () {} : null, + child: LinkCustomizationUtils.buildLink( + customizationState: customizationState!, + themeController: themeController!, + onPressed: customizationState!.hasEnabled == true ? () {} : null, ), ); } else { return LightDarkBox( - child: OudsLink( - label: customizationState!.labelText, - icon: LinkCustomizationUtils.getIcon(customizationState, themeController!), - size: LinkCustomizationUtils.getSize(customizationState?.selectedSize as Object), - layout: LinkCustomizationUtils.getLayout(customizationState?.selectedLayout as Object), - onPressed: customizationState?.hasEnabled == true ? () {} : null, - )); + child: LinkCustomizationUtils.buildLink( + customizationState: customizationState!, + themeController: themeController!, + onPressed: customizationState!.hasEnabled == true ? () {} : null, + ), + ); } } } @@ -183,7 +189,9 @@ class _CustomizationContentState extends State<_CustomizationContent> { @override Widget build(BuildContext context) { - final LinkCustomizationState? customizationState = LinkCustomization.of(context); + final LinkCustomizationState? customizationState = LinkCustomization.of( + context, + ); return CustomizableSection( children: [ @@ -214,6 +222,17 @@ class _CustomizationContentState extends State<_CustomizationContent> { }); }, ), + CustomizableChips( + title: LinkEnumDensity.enumName(context), + options: customizationState.densityState.list, + selectedOption: customizationState.selectedDensity, + getText: (option) => option.stringValue(context), + onSelected: (selectedOption) { + setState(() { + customizationState.selectedDensity = selectedOption; + }); + }, + ), CustomizableChips( title: LinkEnumLayout.enumName(context), options: customizationState.layoutState.list, @@ -225,13 +244,23 @@ class _CustomizationContentState extends State<_CustomizationContent> { }); }, ), + if (customizationState.selectedLayout == LinkEnumLayout.textAndIcon) + CustomizableSwitch( + title: context.l10n.app_components_common_tinted_tech, + value: customizationState.isTinted, + onChanged: (value) { + setState(() { + customizationState.isTinted = value; + }); + }, + ), CustomizableTextField( title: context.l10n.app_components_common_label_label, text: customizationState.labelText, focusNode: labelFocus, fieldType: FieldType.label, - ) + ), ], ); } -} \ No newline at end of file +} diff --git a/app/lib/ui/components/link/link_enum.dart b/app/lib/ui/components/link/link_enum.dart index 349252f7a..1b4508ec1 100644 --- a/app/lib/ui/components/link/link_enum.dart +++ b/app/lib/ui/components/link/link_enum.dart @@ -7,12 +7,14 @@ enum LinkEnumLayout { textOnly, textAndIcon, next, - back; + previous, + external; static String enumName(BuildContext context) { return context.l10n.app_components_common_layout_label; } } + extension CustomElementLayout on LinkEnumLayout { String stringValue(BuildContext context) { final l10n = context.l10n; @@ -23,9 +25,11 @@ extension CustomElementLayout on LinkEnumLayout { case LinkEnumLayout.textAndIcon: return l10n.app_components_common_iconAndTextLayout_label; case LinkEnumLayout.next: - return l10n.app_components_link_nextLayout_label; - case LinkEnumLayout.back: - return l10n.app_components_link_backLayout_label; + return l10n.app_components_link_nextLayout_tech; + case LinkEnumLayout.previous: + return l10n.app_components_link_previousLayout_tech; + case LinkEnumLayout.external: + return l10n.app_components_link_externalLayout_tech; } } } @@ -38,17 +42,36 @@ enum LinkEnumSize { static String enumName(BuildContext context) { return context.l10n.app_components_common_size_label; } - } + extension CustomElementSize on LinkEnumSize { String stringValue(BuildContext context) { - switch (this) { case LinkEnumSize.defaultSize: return capitalizeEnumValue(LinkEnumSize.defaultSize); case LinkEnumSize.small: return capitalizeEnumValue(LinkEnumSize.small); + } + } +} + +/// Represents the density of an OUDS Link. +enum LinkEnumDensity { + defaultDensity, + compact; + static String enumName(BuildContext context) { + return context.l10n.app_components_common_density_label; + } +} + +extension CustomElementDensity on LinkEnumDensity { + String stringValue(BuildContext context) { + switch (this) { + case LinkEnumDensity.defaultDensity: + return capitalizeEnumValue(LinkEnumDensity.defaultDensity); + case LinkEnumDensity.compact: + return capitalizeEnumValue(LinkEnumDensity.compact); } } -} \ No newline at end of file +} diff --git a/app/lib/ui/components/progress_indicator/progress_indicator_customization.dart b/app/lib/ui/components/progress_indicator/progress_indicator_customization.dart index aa99243fa..790b4ffd1 100644 --- a/app/lib/ui/components/progress_indicator/progress_indicator_customization.dart +++ b/app/lib/ui/components/progress_indicator/progress_indicator_customization.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:ouds_flutter_demo/ui/components/link/link_enum.dart'; import 'package:ouds_flutter_demo/ui/components/progress_indicator/progress_indicator_enum.dart'; import 'package:ouds_flutter_demo/ui/utilities/component/status_enum.dart'; import 'package:ouds_flutter_demo/ui/utilities/customizable/customizable_widget_state.dart'; @@ -41,7 +40,6 @@ class ProgressIndicatorCustomization extends StatefulWidget { class ProgressIndicatorCustomizationState extends CustomizationWidgetState { late final ValueState valueState; - late final SizeState sizeState; late final StatusState statusState; late final TrackState trackState; late final AnimatedState animatedState; @@ -57,7 +55,6 @@ class ProgressIndicatorCustomizationState void initState() { super.initState(); valueState = ValueState(setState); - sizeState = SizeState(setState); statusState = StatusState(setState); trackState = TrackState(setState); animatedState = AnimatedState(setState); @@ -73,9 +70,6 @@ class ProgressIndicatorCustomizationState String get value => valueState.value; set value(String value) => valueState.value = value; - LinkEnumSize get selectedSize => sizeState.selected; - set selectedSize(LinkEnumSize value) => sizeState.selected = value; - // Proxy getters and setters to expose state values directly StatusEnum get selectedStatus => statusState.selectedStatus; set selectedStatus(StatusEnum value) => statusState.selectedStatus = value; @@ -213,30 +207,6 @@ class AnimatedState { } } -/// Size State Management -class SizeState { - SizeState(this._setState); - - final void Function(void Function()) _setState; - - List _sizeList = [LinkEnumSize.defaultSize, LinkEnumSize.small]; - LinkEnumSize _selectedSize = LinkEnumSize.defaultSize; - - List get list => _sizeList; - set list(List newList) { - _setState(() { - _sizeList = newList; - }); - } - - LinkEnumSize get selected => _selectedSize; - set selected(LinkEnumSize newValue) { - _setState(() { - _selectedSize = newValue; - }); - } -} - /// Type State Management class TypeState { TypeState(this._setState); diff --git a/app/lib/ui/utilities/light_dark_box.dart b/app/lib/ui/utilities/light_dark_box.dart index 920e4c759..a41636c5d 100644 --- a/app/lib/ui/utilities/light_dark_box.dart +++ b/app/lib/ui/utilities/light_dark_box.dart @@ -210,7 +210,6 @@ class ColoredBox extends StatelessWidget { @override Widget build(BuildContext context) { return Container( - constraints: BoxConstraints(minHeight: 80), width: double.infinity, color: color?.getValue(context), child: Padding( diff --git a/ouds_core/CHANGELOG.md b/ouds_core/CHANGELOG.md index b663443b2..61e84c0f0 100644 --- a/ouds_core/CHANGELOG.md +++ b/ouds_core/CHANGELOG.md @@ -8,6 +8,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - [Library] Create component - `List item` ([#54](https://github.com/Orange-OpenSource/ouds-flutter/issues/54)) ### Changed +- [Library] In library, update `link` component to v2.4.0 ([#861](https://github.com/Orange-OpenSource/ouds-flutter/issues/861)) +- [Library] In library, update `link` component to v2.3.0 ([#838](https://github.com/Orange-OpenSource/ouds-flutter/issues/838)) - [Library] For `button` component, update to version 3.3.0 ([#832](https://github.com/Orange-OpenSource/ouds-flutter/issues/832)) - [Library] Update `progress-indicator`: add size parameter to OudsCircularProgressIndicator for button integration ([#876](https://github.com/Orange-OpenSource/ouds-flutter/issues/876)) ### Fixed diff --git a/ouds_core/README.md b/ouds_core/README.md index 322c98c91..e29c8f17a 100644 --- a/ouds_core/README.md +++ b/ouds_core/README.md @@ -108,7 +108,7 @@ It is intended to replace internal frameworks and the previous [ODS](https://git Link - 2.2.0 + 2.4.0 Navigation Bar diff --git a/ouds_core/lib/components/link/internal/ouds_link_control_state.dart b/ouds_core/lib/components/link/internal/ouds_link_control_state.dart index fa65d464c..ad1f564d2 100644 --- a/ouds_core/lib/components/link/internal/ouds_link_control_state.dart +++ b/ouds_core/lib/components/link/internal/ouds_link_control_state.dart @@ -15,13 +15,7 @@ library; /// Enum representing the state of the Link control. -enum OudsLinkControlState { - enabled, - hovered, - pressed, - disabled, - focused, -} +enum OudsLinkControlState { enabled, hovered, pressed, disabled, focused } /// A class that determines the state of the OudsLink class OudsLinkControlStateDeterminer { diff --git a/ouds_core/lib/components/link/internal/ouds_link_size_modifier.dart b/ouds_core/lib/components/link/internal/ouds_link_size_modifier.dart index 4e75dbf29..99224c3b6 100644 --- a/ouds_core/lib/components/link/internal/ouds_link_size_modifier.dart +++ b/ouds_core/lib/components/link/internal/ouds_link_size_modifier.dart @@ -26,13 +26,24 @@ class OudsLinkSizeModifier { OudsLinkSizeModifier(this.context); - /// Retrieves the min width and min height (double) for the Link based on the provided size enum. - Map getMinWidthAndHeight(OudsLinkSize? size) { + /// Retrieves the min width and min height (double) for the Link based on the provided size and density. + Map getMinWidthAndHeight( + OudsLinkSize size, + OudsLinkDensity density, + ) { final theme = OudsTheme.of(context).componentsTokens(context).link; + if (density == OudsLinkDensity.compact) { + return { + OudsLinkDimensions.width.name: theme.sizeMinWidth, + OudsLinkDimensions.height.name: theme.sizeMinHeightCompactDensity, + }; + } + + //default density if (size == OudsLinkSize.small) { return { - OudsLinkDimensions.width.name: theme.sizeMinWidthSmall, + OudsLinkDimensions.width.name: theme.sizeMinWidth, OudsLinkDimensions.height.name: theme.sizeMinHeightSmall, }; } else { @@ -44,7 +55,7 @@ class OudsLinkSizeModifier { } /// Retrieves the width and height of icon/ chevron (double) for the Link based on the provided size enum. - Map getIconSize(OudsLinkSize? size) { + Map getIconSize(OudsLinkSize size) { final theme = OudsTheme.of(context).componentsTokens(context).link; if (size == OudsLinkSize.small) { @@ -61,9 +72,25 @@ class OudsLinkSizeModifier { } /// Retrieves the spaceColumnGap (double) for the Link based on the provided size enum. - double? getSizeColumnGap(OudsLinkSize? size, OudsLinkLayout layout) { + // the param layout will be removed + double? getSizeColumnGap( + OudsLinkSize size, + OudsLinkLayout layout, + OudsLinkIndicator? indicator, + String? icon, + ) { final linkToken = OudsTheme.of(context).componentsTokens(context).link; + if (indicator == null && icon != null) { + //textAndIcon + return size == OudsLinkSize.small + ? linkToken.spaceColumnGapIconSmall + : linkToken.spaceColumnGapIconDefault; + } else if (indicator != null) { + return size == OudsLinkSize.small + ? linkToken.spaceColumnGapChevronSmall + : linkToken.spaceColumnGapChevronDefault; + } switch (layout) { case OudsLinkLayout.textAndIcon: return size == OudsLinkSize.small @@ -80,4 +107,31 @@ class OudsLinkSizeModifier { : linkToken.spaceColumnGapChevronDefault; } } + + /// Retrieves the block/inline padding ([EdgeInsetsDirectional]) for the Link based on the provided size and density. + EdgeInsetsDirectional getPadding(OudsLinkSize size, OudsLinkDensity density) { + final linkToken = OudsTheme.of(context).componentsTokens(context).link; + + if (density == OudsLinkDensity.compact) { + return EdgeInsetsDirectional.symmetric( + vertical: size == OudsLinkSize.small + ? linkToken.spacePaddingBlockCompactDensitySmall + : linkToken.spacePaddingBlockCompactDensityDefault, + horizontal: linkToken.spacePaddingInline, + ); + } + + // density = Default + if (size == OudsLinkSize.small) { + return EdgeInsetsDirectional.symmetric( + vertical: linkToken.spacePaddingBlockSmall, + horizontal: linkToken.spacePaddingInline, + ); + } else { + return EdgeInsetsDirectional.symmetric( + vertical: linkToken.spacePaddingBlockDefault, + horizontal: linkToken.spacePaddingInline, + ); + } + } } diff --git a/ouds_core/lib/components/link/internal/ouds_link_status_modifier.dart b/ouds_core/lib/components/link/internal/ouds_link_status_modifier.dart index ad97b4cea..8eaaddd68 100644 --- a/ouds_core/lib/components/link/internal/ouds_link_status_modifier.dart +++ b/ouds_core/lib/components/link/internal/ouds_link_status_modifier.dart @@ -16,8 +16,6 @@ library; import 'package:flutter/material.dart'; import 'package:ouds_core/components/link/internal/ouds_link_control_state.dart'; -import 'package:ouds_core/components/link/ouds_link.dart'; -import 'package:ouds_core/components/utilities/app_assets.dart'; import 'package:ouds_theme_contract/ouds_theme.dart'; /// Used to apply the right background color associated to the status @@ -29,54 +27,64 @@ class OudsLinkStatusModifier { /// Returns the text a nd icon color based on the link status. Color getTextAndIconColor(OudsLinkControlState state) { final linkTheme = OudsTheme.of(context).componentsTokens(context).link; - final linkMonoTheme = OudsTheme.of(context).componentsTokens(context).linkMono; + final linkMonoTheme = OudsTheme.of( + context, + ).componentsTokens(context).linkMono; final onColoredSurface = OudsTheme.isOnColoredSurfaceOf(context); switch (state) { case OudsLinkControlState.enabled: - return onColoredSurface ? linkMonoTheme.colorContentEnabled : linkTheme.colorContentEnabled; + return onColoredSurface + ? linkMonoTheme.colorContentEnabled + : linkTheme.colorContentEnabled; case OudsLinkControlState.hovered: - return onColoredSurface ? linkMonoTheme.colorContentHover : linkTheme.colorContentHover; + return onColoredSurface + ? linkMonoTheme.colorContentHover + : linkTheme.colorContentHover; case OudsLinkControlState.focused: - return onColoredSurface ? linkMonoTheme.colorContentFocus : linkTheme.colorContentFocus; + return onColoredSurface + ? linkMonoTheme.colorContentFocus + : linkTheme.colorContentFocus; case OudsLinkControlState.pressed: - return onColoredSurface ? linkMonoTheme.colorContentPressed : linkTheme.colorContentPressed; + return onColoredSurface + ? linkMonoTheme.colorContentPressed + : linkTheme.colorContentPressed; case OudsLinkControlState.disabled: - return onColoredSurface ? linkMonoTheme.colorContentDisabled : OudsTheme.of(context).colorScheme(context).contentDisabled; + return onColoredSurface + ? linkMonoTheme.colorContentDisabled + : OudsTheme.of(context).colorScheme(context).contentDisabled; } } /// Return the arrow color based on link status Color getArrowColor(OudsLinkControlState state) { final linkTheme = OudsTheme.of(context).componentsTokens(context).link; - final linkMonoTheme = OudsTheme.of(context).componentsTokens(context).linkMono; + final linkMonoTheme = OudsTheme.of( + context, + ).componentsTokens(context).linkMono; final onColoredSurface = OudsTheme.isOnColoredSurfaceOf(context); switch (state) { case OudsLinkControlState.enabled: - return onColoredSurface ? linkMonoTheme.colorContentEnabled : linkTheme.colorChevronEnabled; + return onColoredSurface + ? linkMonoTheme.colorContentEnabled + : linkTheme.colorChevronEnabled; case OudsLinkControlState.hovered: - return onColoredSurface ? linkMonoTheme.colorContentHover : linkTheme.colorChevronHover; + return onColoredSurface + ? linkMonoTheme.colorContentHover + : linkTheme.colorContentHover; case OudsLinkControlState.focused: - return onColoredSurface ? linkMonoTheme.colorContentFocus : linkTheme.colorChevronFocus; + return onColoredSurface + ? linkMonoTheme.colorContentFocus + : linkTheme.colorContentFocus; case OudsLinkControlState.pressed: - return onColoredSurface ? linkMonoTheme.colorContentPressed : linkTheme.colorChevronPressed; + return onColoredSurface + ? linkMonoTheme.colorContentPressed + : linkTheme.colorContentPressed; case OudsLinkControlState.disabled: - return onColoredSurface ? linkMonoTheme.colorContentDisabled : OudsTheme.of(context).colorScheme(context).actionDisabled; - } - } - - /// Return the icon based on link layout - String? getStatusIcon(OudsLinkLayout layout, bool isRtlMode) { - switch (layout) { - case OudsLinkLayout.textOnly: - throw UnimplementedError("Error status for textOnly is not relevant"); - case OudsLinkLayout.textAndIcon: - throw UnimplementedError("Error status for textAndIcon is not relevant"); // it will be implemented from user - case OudsLinkLayout.next: - return isRtlMode ? AppAssets.icons.componentLinkPrevious : AppAssets.icons.componentLinkNext; - case OudsLinkLayout.back: - return isRtlMode ? AppAssets.icons.componentLinkNext : AppAssets.icons.componentLinkPrevious; + return onColoredSurface + ? linkMonoTheme.colorContentDisabled + : OudsTheme.of(context).colorScheme(context).actionDisabled; } } } diff --git a/ouds_core/lib/components/link/internal/ouds_link_text_style_modifier.dart b/ouds_core/lib/components/link/internal/ouds_link_text_style_modifier.dart index 0f43273cb..6e2e5b500 100644 --- a/ouds_core/lib/components/link/internal/ouds_link_text_style_modifier.dart +++ b/ouds_core/lib/components/link/internal/ouds_link_text_style_modifier.dart @@ -15,30 +15,28 @@ library; import 'package:flutter/material.dart'; +import 'package:ouds_core/components/link/internal/ouds_link_control_state.dart'; import 'package:ouds_core/components/link/ouds_link.dart'; import 'package:ouds_theme_contract/ouds_theme.dart'; -import 'package:ouds_core/components/link/internal/ouds_link_control_state.dart'; class OudsLinkTextStyleModifier { - final BuildContext context; OudsLinkTextStyleModifier(this.context); - /// Returns the text Style based on the size of link - TextStyle buildLinkTextStyle( - BuildContext context, { - required OudsLinkSize size, - }) { - return size == OudsLinkSize.defaultSize - ? OudsTheme.of(context).typographyTokens.typeLabelStrongLarge(context) - : OudsTheme.of(context).typographyTokens.typeLabelStrongMedium(context); - + /// Returns the text style based on the size of link + TextStyle buildLinkTextStyle({required OudsLinkSize size}) { + return size == OudsLinkSize.defaultSize + ? OudsTheme.of(context).typographyTokens.typeLabelStrongLarge(context) + : OudsTheme.of(context).typographyTokens.typeLabelStrongMedium(context); } /// Returns the text color based on link state - TextDecoration? getTextDecorationStatus(OudsLinkControlState state,OudsLinkLayout layout) { - if (layout == OudsLinkLayout.textOnly) { + TextDecoration? getTextDecorationStatus( + OudsLinkControlState state, + bool isTextOnly, + ) { + if (isTextOnly) { return TextDecoration.underline; } else { switch (state) { diff --git a/ouds_core/lib/components/link/ouds_link.dart b/ouds_core/lib/components/link/ouds_link.dart index 71e69c575..f79e616ea 100644 --- a/ouds_core/lib/components/link/ouds_link.dart +++ b/ouds_core/lib/components/link/ouds_link.dart @@ -23,19 +23,53 @@ import 'package:ouds_core/components/link/internal/ouds_link_control_state.dart' import 'package:ouds_core/components/link/internal/ouds_link_size_modifier.dart'; import 'package:ouds_core/components/link/internal/ouds_link_status_modifier.dart'; import 'package:ouds_core/components/link/internal/ouds_link_text_style_modifier.dart'; +import 'package:ouds_core/components/utilities/app_assets.dart'; import 'package:ouds_core/l10n/gen/ouds_localizations.dart'; import 'package:ouds_theme_contract/ouds_theme.dart'; -///The [OudsLinkLayout] defines the layout of the link’s content. -/// This enum controls whether the link displays text, text and icon, back or next +///The [OudsLinkLayout] defines the layout of the link's content. +/// +/// This enum controls whether the link displays text, text and icon, previous or next. +@Deprecated( + 'OudsLinkLayout is deprecated and will be removed in a future version. ' + 'Use the dedicated OudsLink.icon, OudsLink.previous, OudsLink.next constructors instead. ' + ' a new indicator added OudsLink.external.', +) enum OudsLinkLayout { textOnly, textAndIcon, back, next } +/// @nodoc +/// this is an internal enum should not be exposed to the user +/// Represents the type of chevron displayed in an [OudsLink]. +enum OudsLinkIndicator { + /// Used for "backward" navigation. Positioned before the label, it features a "chevron left" icon, which is not customizable. + previous, + + /// Used in a standard navigation context. Positioned after the label, it features a "chevron right" icon, which is not customizable. + next, + + /// Used to navigate outside the current product, service or application. The external navigation indicator informs users that the destination belongs to another context. + external, +} + /// The [OudsLinkSize] defines the link's visual size. enum OudsLinkSize { defaultSize, small } +/// The [OudsLinkDensity] defines the link's density +enum OudsLinkDensity { + /// This is the default density of the component, recommended for most interfaces and primary navigation contexts. + /// + /// This density which is used for the vast majority of applications, provides a comfortable touch target that meets accessibility recommendations. + defaultDensity, + + /// Reduces the vertical footprint while preserving the same interaction and visual behavior. + /// + /// Use in dense layouts where space is limited and a smaller touch target is acceptable (desktop or pointer-based contexts). + compact, +} + /// [OUDS Link design guidelines](https://r.orange.fr/r/S-ouds-doc-link) /// -/// **Reference design version : 2.2.0** +/// **Reference design version : 2.4.0** /// /// Link is a UI element that allows to navigate from one location to another, either within the same page /// or across different pages in the same resource, or to an external resource. Link's primary function is navigation @@ -44,18 +78,27 @@ enum OudsLinkSize { defaultSize, small } /// /// parameters : /// - [size] : The size of the link, [OudsLinkSize] such as small or default, to fit various visual needs. +/// - [density] : The density of the link, [OudsLinkDensity] such as compact or default. /// - [label] : A text to display in link component. -/// - [icon] : An optional SVG asset name to display an icon within the link. -/// - [layout]: Defines the layout to be used for the link [OudsLinkLayout]. +/// - [icon] : An optional SVG asset name to display an icon within the link (used with [OudsLink.icon]). +/// - [tinted] : tinted Controls whether the icon should be tinted with the theme color. Defaults to `true`. +/// When set to `false`, the icon is displayed with its original colors (e.g., for multi-color icons). +/// Note that untinted icons must ensure sufficient contrast with the background for accessibility reasons, only used with [OudsLink.icon]. /// - [onPressed]: Callback invoked when the link is clicked. /// +/// [OudsLink] provides a dedicated named constructor for every supported variant: +/// - The default (unnamed) constructor renders a text-only link. +/// - [OudsLink.icon] renders a label with a custom icon. +/// - [OudsLink.previous] renders a "backward" navigation link with a leading chevron. +/// - [OudsLink.next] renders a "forward" navigation link with a trailing chevron. +/// - [OudsLink.external] renders a link navigating outside the current context, with a trailing indicator. /// /// ### You can use [OudsLink] component in your project, customizing parameters as needed : /// -/// **Small Text only link :** +/// ### Small Text only : /// /// This is the default layout of the component. - +/// /// ```dart /// OudsLink( /// label: 'Label', @@ -64,24 +107,62 @@ enum OudsLinkSize { defaultSize, small } /// ); /// ``` /// -/// ### Small Next link : -/// This is the default layout of the component. +/// ### Link with Icon : +/// +/// Creates an [OudsLink] displaying its [label] alongside a custom [icon]. +/// /// ```dart -/// OudsLink( +/// OudsLink.icon( /// label: 'Label', -/// size: OudsLinkSize.small, -/// layout : OudsLinkayout.next +/// icon: 'assets/ic_heart.svg', +/// tinted: true, /// onPressed: () {} /// ); /// ``` /// -/// ### Default link with Icon : -/// This is the default layout of the component. +/// ### Next : +/// +/// Creates an [OudsLink] used for standard navigation, displaying a +/// trailing chevron after the [label]. +/// +/// ```dart +/// OudsLink.next( +/// label: 'Label', +/// onPressed: () {} +/// ); +/// ``` +/// +/// ### Previous : +/// +/// Creates an [OudsLink] used for "backward" navigation, displaying a +/// leading chevron before the [label]. +/// ```dart +/// OudsLink.previous( +/// label: 'Label', +/// onPressed: () {} +/// ); +/// ``` +/// +/// ### External : +/// +/// Creates an [OudsLink] used to navigate outside the current product, service or application. +/// The external navigation indicator informs users that the destination belongs to another context. +/// +/// ```dart +/// OudsLink.external( +/// label: 'Label', +/// onPressed: () {} +/// ); +/// ``` +/// +/// ### Compact density link : +/// +/// Reduces the link's minimum height and block padding, useful in dense layouts. +/// /// ```dart /// OudsLink( /// label: 'Label', -/// layout : OudsLinkayout.textAndIcon -/// icon : 'assets/ic_heart.svg' +/// density: OudsLinkDensity.compact, /// onPressed: () {} /// ); /// ``` @@ -90,50 +171,173 @@ enum OudsLinkSize { defaultSize, small } class OudsLink extends StatefulWidget { final String label; final String? icon; + final bool tinted; + @Deprecated( + 'OudsLinkLayout is deprecated and will be removed in a future version. ' + 'Use the dedicated OudsLink.icon, OudsLink.previous, OudsLink.next or ' + 'OudsLink.external constructors instead.', + ) final OudsLinkLayout layout; - final OudsLinkSize? size; + final OudsLinkSize size; + final OudsLinkDensity density; final VoidCallback? onPressed; + final OudsLinkIndicator? _indicator; + + /// Creates a text-only [OudsLink]. + /// + /// - [label] : The text displayed by the link. Required, non-empty; wraps + /// onto multiple lines automatically if it doesn't fit the available width. + /// - [layout] : Deprecated `OudsLinkLayout` (`textOnly`, `textAndIcon`, + /// `back`, `next`). Use [OudsLink.icon], [OudsLink.previous], [OudsLink.next] + /// or [OudsLink.external] instead. Defaults to [OudsLinkLayout.textOnly]. + /// - [size] : The size of the link, [OudsLinkSize.defaultSize] or + /// [OudsLinkSize.small], controlling text style and touch target dimensions. + /// Defaults to [OudsLinkSize.defaultSize]. + /// - [density] : The density of the link, [OudsLinkDensity.defaultDensity] + /// or [OudsLinkDensity.compact], controlling the minimum height and block + /// padding. Defaults to [OudsLinkDensity.defaultDensity]. + /// - [onPressed] : Callback invoked when the link is tapped or activated + /// (keyboard/screen reader). When `null`, the link is rendered and + /// announced as disabled and cannot receive focus. + /// - [icon] : Deprecated SVG asset path used to display a custom icon + /// alongside the [label] when combined with `layout: OudsLinkLayout.textAndIcon`. + /// Use [OudsLink.icon] instead. const OudsLink({ super.key, required this.label, - this.icon, + @Deprecated( + 'OudsLinkLayout is deprecated and will be removed in a future version. ' + 'Use the dedicated OudsLink.icon, OudsLink.previous, OudsLink.next or ' + 'OudsLink.external constructors instead.', + ) this.layout = OudsLinkLayout.textOnly, this.size = OudsLinkSize.defaultSize, + this.density = OudsLinkDensity.defaultDensity, this.onPressed, - }) : assert( - layout != OudsLinkLayout.textAndIcon || icon != null, - 'icon is required when layout is textAndIcon', - ); + this.icon, + }) : _indicator = null, + tinted = true; - static Widget buildIcon( - BuildContext context, - String? assetName, - OudsLinkControlState controlItemState, - OudsLinkLayout layout, - OudsLinkSize size, - ) { - final statusModifier = OudsLinkStatusModifier(context); - final sizeModifier = OudsLinkSizeModifier(context); - final iconSize = sizeModifier.getIconSize(size); - final isIcon = layout == OudsLinkLayout.textAndIcon; - final rtlMode = Directionality.of(context) == TextDirection.rtl; + /// Creates an [OudsLink] displaying its [label] alongside a custom [icon]. + /// + /// - [key] : Controls how one widget replaces another widget in the tree. + /// - [label] : The text displayed by the link. Required, non-empty; wraps + /// onto multiple lines automatically if it doesn't fit the available width. + /// - [icon] : Required SVG asset path (or package asset) of the custom icon + /// displayed alongside the [label]. Its size automatically adapts to [size]. + /// - [size] : The size of the link, [OudsLinkSize.defaultSize] or + /// [OudsLinkSize.small], controlling text style, icon size and touch + /// target dimensions. Defaults to [OudsLinkSize.defaultSize]. + /// - [density] : The density of the link, [OudsLinkDensity.defaultDensity] + /// or [OudsLinkDensity.compact], controlling the minimum height and block + /// padding. Defaults to [OudsLinkDensity.defaultDensity]. + /// - [onPressed] : Callback invoked when the link is tapped or activated + /// (keyboard/screen reader). When `null`, the link is rendered and + /// announced as disabled and cannot receive focus. + /// - [tinted] : Whether the icon should be tinted with the theme color. + /// Defaults to `true`. Set to `false` to keep the icon's original colors + /// (e.g. for multi-color or brand icons); in that case, ensure sufficient + /// contrast with the background for accessibility reasons. + const OudsLink.icon({ + super.key, + required this.label, + required this.icon, + this.size = OudsLinkSize.defaultSize, + this.density = OudsLinkDensity.defaultDensity, + this.onPressed, + this.tinted = true, + }) : _indicator = null, + layout = OudsLinkLayout.textAndIcon; - return SvgPicture.asset( - excludeFromSemantics: true, - assetName ?? statusModifier.getStatusIcon(layout, rtlMode)!, - package: assetName == null ? OudsTheme.of(context).packageName : null, - width: iconSize[OudsLinkDimensions.width.name], - height: iconSize[OudsLinkDimensions.height.name], - fit: BoxFit.contain, - colorFilter: ColorFilter.mode( - !isIcon - ? statusModifier.getArrowColor(controlItemState) - : statusModifier.getTextAndIconColor(controlItemState), - BlendMode.srcIn, - ), - ); - } + /// Creates an [OudsLink] used for "backward" navigation, displaying a + /// leading chevron before the [label]. + /// + /// - [key] : Controls how one widget replaces another widget in the tree. + /// - [label] : The text displayed by the link. Required, non-empty; wraps + /// onto multiple lines automatically if it doesn't fit the available width. + /// - [size] : The size of the link, [OudsLinkSize.defaultSize] or + /// [OudsLinkSize.small], controlling text style, chevron size and touch + /// target dimensions. Defaults to [OudsLinkSize.defaultSize]. + /// - [density] : The density of the link, [OudsLinkDensity.defaultDensity] + /// or [OudsLinkDensity.compact], controlling the minimum height and block + /// padding. Defaults to [OudsLinkDensity.defaultDensity]. + /// - [onPressed] : Callback invoked when the link is tapped or activated + /// (keyboard/screen reader), typically to navigate back. When `null`, + /// the link is rendered and announced as disabled and cannot receive focus. + /// + /// The leading chevron icon is fixed and cannot be customized (unlike + /// [OudsLink.icon]). + const OudsLink.previous({ + super.key, + required this.label, + this.size = OudsLinkSize.defaultSize, + this.density = OudsLinkDensity.defaultDensity, + this.onPressed, + }) : icon = null, + tinted = true, + _indicator = OudsLinkIndicator.previous, + layout = OudsLinkLayout.back; + + /// Creates an [OudsLink] used for standard navigation, displaying a + /// trailing chevron after the [label]. + /// + /// - [key] : Controls how one widget replaces another widget in the tree. + /// - [label] : The text displayed by the link. Required, non-empty; wraps + /// onto multiple lines automatically if it doesn't fit the available width. + /// - [size] : The size of the link, [OudsLinkSize.defaultSize] or + /// [OudsLinkSize.small], controlling text style, chevron size and touch + /// target dimensions. Defaults to [OudsLinkSize.defaultSize]. + /// - [density] : The density of the link, [OudsLinkDensity.defaultDensity] + /// or [OudsLinkDensity.compact], controlling the minimum height and block + /// padding. Defaults to [OudsLinkDensity.defaultDensity]. + /// - [onPressed] : Callback invoked when the link is tapped or activated + /// (keyboard/screen reader), typically to navigate forward. When `null`, + /// the link is rendered and announced as disabled and cannot receive focus. + /// + /// The trailing chevron icon is fixed and cannot be customized (unlike + /// [OudsLink.icon]). + const OudsLink.next({ + super.key, + required this.label, + this.size = OudsLinkSize.defaultSize, + this.density = OudsLinkDensity.defaultDensity, + this.onPressed, + }) : icon = null, + tinted = true, + _indicator = OudsLinkIndicator.next, + layout = OudsLinkLayout.next; + + /// Creates an [OudsLink] navigating outside the current product, service or + /// application, displaying a trailing external indicator after the [label]. + /// + /// - [key] : Controls how one widget replaces another widget in the tree. + /// - [label] : The text displayed by the link. Required, non-empty; wraps + /// onto multiple lines automatically if it doesn't fit the available width. + /// - [size] : The size of the link, [OudsLinkSize.defaultSize] or + /// [OudsLinkSize.small], controlling text style, indicator size and touch + /// target dimensions. Defaults to [OudsLinkSize.defaultSize]. + /// - [density] : The density of the link, [OudsLinkDensity.defaultDensity] + /// or [OudsLinkDensity.compact], controlling the minimum height and block + /// padding. Defaults to [OudsLinkDensity.defaultDensity]. + /// - [onPressed] : Callback invoked when the link is tapped or activated + /// (keyboard/screen reader), typically to open the external destination. + /// When `null`, the link is rendered and announced as disabled and + /// cannot receive focus. + /// + /// The trailing external icon informs users that the destination belongs + /// to another context; it is fixed and cannot be customized (unlike + /// [OudsLink.icon]). + const OudsLink.external({ + super.key, + required this.label, + this.size = OudsLinkSize.defaultSize, + this.density = OudsLinkDensity.defaultDensity, + this.onPressed, + }) : icon = null, + tinted = true, + _indicator = OudsLinkIndicator.external, + layout = OudsLinkLayout.next; @override State createState() => _OudsLinkState(); @@ -146,6 +350,8 @@ class _OudsLinkState extends State { bool _isFocused = false; bool _isPressed = false; + /// Initializes the focus node (listening for focus changes) and schedules + /// the first [_checkTextLines] measurement after the initial layout. @override void initState() { super.initState(); @@ -156,6 +362,8 @@ class _OudsLinkState extends State { WidgetsBinding.instance.addPostFrameCallback((_) => _checkTextLines()); } + /// Re-measures the label's line count whenever the [label] changes, so the + /// icon's vertical alignment stays accurate after an update. @override void didUpdateWidget(covariant OudsLink oldWidget) { super.didUpdateWidget(oldWidget); @@ -200,13 +408,17 @@ class _OudsLinkState extends State { super.dispose(); } + /// Updates [_isFocused] based on the focus node state, and forces it back + /// to `false` when the link is disabled (no [OudsLink.onPressed] callback). void _handleFocusChange(bool focus) { if (widget.onPressed == null) _isFocused = false; setState(() => _isFocused = focus); } - @override - Widget build(BuildContext context) { + /// Builds the link's inner content (label plus, depending on the variant, + /// a leading or trailing icon/chevron), dispatching to the right layout + /// helper based on the resolved indicator/layout for this [widget]. + Widget _buildLinkContent(BuildContext context) { final isDisabled = widget.onPressed == null; final interactionModelHover = OudsInheritedInteractionModel.of( context, @@ -231,34 +443,68 @@ class _OudsLinkState extends State { final linkSizeModifier = OudsLinkSizeModifier(context); final linkTextStyleModifier = OudsLinkTextStyleModifier(context); - Widget content; - - switch (widget.layout) { - case OudsLinkLayout.textOnly: - content = getTextOnlyContent( - linkControlState, - linkStatusModifier, - linkTextStyleModifier, - ); - break; - case OudsLinkLayout.next: - content = getNextContent( - linkControlState, - linkStatusModifier, - linkTextStyleModifier, - linkSizeModifier, - ); - break; - case OudsLinkLayout.back: - case OudsLinkLayout.textAndIcon: - content = getTextIconOrBackContent( - linkControlState, - linkStatusModifier, - linkTextStyleModifier, - linkSizeModifier, - ); - break; + //case link with text only + if (widget._indicator == null && + widget.icon == null && + widget.layout == OudsLinkLayout.textOnly) { + return _buildLabelText( + linkControlState, + linkStatusModifier, + linkTextStyleModifier, + ); + //case link with text and icon + } else if ((widget._indicator == null && widget.icon != null) || + (widget._indicator != null && + widget._indicator == OudsLinkIndicator.previous)) { + return _buildTextIconOrPreviousContent( + linkControlState, + linkStatusModifier, + linkTextStyleModifier, + linkSizeModifier, + ); + } else if (widget._indicator != null && + widget._indicator != OudsLinkIndicator.previous) { + return _getNextOrExternalContent( + linkControlState, + linkStatusModifier, + linkTextStyleModifier, + linkSizeModifier, + ); + //deprecated enum , will be removed + } else { + switch (widget.layout) { + case OudsLinkLayout.textOnly: + return _buildLabelText( + linkControlState, + linkStatusModifier, + linkTextStyleModifier, + ); + case OudsLinkLayout.next: + return _getNextOrExternalContent( + linkControlState, + linkStatusModifier, + linkTextStyleModifier, + linkSizeModifier, + ); + case OudsLinkLayout.back: + case OudsLinkLayout.textAndIcon: + return _buildTextIconOrPreviousContent( + linkControlState, + linkStatusModifier, + linkTextStyleModifier, + linkSizeModifier, + ); + } } + } + + /// Builds the [OudsLink] widget tree: wraps [_buildLinkContent] with + /// accessibility [Semantics] (link role/state) and the interactive + /// container (focus, hover, press handling and constraints). + @override + Widget build(BuildContext context) { + final isDisabled = widget.onPressed == null; + final linkSizeModifier = OudsLinkSizeModifier(context); /// Builds the main link container with proper constraints return Semantics( @@ -270,41 +516,43 @@ class _OudsLinkState extends State { : null, child: _buildLinkContainer( context, - child: content, + child: _buildLinkContent(context), linkSizeModifier: linkSizeModifier, isDisabled: isDisabled, ), ); } - /// Returns a Text widget for a link with `textOnly` layout, - /// applying the appropriate text style and color based on the link state. - Widget getTextOnlyContent( + /// Returns the [Text] widget shared by every layout, applying the + /// appropriate text style, color and decoration based on the link state. + Widget _buildLabelText( OudsLinkControlState linkControlState, OudsLinkStatusModifier linkStatusModifier, OudsLinkTextStyleModifier linkTextStyleModifier, ) { + final textAndIconColor = linkStatusModifier.getTextAndIconColor( + linkControlState, + ); return Text( widget.label, textAlign: TextAlign.left, style: linkTextStyleModifier - .buildLinkTextStyle(context, size: widget.size!) + .buildLinkTextStyle(size: widget.size) .copyWith( - color: linkStatusModifier.getTextAndIconColor(linkControlState), + color: textAndIconColor, decoration: linkTextStyleModifier.getTextDecorationStatus( linkControlState, - widget.layout, - ), - decorationColor: linkStatusModifier.getTextAndIconColor( - linkControlState, + (widget._indicator == null && widget.icon == null) || + widget.layout == OudsLinkLayout.textOnly, ), + decorationColor: textAndIconColor, ), ); } /// Returns a Row widget for a link with `next` layout, including the label /// and a next icon of a link component. - Widget getNextContent( + Widget _getNextOrExternalContent( OudsLinkControlState linkControlState, OudsLinkStatusModifier linkStatusModifier, OudsLinkTextStyleModifier linkTextStyleModifier, @@ -315,34 +563,27 @@ class _OudsLinkState extends State { crossAxisAlignment: isSingleLine ? CrossAxisAlignment.center : CrossAxisAlignment.end, - spacing: linkSizeModifier.getSizeColumnGap(widget.size, widget.layout)!, + spacing: linkSizeModifier.getSizeColumnGap( + widget.size, + widget.layout, + widget._indicator, + widget.icon, + )!, children: [ Flexible( - child: Text( - widget.label, - textAlign: TextAlign.left, - style: linkTextStyleModifier - .buildLinkTextStyle(context, size: widget.size!) - .copyWith( - color: linkStatusModifier.getTextAndIconColor( - linkControlState, - ), - decoration: linkTextStyleModifier.getTextDecorationStatus( - linkControlState, - widget.layout, - ), - decorationColor: linkStatusModifier.getTextAndIconColor( - linkControlState, - ), - ), + child: _buildLabelText( + linkControlState, + linkStatusModifier, + linkTextStyleModifier, ), ), - OudsLink.buildIcon( + _buildIcon( context, widget.icon, linkControlState, widget.layout, - widget.size!, + widget.size, + widget._indicator, ), ], ); @@ -350,7 +591,7 @@ class _OudsLinkState extends State { /// Returns a Row widget for a link with `textAndIcon` or `back` layout, /// including the icon and label, properly aligned and spaced. - Widget getTextIconOrBackContent( + Widget _buildTextIconOrPreviousContent( OudsLinkControlState linkControlState, OudsLinkStatusModifier linkStatusModifier, OudsLinkTextStyleModifier linkTextStyleModifier, @@ -359,41 +600,39 @@ class _OudsLinkState extends State { return Row( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, - spacing: linkSizeModifier.getSizeColumnGap(widget.size, widget.layout)!, + spacing: linkSizeModifier.getSizeColumnGap( + widget.size, + widget.layout, + widget._indicator, + widget.icon, + )!, children: [ if (widget.layout == OudsLinkLayout.back || - widget.layout == OudsLinkLayout.textAndIcon) - OudsLink.buildIcon( + widget.layout == OudsLinkLayout.textAndIcon || + widget._indicator == OudsLinkIndicator.previous || + widget.icon != null) + _buildIcon( context, widget.icon, linkControlState, widget.layout, - widget.size!, + widget.size, + widget._indicator, ), Flexible( - child: Text( - widget.label, - textAlign: TextAlign.left, - style: linkTextStyleModifier - .buildLinkTextStyle(context, size: widget.size!) - .copyWith( - color: linkStatusModifier.getTextAndIconColor( - linkControlState, - ), - decoration: linkTextStyleModifier.getTextDecorationStatus( - linkControlState, - widget.layout, - ), - decorationColor: linkStatusModifier.getTextAndIconColor( - linkControlState, - ), - ), + child: _buildLabelText( + linkControlState, + linkStatusModifier, + linkTextStyleModifier, ), ), ], ); } + /// Wraps [child] in a sized/padded [Container] with an [InkWell] handling + /// tap, hover and press interactions, and draws the focus border(s) when + /// the link is focused. Widget _buildLinkContainer( BuildContext context, { required Widget child, @@ -402,68 +641,193 @@ class _OudsLinkState extends State { }) { final minHeightAndWidth = linkSizeModifier.getMinWidthAndHeight( widget.size, + widget.density, ); - return Container( - constraints: BoxConstraints( - minHeight: minHeightAndWidth[OudsLinkDimensions.height.name]!, - minWidth: minHeightAndWidth[OudsLinkDimensions.width.name]!, - ), - decoration: _isFocused - ? BoxDecoration( - border: OudsBorder().borderAll( - width: OudsTheme.of(context).borderTokens.widthFocusInset, - color: OudsTheme.of( - context, - ).colorScheme(context).borderFocusInset, + final theme = OudsTheme.of(context); + final colorScheme = theme.colorScheme(context); + final borderTokens = theme.borderTokens; + + return _isFocused + ? Stack( + clipBehavior: Clip + .none, // Allows the border to overflow slightly if necessary. + alignment: Alignment.center, + children: [ + Container( + constraints: BoxConstraints( + minHeight: minHeightAndWidth[OudsLinkDimensions.height.name]!, + minWidth: minHeightAndWidth[OudsLinkDimensions.width.name]!, + ), + padding: linkSizeModifier.getPadding( + widget.size, + widget.density, + ), + decoration: BoxDecoration( + border: OudsBorder().borderAll( + width: borderTokens.widthFocusInset, + color: colorScheme.borderFocusInset, + ), + ), + child: InkWell( + onTap: widget.onPressed, + focusNode: _focusNode, + canRequestFocus: !isDisabled, + splashColor: Colors.transparent, + highlightColor: Colors.transparent, + focusColor: Colors.transparent, + hoverColor: Colors.transparent, + onHover: (hovering) { + if (!isDisabled) setState(() => _isHovered = hovering); + }, + onHighlightChanged: (highlighted) { + if (!isDisabled) setState(() => _isPressed = highlighted); + }, + child: Stack(clipBehavior: Clip.none, children: [child]), + ), ), - ) - : null, - child: InkWell( - onTap: widget.onPressed, - focusNode: _focusNode, - canRequestFocus: !isDisabled, - splashColor: Colors.transparent, - highlightColor: Colors.transparent, - focusColor: Colors.transparent, - hoverColor: Colors.transparent, - onHover: (hovering) { - if (!isDisabled) setState(() => _isHovered = hovering); - }, - onHighlightChanged: (highlighted) { - if (!isDisabled) setState(() => _isPressed = highlighted); - }, - child: Stack( - clipBehavior: Clip.none, - children: [ - if (_isFocused) - Positioned( - top: -OudsTheme.of(context).borderTokens.widthFocus, - bottom: -OudsTheme.of(context).borderTokens.widthFocus, - left: -OudsTheme.of(context).borderTokens.widthFocus, - right: -OudsTheme.of(context).borderTokens.widthFocus, - child: Container( - decoration: BoxDecoration( - border: OudsBorder().borderAll( - color: OudsTheme.of( - context, - ).colorScheme(context).borderFocus, - width: OudsTheme.of(context).borderTokens.widthFocus, + // The focus border, drawn on top. + // IgnorePointer prevents this border from intercepting clicks. + Positioned.fill( + //the focus border should be outside + left: -borderTokens.widthFocus / 2, + right: -borderTokens.widthFocus / 2, + bottom: -borderTokens.widthFocus / 2, + top: -borderTokens.widthFocus / 2, + child: IgnorePointer( + child: Container( + decoration: BoxDecoration( + border: OudsBorder().borderAll( + color: colorScheme.borderFocus, + width: borderTokens.widthFocus / 2, + ), + ), + child: Container( + decoration: BoxDecoration( + border: OudsBorder().borderAll( + color: colorScheme.borderFocusInset, + width: borderTokens.widthFocusInset, + ), + ), ), ), ), ), - Container( - padding: EdgeInsets.symmetric( - vertical: OudsTheme.of( - context, - ).componentsTokens(context).link.spacePaddingBlockDefault, - ), - child: child, + ], + ) + : Container( + constraints: BoxConstraints( + minHeight: minHeightAndWidth[OudsLinkDimensions.height.name]!, + minWidth: minHeightAndWidth[OudsLinkDimensions.width.name]!, ), - ], - ), - ), + padding: linkSizeModifier.getPadding(widget.size, widget.density), + child: InkWell( + onTap: widget.onPressed, + focusNode: _focusNode, + canRequestFocus: !isDisabled, + splashColor: Colors.transparent, + highlightColor: Colors.transparent, + focusColor: Colors.transparent, + hoverColor: Colors.transparent, + onHover: (hovering) { + if (!isDisabled) setState(() => _isHovered = hovering); + }, + onHighlightChanged: (highlighted) { + if (!isDisabled) setState(() => _isPressed = highlighted); + }, + child: Stack(clipBehavior: Clip.none, children: [child]), + ), + ); + } + + /// Renders the icon/chevron as an [SvgPicture], resolving its asset via + /// [_getIcon] when no custom [assetName] is provided, and applying a + /// tint color unless [tinted] is `false` for a custom icon. + /// + /// The next/previous/external chevron is scaled with the system text + /// scale factor, clamped between `1.0×` and `2.0×`, so it stays legible + /// when the user increases the system font size. + Widget _buildIcon( + BuildContext context, + String? assetName, + OudsLinkControlState controlItemState, + OudsLinkLayout layout, + OudsLinkSize size, + OudsLinkIndicator? indicator, + ) { + final statusModifier = OudsLinkStatusModifier(context); + final sizeModifier = OudsLinkSizeModifier(context); + final iconSize = sizeModifier.getIconSize(size); + final isIcon = assetName != null || layout == OudsLinkLayout.textAndIcon; + + final baseWidth = iconSize[OudsLinkDimensions.width.name]; + final baseHeight = iconSize[OudsLinkDimensions.height.name]; + // Only the chevron (next/previous/external) grows with the text scale, + // the custom "textAndIcon" icon keeps its fixed token size. + final textScaleFactor = MediaQuery.textScalerOf( + context, + ).scale(1.0).clamp(1.0, 2.0); + final scaledWidth = !isIcon && baseWidth != null + ? baseWidth * textScaleFactor + : baseWidth; + final scaledHeight = !isIcon && baseHeight != null + ? baseHeight * textScaleFactor + : baseHeight; + + final svgIcon = SvgPicture.asset( + excludeFromSemantics: true, + assetName ?? _getIcon(layout, indicator)!, + package: assetName == null ? OudsTheme.of(context).packageName : null, + width: scaledWidth, + height: scaledHeight, + fit: BoxFit.contain, + colorFilter: assetName != null && !widget.tinted + ? null + : ColorFilter.mode( + !isIcon + ? statusModifier.getArrowColor(controlItemState) + : statusModifier.getTextAndIconColor(controlItemState), + BlendMode.srcIn, + ), + matchTextDirection: true, + ); + + return Container( + color: !isIcon && widget.tinted && widget._indicator != null + ? null + : widget.tinted + ? null + : OudsTheme.of(context).colorScheme(context).surfaceBrandPrimary, + + child: svgIcon, ); } + + /// Return the icon based on link layout + String? _getIcon(OudsLinkLayout layout, OudsLinkIndicator? indicator) { + //new API + if (indicator != null) { + switch (indicator) { + case OudsLinkIndicator.next: + return AppAssets.icons.componentLinkNext; + case OudsLinkIndicator.previous: + return AppAssets.icons.componentLinkPrevious; + case OudsLinkIndicator.external: + return AppAssets.icons.componentLinkExternal; + } + } + //old API + switch (layout) { + case OudsLinkLayout.textOnly: + throw UnimplementedError("Error status for textOnly is not relevant"); + case OudsLinkLayout.textAndIcon: + throw UnimplementedError( + "Error status for textAndIcon is not relevant", + ); // it will be implemented from user + case OudsLinkLayout.next: + return AppAssets.icons.componentLinkNext; + case OudsLinkLayout.back: + return AppAssets.icons.componentLinkPrevious; + } + } } diff --git a/ouds_core/lib/components/ouds_colored_box.dart b/ouds_core/lib/components/ouds_colored_box.dart index b2416ce33..e2eac9273 100644 --- a/ouds_core/lib/components/ouds_colored_box.dart +++ b/ouds_core/lib/components/ouds_colored_box.dart @@ -45,28 +45,19 @@ class OudsColoredBox extends StatelessWidget { final Widget child; final OudsColoredBoxColor? color; - const OudsColoredBox({ - required this.child, - this.color, - super.key, - }); + const OudsColoredBox({required this.child, this.color, super.key}); @override Widget build(BuildContext context) { return Container( - constraints: BoxConstraints( - minHeight: 80, - ), width: double.infinity, color: color?.getValue(context), child: Padding( padding: EdgeInsetsDirectional.symmetric( - vertical: OudsTheme.of(context).spaceScheme(context).fixedMedium, - horizontal: OudsTheme.of(context).gridScheme(context).margin - ), - child: Center( - child: child, + vertical: OudsTheme.of(context).spaceScheme(context).fixedMedium, + horizontal: OudsTheme.of(context).gridScheme(context).margin, ), + child: Center(child: child), ), ); } diff --git a/ouds_core/lib/components/utilities/app_assets.dart b/ouds_core/lib/components/utilities/app_assets.dart index ea168bd68..814eb6135 100644 --- a/ouds_core/lib/components/utilities/app_assets.dart +++ b/ouds_core/lib/components/utilities/app_assets.dart @@ -55,6 +55,9 @@ class _Icons { 'assets/component/link/expanded-true.svg'; final String componentLinkExpandedFalse = 'assets/component/link/expanded-false.svg'; + final String componentLinkExternal = + 'assets/component/link/external-link.svg'; + final String componentTagClose = 'assets/component/tag/close.svg'; final String componentButtonNext = 'assets/component/button/next.svg'; final String componentButtonPrevious = 'assets/component/button/previous.svg'; diff --git a/ouds_theme_contract/CHANGELOG.md b/ouds_theme_contract/CHANGELOG.md index 52c3d47b9..9b976e45d 100644 --- a/ouds_theme_contract/CHANGELOG.md +++ b/ouds_theme_contract/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/2.1.0...develop) ### Added ### Changed +- [Library] In library, update `link` component to v2.4.0 ([#861](https://github.com/Orange-OpenSource/ouds-flutter/issues/861)) ### Fixed ## [2.1.0](https://github.com/Orange-OpenSource/ouds-flutter/compare/2.0.0...2.1.0) - 2026-08-07 diff --git a/ouds_theme_contract/lib/ouds_component_version.dart b/ouds_theme_contract/lib/ouds_component_version.dart index 7cad482ea..63e27b105 100644 --- a/ouds_theme_contract/lib/ouds_component_version.dart +++ b/ouds_theme_contract/lib/ouds_component_version.dart @@ -13,35 +13,35 @@ // Generated by Tokenator class OudsComponentVersion { - static const alertMessage = '1.1.1'; - static const appBar = '1.0.0'; - static const badge = '1.2.0'; - static const badgeCount = '1.2.0'; - static const badgeIcon = '1.3.0'; - static const bottomSheet = '0.0.0'; - static const bulletList = '1.1.0'; - static const button = '3.3.0'; - static const checkbox = '2.4.0'; - static const divider = '1.0.0'; - static const filterChip = '1.5.0'; - static const inlineAlert = '1.0.0'; - static const inputTag = '1.2.0'; - static const link = '2.3.0'; - static const navigationBar = '1.0.0'; - static const navigationButton = '3.3.0'; - static const navigationCardItem = '1.0.0'; - static const navigationListItem = '1.0.0'; - static const passwordInput = '1.3.1'; - static const phoneNumberInput = '1.3.1'; - static const pinCodeInput = '1.3.0'; - static const progressIndicator = '1.0.0'; - static const radioButton = '1.4.0'; - static const skeleton = '1.0.0'; - static const staticCardItem = '1.0.0'; - static const staticListItem = '1.0.0'; - static const suggestionChip = '1.5.0'; - static const switchButton = '1.5.0'; - static const tag = '1.5.0'; - static const textArea = '1.2.1'; - static const textInput = '1.4.1'; + static const alertMessage = '1.1.1'; + static const appBar = '1.0.0'; + static const badge = '1.2.0'; + static const badgeCount = '1.2.0'; + static const badgeIcon = '1.3.0'; + static const bottomSheet = '0.0.0'; + static const bulletList = '1.1.0'; + static const button = '3.3.0'; + static const checkbox = '2.4.0'; + static const divider = '1.0.0'; + static const filterChip = '1.5.0'; + static const inlineAlert = '1.0.0'; + static const inputTag = '1.2.0'; + static const link = '2.4.0'; + static const navigationBar = '1.0.0'; + static const navigationButton = '3.3.0'; + static const navigationCardItem = '1.0.0'; + static const navigationListItem = '1.0.0'; + static const passwordInput = '1.3.1'; + static const phoneNumberInput = '1.3.1'; + static const pinCodeInput = '1.3.0'; + static const progressIndicator = '1.0.0'; + static const radioButton = '1.4.0'; + static const skeleton = '1.0.0'; + static const staticCardItem = '1.0.0'; + static const staticListItem = '1.0.0'; + static const suggestionChip = '1.5.0'; + static const switchButton = '1.5.0'; + static const tag = '1.5.0'; + static const textArea = '1.2.1'; + static const textInput = '1.4.1'; } diff --git a/ouds_theme_orange/assets/component/link/external-link.svg b/ouds_theme_orange/assets/component/link/external-link.svg new file mode 100644 index 000000000..64493d2ba --- /dev/null +++ b/ouds_theme_orange/assets/component/link/external-link.svg @@ -0,0 +1,3 @@ + + + diff --git a/ouds_theme_orange_compact/assets/component/link/external-link.svg b/ouds_theme_orange_compact/assets/component/link/external-link.svg new file mode 100644 index 000000000..64493d2ba --- /dev/null +++ b/ouds_theme_orange_compact/assets/component/link/external-link.svg @@ -0,0 +1,3 @@ + + + diff --git a/ouds_theme_sosh/assets/component/link/external-link.svg b/ouds_theme_sosh/assets/component/link/external-link.svg new file mode 100644 index 000000000..1c4fd56f2 --- /dev/null +++ b/ouds_theme_sosh/assets/component/link/external-link.svg @@ -0,0 +1,3 @@ + + + diff --git a/ouds_theme_wireframe/assets/component/link/external-link.svg b/ouds_theme_wireframe/assets/component/link/external-link.svg new file mode 100644 index 000000000..b03350d9f --- /dev/null +++ b/ouds_theme_wireframe/assets/component/link/external-link.svg @@ -0,0 +1,3 @@ + + + diff --git a/skills/ouds-flutter-framework-usage/SKILL.md b/skills/ouds-flutter-framework-usage/SKILL.md index 45fa49767..0a077d9e1 100644 --- a/skills/ouds-flutter-framework-usage/SKILL.md +++ b/skills/ouds-flutter-framework-usage/SKILL.md @@ -539,19 +539,25 @@ Available colors: `defaultColor`, `muted`, `emphasized`, `brandPrimary`, `onBran ### Link +Prefer dedicated named constructors for icon/navigation variants. `OudsLink()` (default constructor) is for text-only links. + ```dart -OudsLink( - label: 'Learn more', - onPressed: () {}, -) +OudsLink(label: 'Learn more', onPressed: () {}) -OudsLink( - label: 'Back', - layout: OudsLinkLayout.back, +OudsLink.icon( + label: 'Favorite', + icon: 'assets/ic_heart.svg', + tinted: true, onPressed: () {}, ) + +OudsLink.previous(label: 'Back', onPressed: () {}) +OudsLink.next(label: 'Next', onPressed: () {}) +OudsLink.external(label: 'Visit website', onPressed: () {}) ``` +> ⚠️ Deprecated: `OudsLink(layout: OudsLinkLayout.back/textAndIcon/next)` → use `OudsLink.previous`, `OudsLink.icon` or `OudsLink.next`/`OudsLink.external` instead. + --- ### Navigation bar diff --git a/skills/ouds-flutter-migration-guide/SKILL.md b/skills/ouds-flutter-migration-guide/SKILL.md index 95669ef08..cbca9cc01 100644 --- a/skills/ouds-flutter-migration-guide/SKILL.md +++ b/skills/ouds-flutter-migration-guide/SKILL.md @@ -681,6 +681,7 @@ Before proposing changes: | Migration | Compatibility | Migration required | Main topics | |-----------|---------------|--------------------|-------------| +| `v1.3.1 → v2.0.0` | Partial | Yes for deprecated APIs | `OudsLink` named constructors (`.icon`, `.previous`, `.next`, `.external`), Markdown support, token/icon updates | | `v1.3.0 → v1.3.1` | Full | No | Maintenance release, bug fixes, accessibility improvements | | `v1.2.0 → v1.3.0` | Partial | Yes for deprecated APIs | `OudsTag` named constructors, `OudsBadge` named constructors, `OudsPinCodeInput.keyboardType`, new alert/bottom-sheet components | | `v1.1.x → v1.2.0` | No | Yes | `OudsTagConfig` removal from theme config, status icon updates, top bar components, French support | @@ -690,6 +691,48 @@ Before proposing changes: ### 3.3 High-impact migrations +#### 3.3.0 `OudsLink` → dedicated named constructors (`v1.3.1 → v2.0.0`) + +**Before:** + +```dart +OudsLink( + label: 'Label', + icon: 'assets/ic_heart.svg', +) + +OudsLink(label: 'Label', layout: OudsLinkLayout.back) + +OudsLink(label: 'Label', layout: OudsLinkLayout.next) +``` + +**After:** + +```dart +OudsLink.icon( + label: 'Label', + icon: 'assets/ic_heart.svg', + tinted: true, +) + +OudsLink.previous(label: 'Label') + +OudsLink.next(label: 'Label') + +OudsLink.external(label: 'Label') +``` + +**Required actions:** + +- Replace `OudsLink(icon: ...)` with `OudsLink.icon(icon: ..., tinted: ...)`. +- Replace `OudsLink(layout: OudsLinkLayout.back)` with `OudsLink.previous(...)`. +- Replace `OudsLink(layout: OudsLinkLayout.next)` with `OudsLink.next(...)`. +- Use `OudsLink.external(...)` for links navigating outside the current product, service or application. +- Text-only links keep using the default `OudsLink(...)` constructor unchanged. +- The deprecated `layout` (`OudsLinkLayout`) parameter on the default constructor still works but should be migrated. + +--- + #### 3.3.1 `OudsTag` → named constructors (`v1.2.x` → `v1.3.0`) **Before:**