Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tabs #44

Merged
merged 33 commits into from
Jun 24, 2024
Merged

Tabs #44

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
820ae3b
first draft
Daviiddoo Jun 6, 2024
704be2e
tabs with tab content
Daviiddoo Jun 16, 2024
9cbe582
Styling done, Which fields should I add to tabStyle and which should …
Daviiddoo Jun 17, 2024
ea12659
Implementation done
Daviiddoo Jun 18, 2024
f745a58
up to date with master
Daviiddoo Jun 18, 2024
7ba134b
change to FTabEntry, removed FTabContent
Daviiddoo Jun 18, 2024
7d8ee2c
Tests are working
Daviiddoo Jun 18, 2024
0d7e43e
fixed dart issues
Daviiddoo Jun 18, 2024
ec8f280
Create flutter_test_config.dart
Daviiddoo Jun 18, 2024
25dfa18
Commit from GitHub Actions (Forui Presubmit)
Daviiddoo Jun 18, 2024
5584b32
Remove outdated golden images
Pante Jun 19, 2024
993cd18
Update forui/lib/src/widgets/tabs/tab_controller.dart
Daviiddoo Jun 19, 2024
9584e32
PR fixes in
Daviiddoo Jun 19, 2024
d984e07
Merge branch 'master' into feature/tabs
Daviiddoo Jun 19, 2024
2e6f8d3
Update tabs_style.dart
Daviiddoo Jun 19, 2024
e9dc98a
Update forui/lib/src/widgets/tabs/tabs.dart
Daviiddoo Jun 22, 2024
e4e31ee
Update forui/lib/src/widgets/tabs/tabs.dart
Daviiddoo Jun 22, 2024
f2fb6ef
Update forui/lib/src/widgets/tabs/tabs.dart
Daviiddoo Jun 22, 2024
4834fad
Update forui/lib/src/widgets/tabs/tabs.dart
Daviiddoo Jun 22, 2024
f51686c
Update tabs.dart
Daviiddoo Jun 22, 2024
bd3192e
more fixes
Daviiddoo Jun 22, 2024
2d25048
added assertion tests
Daviiddoo Jun 23, 2024
f9d170e
Update tabs_style.dart
Daviiddoo Jun 23, 2024
4d2da76
Peer programming session
Pante Jun 23, 2024
38cf797
FTabBarIndicatorSize implmented
Daviiddoo Jun 23, 2024
659e704
Merge branch 'master' into feature/tabs
Daviiddoo Jun 23, 2024
47442a8
merge conflicts fixed
Daviiddoo Jun 23, 2024
8d54944
Update tabs.dart
Daviiddoo Jun 24, 2024
0d3d788
Merge branch 'master' into feature/tabs
Daviiddoo Jun 24, 2024
796cad5
Commit from GitHub Actions (Forui Presubmit)
Daviiddoo Jun 24, 2024
33f7908
Update button_test.dart
Daviiddoo Jun 24, 2024
2ebecd3
Merge branch 'feature/tabs' of https://github.com/forus-labs/forui in…
Daviiddoo Jun 24, 2024
2017a39
Merge branch 'master' into feature/tabs
Daviiddoo Jun 24, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 63 additions & 2 deletions forui/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,71 @@ class _ExampleWidgetState extends State<ExampleWidget> {
}

@override
Widget build(BuildContext context) => const Padding(
padding: EdgeInsets.all(8.0),
Widget build(BuildContext context) => Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
FButton(
style: FButtonStyle.destructive,
label: 'Delete?',
onPress: () => showAdaptiveDialog(
context: context,
builder: (context) => FDialog(
direction: Axis.horizontal,
title: 'Are you absolutely sure?',
body:
'This action cannot be undone. This will permanently delete your account and remove your data from our servers.',
actions: [
FButton(
style: FButtonStyle.outline,
label: 'Cancel',
onPress: () {
Navigator.of(context).pop();
}),
FButton(label: 'Continue', onPress: () {}),
],
),
),
),
const SizedBox(height: 10),
Expanded(
child: FTabs(
tabs: [
FTabEntry(
label: 'Account',
content: FCard(
title: 'Account',
subtitle: 'Make changes to your account here. Click save when you are done.',
child: Column(
children: [
Container(
color: Colors.red,
height: 100,
),
],
),
),
),
FTabEntry(
label: 'Password',
content: FCard(
title: 'Password',
subtitle: 'Change your password here. After saving, you will be logged out.',
child: Column(
children: [
Container(
color: Colors.blue,
height: 100,
)
],
),
),
),
],
),
),
],
),
);
}
1 change: 1 addition & 0 deletions forui/lib/forui.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,4 @@ export 'src/widgets/header/header.dart';
export 'src/widgets/separator.dart';
export 'src/widgets/switch.dart';
export 'src/widgets/text_field/text_field.dart';
export 'src/widgets/tabs/tabs.dart';
10 changes: 10 additions & 0 deletions forui/lib/src/theme/theme_data.dart
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ final class FThemeData with Diagnosticable {
/// The header styles.
final FHeaderStyle headerStyle;

/// The tabs styles.
final FTabsStyle tabsStyle;

/// The text field style.
final FTextFieldStyle textFieldStyle;

Expand All @@ -62,6 +65,7 @@ final class FThemeData with Diagnosticable {
required this.cardStyle,
required this.dialogStyle,
required this.headerStyle,
required this.tabsStyle,
required this.textFieldStyle,
required this.separatorStyles,
required this.switchStyle,
Expand Down Expand Up @@ -90,6 +94,7 @@ final class FThemeData with Diagnosticable {
cardStyle: FCardStyle.inherit(colorScheme: colorScheme, typography: typography, style: style),
dialogStyle: FDialogStyle.inherit(style: style, colorScheme: colorScheme, typography: typography),
headerStyle: FHeaderStyle.inherit(colorScheme: colorScheme, typography: typography),
tabsStyle: FTabsStyle.inherit(colorScheme: colorScheme, typography: typography, style: style),
textFieldStyle: FTextFieldStyle.inherit(colorScheme: colorScheme, typography: typography, style: style),
separatorStyles: FSeparatorStyles.inherit(colorScheme: colorScheme, style: style),
switchStyle: FSwitchStyle.inherit(colorScheme: colorScheme),
Expand Down Expand Up @@ -122,6 +127,7 @@ final class FThemeData with Diagnosticable {
FCardStyle? cardStyle,
FDialogStyle? dialogStyle,
FHeaderStyle? headerStyle,
FTabsStyle? tabsStyle,
FTextFieldStyle? textFieldStyle,
FSeparatorStyles? separatorStyles,
FSwitchStyle? switchStyle,
Expand All @@ -135,6 +141,7 @@ final class FThemeData with Diagnosticable {
cardStyle: cardStyle ?? this.cardStyle,
dialogStyle: dialogStyle ?? this.dialogStyle,
headerStyle: headerStyle ?? this.headerStyle,
tabsStyle: tabsStyle ?? this.tabsStyle,
textFieldStyle: textFieldStyle ?? this.textFieldStyle,
separatorStyles: separatorStyles ?? this.separatorStyles,
switchStyle: switchStyle ?? this.switchStyle,
Expand All @@ -152,6 +159,7 @@ final class FThemeData with Diagnosticable {
..add(DiagnosticsProperty('cardStyle', cardStyle, level: DiagnosticLevel.debug))
..add(DiagnosticsProperty('dialogStyle', dialogStyle, level: DiagnosticLevel.debug))
..add(DiagnosticsProperty('headerStyle', headerStyle, level: DiagnosticLevel.debug))
..add(DiagnosticsProperty('tabsStyle', tabsStyle))
..add(DiagnosticsProperty('textFieldStyle', textFieldStyle, level: DiagnosticLevel.debug))
..add(DiagnosticsProperty('separatorStyles', separatorStyles, level: DiagnosticLevel.debug))
..add(DiagnosticsProperty('switchStyle', switchStyle, level: DiagnosticLevel.debug));
Expand All @@ -170,6 +178,7 @@ final class FThemeData with Diagnosticable {
cardStyle == other.cardStyle &&
dialogStyle == other.dialogStyle &&
headerStyle == other.headerStyle &&
tabsStyle == other.tabsStyle &&
textFieldStyle == other.textFieldStyle &&
separatorStyles == other.separatorStyles &&
switchStyle == other.switchStyle;
Expand All @@ -184,6 +193,7 @@ final class FThemeData with Diagnosticable {
cardStyle.hashCode ^
dialogStyle.hashCode ^
headerStyle.hashCode ^
tabsStyle.hashCode ^
textFieldStyle.hashCode ^
separatorStyles.hashCode ^
switchStyle.hashCode;
Expand Down
37 changes: 37 additions & 0 deletions forui/lib/src/widgets/tabs/tab_controller.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
part of 'tabs.dart';

/// An object that controls selection in a [FTabs]
class FTabController implements Listenable {
final TabController _controller;

/// Creates a [FTabController].
FTabController({
required int length,
required TickerProvider vsync,
int initialIndex = 0,
Duration? animationDuration,
}) : _controller = TabController(
initialIndex: initialIndex,
length: length,
animationDuration: animationDuration,
vsync: vsync,
);

@override
void addListener(VoidCallback listener) => _controller.addListener(listener);

@override
void removeListener(VoidCallback listener) =>
_controller.removeListener(listener);

/// Discards any resources used by the object. After this is called, the
/// object is not in a usable state and should be discarded (calls to
/// [addListener] will throw after the object is disposed).
///
/// This method should only be called by the object's owner.
///
/// This method does not notify listeners, and clears the listener list once
/// it is called. Consumers of this class must decide on whether to notify
/// listeners or not immediately before disposal.
void dispose() => _controller.dispose();
}
160 changes: 160 additions & 0 deletions forui/lib/src/widgets/tabs/tabs.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';

import 'package:flutter_localizations/flutter_localizations.dart';

import 'package:forui/forui.dart';

part 'tabs_style.dart';

part 'tab_controller.dart';

/// An object that represents a tab entry in a group of tabs.
class FTabEntry {
Pante marked this conversation as resolved.
Show resolved Hide resolved
/// The label.
final String? label;

/// A raw label.
final Widget? rawLabel;

/// The content of a tab.
final Widget content;
Daviiddoo marked this conversation as resolved.
Show resolved Hide resolved

/// Creates a [FTabs].
FTabEntry({
required this.content,
this.label,
this.rawLabel,
}) : assert((label == null) ^ (rawLabel == null), 'Either "label" or "rawLabel" must be provided, but not both.');
}

/// A [FTabs] that allows switching between tabs.
class FTabs extends StatefulWidget {
/// The tab and it's corresponding view.
final List<FTabEntry> tabs;

/// The initial tab that is selected.
final int initialIndex;

/// Whether this tab bar can be scrolled horizontally.
///
/// If [scrollable] is true, then each tab is as wide as needed for its label
/// and the entire [TabBar] is scrollable. Otherwise each tab gets an equal
/// share of the available space.
final bool scrollable;

/// The tab controller.
final FTabController? controller;

/// The style.
final FTabsStyle? style;

/// A callback that returns the tab that was tapped.
final ValueChanged<int>? onPress;

/// Creates a [FTabs].
const FTabs({
required this.tabs,
this.initialIndex = 0,
this.scrollable = false,
this.controller,
this.style,
this.onPress,
super.key,
}) : assert(0 < tabs.length, 'Must have at least 1 tab provided');

@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties
..add(IterableProperty('tabs', tabs))
..add(IntProperty('initialIndex', initialIndex))
..add(DiagnosticsProperty('controller', controller))
..add(DiagnosticsProperty('style', style))
..add(ObjectFlagProperty.has('onPress', onPress))
..add(FlagProperty('scrollable', value: scrollable, ifTrue: 'scrollable'));
}

@override
State<FTabs> createState() => _FTabsState();
}

class _FTabsState extends State<FTabs> with SingleTickerProviderStateMixin {
late int _index;
late final FTabController _controller;

@override
void initState() {
super.initState();
_index = widget.initialIndex;
_controller = FTabController(length: widget.tabs.length, vsync: this);
}

@override
Widget build(BuildContext context) {
final theme = context.theme;
final style = widget.style ?? context.theme.tabsStyle;
final tabs = widget.tabs;
final materialLocalizations = Localizations.of<MaterialLocalizations>(context, MaterialLocalizations);

final child = Material(
color: Colors.transparent,
child: Column(
children: [
DecoratedBox(
decoration: style.decoration,
child: TabBar(
tabs: [
for (final tab in tabs)
Tab(
height: style.height,
child: tab.rawLabel ?? Text(tab.label!),
)
],
controller: (widget.controller ?? _controller)._controller,
isScrollable: widget.scrollable,
padding: style.padding,
indicator: style.indicator,
indicatorSize: style.indicatorSize._value,
dividerColor: Colors.transparent,
labelStyle: style.selectedLabel,
unselectedLabelStyle: style.unselectedLabel,
onTap: (index) {
setState(() => _index = index);
widget.onPress?.call(_index);
},
),
),
SizedBox(height: style.spacing),
// A workaround to ensure any widgets under Tabs do not revert to material text style
// TODO: abstract out logic
DefaultTextStyle(
style: theme.typography.base.copyWith(
fontFamily: theme.typography.defaultFontFamily,
color: theme.colorScheme.foreground,
),
child: tabs[_index].content,
),
],
),
);

return materialLocalizations == null
? Localizations(
locale: Localizations.maybeLocaleOf(context) ?? const Locale('en', 'US'),
delegates: const [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
child: child,
)
: child;
}

@override
void dispose() {
_controller.dispose();
super.dispose();
}
}
Loading
Loading