-
Notifications
You must be signed in to change notification settings - Fork 270
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
emoji: Fix bottom padding of emoji picker #1315
base: main
Are you sure you want to change the base?
Conversation
ebfa257
to
584240c
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! Comments below.
I stopped reading the tests when I realized they might not work with an alternative implementation I thought of. (Actually would it be possible to write a set of tests that would work with either/any implementation? That's generally preferable.)
lib/widgets/emoji_reaction.dart
Outdated
// This does remove the bottom inset. | ||
// Leave it to the descendent [ListView]. | ||
useSafeArea: true, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't understand this yet—what do you mean by "remove the bottom inset"?
Here's the doc:
/// The [useSafeArea] parameter specifies whether the sheet will avoid system
/// intrusions on the top, left, and right. If false, no [SafeArea] is added;
/// and [MediaQuery.removePadding] is applied to the top,
/// so that system intrusions at the top will not be avoided by a [SafeArea]
/// inside the bottom sheet either.
/// Defaults to false.
It doesn't say anything about controlling what's done about the bottom inset.
lib/widgets/emoji_reaction.dart
Outdated
context: context, | ||
// The bottom is padded below with `padding` within the [ListView]. | ||
removeBottom: true, | ||
child: ListView.builder( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about using CustomScrollView
, with SliverPadding
for the top padding and SliverSafeArea
for the bottom padding? My feeling is that the code would be simpler; I'm curious if that turns out to be true. We use SliverSafeArea
in a few places already.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indeed! This works for me:
Expanded(child: InsetShadowBox(
top: 8, bottom: 8,
color: designVariables.bgContextMenu,
child: CustomScrollView(
slivers: [
SliverPadding(
padding: EdgeInsets.only(top: 8),
sliver: SliverSafeArea(
minimum: EdgeInsets.only(bottom: 8),
sliver: SliverList.builder(
itemCount: _resultsToDisplay.length,
itemBuilder: (context, i) => EmojiPickerListEntry(
pageContext: widget.pageContext,
emoji: _resultsToDisplay[i].candidate,
message: widget.message)))),
]))),
da4794b
to
0c1d767
Compare
Updated the PR, with new screenshots here: #1315 (comment). The new implementation does require reworking the tests. I have updated them with an approach that should work just fine with the emoji picker implementation on |
Thanks! Reviewing now. One nit: I think this is the wrong column label in the table of screenshots for the current revision:
because the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! One design-level comment I noticed: now that the content can scroll all the way to the bottom edge of the screen (which I also think is an improvement), how about removing the InsetShadowBox
shadow effect at the bottom? We don't have that effect on similar scrollables, such as the "Combined feed" message list.
The shadow effect makes the viewport edge more graceful when there's not already a clear boundary that coincides with it, but the screen edge is such a boundary. Similarly the top edge of the keyboard when that's open. (Clear definition of the top edge of the keyboard is the platform's job, not ours.)
I'd be happy to discuss more in #mobile-design
if needed.
final options = tester.widgetList(find.byType(EmojiPickerListEntry)); | ||
firstOption = options.first; | ||
lastOption = options.last; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
final options = tester.widgetList(find.byType(EmojiPickerListEntry)); | |
firstOption = options.first; | |
lastOption = options.last; | |
final listEntryFinder = find.byType(EmojiPickerListEntry); | |
firstOption = tester.widget(listEntryFinder.first); | |
lastOption = tester.widget(listEntryFinder.last); |
with the goal of leaning on flutter_test
just a little bit more, in case it gives richer / more helpful feedback when a test fails—I don't know if it actually would in this case though.
Actually, with these chained finders (foo.first
and foo.last
), would we still need to store firstOption
and lastOption
to pass to a find.byWidget
, or could we just used the chained finders directly?
double getScrollChildrenTop(WidgetTester tester) => | ||
tester.getTopLeft(find.byWidget(firstOption)).dy; | ||
double getScrollChildrenBottom(WidgetTester tester) => | ||
tester.getBottomLeft(find.byWidget(lastOption)).dy; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The name, return type, and implementation don't seem to fit with each other; I can say more if helpful.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I kind of struggled naming these helpers. I meant to express that this fetches y-coord of the top-most/bottom-most edge of the scrollable content (scroll items/scroll list?).
Are double getScrollElementTopEdgeDy
and double getScrollElementBottomEdgeDy
better?
// The top of the list of children is padded by 8px; | ||
// the bottom is out of view. | ||
check(scrollViewTop).equals(getScrollChildrenTop(tester) - 8); | ||
check(scrollViewBottom).isLessThan(getScrollChildrenBottom(tester)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like this assumes there's a very long list of children, but I'm not seeing that clearly in the setup code. Reading setupEmojiPicker
, called by prepare
, I think I see two items being set up?
store.setServerEmojiData(ServerEmojiData(codeToNames: {
'1f4a4': ['zzz', 'sleepy'], // (just 'zzz' in real data)
}));
await store.handleEvent(RealmEmojiUpdateEvent(id: 1, realmEmoji: {
'1': eg.realmEmojiItem(emojiCode: '1', emojiName: 'buzzing'),
}));
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The rest of them come from EmojiStore.popularEmojiCandidates
.
The test controls the screen height so that the scroll view can be filled. I think we can add some additional checks in prepare
to verify that.
lib/widgets/emoji_reaction.dart
Outdated
// This removes insets from all sides except bottom. | ||
// Leave it to the descendent [CustomScrollView]. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't know what "removes insets" means. Taken literally, it doesn't make sense; the insets are features of the device (which might include hardware) that our app isn't able to remove.
How about:
// The bottom inset is left for [builder] to handle;
// see [EmojiPicker] and its [CustomScrollView] for how we do that.
It's also possible that we don't need a comment at all, and the useSafeArea
dartdoc is sufficient if readers are curious.
Thanks for the review! Just pushed a new revision addressing the comments, and updated the screenshots. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! Small comments on the tests, which actually I'm curious for Greg's thoughts on (so, marking for his review): one is a naming question, the other is about adding a helper that might be helpful but maybe not necessary.
// This makes it easier to convert between device pixels used for | ||
// [FakeViewPadding] and logical pixels used in tests. | ||
tester.view.devicePixelRatio = 1.0; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a way to let callers use less code, but it doesn't really remove the caller's burden to think about the physical/logical pixel distinction. That's because callers are still asked to make a FakeViewPadding
, and the dartdoc of that says it's in physical pixels, while the rest of the test code is naturally in logical pixels. Callers are asked to assume the pixel ratio is 1, but they're not asked explicitly, in prepare
's interface (it has no dartdoc), so they have to look at this line in the implementation, to be sure.
It's helpful that we're not asking callers to do math with devicePixelRatio
, but I think there's a more satisfying way to do that, and with something reusable. (Helpful because this is likely to come up again.) What if we did this:
- New file, like test/test_window.dart
- New private subclass of
FakeViewPadding
:- It implements a method
FakeViewPadding resolve(double devicePixelRatio)
that returns a plainFakeViewPadding
, scaled fromthis
bydevicePixelRatio
- Its
left
,top
,right
, andbottom
getters throw errors saying thatresolve
must be called. (Or maybe call the superclass's getter just if the value is zero.) See for example theoperator
getter onApiNarrowDm
.
- It implements a method
- An extension on
FakeViewPadding
, adding a static method with return typeFakeViewPadding
, named something likefromLogicalPx
, that creates and returns an instance of the private subclass
Then for these tests, callers would say:
FakeViewPadding.fromLogicalPx(bottom: 10)
and prepare
would say
tester.view.viewPadding = viewPadding.resolve(devicePixelRatio);
double getListEntriesTopEdgeOffset(WidgetTester tester) => | ||
tester.getTopLeft(listEntryFinder.first).dy; | ||
double getListEntriesBottomEdgeOffset(WidgetTester tester) => | ||
tester.getBottomLeft(listEntryFinder.last).dy; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are the finders restricted to list entries that are at least partially visible in the viewport, and do the tests depend on that? If so, maybe that should be reflected in these names. I agree naming these is hard 😅 #1315 (comment) —maybe Greg would have some thoughts, having recently worked with the sticky-header code.
Signed-off-by: Zixuan James Li <[email protected]>
Previously, the body of the bottom sheet was wrapped in `SafeArea`. This pads the bottom unconditionally, shifting the bottom of the list view above the device bottom padding. This is undesirable, because the area beneath the bottom padding is still visible, and the list view should extend to the bottom regardless of the bottom inset. By just removing the `SafeArea`, the list view extends to the bottom. However, if the bottom padding is more than 8px, we can't scroll past the last entry in the list view. Essentially, we want the behavior of `SilverSafeArea.minimum` — the bottom edge of the list entries should always be padded by at least 8px, so that we can scroll past the shadow; and if the bottom padding is more than 8px, use that as the padding instead. Signed-off-by: Zixuan James Li <[email protected]>
We don't have other scrollables that are vertical, touching the bottom viewport edge, and have shadow there. This was brought up by Chris: > The shadow effect makes the viewport edge more graceful when there's not already a clear boundary that coincides with it, but the screen edge is such a boundary. Similarly the top edge of the keyboard when that's open. (Clear definition of the top edge of the keyboard is the platform's job, not ours.) CZO discussion: https://chat.zulip.org/#narrow/channel/530-mobile-design Signed-off-by: Zixuan James Li <[email protected]>
Previously, the body of the bottom sheet was wrapped in
SafeArea
. This pads the bottom unconditionally, shifting the bottom of the list view above the device bottom inset.This is undesirable, because the area beneath the bottom inset is still visible, and the list view should extend to the bottom regardless of the bottom inset.
By just removing the
SafeArea
, the list view extends to the bottom. However, if the bottom inset is more than 8px, we can't scroll past the last item in the list view. Essentially, we want the behavior ofSafeArea.minimum
with aListView
— the bottom of the list should always be padded by at least 8px, so that we can scroll past the shadow; and if the bottom inset is more than 8px, use that as the padding instead — which is achieved through the useListView.padding
andMediaQuery
.