Skip to content

Commit

Permalink
feat: add SBBHeaderbox (#266)
Browse files Browse the repository at this point in the history
* feat: add basic static SBBHeaderbox and SBBHeaderboxFlap
* feat: first version of SBBHeaderbox done
* feat: defer default and large to own widgets and add doc
* feat: SBBSliverHeaderbox first version done
* feat: add semantics
* test: add tests for SBBHeaderbox
* docs: add SBBHeaderbox to Changelog
  • Loading branch information
smallTrogdor authored Jan 14, 2025
1 parent 97074fd commit 172b257
Show file tree
Hide file tree
Showing 15 changed files with 947 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ It is expected that you keep this format strictly, since we depend on it in our

### Added

- added the `SBBHeaderBox` and `SBBSliverHeaderbox`
- added an animated bottom loading indicator with a `isLoading` parameter to these widgets:
- `SBBCheckboxListItem`
- `SBBRadioButtonListItem`
Expand Down
5 changes: 5 additions & 0 deletions example/lib/native_app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import 'pages/checkbox_page.dart';
import 'pages/chip_page.dart';
import 'pages/color_page.dart';
import 'pages/group_page.dart';
import 'pages/header_box_page.dart';
import 'pages/header_page.dart';
import 'pages/icon_page.dart';
import 'pages/input_trigger_page.dart';
Expand Down Expand Up @@ -219,6 +220,10 @@ class MyApp extends StatelessWidget {
'Header',
HeaderPage(),
),
_DemoEntry(
'Headerbox',
HeaderBoxPage(),
),
_DemoEntry(
'Modal',
ModalPage(),
Expand Down
248 changes: 248 additions & 0 deletions example/lib/pages/header_box_page.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
import 'package:flutter/material.dart';
import 'package:sbb_design_system_mobile/sbb_design_system_mobile.dart';

class HeaderBoxPage extends StatefulWidget {
const HeaderBoxPage({super.key});

@override
State<HeaderBoxPage> createState() => _HeaderBoxPageState();
}

class _HeaderBoxPageState extends State<HeaderBoxPage> {
final items = <TabBarItem>[
_DemoItem(0, SBBIcons.paragraph_small),
_DemoItem(1, SBBIcons.lock_closed_small),
_DemoItem(2, SBBIcons.arrows_up_down_small),
];

late PageController _pageViewController;
late TabBarController _tabBarController;

@override
void initState() {
super.initState();
_pageViewController = PageController();
_tabBarController = TabBarController(items.first);
}

@override
void dispose() {
super.dispose();
_pageViewController.dispose();
}

@override
Widget build(BuildContext context) {
return Column(
children: [
Expanded(
child: PageView(
physics: NeverScrollableScrollPhysics(),
controller: _pageViewController,
children: <Widget>[
DesignGuidelinePage(),
StaticPage(),
ScrollablePage(),
],
),
),
SBBTabBar(
items: items,
onTabChanged: (task) async {
task.then((value) => _handlePageViewChanged(int.parse(value.id)));
},
controller: _tabBarController,
onTap: (tab) {},
)
],
);
}

void _handlePageViewChanged(int newPageIndex) {
_pageViewController.animateToPage(
newPageIndex,
duration: const Duration(milliseconds: 400),
curve: Curves.easeInOut,
);
}
}

class DesignGuidelinePage extends StatelessWidget {
const DesignGuidelinePage({
super.key,
});

@override
Widget build(BuildContext context) {
final sbbToast = SBBToast.of(context);
return Column(
children: [
const SizedBox(height: sbbDefaultSpacing),
const SBBListHeader('Default'),
SBBHeaderbox(
title: 'Title',
leadingIcon: SBBIcons.dog_small,
secondaryLabel: 'Subtext',
flap: SBBHeaderboxFlap(
title: 'Additional text or information',
leadingIcon: SBBIcons.sign_exclamation_point_small,
trailingIcon: SBBIcons.circle_information_small_small,
),
trailingWidget: SBBTertiaryButtonSmall(
label: 'Label',
icon: SBBIcons.dog_small,
onPressed: () => sbbToast.show(message: 'Default pressed', bottom: sbbDefaultSpacing * 6),
),
),
const SizedBox(height: sbbDefaultSpacing),
const SBBListHeader('Large'),
SBBHeaderbox.large(
title: 'Title',
leadingIcon: SBBIcons.dog_medium,
secondaryLabel: 'Subtext',
trailingWidget: SBBIconButtonLarge(
icon: SBBIcons.dog_small,
onPressed: () => sbbToast.show(message: 'Large pressed', bottom: sbbDefaultSpacing * 6),
),
),
const SizedBox(height: sbbDefaultSpacing),
const SBBListHeader('Custom'),
const SBBHeaderbox.custom(
padding: EdgeInsets.zero,
flap: SBBHeaderboxFlap.custom(child: Center(child: Text('Choooooo!', style: SBBTextStyles.extraSmallBold))),
child: Center(child: Text('🚂。🚋。🚋。🚋。🚋˙⊹⁺.')),
)
],
);
}
}

class StaticPage extends StatefulWidget {
const StaticPage({
super.key,
});

@override
State<StaticPage> createState() => _StaticPageState();
}

class _StaticPageState extends State<StaticPage> {
bool _headerBoxExpanded = false;
final _expandedHeight = 340.0;
final _collapsedHeight = 0.0;

@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
return Stack(
children: [
_body(),
SBBHeaderbox.custom(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Static Screen', style: SBBTextStyles.mediumBold),
Text(
'Click to expand Headerbox.',
style:
SBBTextStyles.smallLight.copyWith(color: isDark ? SBBColors.graphite : SBBColors.granite),
)
],
),
SBBTertiaryButtonSmall(
label: 'Expand', onPressed: () => setState(() => _headerBoxExpanded = !_headerBoxExpanded)),
],
),
AnimatedContainer(
curve: Curves.easeInOut,
height: _headerBoxExpanded ? _expandedHeight : _collapsedHeight,
duration: Durations.long4,
),
],
),
),
],
);
}

Center _body() {
return Center(
child: SBBMessage(
title: 'Cover me!',
description: 'This screen is non scrollable.\nUsing a Stack, the Headerbox will simply lay on top of it.',
),
);
}
}

class ScrollablePage extends StatefulWidget {
const ScrollablePage({super.key});

@override
State<ScrollablePage> createState() => _ScrollablePageState();
}

class _ScrollablePageState extends State<ScrollablePage> {
bool _headerBoxExpanded = false;
final _expandedHeight = 300.0;
final _collapsedHeight = 0.0;
@override
Widget build(BuildContext context) {
final sbbToast = SBBToast.of(context);
final isDark = Theme.of(context).brightness == Brightness.dark;
return CustomScrollView(
slivers: [
SBBSliverHeaderbox.custom(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Scrollable Screen', style: SBBTextStyles.mediumBold),
Text(
'Click to expand Headerbox.',
style:
SBBTextStyles.smallLight.copyWith(color: isDark ? SBBColors.graphite : SBBColors.granite),
)
],
),
SBBTertiaryButtonSmall(
label: 'Expand', onPressed: () => setState(() => _headerBoxExpanded = !_headerBoxExpanded)),
],
),
AnimatedContainer(
curve: Curves.easeInOut,
height: _headerBoxExpanded ? _expandedHeight : _collapsedHeight,
duration: Durations.long4,
),
],
),
),
SliverList.builder(
itemCount: 60,
itemBuilder: (context, index) => SBBListItem(
title: 'Item $index',
onPressed: () => sbbToast.show(message: 'Pressed Item $index', bottom: sbbDefaultSpacing * 6),
),
)
],
);
}
}

class _DemoItem extends TabBarItem {
_DemoItem(int id, IconData icon) : super(id.toString(), icon);

@override
String translate(BuildContext context) => '';
}
1 change: 1 addition & 0 deletions lib/sbb_design_system_mobile.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export 'src/checkbox/checkbox.dart';
export 'src/chip/sbb_chip.dart';
export 'src/group/sbb_group.dart';
export 'src/header/sbb_header.dart';
export 'src/headerbox/headerbox.dart';
export 'src/input_trigger/sbb_input_trigger.dart';
export 'src/link/sbb_link_text.dart';
export 'src/list_header/sbb_list_header.dart';
Expand Down
3 changes: 3 additions & 0 deletions lib/src/headerbox/headerbox.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export 'sbb_headerbox.dart';
export 'sbb_headerbox_flap.dart';
export 'sbb_sliver_headerbox.dart';
37 changes: 37 additions & 0 deletions lib/src/headerbox/render_sliver_pin_header.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import 'dart:math';

import 'package:flutter/rendering.dart';

class RenderSliverPinHeader extends RenderSliverSingleBoxAdapter {
@override
void performLayout() {
child!.layout(constraints.asBoxConstraints(), parentUsesSize: true);
double childExtent;
switch (constraints.axis) {
case Axis.horizontal:
childExtent = child!.size.width;
break;
case Axis.vertical:
childExtent = child!.size.height;
break;
}
final paintedChildExtent = min(
childExtent,
constraints.remainingPaintExtent - constraints.overlap,
);
geometry = SliverGeometry(
paintExtent: paintedChildExtent,
maxPaintExtent: childExtent,
maxScrollObstructionExtent: childExtent,
paintOrigin: constraints.overlap,
scrollExtent: childExtent,
layoutExtent: max(0.0, paintedChildExtent - constraints.scrollOffset),
hasVisualOverflow: paintedChildExtent < childExtent,
);
}

@override
double childMainAxisPosition(RenderBox child) {
return 0;
}
}
Loading

0 comments on commit 172b257

Please sign in to comment.