fix(app): blocks from the / menu go into card fields
Inserting a sound, a YouTube video or a study button through the / menu in a card's field failed (found with the Snap build). Two causes: A card field closes its editor when the focus leaves it, except to a dialog. The / menu takes the focus too, so the editor was gone while its menu was open, and the block chosen went to an editor that no longer existed. The field now stays open while the editor's own popup holds the focus (the editor package's keepEditorFocusNotifier, which its editors heed for themselves, read through RichContentEditor.popupHasFocus). The editor package keeps the menu, and the layer closing it on a tap, above everything until the chosen item's handler returns, so a block asking in a dialog (formula, video, study button) had the menu over that dialog. Our items now close the menu first; the handler is given the menu for that. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
e35ce5c81d
commit
103881a28d
4 changed files with 60 additions and 5 deletions
|
|
@ -105,9 +105,12 @@ CharacterShortcutEvent slashMenu(BuildContext context, EditorBlocks blocks) {
|
|||
),
|
||||
keywords: block.keywords,
|
||||
// The menu's context is in the overlay; the editor's is the one
|
||||
// with its surroundings.
|
||||
handler: (editorState, menuService, menuContext) =>
|
||||
block.insert(context, editorState, editorState.selection),
|
||||
// with its surroundings. The menu goes first: it would stay above
|
||||
// everything until the block is in, a dialog asking for it too.
|
||||
handler: (editorState, menuService, menuContext) {
|
||||
menuService.dismiss();
|
||||
return block.insert(context, editorState, editorState.selection);
|
||||
},
|
||||
),
|
||||
],
|
||||
style: dark ? SelectionMenuStyle.dark : SelectionMenuStyle.light,
|
||||
|
|
|
|||
|
|
@ -34,6 +34,11 @@ class const RichContentEditor({
|
|||
final bool formatBar = false,
|
||||
super.key,
|
||||
}) extends StatefulWidget {
|
||||
/// Whether an editor's popup (its `/` menu) has the focus for now: it
|
||||
/// gives the focus back to its editor as it closes, so the editor losing
|
||||
/// the focus meanwhile hasn't been left, and must stay.
|
||||
static bool get popupHasFocus => keepEditorFocusNotifier.shouldKeepFocus;
|
||||
|
||||
@override
|
||||
State<RichContentEditor> createState() => _RichContentEditorState();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -91,9 +91,11 @@ class _CardFaceState() extends State<CardFace> with WidgetsBindingObserver {
|
|||
}
|
||||
|
||||
void _onFocusChange() {
|
||||
// A dialog opening over the editor takes the focus too; collapsing
|
||||
// then would drop the editor that dialog works for.
|
||||
// A dialog opening over the editor takes the focus too, and so does
|
||||
// the editor's `/` menu; collapsing then would drop the editor they
|
||||
// work for.
|
||||
if (!_focus.hasFocus &&
|
||||
!RichContentEditor.popupHasFocus &&
|
||||
mounted &&
|
||||
(ModalRoute.of(context)?.isCurrent ?? true)) {
|
||||
setState(() => _expanded = false);
|
||||
|
|
|
|||
|
|
@ -273,6 +273,51 @@ void main() {
|
|||
expect(blocks, contains(containsPair('type', mathBlockType)));
|
||||
});
|
||||
|
||||
testWidgets('the / menu takes the focus without closing its field, so the '
|
||||
'block chosen goes into the card', (tester) async {
|
||||
await cards.create('japanese', card('猫', 'cat'));
|
||||
await showStudyset(tester, size: const Size(412, 915));
|
||||
await open(tester, '猫');
|
||||
|
||||
await tester.tap(find.text('cat', findRichText: true));
|
||||
await settle(tester);
|
||||
final editor = tester.widget<AppFlowyEditor>(
|
||||
find.descendant(
|
||||
of: find.byType(RichContentEditor),
|
||||
matching: find.byType(AppFlowyEditor),
|
||||
),
|
||||
);
|
||||
final slash = editor.characterShortcutEvents.singleWhere(
|
||||
(event) => event.character == '/',
|
||||
);
|
||||
await slash.handler(editor.editorState);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(RichContentEditor), findsOneWidget);
|
||||
|
||||
// The menu builds only the items near what it shows.
|
||||
final menu = find.descendant(
|
||||
of: find.byType(SelectionMenuWidget),
|
||||
matching: find.byType(Scrollable),
|
||||
);
|
||||
await tester.scrollUntilVisible(find.text('Formula'), 40, scrollable: menu);
|
||||
await tester.ensureVisible(find.text('Formula'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Formula'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(SelectionMenuWidget), findsNothing);
|
||||
await tester.enterText(find.byType(TextField), 'x^2');
|
||||
await tester.tap(find.text('Save'));
|
||||
await settle(tester);
|
||||
await tester.tap(find.byType(BackButton));
|
||||
await settle(tester);
|
||||
|
||||
final back = (await cards.cardsOf('japanese')).single.content.back;
|
||||
final blocks =
|
||||
(back.editorJson['document']! as Map<String, Object?>)['children']!
|
||||
as List<Object?>;
|
||||
expect(blocks, contains(containsPair('type', mathBlockType)));
|
||||
});
|
||||
|
||||
testWidgets('the card types fit a phone, none of their names broken', (
|
||||
tester,
|
||||
) async {
|
||||
|
|
|
|||
Loading…
Reference in a new issue