Skip to content
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

[web] switch from .didGain/LoseAccessibilityFocus to .focus #53134

Merged
merged 3 commits into from
Jun 11, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
24 changes: 24 additions & 0 deletions lib/web_ui/lib/src/engine/dom.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2749,6 +2749,30 @@ DomCompositionEvent createDomCompositionEvent(String type,
}
}

/// This is a pseudo-type for DOM elements that have the boolean `disabled`
/// property.
///
/// This type cannot be part of the actual type hierarchy because each DOM type
/// defines its `disabled` property ad hoc, without inheriting it from a common
/// type, e.g. [DomHTMLInputElement] and [DomHTMLTextAreaElement].
Comment on lines +2755 to +2757
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😣

///
/// To use, simply cast any element known to have the `disabled` property to
/// this type using `as DomElementWithDisabledProperty`, then read and write
/// this property as normal.
@JS()
@staticInterop
class DomElementWithDisabledProperty extends DomHTMLElement {}

extension DomElementWithDisabledPropertyExtension on DomElementWithDisabledProperty {
@JS('disabled')
external JSBoolean? get _disabled;
bool? get disabled => _disabled?.toDart;

@JS('disabled')
external set _disabled(JSBoolean? value);
set disabled(bool? value) => _disabled = value?.toJS;
}

@JS()
@staticInterop
class DomHTMLInputElement extends DomHTMLElement {}
Expand Down
18 changes: 14 additions & 4 deletions lib/web_ui/lib/src/engine/semantics/text_field.dart
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,7 @@ class TextField extends PrimaryRoleManager {
editableElement = semanticsObject.hasFlag(ui.SemanticsFlag.isMultiline)
? createDomHTMLTextAreaElement()
: createDomHTMLInputElement();
_updateEnabledState();

// On iOS, even though the semantic text field is transparent, the cursor
// and text highlighting are still visible. The cursor and text selection
Expand Down Expand Up @@ -424,20 +425,19 @@ class TextField extends PrimaryRoleManager {
// and wait for a tap event before invoking the iOS workaround and creating
// the editable element.
if (editableElement != null) {
_updateEnabledState();
activeEditableElement.style
..width = '${semanticsObject.rect!.width}px'
..height = '${semanticsObject.rect!.height}px';

if (semanticsObject.hasFocus) {
if (domDocument.activeElement !=
activeEditableElement) {
if (domDocument.activeElement != activeEditableElement && semanticsObject.isEnabled) {
semanticsObject.owner.addOneTimePostUpdateCallback(() {
activeEditableElement.focus();
});
}
SemanticsTextEditingStrategy._instance?.activate(this);
} else if (domDocument.activeElement ==
activeEditableElement) {
} else if (domDocument.activeElement == activeEditableElement) {
if (!isIosSafari) {
SemanticsTextEditingStrategy._instance?.deactivate(this);
// Only apply text, because this node is not focused.
Expand All @@ -457,6 +457,16 @@ class TextField extends PrimaryRoleManager {
}
}

void _updateEnabledState() {
final DomElement? element = editableElement;

if (element == null) {
return;
}

(element as DomElementWithDisabledProperty).disabled = !semanticsObject.isEnabled;
}

@override
void dispose() {
super.dispose();
Expand Down
7 changes: 3 additions & 4 deletions lib/web_ui/test/engine/semantics/semantics_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1802,15 +1802,14 @@ void _testTextField() {


final SemanticsObject node = owner().debugSemanticsTree![0]!;
final TextField textFieldRole = node.primaryRole! as TextField;
final DomHTMLInputElement inputElement = textFieldRole.activeEditableElement as DomHTMLInputElement;

// TODO(yjbanov): this used to attempt to test that value="hello" but the
// test was a false positive. We should revise this test and
// make sure it tests the right things:
// https://github.com/flutter/flutter/issues/147200
expect(
(node.element as DomHTMLInputElement).value,
isNull,
);
expect(inputElement.value, '');

expect(node.primaryRole?.role, PrimaryRole.textField);
expect(
Expand Down
27 changes: 23 additions & 4 deletions lib/web_ui/test/engine/semantics/text_field_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -102,10 +102,21 @@ void testMain() {
// make sure it tests the right things:
// https://github.com/flutter/flutter/issues/147200
final SemanticsObject node = owner().debugSemanticsTree![0]!;
expect(
(node.element as DomHTMLInputElement).value,
isNull,
);
final TextField textFieldRole = node.primaryRole! as TextField;
final DomHTMLInputElement inputElement = textFieldRole.activeEditableElement as DomHTMLInputElement;
expect(inputElement.tagName.toLowerCase(), 'input');
expect(inputElement.value, '');
expect(inputElement.disabled, isFalse);
});

test('renders a disabled text field', () {
createTextFieldSemantics(isEnabled: false, value: 'hello');
expectSemanticsTree(owner(), '''<sem><input /></sem>''');
final SemanticsObject node = owner().debugSemanticsTree![0]!;
final TextField textFieldRole = node.primaryRole! as TextField;
final DomHTMLInputElement inputElement = textFieldRole.activeEditableElement as DomHTMLInputElement;
expect(inputElement.tagName.toLowerCase(), 'input');
expect(inputElement.disabled, isTrue);
});

// TODO(yjbanov): this test will need to be adjusted for Safari when we add
Expand Down Expand Up @@ -425,13 +436,15 @@ void testMain() {
children: <SemanticsNodeUpdate>[
builder.updateNode(
id: 1,
isEnabled: true,
isTextField: true,
value: 'Hello',
isFocused: focusFieldId == 1,
rect: const ui.Rect.fromLTRB(0, 0, 50, 10),
),
builder.updateNode(
id: 2,
isEnabled: true,
isTextField: true,
value: 'World',
isFocused: focusFieldId == 2,
Expand Down Expand Up @@ -882,6 +895,7 @@ void testMain() {
SemanticsObject createTextFieldSemantics({
required String value,
String label = '',
bool isEnabled = true,
bool isFocused = false,
bool isMultiline = false,
ui.Rect rect = const ui.Rect.fromLTRB(0, 0, 100, 50),
Expand All @@ -891,6 +905,7 @@ SemanticsObject createTextFieldSemantics({
final SemanticsTester tester = SemanticsTester(owner());
tester.updateNode(
id: 0,
isEnabled: isEnabled,
label: label,
value: value,
isTextField: true,
Expand Down Expand Up @@ -971,6 +986,7 @@ Map<int, SemanticsObject> createTwoFieldSemanticsForIos(SemanticsTester builder,
children: <SemanticsNodeUpdate>[
builder.updateNode(
id: 1,
isEnabled: true,
isTextField: true,
value: 'Hello',
label: 'Hello',
Expand All @@ -979,6 +995,7 @@ Map<int, SemanticsObject> createTwoFieldSemanticsForIos(SemanticsTester builder,
),
builder.updateNode(
id: 2,
isEnabled: true,
isTextField: true,
value: 'World',
label: 'World',
Expand All @@ -999,6 +1016,7 @@ Map<int, SemanticsObject> createTwoFieldSemanticsForIos(SemanticsTester builder,
children: <SemanticsNodeUpdate>[
builder.updateNode(
id: 1,
isEnabled: true,
isTextField: true,
value: 'Hello',
label: 'Hello',
Expand All @@ -1007,6 +1025,7 @@ Map<int, SemanticsObject> createTwoFieldSemanticsForIos(SemanticsTester builder,
),
builder.updateNode(
id: 2,
isEnabled: true,
isTextField: true,
value: 'World',
label: 'World',
Expand Down