-
Notifications
You must be signed in to change notification settings - Fork 249
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
[Infrastructure] Implement custom dropdown widget (Resolves #1527, Resolves #731, Resolves #689) #1538
[Infrastructure] Implement custom dropdown widget (Resolves #1527, Resolves #731, Resolves #689) #1538
Conversation
@angelosilvestre can we position the popover below the button like a more traditional popup menu? Also can we round the corners to fit better with the toolbar shape? Generally the functionality looks good. Let's make it look the best we can for the Super Editor Example app, and then we can look at which aspects should be configurable. |
@@ -224,6 +224,9 @@ class _EditorToolbarState extends State<EditorToolbar> { | |||
), | |||
]); | |||
} | |||
|
|||
// Rebuild to display the selected item. | |||
setState(() {}); |
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.
Where is the selected item stored? Why do we want setState()
to be empty?
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 dropdown itself doesn't store the selected value, it's always provided via constructor.
In the toolbar, this value also isn't stored, we query the node type from the selected node when we create the dropdown.
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 those explanations. If the selected item isn't stored, then why are calling setState()
?
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'm calling setState
to rebuild the toolbar. Then, it will query the value again.
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 still don't understand. Please try to explain in terms of why this is needed. On the face of it, I don't know why you would need to call setState()
to rebuild a widget tree that internally is using some value that changed elsewhere. Because there isn't much explanation here, I'm not even sure what to ask, but for example, why are we not using a ValueListenable
somewhere, etc?
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.
After taking a better look at this, I think that the main problem here is that we aren't rebuilding the toolbar when the document changes.
After we change the selected node's metadata, we need to rebuild the toolbar so it can "see" the current state of the selected node.
I moved this setState
call to the document change listener. Please let me know if this is an acceptable solution.
this.dropdownKey, | ||
}); | ||
|
||
/// The items that will be displayed in the dropdown list. |
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.
Let's re-order these properties to match our typical ordering.
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.
Re-ordered to match how we are using it on _toolbar.dart
/// when the dropdown is visible. | ||
/// | ||
/// This widget doesn't enforce any style, dropdown position or decoration. | ||
class RawDropdown extends StatefulWidget { |
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.
What does "Raw" mean here?
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 only mean that we don't make any decisions here about how the widget should look like. We could probably find a better name for it... UnstyledDropdown
, OverlayDropdown
, or something else.
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.
Can you elaborate on that explanation? You've said what this widget "isn't" doing. Can you describe what it "is" doing?
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 tried to summarize what this widget does in the previous lines:
- It listens for changes of its
OverlayPortalController
and shows/hides theOverlay
. - Setup focus sharing.
- Requests focus to the dropdown's
FocusNode
when the overlay is displayed. - Delivers key events to
onKeyEvent
. - Closes the dropdown when tapping outside of it.
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'm confused why the follower and aligner are configured elsewhere instead of here? If this widget does what you describe, why wouldn't it also include positioning?
Is this widget expected to be used publicy? If not, can we make it private?
If we make it private and move the follower and aligner in here, let's call this _FollowerPopover
. The content of the popover can then be provided via child
to decide what to display within the popover.
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've made it that way so we (or others) could re-use the RawDropDown
to implement other kinds of dropdown buttons, which could have different following policies.
For example, it would be relatively easy to implement something like a simple (and incomplete) color picker with the following code:
class ColorPicker extends StatefulWidget {
const ColorPicker({super.key});
@override
State<ColorPicker> createState() => _ColorPickerState();
}
class _ColorPickerState extends State<ColorPicker> {
final DropdownController _dropdownController = DropdownController();
// This might be removed.
final FocusNode _focusNode = FocusNode();
final List<MaterialColor> colors = [
Colors.red,
Colors.blue,
Colors.amber,
Colors.brown,
Colors.green,
Colors.orange,
Colors.pink,
Colors.teal
];
@override
void dispose() {
_dropdownController.dispose();
_focusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return RawDropdown(
controller: _dropdownController,
dropdownBuilder: _buildColorPopover,
parentFocusNode: _focusNode,
child: ElevatedButton(
onPressed: () => _dropdownController.open(),
child: Text("Pick a color"),
),
);
}
Widget _buildColorPopover(BuildContext context, LeaderLink link, FollowerBoundary boundary) {
return Follower.withOffset(
link: link,
leaderAnchor: Alignment.bottomCenter,
followerAnchor: Alignment.topCenter,
offset: Offset(0, 10),
child: Material(
elevation: 4.0,
borderRadius: BorderRadius.circular(8.0),
child: Container(
height: 50,
width: 200,
padding: const EdgeInsets.all(8.0),
child: ListView.separated(
scrollDirection: Axis.horizontal,
// TODO: implement selection
itemBuilder: (context, index) => Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
color: colors[index],
),
height: 20,
width: 20,
),
separatorBuilder: (context, index) => SizedBox(width: 10),
itemCount: colors.length,
),
),
),
);
}
}
Gravacao.de.Tela.2023-11-02.as.11.46.47.mov
@matthew-carroll Yes, initially I left the dropdown centered to match the material dropdown. Updated: Gravacao.de.Tela.2023-10-21.as.09.34.45.movWith slow animations: Gravacao.de.Tela.2023-10-21.as.09.35.31.mov |
@angelosilvestre I see in your latest video that the popover prevents itself from moving beyond the bottom of the screen, but that causes it to cover the toolbar. My first thought is that this is unexpected and we don't want it. Was there any particular reason that you went with a screen-constrained popup? The following heuristic comes to mind:
|
@matthew-carroll As discussed, I implemented a positioning behavior similar to Notion's.
I also simplified the transition to be just a fade-in animation. This is how it's looking right now: Gravacao.de.Tela.2023-10-26.as.21.50.39.movFor reference, this is how Notion's dropdown looks: Gravacao.de.Tela.2023-10-26.as.21.54.26.mov |
@@ -224,6 +224,9 @@ class _EditorToolbarState extends State<EditorToolbar> { | |||
), | |||
]); | |||
} | |||
|
|||
// Rebuild to display the selected item. | |||
setState(() {}); |
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 still don't understand. Please try to explain in terms of why this is needed. On the face of it, I don't know why you would need to call setState()
to rebuild a widget tree that internally is using some value that changed elsewhere. Because there isn't much explanation here, I'm not even sure what to ask, but for example, why are we not using a ValueListenable
somewhere, etc?
/// with the [parentFocusNode]. This means that when the dropdown requests focus, [parentFocusNode] | ||
/// still has non-primary focus. | ||
/// | ||
/// The dropdown tries to fit all items on the available space. If there isn't enough room, |
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.
Let's simply enumerate the rules for clarity:
/// The popover list is positioned based on the following rules:
///
/// 1. The popover is displayed below the selected item, if there's enough room, or
/// 2. The popover is displayed above the selected item, if there's enough room, or
/// 3. The popover is displayed with its bottom aligned with the bottom of
/// the given boundary, and it covers the selected item.
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 didn't mention scrolling constraints in there. Please update accordingly. The goal is to make the rules quick and easy to understand.
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'm not sure I understood what "scrolling constraints" means here.
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'm not entirely sure what I meant, but I think I may have been referring to the content in the popover going from fully-visible to partially visible + scrolling. For example, let's say we pull up a list of options in a popover. There's likely some kind of decision whether to make the popover as tall as all those items in the list, or whether to make the popover shorter, and introduce scrolling to the list. That choice about adding scrolling, or not, and the resulting height, impacts all of the rules above. They're like two sides of the same coin.
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.
Updated.
int? newFocusedIndex; | ||
if (event.logicalKey == LogicalKeyboardKey.arrowDown) { | ||
if (_focusedIndex == null || _focusedIndex! >= widget.items.length - 1) { | ||
// We don't have a focused item or we are at the end of the list. Focus the first item. |
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.
What does it mean to not have a focused item? Is that not an error state?
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.
In the current implementation, the popover list opens without an "active item". The active item is set only after the user presses ARROW DOWN or ARROW UP.
This is compatible with other popover list implementations.
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.
Really? If we're looking at a typical address form on the web, and there's a country selector, and we have United States selected, when we open that selector and show the popover, "United States" isn't active? And the popover isn't centered on the "United States" item?
On a related note, if no item is active initially, which one becomes active when you press UP or DOWN?
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.
Really? If we're looking at a typical address form on the web, and there's a country selector, and we have United States selected, when we open that selector and show the popover, "United States" isn't active? And the popover isn't centered on the "United States" item?
It makes sense. Updated to make the selected value "active" in the list.
On a related note, if no item is active initially, which one becomes active when you press UP or DOWN?
Pressing UP makes the last item active and pressing DOWN makes the first item active. We could also change the behavior to make the first item active if there isn't a selected item.
parentFocusNode: widget.parentFocusNode, | ||
onKeyEvent: _onKeyEvent, | ||
child: ConstrainedBox( | ||
// TODO: what value should we use? |
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.
When I first read this, I thought this was the popover list constraint. But this is constraining the selected item display, right? If so, why are we constraining that at all?
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 remember why I added that, but it seems it's unnecessary. Removed.
} | ||
|
||
Widget _buildSelectedItem() { | ||
return Row( |
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.
Should this be a Row
or a Stack
? Imagine the developer wants to display a selected item with their own background color or gradient, how would they get a full-bleed display if we're pushing their widget to the side for a dropdown arrow?
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.
Updated it to be a Stack
.
/// when the dropdown is visible. | ||
/// | ||
/// This widget doesn't enforce any style, dropdown position or decoration. | ||
class RawDropdown extends StatefulWidget { |
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'm confused why the follower and aligner are configured elsewhere instead of here? If this widget does what you describe, why wouldn't it also include positioning?
Is this widget expected to be used publicy? If not, can we make it private?
If we make it private and move the follower and aligner in here, let's call this _FollowerPopover
. The content of the popover can then be provided via child
to decide what to display within the popover.
@matthew-carroll I made some changes based on your comments, and replied some of them with other questions. |
// The document has changed. | ||
// Some of the selected node's metadata, for example, the blockType, influences how the toolbar is displayed. | ||
// Reflow the toolbar to acount for the new state of the selected node. | ||
setState(() {}); |
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.
Is there any particular reason that we don't listen for this change around, or within the pieces of the toolbar that care, rather than rebuilding the whole toolbar?
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.
Updated.
@@ -521,26 +541,30 @@ class _EditorToolbarState extends State<EditorToolbar> { | |||
if (_isConvertibleNode()) ...[ | |||
Tooltip( | |||
message: AppLocalizations.of(context)!.labelTextBlockType, | |||
child: DropdownButton<_TextType>( | |||
child: ItemSelector<_TextType>( |
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'm wondering if there's a more useful way to breakdown responsibilities for these widgets. After you go through the other new comments, let's jump on a call and talk about how to break things up.
My gut feeling is that we want two different levels of widgets here. We want foundational pieces that make almost zero visual designs but provide some base level relationships. Then we want some widgets that give us default styling, and also take a few properties to configure obvious UI details. At the moment, we're trying to do both of those things in one group of widgets.
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 makes sense, we can jump on a call to talk about that.
@angelosilvestre I'm taking look at it as part of the #1407 |
@matthew-carroll As discussed, the API is changing. Some things are a bit different from what we've talked. Here's the summary: We have now the following widgets:
|
b50ddf9
to
4b5e83d
Compare
Rebased to fix an import. |
@angelosilvestre can you please edit your comment up above to point out what you changed from our discussion and why? Based on what I'm seeing in that comment right now, here are a couple comments: PopoverScaffold
What do you mean "This widget uses a SuperEditorPopover"? The scaffold is the lowest level widget here and "instead of adding a Focus ourselves" - I'm not sure what that means either, but you said the scaffold takes a ItemSelectionListWhy is this list taking a geometry? I believe the purpose of this widget, as discussed on our call, was to provide a list widget. That list widget might be placed within a popover, but its not, itself, a popover. It's a list that understands the concept of "selection" and "activation" and arrow key navigation, and ENTER to select the activated item, and auto-scrolling when pressing arrows keys near the top and bottom of the visible space. All of that is independent from use within a popover. Such a widget would be equally applicable for a selection list that's placed within a typical screen UI. Please let me know if I'm missing something. |
Maybe we have some naming issue... super_editor/super_editor/lib/src/infrastructure/popovers.dart Lines 5 to 64 in 6e2040f
I think I misunderstood it a little bit. I'll update it. |
@matthew-carroll Updated the PR. I think now it's closer to what we've talked. I temporarily commented the test until we are sure the API is right. Can you do a first pass review? |
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 did an initial review. One big initial change should be to move all the demo stuff into the demo app, instead of in the package. That might clarify some of the relationships and also point out where some things are named poorly.
child: Padding( | ||
padding: const EdgeInsets.only(left: 16.0), | ||
child: Text(_getTextTypeName(textType)), | ||
child: _DocumentListenableBuilder( |
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.
Why do we need the document listenable?
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.
With the recent changes we don't need it anymore.
} | ||
} | ||
|
||
/// A rounded rectangle shape with a fade-in transition. |
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 think we should combine the shape with the transition animation. Those don't seem related. Also, this popover shape doesn't allow any configuration for the shape...
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'm currently using a Material
widget to build the popover shape, should I take a ShapeBorder
to customize the popover shape?
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'm confused - how does the use of Material
relate to the two issues I raised above?
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 Material
widget is related just with the second sentence:
Also, this popover shape doesn't allow any configuration for the shape...
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.
Sure you can take a BorderShape
if that makes sense. But first, let's figure out what the division should look like between the shape and the transition.
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.
If we split the transition from the shape, then the PopoverShape
widget won't bring much value, because its implementation will be just:
Widget build(BuildContext context) {
return Material(
elevation: 8,
borderRadius: BorderRadius.circular(12),
clipBehavior: Clip.hardEdge,
child: child
);
}
} | ||
} | ||
|
||
/// Controls the size and position of a popover. |
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 data structure doesn't control anything. This data structure is the offset and size of a popover in screen space. Please try to avoid the term "position" when you're not talking about text or document positions.
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.
Updated.
Test Update tests Update Focus color Animation Animation
@matthew-carroll Added tests |
|
||
/// Builds a popover list item. | ||
/// | ||
/// [isActive] is `true` if [item] is the currently active item on the list, or `false` otherwise. |
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's probably a good idea to say something about what it means to be active, e.g., the item in the list that currently has focus and can be moved up/down with arrow keys.
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.
Updated.
expect(find.byType(RoundedRectanglePopoverAppearance), findsNothing); | ||
}); | ||
|
||
testWidgetsOnAllPlatforms('enforces the given popover geometry', (tester) async { |
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.
If this is only testing height constraints, then please name the test accordingly. This name suggests that all aspects of geometry are being verified against all possible conditions.
Related to that, is testing just the height of the popover, in just one set of conditions, sufficient to lock down the geometry implementation?
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.
Updated.
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'm still confused about why a single test is sufficient for this. Don't we have rules about where the popover appears based on surrounding space, and how large the popover is allowed to be? If so, then it seems like we should have a thorough collection of tests to lock down each of those behaviors...
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.
Ok, this test is to ensure we honor an app-configured geometry. We don't have tests for our default geometry. I'll add those.
expect(tester.getRect(find.byType(RoundedRectanglePopoverAppearance)).height, 10); | ||
}); | ||
|
||
testWidgetsOnAllPlatforms('shares focus with SuperEditor', (tester) async { |
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 test seems overly specific. Do we actually care about the popover sharing focus with SuperEditor? Or do we care about sharing focus with any arbitrary widget that wants to share?
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.
Updated.
@angelosilvestre wanted to make sure you didn't forget about this PR. |
@matthew-carroll Updated the PR |
@matthew-carroll Added teste for the default popover aligner. |
expect(popoverRect.top, greaterThan(buttonRect.bottom)); | ||
}); | ||
|
||
testWidgetsOnAllPlatforms('positions the popover above button if there is room', (tester) async { |
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.
Can you adjust the test descriptions to make it clear that there are competing locations? If you read the test above this one, and then you read this test, there's no indication about why one might happen instead of the other. All we know is that if there's room below, it goes below, and if there's room above, it goes above, which begs the question, what if there's room below and above?
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.
Updated.
final popoverRect = tester.getRect(find.byType(RoundedRectanglePopoverAppearance)); | ||
|
||
// Ensure popover was pinned of the bottom to the boundary widget. | ||
expect(popoverRect.bottom, 600); |
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.
Do we care about the size in this situation? Is the size impacted by the fact that there's limited room?
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, updated.
}); | ||
|
||
testWidgetsOnAllPlatforms( | ||
'pins the popover to the bottom if there isn\'t room neither below or above the button', (tester) async { |
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.
All of these test descriptions for "without a boundary" are the same as the ones with a boundary, which leads me to believe there's no difference in behavior whether we use a boundary or not. Shouldn't the boundary do something?
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 modified the tests to use a different screen size and different widget sizes with a comment explaining why.
}); | ||
}); | ||
|
||
testWidgetsOnAllPlatforms('shares focus with other widgets', (tester) async { |
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.
Let's be much more specific about the feature this is locking down. It's not simply that it shares focus with other widgets - all widgets share focus with other widgets. Please describe this test in terms of the special situation we've implemented for this popover system.
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.
Updated.
expect(popoverRect.bottom, lessThan(buttonRect.top)); | ||
}); | ||
|
||
testWidgetsOnAllPlatforms('pins the popover to the bottom if there is not room below or above the button', |
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.
Can you clarify "to the bottom" of what? screen? boundary? button?
In addition to earlier comments, I checked out this branch so I could work on paragraph alignment. I discovered that we're displaying too many alignment options in the drop down. This is because we're looping through all items: TextAlign.values
.map(
(alignment) => SuperEditorDemoIconItem(
icon: _buildTextAlignIcon(alignment),
id: alignment.name,
),
)
.toList(), |
@angelosilvestre I'm gonna merge this so that I can fix paragraph alignment, which I based off of this branch so that I could select the paragarph alignment from the toolbar. Please put up a followup PR with the final adjustments from the review. |
Also, please cherry pick this to stable, per usual. |
[Infrastructure] Implement custom dropdown widget. Resolves #1527, Resolves #731, Resolves #689
Tapping at any
DropdownButton
in the example app toolbar causes the toolbar to immediately close. The issue is that FlutterDropdownButton
displays the dropdown in its own route, which steals the focus from the editor.This PR introduces a custom dropdown button. I called it
SuperDropdownButton
, but we can choose a better name. This widget displays the dropdown using anOverlayPortal
. By doing so, it can share focus withSuperEditor
.We need to define how we want the public API:
For example: What should be its default height? Do we want to make the width as large as the largest item in the list or just enough to fit the selected item?
This is how it looks so far:
Gravacao.de.Tela.2023-10-19.as.23.28.14.mov
I noticed that changing the paragraph alignment doesn't immediately rebuilds the paragraph, but it isn't related to the dropdown. I will file a ticket after this PR is merged.
Another example, constraining the dropdown height:
Gravacao.de.Tela.2023-10-19.as.23.30.06.mov