Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- Fixed an issue where bullet points became visually detached from the text body when toggling text direction formatting (RTL) by locking the list leading block to the editor's base text direction.
- Fixed typed text being inserted at the previous caret position on Android after moving the caret with a tap/mouse by keeping the platform IME's editing state in sync with the selection even when the keyboard is hidden.
- Fixed toolbar buttons not being labeled for screen readers (e.g. VoiceOver) by forwarding the `tooltip` to the underlying button in the toggle-style buttons, and by labeling each header-style button individually (N, H1, H2, …) instead of sharing a single tooltip.
- Fixed typing being broken on Flutter Web when semantics is enabled (e.g. `SemanticsBinding.instance.ensureSemantics()`, a screen reader, or Tab navigation) by publishing a text-field semantics node on the editor so the web engine creates and attaches its editable DOM element ([#2531](https://github.com/singerdmx/flutter-quill/issues/2531)).

### Removed

Expand Down
54 changes: 54 additions & 0 deletions lib/src/editor/raw_editor/raw_editor_state.dart
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,53 @@ class QuillRawEditorState extends EditorState
);
}

/// Publishes a text-field [Semantics] node (mirroring [RenderEditable]) so
/// typing works on the web when semantics is enabled: the engine only creates
/// and focuses its editable DOM element for a focused text-field node, and
/// the editor otherwise exposes none, so typing silently fails (see #2531).
///
/// Only applied on the web while editable; other platforms drive the IME
/// through `flutter/textinput` regardless of semantics.
Widget _webTextFieldSemantics(Widget child) {
if (!kIsWeb || widget.config.readOnly) {
return child;
}
return Semantics(
container: true,
textField: true,
multiline: true,
// Otherwise the editable element is created hard-`disabled`.
enabled: true,
// The activation lever: the engine attaches its editable element only
// while this node reports focus. [_handleFocusChanged] rebuilds to sync.
focused: _hasFocus,
onFocus: () => widget.config.focusNode.requestFocus(),
onSetSelection: _semanticsSetSelection,
onSetText: _semanticsSetText,
// Make this the single text-field leaf, otherwise an inner focusable
// descendant steals the click before it reaches the editable element.
excludeSemantics: true,
child: child,
);
}

void _semanticsSetSelection(TextSelection selection) {
userUpdateTextEditingValue(
textEditingValue.copyWith(selection: selection),
SelectionChangedCause.keyboard,
);
}

void _semanticsSetText(String text) {
userUpdateTextEditingValue(
TextEditingValue(
text: text,
selection: TextSelection.collapsed(offset: text.length),
),
SelectionChangedCause.keyboard,
);
}

@override
Widget build(BuildContext context) {
assert(debugCheckHasMediaQuery(context));
Expand Down Expand Up @@ -472,6 +519,7 @@ class QuillRawEditorState extends EditorState
),
);
}
child = _webTextFieldSemantics(child);
final constraints = widget.config.expands
? const BoxConstraints.expand()
: BoxConstraints(
Expand Down Expand Up @@ -1139,6 +1187,12 @@ class QuillRawEditorState extends EditorState
WidgetsBinding.instance.removeObserver(this);
}
updateKeepAlive();

if (kIsWeb && mounted) {
// Rebuild so [_webTextFieldSemantics]'s `focused:` flag tracks the
// FocusNode; a stale flag leaves keyboard focus (e.g. Tab) unable to type.
_markNeedsBuild();
}
}

void _onChangedClipboardStatus() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ class QuillToolbarSelectHeaderStyleButtonsState
return Padding(
padding: const EdgeInsets.symmetric(horizontal: !kIsWeb ? 1.0 : 5.0),
child: QuillToolbarIconButton(
tooltip: tooltip,
tooltip: _valueToText[attribute],
iconTheme: iconTheme,
isSelected: isSelected,
onPressed: () => _sharedOnPressed(attribute),
Expand Down
3 changes: 3 additions & 0 deletions lib/src/toolbar/buttons/toggle_style_button.dart
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ class QuillToolbarToggleStyleButtonState
iconSize,
iconButtonFactor,
iconTheme,
tooltip,
),
);
}
Expand Down Expand Up @@ -170,6 +171,7 @@ Widget defaultToggleStyleButtonBuilder(
double iconSize = kDefaultIconSize,
double iconButtonFactor = kDefaultIconButtonFactor,
QuillIconTheme? iconTheme,
String? tooltip,
]) {
final isEnabled = onPressed != null;
return QuillToolbarIconButton(
Expand All @@ -178,5 +180,6 @@ Widget defaultToggleStyleButtonBuilder(
onPressed: onPressed,
afterPressed: afterPressed,
iconTheme: iconTheme,
tooltip: tooltip,
);
}