From d0f0e4e08064c959607b2e80a5924374ab3e5a3f Mon Sep 17 00:00:00 2001 From: Tayeb Sedraia Date: Tue, 20 Feb 2024 15:12:25 +0100 Subject: [PATCH 01/10] app : add image illustration navigation rail --- NOTICE.txt | 1 + app/assets/il_navigation_rail.png | Bin 0 -> 3066 bytes 2 files changed, 1 insertion(+) create mode 100644 app/assets/il_navigation_rail.png diff --git a/NOTICE.txt b/NOTICE.txt index 075a1a50..174017d3 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -43,6 +43,7 @@ ods-flutter/app/assets/il_dialogs.png ods-flutter/app/assets/ic_profile.svg ods-flutter/app/assets/il_menu.png ods-flutter/app/assets/il_text-fields.png +ods-flutter/app/assets/il_navigation_rail.png ods-flutter/app/assets/1.5x/il_about.png ods-flutter/app/assets/1.5x/il_switches.png diff --git a/app/assets/il_navigation_rail.png b/app/assets/il_navigation_rail.png new file mode 100644 index 0000000000000000000000000000000000000000..a62904efddd20783605a69ccd0864decb03043ec GIT binary patch literal 3066 zcmd5;X;>5I79Oo!SsYnJAqlc75Fijj76=f8fglyrD#CG77!j{A$QW-_ObW5{pXMSW9B>O{oePS^L_Kp z%*hMl`h(hdZ2$nkfX%+a0Pq0>0Lu`~4>05mSek_m=5d>Y0|7u{4FDe<0{|rkeWU<@ zR5Ace>;wSjc>pjFMd}>gu?-xVE-78*{vq70%Vh*pp}!;J6NQSVyxqcCf^mug9C35SX?` zY|^?7W_Zd*V{bbnq6IF^Td#1->I$At4qI2~VZd`)TfA+RT)0A&4$P)5Yuml@Muhfw z$_G0cYnr1qdlEEwJRY{DvfQ#TY*;Q1=7!vRUGQYA6!Vtt-s>6(vUt)Of_lS z@9<^7N9)#!+BHv+h$@wr-wwmPsa@x@@l9_py;OSe-XIgT^)J31FWzdu>1oer$aUaq zAatr*FP^#r2YR#Ln^` z_04zax%M7HDkL0vq^)@S`H#Ccu;u9ENR4PSWyjNeXNI)e++)m>rk6AzO-b8|6VXvYC z9auqP4+YKW7~Ul)KfTfwfx(%nsS05ZJx)}WZl5MpY|RP7%mszK3vtqlDv%<#***;T z?uJB04_uOb{FTrp*h*CO%dZoY$aT#G<0Pp5Nr&@V4BC&r*apSwe@pIx@ufGEDY-ZC zNl*dlW5h`_0egtpR^3*cxIGYq_8|VmErbR-<3{(k%S?I^i}plK3O?rqY+nAGIfhq0 zdz7!-uXsoi;d}IOU{dZ0nCi=h@RlL9NXxG`s!HJ)hu*MSi-w)=$o??T2;1E>9MAu* zD7OGV={^OwV`!tuF`^NEnv?XGZ1@~bBfO-XDO;k??k=u70J>G))9A$%iJPH#YJ~*_ zE1~!RxHvmnmTfAz$=UJ0Li}0#A4U9iMgI@z)?U;p?Rd-@KT;r+HlSk7mQ^K1;+11& zFI6Uz?R`=QP#zu2$aOW^+JnAzmhRKr?|8NX(T!_2|M8y={*YPQDeni35rr_Tz~X4` z%nd^@%M|*D{)=5iymG{tlIyy%6GhKC$LsB1eHKAfCo$3z$s3AQvwt(x$nfK2#RZJh z-K+$&+~HN0%CNi08Z1Rw@)&{Xh5?c=8PbY{Q$)UjOVksw4+#PsmT1v#IA};c5jqJ+ zVYT>P7clx*>JYpp1gBcl6MxTvYYb;#kxjLXQtW_>;(N2lz+&*po2k|8Zf#yo>w&&s&5@N@Sl zGQf7eO&H9U-fB+mIa-hbIi1{M$B@={uTmUjZ>u{VW5MqeDTbf-6Ay*@ZmXtCWjASU zQ;UdE0O&_(YH@c$<}XiyGLD_?59r)?IBn%UTg&j9h^%rCCs8PjQ+!Ox(jW}FcV;~~ z_ttHP&anxaG|0!&GOQNSSYx1nO#qTEqW%5P(R}{kzi85|xycm-o!RmTXpg*ass9#o zuJg=efg-!|SlyvZV^^DlL6Or^g5vZgWT5j{ksxsn*Z9I|Zhk&?X<}dDQm8-?lVP^< z`s$M`_w2>8MVc-gL+%Z|hV&m_zdGAp+4_2NttVdckQ0W$bAy8)re}(Ko)M1bnywX8 zP0G<^e^Ozr#NYSu1hub@I6qev-MM(N#*JCsidSjQ>RtT3FwfO4Ek)qt{~F)rM)kMh z;{~twwlHN#d26Nb-NiFpHKQWMwGs)pQ9b@~T|I8px67dg;cZpK|UNh-g`4H0p0e)QHTA!Uc{{~bD6!icA literal 0 HcmV?d00001 From 6dab89052a7db17a500e4b35303763ed13c61618 Mon Sep 17 00:00:00 2001 From: Tayeb Sedraia Date: Tue, 20 Feb 2024 15:12:59 +0100 Subject: [PATCH 02/10] app : delete material 3 demo example --- app/lib/ui/components/components.dart | 28 +- .../material/component_material.dart | 1413 ----------------- 2 files changed, 13 insertions(+), 1428 deletions(-) delete mode 100644 app/lib/ui/components/material/component_material.dart diff --git a/app/lib/ui/components/components.dart b/app/lib/ui/components/components.dart index 08d8f0ec..ce7b4b66 100644 --- a/app/lib/ui/components/components.dart +++ b/app/lib/ui/components/components.dart @@ -38,6 +38,7 @@ import 'package:ods_flutter_demo/ui/components/lists/lists_switches.dart'; import 'package:ods_flutter_demo/ui/components/menus/menu_dropdown.dart'; import 'package:ods_flutter_demo/ui/components/menus/menu_exposed_dropdown.dart'; import 'package:ods_flutter_demo/ui/components/navigation_bar/navigation_bar.dart'; +import 'package:ods_flutter_demo/ui/components/navigation_rail/navigation_rail.dart'; import 'package:ods_flutter_demo/ui/components/progress/progress_circular.dart'; import 'package:ods_flutter_demo/ui/components/progress/progress_linear.dart'; import 'package:ods_flutter_demo/ui/components/radio_buttons/radio_buttons.dart'; @@ -239,6 +240,18 @@ List components(BuildContext context) { ), ], ), + Component( + AppLocalizations.of(context)!.componentNavigationRail, + 'assets/il_navigation_rail.png', + AppLocalizations.of(context)!.componentNavigationRailDescription, + [ + Variant( + AppLocalizations.of(context)!.componentNavigationRail, + AppLocalizations.of(context)!.componentNavigationRailSubtitle, + ComponentNavigationRail(), + ), + ], + ), Component( AppLocalizations.of(context)!.componentProgressTitle, 'assets/il_progress.png', @@ -328,20 +341,5 @@ List components(BuildContext context) { ), ], ), - - /// Delete material 3 demo page - /* - Component( - AppLocalizations.of(context)!.componentMaterialsTitle, - 'assets/placeholder.png', - AppLocalizations.of(context)!.componentMaterialsDescription, - [ - Variant( - AppLocalizations.of(context)!.materialsVariantTitle, - AppLocalizations.of(context)!.materialsVariantSubtitle, - ComponentMaterial()), - ], - ), - */ ]; } diff --git a/app/lib/ui/components/material/component_material.dart b/app/lib/ui/components/material/component_material.dart deleted file mode 100644 index 6c7d752d..00000000 --- a/app/lib/ui/components/material/component_material.dart +++ /dev/null @@ -1,1413 +0,0 @@ -/* - * Software Name : Orange Design System - * SPDX-FileCopyrightText: Copyright (c) Orange SA - * SPDX-License-Identifier: MIT - * - * This software is distributed under the MIT licence, - * the text of which is available at https://opensource.org/license/MIT/ - * or see the "LICENSE" file for more details. - * - * Software description: Flutter library of reusable graphical components for Android and iOS - */ - -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:ods_flutter/guidelines/spacings.dart'; -import 'package:ods_flutter_demo/ui/main_app_bar.dart'; - -const rowSpacer = SizedBox(width: spacingM); -const componentSpacer = SizedBox(height: spacingS); -const double cardWidth = 240; -const double widthConstraint = 450; - -class ComponentMaterial extends StatefulWidget { - @override - State createState() => _ComponentMaterialState(); -} - -class _ComponentMaterialState extends State { - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: MainAppBar('Material components'), - body: ListView( - children: [ - //ButtonsFab(), - //ButtonsSegmented(), - //BottomSheets(), - //Cards(), - //Chips(), - //Dialogs(), - //Menus(), - //ProgressIndicators(), - //Sliders(), - //SnackBars(), - //TextFields(), - ], - ), - ); - } -} - -class ButtonsFab extends StatelessWidget { - const ButtonsFab({super.key}); - - @override - Widget build(BuildContext context) { - return Semantics( - header: true, - child: _Component( - name: 'Floating Action Buttons (FAB)', - child: Wrap( - crossAxisAlignment: WrapCrossAlignment.center, - runSpacing: spacingS, - spacing: spacingS, - children: [ - FloatingActionButton.small( - heroTag: 'btn1', - onPressed: () {}, - child: const Icon(Icons.add), - ), - FloatingActionButton.extended( - heroTag: 'btn2', - onPressed: () {}, - icon: const Icon(Icons.add), - label: const Text('Create'), - ), - FloatingActionButton( - heroTag: 'btn3', - onPressed: () {}, - child: const Icon(Icons.add), - ), - FloatingActionButton.large( - heroTag: 'btn4', - onPressed: () {}, - child: const Icon(Icons.add), - ), - ], - ), - )); - } -} - -class Cards extends StatelessWidget { - const Cards({super.key}); - - @override - Widget build(BuildContext context) { - return Semantics( - header: true, - child: _Component( - name: 'Cards', - child: Column( - children: [ - SizedBox( - width: cardWidth, - child: Card( - child: Container( - padding: const EdgeInsets.fromLTRB( - spacingM, spacingS, spacingS, spacingM), - child: Column( - children: [ - Align( - alignment: Alignment.topRight, - child: IconButton( - icon: const Icon(Icons.more_vert), - onPressed: () {}, - ), - ), - const SizedBox(height: spacingL), - const Align( - alignment: Alignment.bottomLeft, - child: Text('Elevated'), - ) - ], - ), - ), - ), - ), - SizedBox( - width: cardWidth, - child: Card( - color: Theme.of(context).colorScheme.surfaceVariant, - elevation: 0, - child: Container( - padding: const EdgeInsets.fromLTRB(10, 5, 5, 10), - child: Column( - children: [ - Align( - alignment: Alignment.topRight, - child: IconButton( - icon: const Icon(Icons.more_vert), - onPressed: () {}, - ), - ), - const SizedBox(height: 20), - const Align( - alignment: Alignment.bottomLeft, - child: Text('Filled'), - ) - ], - ), - ), - ), - ), - SizedBox( - width: cardWidth, - child: Card( - elevation: 0, - shape: RoundedRectangleBorder( - side: BorderSide( - color: Theme.of(context).colorScheme.outline, - ), - borderRadius: const BorderRadius.all(Radius.circular(12)), - ), - child: Container( - padding: const EdgeInsets.fromLTRB(10, 5, 5, 10), - child: Column( - children: [ - Align( - alignment: Alignment.topRight, - child: IconButton( - icon: const Icon(Icons.more_vert), - onPressed: () {}, - ), - ), - const SizedBox(height: 20), - const Align( - alignment: Alignment.bottomLeft, - child: Text('Outlined'), - ) - ], - ), - ), - ), - ), - ], - ), - )); - } -} - -class _ClearButton extends StatelessWidget { - const _ClearButton({required this.controller}); - - final TextEditingController controller; - - @override - Widget build(BuildContext context) => IconButton( - icon: const Icon(Icons.clear), - onPressed: () => controller.clear(), - ); -} - -class TextFields extends StatefulWidget { - const TextFields({super.key}); - - @override - State createState() => _TextFieldsState(); -} - -class _TextFieldsState extends State { - final TextEditingController _controllerFilled = TextEditingController(); - final TextEditingController _controllerOutlined = TextEditingController(); - - @override - Widget build(BuildContext context) { - return _Component( - name: 'Text fields', - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.all(spacingS), - child: TextField( - controller: _controllerFilled, - decoration: InputDecoration( - prefixIcon: const Icon(Icons.search), - suffixIcon: _ClearButton(controller: _controllerFilled), - labelText: 'Filled', - hintText: 'hint text', - helperText: 'helper text', - filled: true, - ), - ), - ), - Padding( - padding: const EdgeInsets.all(spacingS), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Flexible( - child: SizedBox( - width: 200, - child: TextField( - maxLength: 10, - maxLengthEnforcement: MaxLengthEnforcement.none, - controller: _controllerFilled, - decoration: InputDecoration( - prefixIcon: const Icon(Icons.search), - suffixIcon: _ClearButton(controller: _controllerFilled), - labelText: 'Filled', - hintText: 'hint text', - helperText: 'helper text', - filled: true, - errorText: 'error text', - ), - ), - ), - ), - const SizedBox(width: spacingS), - Flexible( - child: SizedBox( - width: 200, - child: TextField( - controller: _controllerFilled, - enabled: false, - decoration: InputDecoration( - prefixIcon: const Icon(Icons.search), - suffixIcon: _ClearButton(controller: _controllerFilled), - labelText: 'Disabled', - hintText: 'hint text', - helperText: 'helper text', - filled: true, - ), - ), - ), - ), - ], - ), - ), - Padding( - padding: const EdgeInsets.all(spacingS), - child: TextField( - controller: _controllerOutlined, - decoration: InputDecoration( - prefixIcon: const Icon(Icons.search), - suffixIcon: _ClearButton(controller: _controllerOutlined), - labelText: 'Outlined', - hintText: 'hint text', - helperText: 'helper text', - border: const OutlineInputBorder(), - ), - ), - ), - Padding( - padding: const EdgeInsets.all(spacingS), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Flexible( - child: SizedBox( - width: 200, - child: TextField( - controller: _controllerOutlined, - decoration: InputDecoration( - prefixIcon: const Icon(Icons.search), - suffixIcon: - _ClearButton(controller: _controllerOutlined), - labelText: 'Outlined', - hintText: 'hint text', - helperText: 'helper text', - errorText: 'error text', - border: const OutlineInputBorder(), - filled: true, - ), - ), - ), - ), - const SizedBox(width: spacingS), - Flexible( - child: SizedBox( - width: 200, - child: TextField( - controller: _controllerOutlined, - enabled: false, - decoration: InputDecoration( - prefixIcon: const Icon(Icons.search), - suffixIcon: - _ClearButton(controller: _controllerOutlined), - labelText: 'Disabled', - hintText: 'hint text', - helperText: 'helper text', - border: const OutlineInputBorder(), - filled: true, - ), - ), - ), - ), - ])), - ], - ), - ); - } -} - -class Dialogs extends StatefulWidget { - const Dialogs({super.key}); - - @override - State createState() => _DialogsState(); -} - -class _DialogsState extends State { - void openDialog(BuildContext context) { - showDialog( - context: context, - builder: (context) => AlertDialog( - title: const Text('What is a dialog?'), - content: const Text( - 'A dialog is a type of modal window that appears in front of app content to provide critical information, or prompt for a decision to be made.'), - actions: [ - TextButton( - child: const Text('Okay'), - onPressed: () => Navigator.of(context).pop(), - ), - FilledButton( - child: const Text('Dismiss'), - onPressed: () => Navigator.of(context).pop(), - ), - ], - ), - ); - } - - void openFullscreenDialog(BuildContext context) { - showDialog( - context: context, - builder: (context) => Dialog.fullscreen( - child: Padding( - padding: const EdgeInsets.all(20.0), - child: Scaffold( - appBar: AppBar( - title: const Text('Full-screen dialog'), - centerTitle: false, - leading: IconButton( - icon: const Icon(Icons.close), - onPressed: () => Navigator.of(context).pop(), - ), - actions: [ - TextButton( - child: const Text('Close'), - onPressed: () => Navigator.of(context).pop(), - ), - ], - ), - ), - ), - ), - ); - } - - @override - Widget build(BuildContext context) { - return _Component( - name: 'Dialogs', - child: Wrap( - alignment: WrapAlignment.spaceBetween, - children: [ - TextButton( - child: const Text( - 'Show dialog', - style: TextStyle(fontWeight: FontWeight.bold), - ), - onPressed: () => openDialog(context), - ), - TextButton( - child: const Text( - 'Show full-screen dialog', - style: TextStyle(fontWeight: FontWeight.bold), - ), - onPressed: () => openFullscreenDialog(context), - ), - ], - ), - ); - } -} - -class ProgressIndicators extends StatefulWidget { - const ProgressIndicators({super.key}); - - @override - State createState() => _ProgressIndicatorsState(); -} - -class _ProgressIndicatorsState extends State { - @override - Widget build(BuildContext context) { - return Semantics( - header: true, - child: _Component( - name: 'Progress indicators', - child: Column( - children: [ - Row( - children: [ - Expanded( - child: Row( - children: [ - rowSpacer, - CircularProgressIndicator( - value: null, - ), - rowSpacer, - Expanded( - child: LinearProgressIndicator( - value: null, - ), - ), - rowSpacer, - ], - ), - ), - ], - ), - ], - ), - )); - } -} - -class Chips extends StatefulWidget { - const Chips({super.key}); - - @override - State createState() => _ChipsState(); -} - -class _ChipsState extends State { - bool isFiltered = true; - - @override - Widget build(BuildContext context) { - return Semantics( - header: true, - child: _Component( - name: 'Chips', - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Wrap( - spacing: spacingS, - runSpacing: spacingS, - children: [ - ActionChip( - label: const Text('Assist'), - avatar: const Icon(Icons.event), - onPressed: () {}, - ), - FilterChip( - label: const Text('Filter'), - selected: isFiltered, - onSelected: (selected) { - setState(() => isFiltered = selected); - }, - ), - InputChip( - label: const Text('Input'), - onPressed: () {}, - onDeleted: () {}, - ), - ActionChip( - label: const Text('Suggestion'), - onPressed: () {}, - ), - ], - ), - componentSpacer, - Wrap( - spacing: spacingS, - runSpacing: spacingS, - children: [ - const ActionChip( - label: Text('Assist'), - avatar: Icon(Icons.event), - ), - FilterChip( - label: const Text('Filter'), - selected: isFiltered, - onSelected: null, - ), - InputChip( - label: const Text('Input'), - onDeleted: () {}, - isEnabled: false, - ), - const ActionChip( - label: Text('Suggestion'), - ), - ], - ), - ], - ), - )); - } -} - -class ButtonsSegmented extends StatelessWidget { - const ButtonsSegmented({super.key}); - - @override - Widget build(BuildContext context) { - return Semantics( - header: true, - child: _Component( - name: 'Segmented buttons', - child: Column( - children: const [ - SingleChoice(), - componentSpacer, - MultipleChoice(), - ], - ), - )); - } -} - -enum Calendar { day, week, month, year } - -class SingleChoice extends StatefulWidget { - const SingleChoice({super.key}); - - @override - State createState() => _SingleChoiceState(); -} - -class _SingleChoiceState extends State { - Calendar calendarView = Calendar.day; - - @override - Widget build(BuildContext context) { - return SegmentedButton( - segments: const >[ - ButtonSegment( - value: Calendar.day, - label: Text('Day'), - icon: Icon(Icons.calendar_view_day)), - ButtonSegment( - value: Calendar.week, - label: Text('Week'), - icon: Icon(Icons.calendar_view_week)), - ButtonSegment( - value: Calendar.month, - label: Text('Month'), - icon: Icon(Icons.calendar_view_month)), - ButtonSegment( - value: Calendar.year, - label: Text('Year'), - icon: Icon(Icons.calendar_today)), - ], - selected: {calendarView}, - onSelectionChanged: (newSelection) { - setState(() { - // By default there is only a single segment that can be - // selected at one time, so its value is always the first - // item in the selected set. - calendarView = newSelection.first; - }); - }, - ); - } -} - -enum Sizes { extraSmall, small, medium, large, extraLarge } - -class MultipleChoice extends StatefulWidget { - const MultipleChoice({super.key}); - - @override - State createState() => _MultipleChoiceState(); -} - -class _MultipleChoiceState extends State { - Set selection = {Sizes.large, Sizes.extraLarge}; - - @override - Widget build(BuildContext context) { - return SegmentedButton( - segments: const >[ - ButtonSegment(value: Sizes.extraSmall, label: Text('XS')), - ButtonSegment(value: Sizes.small, label: Text('S')), - ButtonSegment(value: Sizes.medium, label: Text('M')), - ButtonSegment( - value: Sizes.large, - label: Text('L'), - ), - ButtonSegment(value: Sizes.extraLarge, label: Text('XL')), - ], - selected: selection, - onSelectionChanged: (newSelection) { - setState(() { - selection = newSelection; - }); - }, - multiSelectionEnabled: true, - ); - } -} - -class SnackBars extends StatelessWidget { - const SnackBars({super.key}); - - @override - Widget build(BuildContext context) { - return _Component( - name: 'Snackbars', - child: TextButton( - onPressed: () { - final snackBar = SnackBar( - behavior: SnackBarBehavior.floating, - width: 400.0, - content: const Text('This is a snackbar'), - action: SnackBarAction( - label: 'Close', - onPressed: () {}, - ), - ); - - ScaffoldMessenger.of(context).hideCurrentSnackBar(); - ScaffoldMessenger.of(context).showSnackBar(snackBar); - }, - child: const Text( - 'Show snackbar', - style: TextStyle(fontWeight: FontWeight.bold), - ), - ), - ); - } -} - -class BottomSheets extends StatefulWidget { - const BottomSheets({super.key}); - - @override - State createState() => _BottomSheetsState(); -} - -class _BottomSheetsState extends State { - bool isNonModalBottomSheetOpen = false; - PersistentBottomSheetController? _nonModalBottomSheetController; - - @override - Widget build(BuildContext context) { - List buttonList = [ - IconButton(onPressed: () {}, icon: const Icon(Icons.share_outlined)), - IconButton(onPressed: () {}, icon: const Icon(Icons.add)), - IconButton(onPressed: () {}, icon: const Icon(Icons.delete_outline)), - IconButton(onPressed: () {}, icon: const Icon(Icons.archive_outlined)), - IconButton(onPressed: () {}, icon: const Icon(Icons.settings_outlined)), - IconButton(onPressed: () {}, icon: const Icon(Icons.favorite_border)), - ]; - List labelList = const [ - Text('Share'), - Text('Add to'), - Text('Trash'), - Text('Archive'), - Text('Settings'), - Text('Favorite') - ]; - - buttonList = List.generate( - buttonList.length, - (index) => Padding( - padding: const EdgeInsets.fromLTRB(20.0, 30.0, 20.0, 20.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - buttonList[index], - labelList[index], - ], - ), - )); - - return Semantics( - header: true, - child: _Component( - name: 'Bottom sheet', - child: Wrap( - alignment: WrapAlignment.spaceEvenly, - children: [ - TextButton( - child: const Text( - 'Show modal bottom sheet', - style: TextStyle(fontWeight: FontWeight.bold), - ), - onPressed: () { - showModalBottomSheet( - context: context, - // TODO: Remove when this is in the framework https://github.com/flutter/flutter/issues/118619 - constraints: const BoxConstraints(maxWidth: 640), - builder: (context) { - return SizedBox( - height: 150, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 32.0), - child: ListView( - shrinkWrap: true, - scrollDirection: Axis.horizontal, - children: buttonList, - ), - ), - ); - }, - ); - }, - ), - TextButton( - child: Text( - isNonModalBottomSheetOpen - ? 'Hide bottom sheet' - : 'Show bottom sheet', - style: const TextStyle(fontWeight: FontWeight.bold), - ), - onPressed: () { - if (isNonModalBottomSheetOpen) { - _nonModalBottomSheetController?.close(); - setState(() { - isNonModalBottomSheetOpen = false; - }); - return; - } else { - setState(() { - isNonModalBottomSheetOpen = true; - }); - } - - _nonModalBottomSheetController = showBottomSheet( - elevation: 8.0, - context: context, - // TODO: Remove when this is in the framework https://github.com/flutter/flutter/issues/118619 - constraints: const BoxConstraints(maxWidth: 640), - builder: (context) { - return SizedBox( - height: 150, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 32.0), - child: ListView( - shrinkWrap: true, - scrollDirection: Axis.horizontal, - children: buttonList, - ), - ), - ); - }, - ); - }, - ), - ], - ), - ), - ); - } -} - -class BottomAppBars extends StatelessWidget { - const BottomAppBars({super.key}); - - @override - Widget build(BuildContext context) { - return _Component( - name: 'Bottom app bar', - child: Column( - children: [ - SizedBox( - height: 80, - child: Scaffold( - floatingActionButton: FloatingActionButton( - onPressed: () {}, - elevation: 0.0, - child: const Icon(Icons.add), - ), - floatingActionButtonLocation: - FloatingActionButtonLocation.endContained, - bottomNavigationBar: BottomAppBar( - child: Row( - children: [ - const IconButtonAnchorExample(), - IconButton( - tooltip: 'Search', - icon: const Icon(Icons.search), - onPressed: () {}, - ), - IconButton( - tooltip: 'Favorite', - icon: const Icon(Icons.favorite), - onPressed: () {}, - ), - ], - ), - ), - ), - ), - ], - ), - ); - } -} - -class IconButtonAnchorExample extends StatelessWidget { - const IconButtonAnchorExample({super.key}); - - @override - Widget build(BuildContext context) { - return MenuAnchor( - builder: (context, controller, child) { - return IconButton( - onPressed: () { - if (controller.isOpen) { - controller.close(); - } else { - controller.open(); - } - }, - icon: const Icon(Icons.more_vert), - ); - }, - menuChildren: [ - MenuItemButton( - child: const Text('Menu 1'), - onPressed: () {}, - ), - MenuItemButton( - child: const Text('Menu 2'), - onPressed: () {}, - ), - SubmenuButton( - menuChildren: [ - MenuItemButton( - onPressed: () {}, - child: const Text('Menu 3.1'), - ), - MenuItemButton( - onPressed: () {}, - child: const Text('Menu 3.2'), - ), - MenuItemButton( - onPressed: () {}, - child: const Text('Menu 3.3'), - ), - ], - child: const Text('Menu 3'), - ), - ], - ); - } -} - -class ButtonAnchorExample extends StatelessWidget { - const ButtonAnchorExample({super.key}); - - @override - Widget build(BuildContext context) { - return MenuAnchor( - builder: (context, controller, child) { - return FilledButton.tonal( - onPressed: () { - if (controller.isOpen) { - controller.close(); - } else { - controller.open(); - } - }, - child: const Text('Show menu'), - ); - }, - menuChildren: [ - MenuItemButton( - leadingIcon: const Icon(Icons.people_alt_outlined), - child: const Text('Item 1'), - onPressed: () {}, - ), - MenuItemButton( - leadingIcon: const Icon(Icons.remove_red_eye_outlined), - child: const Text('Item 2'), - onPressed: () {}, - ), - MenuItemButton( - leadingIcon: const Icon(Icons.refresh), - onPressed: () {}, - child: const Text('Item 3'), - ), - ], - ); - } -} - -class NavigationDrawers extends StatelessWidget { - const NavigationDrawers({super.key, required this.scaffoldKey}); - - final GlobalKey scaffoldKey; - - @override - Widget build(BuildContext context) { - return _Component( - name: 'Navigation drawer', - child: Column( - children: [ - const SizedBox(height: 520, child: NavigationDrawerSection()), - componentSpacer, - componentSpacer, - TextButton( - child: const Text('Show modal navigation drawer', - style: TextStyle(fontWeight: FontWeight.bold)), - onPressed: () { - scaffoldKey.currentState!.openEndDrawer(); - }, - ), - ], - ), - ); - } -} - -class NavigationDrawerSection extends StatefulWidget { - const NavigationDrawerSection({super.key}); - - @override - State createState() => - _NavigationDrawerSectionState(); -} - -class _NavigationDrawerSectionState extends State { - int navDrawerIndex = 0; - - @override - Widget build(BuildContext context) { - return NavigationDrawer( - onDestinationSelected: (selectedIndex) { - setState(() { - navDrawerIndex = selectedIndex; - }); - }, - selectedIndex: navDrawerIndex, - children: [ - Padding( - padding: const EdgeInsets.fromLTRB(28, 16, 16, 10), - child: Text( - 'Mail', - style: Theme.of(context).textTheme.titleSmall, - ), - ), - ...destinations.map((destination) { - return NavigationDrawerDestination( - label: Text(destination.label), - icon: destination.icon, - selectedIcon: destination.selectedIcon, - ); - }), - const Divider(indent: 28, endIndent: 28), - Padding( - padding: const EdgeInsets.fromLTRB(28, 16, 16, 10), - child: Text( - 'Labels', - style: Theme.of(context).textTheme.titleSmall, - ), - ), - ...labelDestinations.map((destination) { - return NavigationDrawerDestination( - label: Text(destination.label), - icon: destination.icon, - selectedIcon: destination.selectedIcon, - ); - }), - ], - ); - } -} - -class ExampleDestination { - const ExampleDestination(this.label, this.icon, this.selectedIcon); - - final String label; - final Widget icon; - final Widget selectedIcon; -} - -const List destinations = [ - ExampleDestination('Inbox', Icon(Icons.inbox_outlined), Icon(Icons.inbox)), - ExampleDestination('Outbox', Icon(Icons.send_outlined), Icon(Icons.send)), - ExampleDestination( - 'Favorites', Icon(Icons.favorite_outline), Icon(Icons.favorite)), - ExampleDestination('Trash', Icon(Icons.delete_outline), Icon(Icons.delete)), -]; - -const List labelDestinations = [ - ExampleDestination( - 'Family', Icon(Icons.bookmark_border), Icon(Icons.bookmark)), - ExampleDestination( - 'School', Icon(Icons.bookmark_border), Icon(Icons.bookmark)), - ExampleDestination('Work', Icon(Icons.bookmark_border), Icon(Icons.bookmark)), -]; - -class NavigationRails extends StatelessWidget { - const NavigationRails({super.key}); - - @override - Widget build(BuildContext context) { - return const _Component( - name: 'Navigation rail', - child: IntrinsicWidth( - child: SizedBox(height: 420, child: NavigationRailSection())), - ); - } -} - -class NavigationRailSection extends StatefulWidget { - const NavigationRailSection({super.key}); - - @override - State createState() => _NavigationRailSectionState(); -} - -class _NavigationRailSectionState extends State { - int navRailIndex = 0; - - @override - Widget build(BuildContext context) { - return NavigationRail( - onDestinationSelected: (selectedIndex) { - setState(() { - navRailIndex = selectedIndex; - }); - }, - elevation: 4, - leading: FloatingActionButton( - child: const Icon(Icons.create), onPressed: () {}), - groupAlignment: 0.0, - selectedIndex: navRailIndex, - labelType: NavigationRailLabelType.selected, - destinations: [ - ...destinations.map((destination) { - return NavigationRailDestination( - label: Text(destination.label), - icon: destination.icon, - selectedIcon: destination.selectedIcon, - ); - }), - ], - ); - } -} - -class Tabs extends StatefulWidget { - const Tabs({super.key}); - - @override - State createState() => _TabsState(); -} - -class _TabsState extends State with TickerProviderStateMixin { - late TabController _tabController; - - @override - void initState() { - super.initState(); - _tabController = TabController(length: 3, vsync: this); - } - - @override - Widget build(BuildContext context) { - return _Component( - name: 'Tabs', - child: SizedBox( - height: 80, - child: Scaffold( - appBar: AppBar( - bottom: TabBar( - controller: _tabController, - tabs: const [ - Tab( - icon: Icon(Icons.videocam_outlined), - text: 'Video', - iconMargin: EdgeInsets.only(bottom: 0.0), - ), - Tab( - icon: Icon(Icons.photo_outlined), - text: 'Photos', - iconMargin: EdgeInsets.only(bottom: 0.0), - ), - Tab( - icon: Icon(Icons.audiotrack_sharp), - text: 'Audio', - iconMargin: EdgeInsets.only(bottom: 0.0), - ), - ], - ), - // TODO: Showcase secondary tab bar https://github.com/flutter/flutter/issues/111962 - ), - ), - ), - ); - } -} - -class TopAppBars extends StatelessWidget { - const TopAppBars({super.key}); - - static final actions = [ - IconButton(icon: const Icon(Icons.attach_file), onPressed: () {}), - IconButton(icon: const Icon(Icons.event), onPressed: () {}), - IconButton(icon: const Icon(Icons.more_vert), onPressed: () {}), - ]; - - @override - Widget build(BuildContext context) { - return _Component( - name: 'Top app bars', - child: Column( - children: [ - AppBar( - title: const Text('Center-aligned'), - leading: const BackButton(), - actions: [ - IconButton( - iconSize: 32, - icon: const Icon(Icons.account_circle_outlined), - onPressed: () {}, - ), - ], - centerTitle: true, - ), - componentSpacer, - AppBar( - title: const Text('Small'), - leading: const BackButton(), - actions: actions, - centerTitle: false, - ), - componentSpacer, - SizedBox( - height: 100, - child: CustomScrollView( - slivers: [ - SliverAppBar.medium( - title: const Text('Medium'), - leading: const BackButton(), - actions: actions, - ), - const SliverFillRemaining(), - ], - ), - ), - componentSpacer, - SizedBox( - height: 130, - child: CustomScrollView( - slivers: [ - SliverAppBar.large( - title: const Text('Large'), - leading: const BackButton(), - actions: actions, - ), - const SliverFillRemaining(), - ], - ), - ), - ], - ), - ); - } -} - -class Menus extends StatefulWidget { - const Menus({super.key}); - - @override - State createState() => _MenusState(); -} - -class _MenusState extends State { - final TextEditingController colorController = TextEditingController(); - final TextEditingController iconController = TextEditingController(); - IconLabel? selectedIcon = IconLabel.smile; - ColorLabel? selectedColor; - - @override - Widget build(BuildContext context) { - final List> colorEntries = - >[]; - for (final ColorLabel color in ColorLabel.values) { - colorEntries.add(DropdownMenuEntry( - value: color, label: color.label, enabled: color.label != 'Grey')); - } - - final List> iconEntries = - >[]; - for (final IconLabel icon in IconLabel.values) { - iconEntries - .add(DropdownMenuEntry(value: icon, label: icon.label)); - } - - return _Component( - name: 'Menus', - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: const [ - ButtonAnchorExample(), - rowSpacer, - IconButtonAnchorExample(), - ], - ), - componentSpacer, - Wrap( - alignment: WrapAlignment.spaceAround, - runAlignment: WrapAlignment.center, - crossAxisAlignment: WrapCrossAlignment.center, - spacing: spacingS, - runSpacing: spacingS, - children: [ - DropdownMenu( - controller: colorController, - label: const Text('Color'), - enableFilter: true, - dropdownMenuEntries: colorEntries, - inputDecorationTheme: const InputDecorationTheme(filled: true), - onSelected: (color) { - setState(() { - selectedColor = color; - }); - }, - ), - DropdownMenu( - initialSelection: IconLabel.smile, - controller: iconController, - leadingIcon: const Icon(Icons.search), - label: const Text('Icon'), - dropdownMenuEntries: iconEntries, - onSelected: (icon) { - setState(() { - selectedIcon = icon; - }); - }, - ), - Icon( - selectedIcon?.icon, - color: selectedColor?.color ?? Colors.grey.withOpacity(0.5), - ) - ], - ), - ], - ), - ); - } -} - -enum ColorLabel { - blue('Blue', Colors.blue), - pink('Pink', Colors.pink), - green('Green', Colors.green), - yellow('Yellow', Colors.yellow), - grey('Grey', Colors.grey); - - const ColorLabel(this.label, this.color); - - final String label; - final Color color; -} - -enum IconLabel { - smile('Smile', Icons.sentiment_satisfied_outlined), - cloud( - 'Cloud', - Icons.cloud_outlined, - ), - brush('Brush', Icons.brush_outlined), - heart('Heart', Icons.favorite); - - const IconLabel(this.label, this.icon); - - final String label; - final IconData icon; -} - -class Sliders extends StatefulWidget { - const Sliders({super.key}); - - @override - State createState() => _SlidersState(); -} - -class _SlidersState extends State { - double sliderValue0 = 30.0; - double sliderValue1 = 20.0; - - @override - Widget build(BuildContext context) { - return _Component( - name: 'Sliders', - child: Column( - children: [ - Slider( - max: 100, - value: sliderValue0, - onChanged: (value) { - setState(() { - sliderValue0 = value; - }); - }, - ), - const SizedBox(height: 20), - Slider( - max: 100, - divisions: 5, - value: sliderValue1, - label: sliderValue1.round().toString(), - onChanged: (value) { - setState(() { - sliderValue1 = value; - }); - }, - ), - ], - )); - } -} - -class _Component extends StatefulWidget { - const _Component({ - super.key, - required this.name, - required this.child, - }); - - final String name; - final Widget child; - - @override - State<_Component> createState() => _ComponentState(); -} - -class _ComponentState extends State<_Component> { - @override - Widget build(BuildContext context) { - return RepaintBoundary( - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: spacingS, horizontal: spacingM), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - widget.name, - style: Theme.of(context).textTheme.titleLarge, - ), - ConstrainedBox( - constraints: - const BoxConstraints.tightFor(width: widthConstraint), - child: Padding( - padding: const EdgeInsets.symmetric(vertical: spacingM), - child: Center( - child: widget.child, - ), - ), - ), - componentSpacer, - ], - ), - ), - ); - } -} From 5ccabb745349ff792aadf6285ac6139ffc334533 Mon Sep 17 00:00:00 2001 From: Tayeb Sedraia Date: Tue, 20 Feb 2024 15:20:59 +0100 Subject: [PATCH 03/10] app : add l10n keys --- app/lib/l10n/ods_flutter_en.arb | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/app/lib/l10n/ods_flutter_en.arb b/app/lib/l10n/ods_flutter_en.arb index bce5cea4..057316dd 100644 --- a/app/lib/l10n/ods_flutter_en.arb +++ b/app/lib/l10n/ods_flutter_en.arb @@ -45,6 +45,8 @@ "componentElementTypes": "Types", "componentElementToggleCount": "Toggle count", "componentElementEnabled": "Enabled", + "componentTrailing": "Trailing element", + "componentLeading": "Leading", "@_COMPONENTS_CARDS": {}, "componentCardsTitle": "Cards", @@ -262,6 +264,16 @@ "componentMenuIcons" : "Icons", "componentMenuEnabled" : "Enabled", + "@_COMPONENTS_NAVIGATION_RAIL": {}, + "componentNavigationRail": "Navigation rail", + "componentNavigationRailDescription": "Navigation rail is meant to be displayed at the left or right of an app to navigate between a small number of views.", + + "@_NAVIGATION_RAIL_VARIANT": {}, + "componentNavigationRailSubtitle": "OdsNavigationRail", + "componentNavigationRailLeadingNone": "None", + "componentNavigationRailLeadingFirst": "First", + "componentNavigationRailLeadingSecond": "Second", + "@_COMPONENTS_SHEETS_BOTTOM": {}, "componentSheetsBottomTitle": "Sheets: bottom", "componentSheetsBottomDescription": "Bottom sheets are surfaces anchored to the bottom of the screen that present users supplemental content.", From 40302069cac3dc8d3fd8ab455bb77bc481cff501 Mon Sep 17 00:00:00 2001 From: Tayeb Sedraia Date: Tue, 20 Feb 2024 15:21:32 +0100 Subject: [PATCH 04/10] app : add ods navigation rail component --- .../navigation_rail/navigation_rail.dart | 289 ++++++++++++++++++ .../navigation_rail_customization.dart | 108 +++++++ .../navigation_rail/navigation_rail_enum.dart | 33 ++ 3 files changed, 430 insertions(+) create mode 100644 app/lib/ui/components/navigation_rail/navigation_rail.dart create mode 100644 app/lib/ui/components/navigation_rail/navigation_rail_customization.dart create mode 100644 app/lib/ui/components/navigation_rail/navigation_rail_enum.dart diff --git a/app/lib/ui/components/navigation_rail/navigation_rail.dart b/app/lib/ui/components/navigation_rail/navigation_rail.dart new file mode 100644 index 00000000..0dfe336c --- /dev/null +++ b/app/lib/ui/components/navigation_rail/navigation_rail.dart @@ -0,0 +1,289 @@ +/* + * Software Name : Orange Design System + * SPDX-FileCopyrightText: Copyright (c) Orange SA + * SPDX-License-Identifier: MIT + * + * This software is distributed under the MIT licence, + * the text of which is available at https://opensource.org/license/MIT/ + * or see the "LICENSE" file for more details. + * + * Software description: Flutter library of reusable graphical components for Android and iOS + */ + +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/ods_flutter_app_localizations.dart'; +import 'package:ods_flutter/components/button/segmented/button_icon/ods_button_icon.dart'; +import 'package:ods_flutter/components/chips/ods_choice_chips.dart'; +import 'package:ods_flutter/components/floating_action_button/ods_fab.dart'; +import 'package:ods_flutter/components/lists/ods_list_switch.dart'; +import 'package:ods_flutter/components/navigation_rail/ods_navigation_rail.dart'; +import 'package:ods_flutter/components/navigation_rail/ods_navigation_rail_item.dart'; +import 'package:ods_flutter/components/sheets_bottom/ods_sheets_bottom.dart'; +import 'package:ods_flutter/guidelines/spacings.dart'; +import 'package:ods_flutter_demo/ui/components/navigation_bar/navigation_bar_customization.dart'; +import 'package:ods_flutter_demo/ui/components/navigation_rail/navigation_rail_customization.dart'; +import 'package:ods_flutter_demo/ui/components/navigation_rail/navigation_rail_enum.dart'; +import 'package:ods_flutter_demo/ui/main_app_bar.dart'; +import 'package:ods_flutter_demo/ui/utilities/component_count_row.dart'; + +class ComponentNavigationRail extends StatefulWidget { + const ComponentNavigationRail({Key? key}) : super(key: key); + + @override + State createState() => + _ComponentNavigationBarState(); +} + +class _ComponentNavigationBarState extends State { + final _scaffoldKey = GlobalKey(); + + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + return NavigationRailCustomization( + child: Scaffold( + bottomSheet: OdsSheetsBottom( + sheetContent: _CustomizationContent(), + title: AppLocalizations.of(context)!.componentCustomizeTitle, + ), + key: _scaffoldKey, + appBar: MainAppBar(AppLocalizations.of(context)!.componentNavigationRail), + body: _NavBarDemo(), + )); + } +} + +class _NavBarDemo extends StatefulWidget { + _NavBarDemo(); + + @override + State<_NavBarDemo> createState() => _NavBarDemoState(); +} + +class _NavBarDemoState extends State<_NavBarDemo> { + late int selectedIndex; + + @override + void initState() { + super.initState(); + selectedIndex = 0; + } + + @override + Widget build(BuildContext context) { + final NavigationRailCustomizationState? customizationState = + NavigationRailCustomization.of(context); + + List navigationDestinations = + _destinations(context).sublist(0, customizationState?.numberOfItems); + + Widget? firstIcon; + Widget? secondIcon; + if (selectedIndex >= navigationDestinations.length) { + selectedIndex = navigationDestinations.length - 1; + } + + switch (customizationState?.selectedElement) { + case NavigationRailsEnum.none: + firstIcon = null; + secondIcon = null; + break; + case NavigationRailsEnum.firstIcon: + firstIcon = OdsButtonIcon( + icon: Icon(Icons.menu), + style: OdsButtonIconStyle + .functionalStandard, // Optional by default OdsButtonIconStyle.functionalStandard + isEnabled: true, // Optional by default true + onClick: () { + setState(() { + print("Click"); + }); + }, + ); + secondIcon = null; + break; + case NavigationRailsEnum.secondIcon: + firstIcon = OdsButtonIcon( + icon: Icon(Icons.menu), + style: OdsButtonIconStyle + .functionalStandard, // Optional by default OdsButtonIconStyle.functionalStandard + isEnabled: true, // Optional by default true + onClick: () { + setState(() { + print("Click"); + }); + }, + ); + secondIcon = OdsFloatingActionButton( + onClick: () {}, + icon: const Icon(Icons.person), + semanticsLabel: 'Add', //Optional + ); + break; + } + + return Row( + children: [ + OdsNavigationRail( + selectedIndex: selectedIndex, + onDestinationSelected: (int index) { + setState(() { + selectedIndex = index; + }); + }, + leadingIconFirst: firstIcon, + leadingIconSecond: secondIcon, + destinations: navigationDestinations, + ), + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + getScreen(selectedIndex), + ), + ], + ), + ), + ], + ); + } + + String getScreen(int number) { + switch (number) { + case 0: + return "Cooking"; + case 1: + return "Coffee"; + case 2: + return "Ice Cream"; + case 3: + return "Restaurant"; + case 4: + return "Favorites"; + default: + return ""; + } + } + + List _destinations(BuildContext context) { + final NavigationRailCustomizationState? customizationState = + NavigationRailCustomization.of(context); + return [ + OdsNavigationRailItem( + context: context, + label: AppLocalizations.of(context)!.navigationBarItemCookingPot, + icon: "assets/recipes/ic_cooking_pot.svg", + badge: customizationState?.hasBadge == true ? "3" : null, + ), + OdsNavigationRailItem( + context: context, + label: AppLocalizations.of(context)!.navigationBarItemCoffee, + icon: Icon( + Icons.coffee_sharp, + ), + ), + OdsNavigationRailItem( + context: context, + label: AppLocalizations.of(context)!.navigationBarItemIceCream, + icon: "assets/recipes/ic_ice_cream.svg", + ), + OdsNavigationRailItem( + context: context, + label: AppLocalizations.of(context)!.navigationBarItemRestaurant, + icon: "assets/recipes/ic_restaurant.svg", + ), + OdsNavigationRailItem( + context: context, + label: AppLocalizations.of(context)!.navigationBarItemFavorites, + icon: "assets/recipes/ic_heart_favorite.svg", + ), + ]; + } +} + +/// Bottom Sheet content +class _CustomizationContent extends StatefulWidget { + @override + State<_CustomizationContent> createState() => _CustomizationContentState(); +} + +class _CustomizationContentState extends State<_CustomizationContent> { + @override + Widget build(BuildContext context) { + final NavigationRailCustomizationState? customizationState = + NavigationRailCustomization.of(context); + return Column( + children: [ + ComponentCountRow( + title: AppLocalizations.of(context)!.navigationBarItemCount, + minCount: NavigationBarCustomizationState.minNavigationItemCount, + maxCount: NavigationBarCustomizationState.maxNavigationItemCount, + count: customizationState!.numberOfItems, + onChanged: (value) { + customizationState.numberOfItems = value; + }), + OdsListSwitch( + title: AppLocalizations.of(context)!.navigationBarItemBadge, + checked: customizationState.hasBadge, + onCheckedChange: (bool value) { + customizationState.hasBadge = value; + }, + ), + Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: const EdgeInsets.all(spacingM), + child: Text( + AppLocalizations.of(context)!.componentLeading, + style: Theme.of(context).textTheme.bodyMedium, + textAlign: TextAlign.left, + ), + ), + ), + Align( + alignment: Alignment.centerLeft, + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: List.generate( + customizationState!.elements.length, + (int index) { + NavigationRailsEnum currentElement = + customizationState.elements[index]; + bool isSelected = + currentElement == customizationState.selectedElement; + return Padding( + padding: EdgeInsets.only(right: spacingXs, left: spacingS), + child: OdsChoiceChip( + text: customizationState.elements[index] + .stringValue(context), + selected: isSelected, + onClick: (selected) { + setState( + () { + FocusScope.of(context).unfocus(); + customizationState.selectedElement = + customizationState.elements[index]; + Future.delayed(Duration(milliseconds: 100)) + .then((value) { + FocusScope.of(context).requestFocus(); + }); + }, + ); + }, + ), + ); + }, + ), + ), + ), + ), + ], + ); + } +} diff --git a/app/lib/ui/components/navigation_rail/navigation_rail_customization.dart b/app/lib/ui/components/navigation_rail/navigation_rail_customization.dart new file mode 100644 index 00000000..9cd059cf --- /dev/null +++ b/app/lib/ui/components/navigation_rail/navigation_rail_customization.dart @@ -0,0 +1,108 @@ +/* + * Software Name : Orange Design System + * SPDX-FileCopyrightText: Copyright (c) Orange SA + * SPDX-License-Identifier: MIT + * + * This software is distributed under the MIT licence, + * the text of which is available at https://opensource.org/license/MIT/ + * or see the "LICENSE" file for more details. + * + * Software description: Flutter library of reusable graphical components for Android and iOS + */ + +import 'package:flutter/material.dart'; +import 'package:ods_flutter_demo/ui/components/navigation_rail/navigation_rail_enum.dart'; + +class _NavigationRailCustomization extends InheritedWidget { + _NavigationRailCustomization({ + Key? key, + required Widget child, + required this.data, + }) : super(key: key, child: child); + + final NavigationRailCustomizationState data; + + @override + bool updateShouldNotify(_NavigationRailCustomization oldWidget) => true; +} + +class NavigationRailCustomization extends StatefulWidget { + const NavigationRailCustomization({ + super.key, + required this.child, + }); + + final Widget child; + + @override + NavigationRailCustomizationState createState() => + NavigationRailCustomizationState(); + + static NavigationRailCustomizationState? of(BuildContext context) { + return (context + .dependOnInheritedWidgetOfExactType<_NavigationRailCustomization>()) + ?.data; + } +} + +class NavigationRailCustomizationState + extends State { + static get minNavigationItemCount => 2; + static get maxNavigationItemCount => 5; + bool _hasBadge = true; + bool _hasTrailing = false; + + int _numberOfItems = minNavigationItemCount; + int get numberOfItems => _numberOfItems; + + set numberOfItems(int value) { + setState(() { + _numberOfItems = value; + }); + } + + bool get hasBadge => _hasBadge; + set hasBadge(bool value) { + setState(() { + _hasBadge = value; + }); + } + + bool get hasTrailing => _hasTrailing; + set hasTrailing(bool value) { + setState(() { + _hasTrailing = value; + }); + } + + /// Enum + List _elements = [ + NavigationRailsEnum.none, + NavigationRailsEnum.firstIcon, + NavigationRailsEnum.secondIcon, + ]; + NavigationRailsEnum _selectedElement = NavigationRailsEnum.none; + + List get elements => _elements; + set elements(List value) { + setState(() { + _elements = value; + }); + } + + NavigationRailsEnum get selectedElement => _selectedElement; + + set selectedElement(NavigationRailsEnum value) { + setState(() { + _selectedElement = value; + }); + } + + @override + Widget build(BuildContext context) { + return _NavigationRailCustomization( + data: this, + child: widget.child, + ); + } +} diff --git a/app/lib/ui/components/navigation_rail/navigation_rail_enum.dart b/app/lib/ui/components/navigation_rail/navigation_rail_enum.dart new file mode 100644 index 00000000..1c34279f --- /dev/null +++ b/app/lib/ui/components/navigation_rail/navigation_rail_enum.dart @@ -0,0 +1,33 @@ +/* + * Software Name : Orange Design System + * SPDX-FileCopyrightText: Copyright (c) Orange SA + * SPDX-License-Identifier: MIT + * + * This software is distributed under the MIT licence, + * the text of which is available at https://opensource.org/license/MIT/ + * or see the "LICENSE" file for more details. + * + * Software description: Flutter library of reusable graphical components for Android and iOS + */ + +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/ods_flutter_app_localizations.dart'; + +enum NavigationRailsEnum { none, firstIcon, secondIcon } + +extension CustomElementExtension on NavigationRailsEnum { + String stringValue(BuildContext context) { + switch (this) { + case NavigationRailsEnum.none: + return AppLocalizations.of(context)!.componentNavigationRailLeadingNone; + case NavigationRailsEnum.firstIcon: + return AppLocalizations.of(context)! + .componentNavigationRailLeadingFirst; + case NavigationRailsEnum.secondIcon: + return AppLocalizations.of(context)! + .componentNavigationRailLeadingSecond; + default: + return ""; + } + } +} From c93598615a64c812152df9c7ba4c5a07d3597b08 Mon Sep 17 00:00:00 2001 From: Tayeb Sedraia Date: Tue, 20 Feb 2024 15:21:54 +0100 Subject: [PATCH 05/10] app : replace navigation rail with ods component --- app/lib/ui/main_screen.dart | 4 +- app/lib/ui/utilities/navigation_items.dart | 46 +++++++++------------- 2 files changed, 20 insertions(+), 30 deletions(-) diff --git a/app/lib/ui/main_screen.dart b/app/lib/ui/main_screen.dart index 04991619..ba319b90 100644 --- a/app/lib/ui/main_screen.dart +++ b/app/lib/ui/main_screen.dart @@ -12,6 +12,7 @@ import 'package:flutter/material.dart'; import 'package:ods_flutter/components/navigation_bar/ods_navigation_bar.dart'; +import 'package:ods_flutter/components/navigation_rail/ods_navigation_rail.dart'; import 'package:ods_flutter_demo/ui/utilities/navigation_items.dart'; import 'main_app_bar.dart'; @@ -45,7 +46,7 @@ class _MainScreenState extends State { body: Row( children: [ if (MediaQuery.of(context).size.width >= 640) - NavigationRail( + OdsNavigationRail( onDestinationSelected: (int index) { setState(() { _selectedIndex = index; @@ -53,7 +54,6 @@ class _MainScreenState extends State { }, selectedIndex: _selectedIndex, destinations: navigationItems.getNavigationRailDestinations(), - labelType: NavigationRailLabelType.all, // Called when one tab is selected, ), Expanded( diff --git a/app/lib/ui/utilities/navigation_items.dart b/app/lib/ui/utilities/navigation_items.dart index bfc49ca3..7f8ef528 100644 --- a/app/lib/ui/utilities/navigation_items.dart +++ b/app/lib/ui/utilities/navigation_items.dart @@ -12,8 +12,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/ods_flutter_app_localizations.dart'; -import 'package:flutter_svg/svg.dart'; import 'package:ods_flutter/components/navigation_bar/ods_navigation_bar_item.dart'; +import 'package:ods_flutter/components/navigation_rail/ods_navigation_rail_item.dart'; import 'package:ods_flutter_demo/ui/about/about_screen.dart'; import 'package:ods_flutter_demo/ui/components/components.dart'; import 'package:ods_flutter_demo/ui/components/components_screen.dart'; @@ -24,7 +24,7 @@ import 'package:ods_flutter_demo/ui/modules/modules_screen.dart'; class NavigationItems { late BuildContext context; late List _destinationsStatic; - late List _destinationsRailStatic; + late List _destinationsRailStatic; late List _screens; NavigationItems(this.context) { @@ -58,35 +58,25 @@ class NavigationItems { ]; _destinationsRailStatic = [ - NavigationRailDestination( - icon: SvgPicture.asset("assets/ic_guidelines_dna.svg", - colorFilter: colorFilter), - selectedIcon: SvgPicture.asset("assets/ic_guidelines_dna.svg", - colorFilter: activeColorFilter), - label: Text( - AppLocalizations.of(context)!.bottomNavigationGuideline, - ), + OdsNavigationRailItem( + context: context, + label: AppLocalizations.of(context)!.bottomNavigationGuideline, + icon: "assets/ic_guidelines_dna.svg", ), - NavigationRailDestination( - icon: SvgPicture.asset("assets/ic_components_atom.svg", - colorFilter: colorFilter), - selectedIcon: SvgPicture.asset("assets/ic_components_atom.svg", - colorFilter: activeColorFilter), - label: Text(AppLocalizations.of(context)!.bottomNavigationComponents), + OdsNavigationRailItem( + context: context, + label: AppLocalizations.of(context)!.bottomNavigationComponents, + icon: "assets/ic_components_atom.svg", ), - NavigationRailDestination( - icon: SvgPicture.asset("assets/ic_modules_molecule.svg", - colorFilter: colorFilter), - selectedIcon: SvgPicture.asset("assets/ic_modules_molecule.svg", - colorFilter: activeColorFilter), - label: Text(AppLocalizations.of(context)!.bottomNavigationModules), + OdsNavigationRailItem( + context: context, + label: AppLocalizations.of(context)!.bottomNavigationModules, + icon: "assets/ic_modules_molecule.svg", ), - NavigationRailDestination( - icon: SvgPicture.asset("assets/ic_about_info.svg", - colorFilter: colorFilter), - selectedIcon: SvgPicture.asset("assets/ic_about_info.svg", - colorFilter: activeColorFilter), - label: Text(AppLocalizations.of(context)!.bottomNavigationAbout), + OdsNavigationRailItem( + context: context, + label: AppLocalizations.of(context)!.bottomNavigationAbout, + icon: "assets/ic_about_info.svg", ), ]; _screens = [ From 0de8c34e29ec4b5826140d67550c9281ebc90bf0 Mon Sep 17 00:00:00 2001 From: Tayeb Sedraia Date: Tue, 20 Feb 2024 15:22:36 +0100 Subject: [PATCH 06/10] library : add navigation rail theme data --- library/lib/theme/ods_theme.dart | 46 +++++++++++++++++++++++++++----- 1 file changed, 40 insertions(+), 6 deletions(-) diff --git a/library/lib/theme/ods_theme.dart b/library/lib/theme/ods_theme.dart index 279533fc..c9fb5f78 100644 --- a/library/lib/theme/ods_theme.dart +++ b/library/lib/theme/ods_theme.dart @@ -75,12 +75,29 @@ class OdsTheme { }, ), ), - navigationRailTheme: const NavigationRailThemeData( + navigationRailTheme: NavigationRailThemeData( + //surfaceTintColor: lightColorScheme.onSecondary, elevation: 3.0, indicatorColor: grey200, indicatorShape: null, - selectedLabelTextStyle: - TextStyle(color: orange200, overflow: TextOverflow.ellipsis), + selectedIconTheme: IconThemeData(color: lightColorScheme.primary), + unselectedIconTheme: IconThemeData(color: lightColorScheme.secondary), + selectedLabelTextStyle: TextStyle( + color: lightColorScheme.primary, + overflow: TextOverflow.ellipsis, + fontSize: 12, + fontWeight: FontWeight.w500, + height: 1.43, + letterSpacing: 0.25, + ), + unselectedLabelTextStyle: TextStyle( + color: lightColorScheme.secondary, + overflow: TextOverflow.ellipsis, + fontSize: 12, + fontWeight: FontWeight.w400, + height: 1.43, + letterSpacing: 0.25, + ), ), cardTheme: CardTheme( surfaceTintColor: lightColorScheme.onSecondary, @@ -357,12 +374,29 @@ class OdsTheme { letterSpacing: 0.25); }), ), - navigationRailTheme: const NavigationRailThemeData( + navigationRailTheme: NavigationRailThemeData( + //surfaceTintColor: lightColorScheme.onSecondary, elevation: 3.0, indicatorColor: grey800, indicatorShape: null, - selectedLabelTextStyle: - TextStyle(color: orange100, overflow: TextOverflow.ellipsis), + selectedIconTheme: IconThemeData(color: darkColorScheme.primary), + unselectedIconTheme: IconThemeData(color: darkColorScheme.secondary), + selectedLabelTextStyle: TextStyle( + color: darkColorScheme.primary, + overflow: TextOverflow.ellipsis, + fontSize: 12, + fontWeight: FontWeight.w500, + height: 1.43, + letterSpacing: 0.25, + ), + unselectedLabelTextStyle: TextStyle( + color: darkColorScheme.secondary, + overflow: TextOverflow.ellipsis, + fontSize: 12, + fontWeight: FontWeight.w400, + height: 1.43, + letterSpacing: 0.25, + ), ), cardTheme: CardTheme( surfaceTintColor: darkColorScheme.onSecondary, From 90d501846d9d5991827cefa2745bb86dd4341b98 Mon Sep 17 00:00:00 2001 From: Tayeb Sedraia Date: Tue, 20 Feb 2024 15:22:53 +0100 Subject: [PATCH 07/10] library : add ods navigation rail and item --- .../navigation_rail/ods_navigation_rail.dart | 77 ++++++++++++++++++ .../ods_navigation_rail_item.dart | 80 +++++++++++++++++++ 2 files changed, 157 insertions(+) create mode 100644 library/lib/components/navigation_rail/ods_navigation_rail.dart create mode 100644 library/lib/components/navigation_rail/ods_navigation_rail_item.dart diff --git a/library/lib/components/navigation_rail/ods_navigation_rail.dart b/library/lib/components/navigation_rail/ods_navigation_rail.dart new file mode 100644 index 00000000..3a031eb1 --- /dev/null +++ b/library/lib/components/navigation_rail/ods_navigation_rail.dart @@ -0,0 +1,77 @@ +/* + * Software Name : Orange Design System + * SPDX-FileCopyrightText: Copyright (c) Orange SA + * SPDX-License-Identifier: MIT + * + * This software is distributed under the MIT licence, + * the text of which is available at https://opensource.org/license/MIT/ + * or see the "LICENSE" file for more details. + * + * Software description: Flutter library of reusable graphical components for Android and iOS + */ + +import 'package:flutter/material.dart'; +import 'package:ods_flutter/components/navigation_rail/ods_navigation_rail_item.dart'; +import 'package:ods_flutter/guidelines/spacings.dart'; + +/// ODS Navigation Rail. +/// +/// The Navigation Rail is meant to be displayed at the left or right of an app to navigate between +/// a small number of views, typically between three and five. +class OdsNavigationRail extends StatefulWidget { + /// Creates an ODS Button. + /// + /// * [selectedIndex] - The index into [destinations] for the current selected NavigationRailDestination or null if no destination is selected. + /// * [destinations] - Defines the appearance of the button items that are arrayed within the navigation rail. + /// * [onDestinationSelected] - Called when one of the destinations is selected.. + /// * [leadingIconFirst] - The first leading widget in the rail that is placed above the destinations. + /// * [leadingIconSecond] - The second leading widget in the rail that is placed above the destinations. + const OdsNavigationRail({ + Key? key, + required this.selectedIndex, + required this.destinations, + this.onDestinationSelected, + this.leadingIconFirst, + this.leadingIconSecond, + }) : super(key: key); + + /// The index into [destinations] for the current selected NavigationRailDestination or null if no destination is selected. + final int selectedIndex; + + /// Defines the appearance of the button items that are arrayed within the navigation rail. + final List destinations; + + /// The callback function called when one of the destinations is selected.. + final void Function(int)? onDestinationSelected; + + /// The first leading widget in the rail that is placed above the destinations. + final Widget? leadingIconFirst; + + /// The second leading widget in the rail that is placed above the destinations. + final Widget? leadingIconSecond; + + @override + State createState() => _OdsNavigationBarState(); +} + +class _OdsNavigationBarState extends State { + @override + Widget build(BuildContext context) { + return NavigationRail( + selectedIndex: widget.selectedIndex, + onDestinationSelected: widget.onDestinationSelected, + destinations: widget.destinations, + labelType: NavigationRailLabelType.all, + leading: widget.leadingIconFirst != null || + widget.leadingIconSecond != null + ? Column( + children: [ + if (widget.leadingIconFirst != null) widget.leadingIconFirst!, + if (widget.leadingIconSecond != null) widget.leadingIconSecond!, + const SizedBox(height: spacingXl), + ], + ) + : null, + ); + } +} diff --git a/library/lib/components/navigation_rail/ods_navigation_rail_item.dart b/library/lib/components/navigation_rail/ods_navigation_rail_item.dart new file mode 100644 index 00000000..5e9adec4 --- /dev/null +++ b/library/lib/components/navigation_rail/ods_navigation_rail_item.dart @@ -0,0 +1,80 @@ +/* + * Software Name : Orange Design System + * SPDX-FileCopyrightText: Copyright (c) Orange SA + * SPDX-License-Identifier: MIT + * + * This software is distributed under the MIT licence, + * the text of which is available at https://opensource.org/license/MIT/ + * or see the "LICENSE" file for more details. + * + * Software description: Flutter library of reusable graphical components for Android and iOS + */ + +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:ods_flutter/l10n/gen/ods_localizations.dart'; + +/// ODS Navigation Rail Item. +/// +/// Creates a destination that is used with NavigationRailItem.destinations. +class OdsNavigationRailItem extends NavigationRailDestination { + OdsNavigationRailItem({ + Key? key, + required dynamic icon, + String? badge, + required String label, + required BuildContext context, + }) : super( + icon: _buildIcon(icon, badge, context), + label: Text(label), + selectedIcon: _buildIcon(icon, badge, context, isSelected: true), + ); + + static Widget _buildIcon( + dynamic iconData, String? badge, BuildContext context, + {bool isSelected = false}) { + var colorScheme = Theme.of(context).colorScheme; + var colorFilter = isSelected + ? ColorFilter.mode(colorScheme.primary, BlendMode.srcIn) + : ColorFilter.mode(colorScheme.secondary, BlendMode.srcIn); + + /// If the type is IconType.icon, use the provided icon (of type Icon) + Widget iconWidget = iconData is Widget + ? iconData + + /// If the type is IconType.svg, use the SVG icon + : (iconData is String && iconData.endsWith('.svg') + ? Semantics( + excludeSemantics: true, + child: SvgPicture.asset( + iconData, + colorFilter: colorFilter, + ), + ) + + /// If the type is IconType.png, use the PNG icon + : (iconData is String && iconData.endsWith('.png') + ? Semantics( + excludeSemantics: true, + child: Image.asset(iconData), + ) + : throw Exception( + 'Invalid icon type: ${iconData.runtimeType}'))); + + /// If the odsBottomNavigationItemIcon.badge parameter is not empty, use the Widget Badge + return badge != null + ? Badge( + label: Semantics( + label: + "${badge!} ${OdsLocalizations.of(context)!.componentNavigationBarNotification}", + excludeSemantics: true, + child: Text( + badge!, + textScaleFactor: 1.0, + ), + ), + child: iconWidget, + ) + : iconWidget; + } +} From b48a2905c7c3322a5b830dc61ae703b5f82ef0f6 Mon Sep 17 00:00:00 2001 From: Tayeb Sedraia Date: Tue, 20 Feb 2024 15:23:04 +0100 Subject: [PATCH 08/10] library and app : add changelog --- app/CHANGELOG.md | 1 + library/CHANGELOG.md | 1 + 2 files changed, 2 insertions(+) diff --git a/app/CHANGELOG.md b/app/CHANGELOG.md index 1f2e2121..c4abfa89 100644 --- a/app/CHANGELOG.md +++ b/app/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [Bug]: ODS - Components - Cards - Horizontal card - divider ([#271](https://github.com/Orange-OpenSource/ods-flutter/issues/271)) - ODS - Component - Buttons: Segmented Buttons ([#7](https://github.com/Orange-OpenSource/ods-flutter/issues/7)) - ODS - Component - Buttons: icon Buttons ([#6](https://github.com/Orange-OpenSource/ods-flutter/issues/6)) +- ODS - Component - Navigation Rail ([#25](https://github.com/Orange-OpenSource/ods-flutter/issues/25)) ## Changed diff --git a/library/CHANGELOG.md b/library/CHANGELOG.md index 6de0c2fc..7d6d1d1f 100644 --- a/library/CHANGELOG.md +++ b/library/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [Bug]: ODS - Components - Cards - Horizontal card - divider ([#271](https://github.com/Orange-OpenSource/ods-flutter/issues/271)) - ODS - Component - Buttons: Segmented Buttons ([#7](https://github.com/Orange-OpenSource/ods-flutter/issues/7)) - ODS - Component - Buttons: icon Buttons ([#6](https://github.com/Orange-OpenSource/ods-flutter/issues/6)) +- ODS - Component - Navigation Rail ([#25](https://github.com/Orange-OpenSource/ods-flutter/issues/25)) ### Changed From 5119eb434e8abc1e1c053af9eeba2eef03c121e6 Mon Sep 17 00:00:00 2001 From: Tayeb Sedraia Date: Tue, 20 Feb 2024 17:48:33 +0100 Subject: [PATCH 09/10] library : review fix single child scroll --- .../navigation_rail/ods_navigation_rail.dart | 49 +++++++++++++------ 1 file changed, 33 insertions(+), 16 deletions(-) diff --git a/library/lib/components/navigation_rail/ods_navigation_rail.dart b/library/lib/components/navigation_rail/ods_navigation_rail.dart index 3a031eb1..2f9a80ce 100644 --- a/library/lib/components/navigation_rail/ods_navigation_rail.dart +++ b/library/lib/components/navigation_rail/ods_navigation_rail.dart @@ -23,7 +23,7 @@ class OdsNavigationRail extends StatefulWidget { /// /// * [selectedIndex] - The index into [destinations] for the current selected NavigationRailDestination or null if no destination is selected. /// * [destinations] - Defines the appearance of the button items that are arrayed within the navigation rail. - /// * [onDestinationSelected] - Called when one of the destinations is selected.. + /// * [onDestinationSelected] - Called when one of the destinations is selected. /// * [leadingIconFirst] - The first leading widget in the rail that is placed above the destinations. /// * [leadingIconSecond] - The second leading widget in the rail that is placed above the destinations. const OdsNavigationRail({ @@ -57,21 +57,38 @@ class OdsNavigationRail extends StatefulWidget { class _OdsNavigationBarState extends State { @override Widget build(BuildContext context) { - return NavigationRail( - selectedIndex: widget.selectedIndex, - onDestinationSelected: widget.onDestinationSelected, - destinations: widget.destinations, - labelType: NavigationRailLabelType.all, - leading: widget.leadingIconFirst != null || - widget.leadingIconSecond != null - ? Column( - children: [ - if (widget.leadingIconFirst != null) widget.leadingIconFirst!, - if (widget.leadingIconSecond != null) widget.leadingIconSecond!, - const SizedBox(height: spacingXl), - ], - ) - : null, + return Row( + children: [ + LayoutBuilder( + builder: (context, constraint) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints(minHeight: constraint.maxHeight), + child: IntrinsicHeight( + child: NavigationRail( + selectedIndex: widget.selectedIndex, + onDestinationSelected: widget.onDestinationSelected, + destinations: widget.destinations, + labelType: NavigationRailLabelType.all, + leading: widget.leadingIconFirst != null || + widget.leadingIconSecond != null + ? Column( + children: [ + if (widget.leadingIconFirst != null) + widget.leadingIconFirst!, + if (widget.leadingIconSecond != null) + widget.leadingIconSecond!, + const SizedBox(height: spacingXl), + ], + ) + : null, + ), + ), + ), + ); + }, + ), + ], ); } } From e53ab57951f75f32de6f321b11943b989e3ee556 Mon Sep 17 00:00:00 2001 From: Tayeb Sedraia Date: Tue, 20 Feb 2024 19:22:55 +0100 Subject: [PATCH 10/10] app : review fix name class body --- .../ui/components/navigation_rail/navigation_rail.dart | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/lib/ui/components/navigation_rail/navigation_rail.dart b/app/lib/ui/components/navigation_rail/navigation_rail.dart index 0dfe336c..36c79977 100644 --- a/app/lib/ui/components/navigation_rail/navigation_rail.dart +++ b/app/lib/ui/components/navigation_rail/navigation_rail.dart @@ -52,19 +52,19 @@ class _ComponentNavigationBarState extends State { ), key: _scaffoldKey, appBar: MainAppBar(AppLocalizations.of(context)!.componentNavigationRail), - body: _NavBarDemo(), + body: _NavRailDemo(), )); } } -class _NavBarDemo extends StatefulWidget { - _NavBarDemo(); +class _NavRailDemo extends StatefulWidget { + _NavRailDemo(); @override - State<_NavBarDemo> createState() => _NavBarDemoState(); + State<_NavRailDemo> createState() => _NavRailDemoState(); } -class _NavBarDemoState extends State<_NavBarDemo> { +class _NavRailDemoState extends State<_NavRailDemo> { late int selectedIndex; @override