Skip to content

Commit e16251f

Browse files
committed
compose_box: Cast inset shadow for scrollable contents.
This also supports horizontal scrolling, to later support more compose box icons. But we leave those for later. Fixex: zulip#915 Signed-off-by: Zixuan James Li <[email protected]>
1 parent 9ee3ed0 commit e16251f

File tree

2 files changed

+109
-19
lines changed

2 files changed

+109
-19
lines changed

lib/widgets/compose_box.dart

+79-19
Original file line numberDiff line numberDiff line change
@@ -300,28 +300,88 @@ class _ContentInput extends StatelessWidget {
300300
narrow: narrow,
301301
controller: controller,
302302
focusNode: focusNode,
303-
fieldViewBuilder: (context) => SingleChildScrollView(
304-
// While the [TextField] is scrollable, we need to wrap it with
305-
// [SingleChildScrollView] to prepend a fixed-height padding that can
306-
// be scrolled along with the text.
307-
child: Padding(
308-
padding: const EdgeInsets.only(top: topPadding),
309-
child: TextField(
310-
controller: controller,
311-
focusNode: focusNode,
312-
decoration: InputDecoration.collapsed(
313-
hintText: hintText,
314-
hintStyle: TextStyle(color: designVariables.textInput.withOpacity(0.5))),
315-
minLines: 2,
316-
maxLines: null,
317-
textCapitalization: TextCapitalization.sentences,
318-
style: TextStyle(
319-
fontSize: 17,
320-
height: (contentLineHeight / 17),
321-
color: designVariables.textInput))))));
303+
fieldViewBuilder: (context) => _ShadowBox(
304+
color: designVariables.bgComposeBox,
305+
child: SingleChildScrollView(
306+
// While the [TextField] is scrollable, we need to wrap it with
307+
// [SingleChildScrollView] to prepend a fixed-height padding that can
308+
// be scrolled along with the text.
309+
child: Padding(
310+
padding: const EdgeInsets.only(top: topPadding),
311+
child: TextField(
312+
controller: controller,
313+
focusNode: focusNode,
314+
decoration: InputDecoration.collapsed(
315+
hintText: hintText,
316+
hintStyle: TextStyle(color: designVariables.textInput.withOpacity(0.5))),
317+
minLines: 2,
318+
maxLines: null,
319+
textCapitalization: TextCapitalization.sentences,
320+
style: TextStyle(
321+
fontSize: 17,
322+
height: (contentLineHeight / 17),
323+
color: designVariables.textInput)))),
324+
)));
322325
}
323326
}
324327

328+
/// Overlay inset shadows on the child from all scrollable directions.
329+
class _ShadowBox extends StatefulWidget {
330+
const _ShadowBox({required this.color, required this.child});
331+
332+
final Color color;
333+
final Widget child;
334+
335+
@override
336+
State<_ShadowBox> createState() => _ShadowBoxState();
337+
}
338+
339+
class _ShadowBoxState extends State<_ShadowBox> {
340+
bool showTopShadow = false; bool showBottomShadow = false;
341+
bool showLeftShadow = false; bool showRightShadow = false;
342+
343+
bool handleScroll(ScrollNotification notification) {
344+
final metrics = notification.metrics;
345+
setState(() {
346+
switch (metrics.axisDirection) {
347+
case AxisDirection.up:
348+
case AxisDirection.down:
349+
showTopShadow = metrics.extentBefore != 0;
350+
showBottomShadow = metrics.extentAfter != 0;
351+
case AxisDirection.right:
352+
case AxisDirection.left:
353+
showLeftShadow = metrics.extentBefore != 0;
354+
showRightShadow = metrics.extentAfter != 0;
355+
}
356+
});
357+
return false;
358+
}
359+
360+
@override
361+
Widget build(BuildContext context) {
362+
BoxDecoration shadowFrom(AlignmentGeometry begin) =>
363+
BoxDecoration(gradient: LinearGradient(begin: begin, end: -begin,
364+
colors: [widget.color, widget.color.withOpacity(0)]));
365+
366+
return NotificationListener<ScrollNotification>(
367+
onNotification: handleScroll,
368+
child: Stack(
369+
children: [
370+
widget.child,
371+
if (showTopShadow) Positioned(top: 0, left: 0, right: 0,
372+
child: Container(height: 8, decoration: shadowFrom(Alignment.topCenter))),
373+
if (showBottomShadow) Positioned(bottom: 0, left: 0, right: 0,
374+
child: Container(height: 8, decoration: shadowFrom(Alignment.bottomCenter))),
375+
if (showLeftShadow) Positioned(left: 0, top: 0, bottom: 0,
376+
child: Container(width: 8, decoration: shadowFrom(Alignment.centerLeft))),
377+
if (showRightShadow) Positioned(right: 0, top: 0, bottom: 0,
378+
child: Container(width: 8, decoration: shadowFrom(Alignment.centerRight))),
379+
],
380+
));
381+
}
382+
}
383+
384+
325385
/// The content input for _StreamComposeBox.
326386
class _StreamContentInput extends StatefulWidget {
327387
const _StreamContentInput({

test/widgets/compose_box_test.dart

+30
Original file line numberDiff line numberDiff line change
@@ -481,4 +481,34 @@ void main() {
481481
});
482482
});
483483
});
484+
485+
testWidgets('cast shadow when scrollable', (tester) async {
486+
Finder shadowFinderFrom(Alignment alignment) => find.byWidgetPredicate((widget) =>
487+
widget is Container && ((widget.decoration as BoxDecoration?)?.gradient as LinearGradient?)?.begin == alignment);
488+
489+
await prepareComposeBox(tester, narrow: const TopicNarrow(123, 'some topic'));
490+
check(shadowFinderFrom(Alignment.topCenter).evaluate()).isEmpty();
491+
check(shadowFinderFrom(Alignment.bottomCenter).evaluate()).isEmpty();
492+
493+
final contentInputFinder = find.byWidgetPredicate(
494+
(widget) => widget is TextField && widget.controller is ComposeContentController);
495+
496+
// Entering 7 lines to fully extend the compose box.
497+
await tester.enterText(contentInputFinder, 'newlines\n' * 8);
498+
await tester.pumpAndSettle();
499+
check(shadowFinderFrom(Alignment.topCenter).evaluate()).single;
500+
check(shadowFinderFrom(Alignment.bottomCenter).evaluate()).isEmpty();
501+
502+
// Scroll back up and the bottom shadow should be visible now.
503+
await tester.drag(contentInputFinder, const Offset(0, 22));
504+
await tester.pumpAndSettle();
505+
check(shadowFinderFrom(Alignment.topCenter).evaluate()).single;
506+
check(shadowFinderFrom(Alignment.bottomCenter).evaluate()).single;
507+
508+
// Scroll back to the top and the top shadow is no longer visible.
509+
await tester.drag(contentInputFinder, const Offset(0, 99));
510+
await tester.pumpAndSettle();
511+
check(shadowFinderFrom(Alignment.topCenter).evaluate()).isEmpty();
512+
check(shadowFinderFrom(Alignment.bottomCenter).evaluate()).single;
513+
});
484514
}

0 commit comments

Comments
 (0)