Skip to content

Commit

Permalink
Add link support in web accessibility (flutter#46117)
Browse files Browse the repository at this point in the history
fixes flutter/flutter#134795

## Pre-launch Checklist

- [ ] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [ ] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [ ] I read and followed the [Flutter Style Guide] and the [C++,
Objective-C, Java style guides].
- [ ] I listed at least one issue that this PR fixes in the description
above.
- [ ] I added new tests to check the change I am making or feature I am
adding, or the PR is [test-exempt]. See [testing the engine] for
instructions on writing and running engine tests.
- [ ] I updated/added relevant documentation (doc comments with `///`).
- [ ] I signed the [CLA].
- [ ] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/wiki/Tree-hygiene#overview
[Tree Hygiene]: https://github.com/flutter/flutter/wiki/Tree-hygiene
[test-exempt]:
https://github.com/flutter/flutter/wiki/Tree-hygiene#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo
[C++, Objective-C, Java style guides]:
https://github.com/flutter/engine/blob/main/CONTRIBUTING.md#style
[testing the engine]:
https://github.com/flutter/flutter/wiki/Testing-the-engine
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/wiki/Tree-hygiene#handling-breaking-changes
[Discord]: https://github.com/flutter/flutter/wiki/Chat
  • Loading branch information
chunhtai authored Oct 20, 2023
1 parent 471fbc5 commit b27e1b3
Show file tree
Hide file tree
Showing 17 changed files with 260 additions and 121 deletions.
2 changes: 2 additions & 0 deletions ci/licenses_golden/licenses_flutter
Original file line number Diff line number Diff line change
Expand Up @@ -2720,6 +2720,7 @@ ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/focusable.dart + ..
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/image.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/incrementable.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/label_and_value.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/link.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/live_region.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/platform_view.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/scrollable.dart + ../../../flutter/LICENSE
Expand Down Expand Up @@ -5496,6 +5497,7 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/focusable.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/image.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/incrementable.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/label_and_value.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/link.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/live_region.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/platform_view.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/scrollable.dart
Expand Down
1 change: 1 addition & 0 deletions lib/web_ui/lib/src/engine.dart
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ export 'engine/semantics/focusable.dart';
export 'engine/semantics/image.dart';
export 'engine/semantics/incrementable.dart';
export 'engine/semantics/label_and_value.dart';
export 'engine/semantics/link.dart';
export 'engine/semantics/live_region.dart';
export 'engine/semantics/platform_view.dart';
export 'engine/semantics/scrollable.dart';
Expand Down
1 change: 1 addition & 0 deletions lib/web_ui/lib/src/engine/semantics.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export 'semantics/focusable.dart';
export 'semantics/image.dart';
export 'semantics/incrementable.dart';
export 'semantics/label_and_value.dart';
export 'semantics/link.dart';
export 'semantics/live_region.dart';
export 'semantics/platform_view.dart';
export 'semantics/scrollable.dart';
Expand Down
19 changes: 8 additions & 11 deletions lib/web_ui/lib/src/engine/semantics/checkable.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@

import 'package:ui/ui.dart' as ui;

import '../dom.dart';
import 'semantics.dart';

/// The specific type of checkable control.
Expand Down Expand Up @@ -63,18 +62,18 @@ class Checkable extends PrimaryRoleManager {
if (semanticsObject.isFlagsDirty) {
switch (_kind) {
case _CheckableKind.checkbox:
semanticsObject.setAriaRole('checkbox');
setAriaRole('checkbox');
case _CheckableKind.radio:
semanticsObject.setAriaRole('radio');
setAriaRole('radio');
case _CheckableKind.toggle:
semanticsObject.setAriaRole('switch');
setAriaRole('switch');
}

/// Adding disabled and aria-disabled attribute to notify the assistive
/// technologies of disabled elements.
_updateDisabledAttribute();

semanticsObject.element.setAttribute(
setAttribute(
'aria-checked',
(semanticsObject.hasFlag(ui.SemanticsFlag.isChecked) ||
semanticsObject.hasFlag(ui.SemanticsFlag.isToggled))
Expand All @@ -92,17 +91,15 @@ class Checkable extends PrimaryRoleManager {

void _updateDisabledAttribute() {
if (semanticsObject.enabledState() == EnabledState.disabled) {
final DomElement element = semanticsObject.element;
element
..setAttribute('aria-disabled', 'true')
..setAttribute('disabled', 'true');
setAttribute('aria-disabled', 'true');
setAttribute('disabled', 'true');
} else {
_removeDisabledAttribute();
}
}

void _removeDisabledAttribute() {
final DomElement element = semanticsObject.element;
element..removeAttribute('aria-disabled')..removeAttribute('disabled');
removeAttribute('aria-disabled');
removeAttribute('disabled');
}
}
13 changes: 8 additions & 5 deletions lib/web_ui/lib/src/engine/semantics/dialog.dart
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ class Dialog extends PrimaryRoleManager {
}
return true;
}());
semanticsObject.element.setAttribute('aria-label', label ?? '');
semanticsObject.setAriaRole('dialog');
setAttribute('aria-label', label ?? '');
setAriaRole('dialog');
}
}

Expand All @@ -51,8 +51,8 @@ class Dialog extends PrimaryRoleManager {
return;
}

semanticsObject.setAriaRole('dialog');
semanticsObject.element.setAttribute(
setAriaRole('dialog');
setAttribute(
'aria-describedby',
routeName.semanticsObject.element.id,
);
Expand All @@ -61,7 +61,10 @@ class Dialog extends PrimaryRoleManager {

/// Supplies a description for the nearest ancestor [Dialog].
class RouteName extends RoleManager {
RouteName(SemanticsObject semanticsObject) : super(Role.routeName, semanticsObject);
RouteName(
SemanticsObject semanticsObject,
PrimaryRoleManager owner,
) : super(Role.routeName, semanticsObject, owner);

Dialog? _dialog;

Expand Down
6 changes: 3 additions & 3 deletions lib/web_ui/lib/src/engine/semantics/focusable.dart
Original file line number Diff line number Diff line change
Expand Up @@ -28,17 +28,17 @@ import 'semantics.dart';
///
/// * https://developer.mozilla.org/en-US/docs/Web/Accessibility/Keyboard-navigable_JavaScript_widgets
class Focusable extends RoleManager {
Focusable(SemanticsObject semanticsObject)
Focusable(SemanticsObject semanticsObject, PrimaryRoleManager owner)
: _focusManager = AccessibilityFocusManager(semanticsObject.owner),
super(Role.focusable, semanticsObject);
super(Role.focusable, semanticsObject, owner);

final AccessibilityFocusManager _focusManager;

@override
void update() {
if (semanticsObject.isFocusable) {
if (!_focusManager.isManaging) {
_focusManager.manage(semanticsObject.id, semanticsObject.element);
_focusManager.manage(semanticsObject.id, owner.element);
}
_focusManager.changeFocus(semanticsObject.hasFocus && (!semanticsObject.hasEnabledState || semanticsObject.isEnabled));
} else {
Expand Down
8 changes: 4 additions & 4 deletions lib/web_ui/lib/src/engine/semantics/image.dart
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,14 @@ class ImageRoleManager extends PrimaryRoleManager {
..height = '${semanticsObject.rect!.height}px';
}
_auxiliaryImageElement!.style.fontSize = '6px';
semanticsObject.element.append(_auxiliaryImageElement!);
append(_auxiliaryImageElement!);
}

_auxiliaryImageElement!.setAttribute('role', 'img');
_setLabel(_auxiliaryImageElement);
} else if (semanticsObject.isVisualOnly) {
semanticsObject.setAriaRole('img');
_setLabel(semanticsObject.element);
setAriaRole('img');
_setLabel(element);
_cleanUpAuxiliaryElement();
} else {
_cleanUpAuxiliaryElement();
Expand All @@ -78,7 +78,7 @@ class ImageRoleManager extends PrimaryRoleManager {
}

void _cleanupElement() {
semanticsObject.element.removeAttribute('aria-label');
removeAttribute('aria-label');
}

@override
Expand Down
2 changes: 1 addition & 1 deletion lib/web_ui/lib/src/engine/semantics/incrementable.dart
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ class Incrementable extends PrimaryRoleManager {
addRouteName();
addLabelAndValue();

semanticsObject.element.append(_element);
append(_element);
_element.type = 'range';
_element.setAttribute('role', 'slider');

Expand Down
10 changes: 4 additions & 6 deletions lib/web_ui/lib/src/engine/semantics/label_and_value.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import '../dom.dart';
import 'semantics.dart';

/// Renders [SemanticsObject.label] and/or [SemanticsObject.value] to the semantics DOM.
Expand All @@ -26,8 +25,8 @@ import 'semantics.dart';
/// This role manager does not manage images and text fields. See
/// [ImageRoleManager] and [TextField].
class LabelAndValue extends RoleManager {
LabelAndValue(SemanticsObject semanticsObject)
: super(Role.labelAndValue, semanticsObject);
LabelAndValue(SemanticsObject semanticsObject, PrimaryRoleManager owner)
: super(Role.labelAndValue, semanticsObject, owner);

@override
void update() {
Expand Down Expand Up @@ -62,12 +61,11 @@ class LabelAndValue extends RoleManager {
combinedValue.write(semanticsObject.value);
}

semanticsObject.element
.setAttribute('aria-label', combinedValue.toString());
owner.setAttribute('aria-label', combinedValue.toString());
}

void _cleanUpDom() {
semanticsObject.element.removeAttribute('aria-label');
owner.removeAttribute('aria-label');
}

@override
Expand Down
21 changes: 21 additions & 0 deletions lib/web_ui/lib/src/engine/semantics/link.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import '../dom.dart';
import '../semantics.dart';

/// Provides accessibility for links.
class Link extends PrimaryRoleManager {
Link(SemanticsObject semanticsObject) : super.withBasics(PrimaryRole.link, semanticsObject);

@override
DomElement createElement() {
final DomElement element = domDocument.createElement('a');
// TODO(chunhtai): Fill in the real link once the framework sends entire uri.
// https://github.com/flutter/flutter/issues/102535.
element.setAttribute('href', '#');
element.style.display = 'block';
return element;
}
}
4 changes: 2 additions & 2 deletions lib/web_ui/lib/src/engine/semantics/live_region.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ import 'semantics.dart';
/// label of the element. See [LabelAndValue]. If there is no label provided
/// no content will be read.
class LiveRegion extends RoleManager {
LiveRegion(SemanticsObject semanticsObject)
: super(Role.liveRegion, semanticsObject);
LiveRegion(SemanticsObject semanticsObject, PrimaryRoleManager owner)
: super(Role.liveRegion, semanticsObject, owner);

String? _lastAnnouncement;

Expand Down
5 changes: 2 additions & 3 deletions lib/web_ui/lib/src/engine/semantics/platform_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import '../dom.dart';
import '../platform_views/slots.dart';
import 'semantics.dart';

Expand Down Expand Up @@ -30,13 +29,13 @@ class PlatformViewRoleManager extends PrimaryRoleManager {

if (semanticsObject.isPlatformView) {
if (semanticsObject.isPlatformViewIdDirty) {
semanticsObject.element.setAttribute(
setAttribute(
'aria-owns',
getPlatformViewDomId(semanticsObject.platformViewId),
);
}
} else {
semanticsObject.element.removeAttribute('aria-owns');
removeAttribute('aria-owns');
}
}
}
16 changes: 7 additions & 9 deletions lib/web_ui/lib/src/engine/semantics/scrollable.dart
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ class Scrollable extends PrimaryRoleManager {
..transformOrigin = '0 0 0'
// Ignore pointer events since this is a dummy element.
..pointerEvents = 'none';
semanticsObject.element.append(_scrollOverflowElement);
append(_scrollOverflowElement);
}

/// Disables browser-driven scrolling in the presence of pointer events.
Expand Down Expand Up @@ -112,7 +112,7 @@ class Scrollable extends PrimaryRoleManager {
// This is effective only in Chrome. Safari does not implement this
// CSS property. In Safari the `PointerBinding` uses `preventDefault`
// to prevent browser scrolling.
semanticsObject.element.style.touchAction = 'none';
element.style.touchAction = 'none';
_gestureModeDidChange();

// Memoize the tear-off because Dart does not guarantee that two
Expand All @@ -126,17 +126,17 @@ class Scrollable extends PrimaryRoleManager {
_scrollListener = createDomEventListener((_) {
_recomputeScrollPosition();
});
semanticsObject.element.addEventListener('scroll', _scrollListener);
addEventListener('scroll', _scrollListener);
}
}

/// The value of "scrollTop" or "scrollLeft", depending on the scroll axis.
int get _domScrollPosition {
if (semanticsObject.isVerticalScrollContainer) {
return semanticsObject.element.scrollTop.toInt();
return element.scrollTop.toInt();
} else {
assert(semanticsObject.isHorizontalScrollContainer);
return semanticsObject.element.scrollLeft.toInt();
return element.scrollLeft.toInt();
}
}

Expand All @@ -153,7 +153,6 @@ class Scrollable extends PrimaryRoleManager {
void _neutralizeDomScrollPosition() {
// This value is arbitrary.
const int canonicalNeutralScrollPosition = 10;
final DomElement element = semanticsObject.element;
final ui.Rect? rect = semanticsObject.rect;
if (rect == null) {
printWarning('Warning! the rect attribute of semanticsObject is null');
Expand Down Expand Up @@ -197,7 +196,6 @@ class Scrollable extends PrimaryRoleManager {
}

void _gestureModeDidChange() {
final DomElement element = semanticsObject.element;
switch (semanticsObject.owner.gestureMode) {
case GestureMode.browserGestures:
// overflow:scroll will cause the browser report "scroll" events when
Expand Down Expand Up @@ -227,13 +225,13 @@ class Scrollable extends PrimaryRoleManager {
@override
void dispose() {
super.dispose();
final DomCSSStyleDeclaration style = semanticsObject.element.style;
final DomCSSStyleDeclaration style = element.style;
assert(_gestureModeListener != null);
style.removeProperty('overflowY');
style.removeProperty('overflowX');
style.removeProperty('touch-action');
if (_scrollListener != null) {
semanticsObject.element.removeEventListener('scroll', _scrollListener);
removeEventListener('scroll', _scrollListener);
}
semanticsObject.owner.removeGestureModeListener(_gestureModeListener);
_gestureModeListener = null;
Expand Down
Loading

0 comments on commit b27e1b3

Please sign in to comment.