Skip to content

Commit 78cab2d

Browse files
authored
Migrate screenshot carousel + focusability to package:web. (dart-lang#9326)
1 parent 5b4f89d commit 78cab2d

4 files changed

Lines changed: 141 additions & 46 deletions

File tree

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
// Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
// TODO: migrate to package:web
6+
// ignore: deprecated_member_use
7+
import 'dart:html';
8+
9+
/// These selectors provide the elements that are focusable through tab or
10+
/// keyboard navigation.
11+
const _focusableSelectors = <String>[
12+
'a',
13+
'audio',
14+
'button',
15+
'canvas',
16+
'details',
17+
'iframe',
18+
'input',
19+
'select',
20+
'summary',
21+
'textarea',
22+
'video',
23+
'[accesskey]',
24+
'[contenteditable]',
25+
'[tabindex]',
26+
];
27+
28+
/// Disables all focusable elements, except for the elements inside
29+
/// [allowedComponents]. Returns a [Function] that will restore the
30+
/// original focusability state of the disabled elements.
31+
void Function() disableAllFocusability({
32+
required List<Element> allowedComponents,
33+
}) {
34+
final focusableElements = document.body!.querySelectorAll(
35+
_focusableSelectors.join(', '),
36+
);
37+
final restoreFocusabilityFns = <void Function()>[];
38+
for (final e in focusableElements) {
39+
if (allowedComponents.any((content) => _isInsideContent(e, content))) {
40+
continue;
41+
}
42+
restoreFocusabilityFns.add(_disableFocusability(e));
43+
}
44+
return () {
45+
for (final fn in restoreFocusabilityFns) {
46+
fn();
47+
}
48+
};
49+
}
50+
51+
/// Update [e] to disable focusability and return a [Function] that can be
52+
/// called to revert its original state.
53+
void Function() _disableFocusability(Element e) {
54+
final isLink = e.tagName.toLowerCase() == 'a';
55+
final hasTabindex = e.hasAttribute('tabindex');
56+
final attributesToSet = <String, String>{
57+
if (isLink || hasTabindex) 'tabindex': '-1',
58+
if (!isLink) 'disabled': 'disabled',
59+
'aria-hidden': 'true',
60+
};
61+
final attributesToRestore = attributesToSet.map(
62+
(key, _) => MapEntry(key, e.getAttribute(key)),
63+
);
64+
for (final a in attributesToSet.entries) {
65+
e.setAttribute(a.key, a.value);
66+
}
67+
return () {
68+
for (final a in attributesToRestore.entries) {
69+
final value = a.value;
70+
if (value == null) {
71+
e.removeAttribute(a.key);
72+
} else {
73+
e.setAttribute(a.key, value);
74+
}
75+
}
76+
};
77+
}
78+
79+
bool _isInsideContent(Element e, Element content) {
80+
// check if [e] is under [content].
81+
Element? p = e;
82+
while (p != null) {
83+
if (p == content) {
84+
return true;
85+
}
86+
p = p.parent;
87+
}
88+
return false;
89+
}

pkg/web_app/lib/src/_dom_helper.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import 'dart:html';
99

1010
import 'package:mdc_web/mdc_web.dart' show MDCDialog;
1111

12-
import '_focusability.dart';
12+
import '_dart_html_focusability.dart';
1313
import 'deferred/markdown.dart' deferred as md;
1414

1515
/// Displays a message via the modal window.

pkg/web_app/lib/src/_focusability.dart

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,8 @@
22
// for details. All rights reserved. Use of this source code is governed by a
33
// BSD-style license that can be found in the LICENSE file.
44

5-
// TODO: migrate to package:web
6-
// ignore: deprecated_member_use
7-
import 'dart:html';
5+
import 'package:web/web.dart';
6+
import 'package:web_app/src/web_util.dart';
87

98
/// These selectors provide the elements that are focusable through tab or
109
/// keyboard navigation.
@@ -35,7 +34,7 @@ void Function() disableAllFocusability({
3534
_focusableSelectors.join(', '),
3635
);
3736
final restoreFocusabilityFns = <void Function()>[];
38-
for (final e in focusableElements) {
37+
for (final e in focusableElements.toElementList()) {
3938
if (allowedComponents.any((content) => _isInsideContent(e, content))) {
4039
continue;
4140
}
@@ -83,7 +82,7 @@ bool _isInsideContent(Element e, Element content) {
8382
if (p == content) {
8483
return true;
8584
}
86-
p = p.parent;
85+
p = p.parentElement;
8786
}
8887
return false;
8988
}

pkg/web_app/lib/src/screenshot_carousel.dart

Lines changed: 47 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@
33
// BSD-style license that can be found in the LICENSE file.
44

55
import 'dart:convert';
6-
// TODO: migrate to package:web
7-
// ignore: deprecated_member_use
8-
import 'dart:html';
6+
import 'dart:js_interop';
7+
8+
import 'package:web/web.dart';
9+
import 'package:web_app/src/web_util.dart';
910

1011
import '_focusability.dart';
1112

@@ -14,46 +15,47 @@ void setupScreenshotCarousel() {
1415
}
1516

1617
void _setEventForScreenshot() {
17-
final carousel = document.getElementById('-screenshot-carousel');
18+
final carousel =
19+
document.getElementById('-screenshot-carousel') as HTMLElement?;
1820
if (carousel == null) {
1921
return;
2022
}
2123
final thumbnails = document.querySelectorAll('div[data-thumbnail]');
2224
final imageContainer = document.getElementById('-image-container')!;
23-
final prev = document.getElementById('-carousel-prev')!;
24-
final next = document.getElementById('-carousel-next')!;
25+
final prev = document.getElementById('-carousel-prev') as HTMLElement;
26+
final next = document.getElementById('-carousel-next') as HTMLElement;
2527
final description =
26-
document.getElementById('-screenshot-description') as ParagraphElement;
28+
document.getElementById('-screenshot-description') as HTMLElement;
2729
final existingImageElement =
28-
document.getElementById('-carousel-image') as ImageElement?;
29-
final ImageElement imageElement;
30+
document.getElementById('-carousel-image') as HTMLElement?;
31+
final HTMLElement imageElement;
3032
if (existingImageElement != null) {
3133
imageElement = existingImageElement;
3234
} else {
33-
imageElement = ImageElement();
35+
imageElement = document.createElement('img') as HTMLElement;
3436
imageElement.id = '-carousel-image';
3537
imageContainer.append(imageElement);
3638
imageElement.className = 'carousel-image';
3739
}
3840

39-
Element? focusedTriggerSourceElement;
41+
HTMLElement? focusedTriggerSourceElement;
4042
void Function()? restoreFocusabilityFn;
4143
var images = <String>[];
4244
var descriptions = <String>[];
4345

44-
void hideElement(Element element) {
46+
void hideElement(HTMLElement element) {
4547
element.style.display = 'none';
4648
}
4749

48-
void showElement(Element element) {
50+
void showElement(HTMLElement element) {
4951
element.style.display = 'flex';
5052
}
5153

5254
void showImage(int index) {
5355
hideElement(description);
5456
hideElement(imageElement);
55-
imageElement.src = images[index];
56-
description.text = descriptions[index];
57+
imageElement.setAttribute('src', images[index]);
58+
description.innerText = descriptions[index];
5759

5860
if (index == images.length - 1) {
5961
hideElement(next);
@@ -75,23 +77,25 @@ void _setEventForScreenshot() {
7577
}
7678

7779
var screenshotIndex = 0;
78-
for (final thumbnail in thumbnails) {
80+
for (final thumbnail in thumbnails.toElementList()) {
7981
void setup() {
8082
restoreFocusabilityFn = disableAllFocusability(
8183
allowedComponents: [prev, next],
8284
);
83-
focusedTriggerSourceElement = thumbnail;
85+
focusedTriggerSourceElement = thumbnail as HTMLElement;
8486
showElement(carousel);
85-
document.body!.classes
87+
document.body!.classList
8688
..remove('overflow-auto')
8789
..add('overflow-hidden');
88-
images = thumbnail.dataset['thumbnail']!.split(',');
89-
final raw = jsonDecode(thumbnail.dataset['thumbnail-descriptions-json']!);
90+
images = thumbnail.getAttribute('data-thumbnail')!.split(',');
91+
final raw = jsonDecode(
92+
thumbnail.getAttribute('data-thumbnail-descriptions-json')!,
93+
);
9094
descriptions = (raw as List).cast<String>();
9195
showImage(screenshotIndex);
9296
}
9397

94-
thumbnail.parent!.onClick.listen((event) {
98+
thumbnail.parentElement!.onClick.listen((event) {
9599
event.stopPropagation();
96100
setup();
97101
});
@@ -108,7 +112,7 @@ void _setEventForScreenshot() {
108112
hideElement(next);
109113
hideElement(prev);
110114
hideElement(description);
111-
document.body!.classes
115+
document.body!.classList
112116
..remove('overflow-hidden')
113117
..add('overflow-auto');
114118
screenshotIndex = 0;
@@ -158,26 +162,29 @@ void _setEventForScreenshot() {
158162
closeCarousel();
159163
});
160164

161-
document.onKeyDown.listen((event) {
162-
if (carousel.style.display == 'none') {
163-
return;
164-
}
165+
document.addEventListener(
166+
'keydown',
167+
(KeyboardEvent event) {
168+
if (carousel.style.display == 'none') {
169+
return;
170+
}
165171

166-
if (event.key == 'Escape') {
167-
event.stopPropagation();
168-
closeCarousel();
169-
}
170-
if (event.key == 'ArrowLeft') {
171-
if (screenshotIndex > 0) {
172+
if (event.key == 'Escape') {
172173
event.stopPropagation();
173-
gotoPrev();
174+
closeCarousel();
174175
}
175-
}
176-
if (event.key == 'ArrowRight') {
177-
if (screenshotIndex < images.length - 1) {
178-
event.stopPropagation();
179-
gotoNext();
176+
if (event.key == 'ArrowLeft') {
177+
if (screenshotIndex > 0) {
178+
event.stopPropagation();
179+
gotoPrev();
180+
}
180181
}
181-
}
182-
});
182+
if (event.key == 'ArrowRight') {
183+
if (screenshotIndex < images.length - 1) {
184+
event.stopPropagation();
185+
gotoNext();
186+
}
187+
}
188+
}.toJS,
189+
);
183190
}

0 commit comments

Comments
 (0)