From 0cc19b1497d49145452f4a890515f9387f5e9517 Mon Sep 17 00:00:00 2001 From: Matthias Ngeo Date: Thu, 5 Dec 2024 14:48:34 +0800 Subject: [PATCH] Update sheets documentation --- docs/pages/docs/overlay/modal-sheet.mdx | 3 + docs/pages/docs/overlay/sheet.mdx | 224 +++++++++++++++++++++--- forui/lib/src/widgets/sheet/sheets.dart | 14 +- samples/lib/widgets/sheets.dart | 76 +++++--- 4 files changed, 261 insertions(+), 56 deletions(-) diff --git a/docs/pages/docs/overlay/modal-sheet.mdx b/docs/pages/docs/overlay/modal-sheet.mdx index 464c28041..079da0efb 100644 --- a/docs/pages/docs/overlay/modal-sheet.mdx +++ b/docs/pages/docs/overlay/modal-sheet.mdx @@ -7,6 +7,9 @@ import LinkBadgeGroup from "../../../components/link-badge/link-badge-group.tsx" A modal sheet is an alternative to a menu or a dialog and prevents the user from interacting with the rest of the app. +A closely related widget is a [sheet](/docs/overlay/sheet), which shows information that supplements the primary content +of the app without preventing the user from interacting with the app. + diff --git a/docs/pages/docs/overlay/sheet.mdx b/docs/pages/docs/overlay/sheet.mdx index b310ec2c6..649e19403 100644 --- a/docs/pages/docs/overlay/sheet.mdx +++ b/docs/pages/docs/overlay/sheet.mdx @@ -8,6 +8,9 @@ import LinkBadgeGroup from "../../../components/link-badge/link-badge-group.tsx" A sheet that are displayed above another widget. It is part of [FScaffold](/docs/layout/scaffold), which should be preferred in most cases. +A closely related widget is a [modal sheet](/docs/overlay/modal-sheet) which prevents the user from interacting with the +rest of the app. + @@ -38,7 +41,7 @@ preferred in most cases. controller = _controllers[side] ??= showFSheet( context: context, side: Layout.ltr, - builder: (context) => Form(side: side), + builder: (context, controller) => Form(side: side, controller: controller), ); } else { controller.toggle(); @@ -77,8 +80,9 @@ preferred in most cases. class Form extends StatelessWidget { final Layout side; + final FSheetController controller; - const Form({required this.side, super.key}); + const Form({required this.side, required this.controller, super.key}); @override Widget build(BuildContext context) => Container( @@ -171,6 +175,145 @@ showFSheet( ## Examples +### With `KeepAliveOffstage` + + + + + + + ```dart {17} + class Sheets extends StatefulWidget { + @override + State createState() => _State(); + } + + class _State extends State { + final Map _controllers = {}; + + @override + Widget build(BuildContext context) { + VoidCallback onPress(Layout side) => () { + var controller = _controllers[side]; + if (controller == null) { + controller = _controllers[side] ??= showFSheet( + context: context, + side: Layout.ltr, + keepAliveOffstage: true, + builder: (context, controller) => Form(side: side, controller: controller), + ); + } else { + controller.toggle(); + } + }; + + return FScaffold( // This can be replaced with FSheets + content: Column( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + FButton( + label: const Text('Left'), + onPress: onPress(Layout.ltr), + ), + const SizedBox(height: 5), + FButton( + label: const Text('Top'), + onPress: onPress(Layout.ttb), + ), + const SizedBox(height: 5), + FButton( + label: const Text('Right'), + onPress: onPress(Layout.rtl), + ), + const SizedBox(height: 5), + FButton( + label: const Text('Bottom'), + onPress: onPress(Layout.btt), + ), + ], + ), + ); + } + } + + class Form extends StatelessWidget { + final Layout side; + final FSheetController controller; + + const Form({required this.side, required this.controller, super.key}); + + @override + Widget build(BuildContext context) => Container( + height: double.infinity, + width: double.infinity, + decoration: BoxDecoration( + color: context.theme.colorScheme.background, + border: side.vertical + ? Border.symmetric(horizontal: BorderSide(color: context.theme.colorScheme.border)) + : Border.symmetric(vertical: BorderSide(color: context.theme.colorScheme.border)), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 8.0), + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Account', + style: context.theme.typography.xl2.copyWith( + fontWeight: FontWeight.w600, + color: context.theme.colorScheme.foreground, + height: 1.5, + ), + ), + Text( + 'Make changes to your account here. Click save when you are done.', + style: context.theme.typography.sm.copyWith( + color: context.theme.colorScheme.mutedForeground, + ), + ), + const SizedBox(height: 8), + SizedBox( + width: 450, + child: Column( + children: [ + const FTextField( + label: Text('Name'), + hint: 'John Renalo', + ), + const SizedBox(height: 10), + const FTextField( + label: Text('Email'), + hint: 'john@doe.com', + ), + const SizedBox(height: 16), + FButton( + label: const Text('Save'), + onPress: () => Navigator.of(context).pop(), + ), + ], + ), + ), + ], + ), + ), + ), + ); + + @override + void dispose() { + for (final controller in _controllers.values) { + controller.dispose(); + } + super.dispose(); + } + } + ``` + + + ### With `DraggableScrollableSheet` @@ -179,33 +322,58 @@ showFSheet( ```dart - FScaffold( // This can be replaced with FSheets - content: FButton( - label: const Text('Click me'), - onPress: () => showFSheet( - context: context, - side: Layout.btt, - mainAxisMaxRatio: null, - builder: (context) => DraggableScrollableSheet( - expand: false, - builder: (context, controller) => ScrollConfiguration( - // This is required to enable dragging on desktop. - // See https://github.com/flutter/flutter/issues/101903 for more information. - behavior: ScrollConfiguration.of(context).copyWith(dragDevices: { - PointerDeviceKind.touch, - PointerDeviceKind.mouse, - PointerDeviceKind.trackpad, - }), - child: FTileGroup.builder( - count: 25, - controller: controller, - tileBuilder: (context, index) => FTile(title: Text('Tile $index')), - ), + class DraggableSheets extends StatefulWidget { + @override + State createState() => _State(); + } + + class _State extends State { + FSheetController? controller; + + @override + Widget build(BuildContext context) => FScaffold( + content: FButton( + label: const Text('Click me'), + onPress: () { + if (controller != null) { + controller!.toggle(); + return; + } + + controller = showFSheet( + context: context, + side: Layout.btt, + mainAxisMaxRatio: null, + builder: (context, _) => DraggableScrollableSheet( + expand: false, + builder: (context, controller) => ScrollConfiguration( + // This is required to enable dragging on desktop. + // See https://github.com/flutter/flutter/issues/101903 for more information. + behavior: ScrollConfiguration.of(context).copyWith( + dragDevices: { + PointerDeviceKind.touch, + PointerDeviceKind.mouse, + PointerDeviceKind.trackpad, + }, + ), + child: FTileGroup.builder( + count: 25, + controller: controller, + tileBuilder: (context, index) => FTile(title: Text('Tile $index')), + ), + ), + ), + ); + }, ), - ), - ), - ), - ); + ); + + @override + void dispose() { + controller?.dispose(); + super.dispose(); + } + } ``` diff --git a/forui/lib/src/widgets/sheet/sheets.dart b/forui/lib/src/widgets/sheet/sheets.dart index ab6932528..b9009d8c1 100644 --- a/forui/lib/src/widgets/sheet/sheets.dart +++ b/forui/lib/src/widgets/sheet/sheets.dart @@ -1,6 +1,7 @@ import 'dart:math'; import 'package:flutter/foundation.dart'; +import 'package:flutter/scheduler.dart'; import 'package:flutter/widgets.dart'; import 'package:forui/forui.dart'; import 'package:forui/src/widgets/sheet/sheet.dart'; @@ -229,9 +230,18 @@ class FSheetsState extends State with TickerProviderStateMixin { setState(() => sheets[controller.key] = (controller, sheet)); } - void _remove(Key key) { - if (mounted) { + Future _remove(Key key) async { + // This checks if the method was called during the build phase, and schedules the removal for the next frame. + // This done as _remove is called in FSheetController.dispose, and subsequently StatefulWidget.dispose, which is + // part of the build phase. + if (mounted && SchedulerBinding.instance.schedulerPhase == SchedulerPhase.idle) { setState(() => sheets.remove(key)); + } else { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + setState(() => sheets.remove(key)); + } + }); } } diff --git a/samples/lib/widgets/sheets.dart b/samples/lib/widgets/sheets.dart index f12f819bd..9590066a3 100644 --- a/samples/lib/widgets/sheets.dart +++ b/samples/lib/widgets/sheets.dart @@ -8,15 +8,18 @@ import 'package:forui_samples/sample.dart'; @RoutePage() class SheetsPage extends StatefulSample { + final bool keepAliveOffstage; + SheetsPage({ @queryParam super.theme, + @queryParam this.keepAliveOffstage = false, }); @override - State createState() => _State(); + State createState() => _SheetsState(); } -class _State extends StatefulSampleState { +class _SheetsState extends StatefulSampleState { final Map _controllers = {}; @override @@ -27,6 +30,7 @@ class _State extends StatefulSampleState { controller = _controllers[side] ??= showFSheet( context: context, side: side, + keepAliveOffstage: widget.keepAliveOffstage, builder: (context, controller) => Form(side: side, controller: controller), ); } else { @@ -74,7 +78,7 @@ class Form extends StatelessWidget { final Layout side; final FSheetController controller; - Form({required this.side, required this.controller, super.key}); + const Form({required this.side, required this.controller, super.key}); @override Widget build(BuildContext context) => Container( @@ -137,37 +141,57 @@ class Form extends StatelessWidget { } @RoutePage() -class DraggableSheetsPage extends Sample { +class DraggableSheetsPage extends StatefulSample { DraggableSheetsPage({ @queryParam super.theme, }); + @override + State createState() => _DraggableSheetsState(); +} + +class _DraggableSheetsState extends StatefulSampleState { + FSheetController? controller; + @override Widget sample(BuildContext context) => FButton( label: const Text('Click me'), - onPress: () => showFSheet( - context: context, - side: Layout.btt, - mainAxisMaxRatio: null, - builder: (context, _) => DraggableScrollableSheet( - expand: false, - builder: (context, controller) => ScrollConfiguration( - // This is required to enable dragging on desktop. - // See https://github.com/flutter/flutter/issues/101903 for more information. - behavior: ScrollConfiguration.of(context).copyWith( - dragDevices: { - PointerDeviceKind.touch, - PointerDeviceKind.mouse, - PointerDeviceKind.trackpad, - }, - ), - child: FTileGroup.builder( - count: 25, - controller: controller, - tileBuilder: (context, index) => FTile(title: Text('Tile $index')), + onPress: () { + if (controller != null) { + controller!.toggle(); + return; + } + + controller = showFSheet( + context: context, + side: Layout.btt, + mainAxisMaxRatio: null, + builder: (context, _) => DraggableScrollableSheet( + expand: false, + builder: (context, controller) => ScrollConfiguration( + // This is required to enable dragging on desktop. + // See https://github.com/flutter/flutter/issues/101903 for more information. + behavior: ScrollConfiguration.of(context).copyWith( + dragDevices: { + PointerDeviceKind.touch, + PointerDeviceKind.mouse, + PointerDeviceKind.trackpad, + }, + ), + child: FTileGroup.builder( + count: 25, + controller: controller, + tileBuilder: (context, index) => FTile(title: Text('Tile $index')), + ), ), ), - ), - ), + ); + }, ); + + @override + void dispose() { + controller?.dispose(); + super.dispose(); + } }