feat: add keyboard shortcuts and Tab link traversal on pointer hosts
This commit is contained in:
parent
fa7aa5557c
commit
56db479df0
6 changed files with 798 additions and 272 deletions
|
|
@ -1,6 +1,7 @@
|
|||
import 'dart:io' show File;
|
||||
|
||||
import 'package:flutter/foundation.dart' show TargetPlatform, debugDefaultTargetPlatformOverride;
|
||||
import 'package:flutter/services.dart' show LogicalKeyboardKey;
|
||||
import 'package:drift/native.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart' show RenderParagraph;
|
||||
|
|
@ -529,6 +530,81 @@ void main() {
|
|||
debugDefaultTargetPlatformOverride = null;
|
||||
});
|
||||
|
||||
testWidgets('the address bar stays on screen on pointer hosts', (tester) async {
|
||||
debugDefaultTargetPlatformOverride = TargetPlatform.linux;
|
||||
await pumpApp(tester);
|
||||
|
||||
// A page long enough to scroll.
|
||||
final tall = pageSource.replaceFirst(
|
||||
'</body>',
|
||||
'<p>Padding paragraph to make the page scroll.</p>' * 80 + '</body>');
|
||||
final page = parseSource(tall, 'https://example.com/index.html');
|
||||
deps.browser.currentPage.value = page;
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Scrolling down leaves the bar where it is: a pointer host has keys to
|
||||
// reach it with, so it never hides.
|
||||
await tester.drag(find.byType(ListView), const Offset(0, -300));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(AddressBar), findsOneWidget);
|
||||
|
||||
debugDefaultTargetPlatformOverride = null;
|
||||
});
|
||||
|
||||
testWidgets('the keyboard drives the reader on pointer hosts', (tester) async {
|
||||
final stubDeps = AppDependencies(
|
||||
database: MewsDatabase.test(NativeDatabase.memory()),
|
||||
fetch: FakeFetch({
|
||||
'https://example.com/a.html':
|
||||
_page('Page A', '<h1>Page A</h1><p>The first pages content.</p>'),
|
||||
'https://example.com/b.html':
|
||||
_page('Page B', '<h1>Page B</h1><p>The second pages content.</p>'),
|
||||
}),
|
||||
strings: strings,
|
||||
);
|
||||
debugDefaultTargetPlatformOverride = TargetPlatform.linux;
|
||||
|
||||
await tester.pumpWidget(MewsApp(deps: stubDeps, strings: strings));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await stubDeps.browser.open('https://example.com/a.html');
|
||||
await stubDeps.browser.open('https://example.com/b.html');
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Alt+Left goes back through the walk, Alt+Right forward again.
|
||||
await tester.sendKeyDownEvent(LogicalKeyboardKey.altLeft);
|
||||
await tester.sendKeyDownEvent(LogicalKeyboardKey.arrowLeft);
|
||||
await tester.pumpAndSettle();
|
||||
expect(stubDeps.browser.currentPage.value?.url, 'https://example.com/a.html');
|
||||
await tester.sendKeyUpEvent(LogicalKeyboardKey.arrowLeft);
|
||||
await tester.sendKeyDownEvent(LogicalKeyboardKey.arrowRight);
|
||||
await tester.pumpAndSettle();
|
||||
expect(stubDeps.browser.currentPage.value?.url, 'https://example.com/b.html');
|
||||
await tester.sendKeyUpEvent(LogicalKeyboardKey.altLeft);
|
||||
await tester.sendKeyUpEvent(LogicalKeyboardKey.arrowRight);
|
||||
|
||||
// The Mac spellings walk the same way.
|
||||
await tester.sendKeyDownEvent(LogicalKeyboardKey.metaLeft);
|
||||
await tester.sendKeyDownEvent(LogicalKeyboardKey.arrowLeft);
|
||||
await tester.pumpAndSettle();
|
||||
expect(stubDeps.browser.currentPage.value?.url, 'https://example.com/a.html');
|
||||
await tester.sendKeyDownEvent(LogicalKeyboardKey.bracketRight);
|
||||
await tester.pumpAndSettle();
|
||||
expect(stubDeps.browser.currentPage.value?.url, 'https://example.com/b.html');
|
||||
await tester.sendKeyUpEvent(LogicalKeyboardKey.metaLeft);
|
||||
await tester.sendKeyUpEvent(LogicalKeyboardKey.bracketRight);
|
||||
|
||||
// Ctrl+L hands the focus to the address field, with its text selected.
|
||||
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
|
||||
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyL);
|
||||
await tester.pump();
|
||||
final field = tester.widget<TextField>(find.byType(TextField).first);
|
||||
expect(field.focusNode!.hasFocus, isTrue);
|
||||
|
||||
// The invariant check runs before test tear-downs, so reset by hand.
|
||||
debugDefaultTargetPlatformOverride = null;
|
||||
});
|
||||
|
||||
testWidgets('bookmarks can be searched and removed from their own page',
|
||||
(tester) async {
|
||||
await pumpApp(tester);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import 'dart:convert' show base64;
|
||||
|
||||
import 'package:flutter/services.dart' show LogicalKeyboardKey;
|
||||
import 'dart:io' show File;
|
||||
import 'dart:typed_data';
|
||||
|
||||
|
|
@ -80,6 +82,135 @@ void main() {
|
|||
expect(opened, 'https://example.com/next.html');
|
||||
});
|
||||
|
||||
testWidgets('Tab walks the links on a page', (tester) async {
|
||||
final page = MewsPage(
|
||||
url: 'https://example.com/index.html',
|
||||
lang: 'en',
|
||||
direction: TextDirection.ltr,
|
||||
variant: MewsVariant.plain,
|
||||
specVersion: '0.1',
|
||||
entries: const [],
|
||||
body: const [
|
||||
ParagraphBlock([MewsText('Read on for more.')]),
|
||||
ParagraphBlock([
|
||||
MewsLink('https://example.com/first.html', [MewsText('the first page')]),
|
||||
]),
|
||||
ParagraphBlock([
|
||||
MewsLink('https://example.com/second.html', [MewsText('the second page')]),
|
||||
]),
|
||||
],
|
||||
);
|
||||
|
||||
final opened = <String>[];
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: StringsScope(
|
||||
strings: await _strings(),
|
||||
child: MewsPageView(
|
||||
page: page,
|
||||
palette: paletteFor(
|
||||
pageVariant: MewsVariant.plain, readerVariant: 'site', dark: false),
|
||||
fontFamily: 'Atkinson Hyperlegible',
|
||||
fontScale: 1.0,
|
||||
visitedUrls: const {},
|
||||
onLinkTap: opened.add,
|
||||
onFormSubmit: (_, _) {},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Pressing the page puts the keyboard focus on it.
|
||||
final body = tester.getRect(find.text('Read on for more.'));
|
||||
await tester.tapAt(body.center);
|
||||
await tester.pump();
|
||||
|
||||
// Tab highlights each link in reading order, and Enter opens the one
|
||||
// highlighted.
|
||||
Future<void> press(LogicalKeyboardKey key) async {
|
||||
await tester.sendKeyDownEvent(key);
|
||||
await tester.pump();
|
||||
await tester.sendKeyUpEvent(key);
|
||||
await tester.pump();
|
||||
}
|
||||
|
||||
await press(LogicalKeyboardKey.tab);
|
||||
await press(LogicalKeyboardKey.enter);
|
||||
expect(opened, ['https://example.com/first.html']);
|
||||
|
||||
await press(LogicalKeyboardKey.tab);
|
||||
await press(LogicalKeyboardKey.enter);
|
||||
expect(opened, ['https://example.com/first.html', 'https://example.com/second.html']);
|
||||
|
||||
// Shift+Tab walks back, and Escape puts the highlight away, so Enter
|
||||
// after it opens nothing.
|
||||
await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft);
|
||||
await tester.sendKeyDownEvent(LogicalKeyboardKey.tab);
|
||||
await tester.pump();
|
||||
await tester.sendKeyUpEvent(LogicalKeyboardKey.tab);
|
||||
await tester.sendKeyUpEvent(LogicalKeyboardKey.shiftLeft);
|
||||
await press(LogicalKeyboardKey.enter);
|
||||
expect(opened, ['https://example.com/first.html', 'https://example.com/second.html',
|
||||
'https://example.com/first.html']);
|
||||
|
||||
await press(LogicalKeyboardKey.escape);
|
||||
await press(LogicalKeyboardKey.enter);
|
||||
expect(opened, hasLength(3));
|
||||
});
|
||||
|
||||
testWidgets('the scroll follows the Tab walk', (tester) async {
|
||||
final page = MewsPage(
|
||||
url: 'https://example.com/index.html',
|
||||
lang: 'en',
|
||||
direction: TextDirection.ltr,
|
||||
variant: MewsVariant.plain,
|
||||
specVersion: '0.1',
|
||||
entries: const [],
|
||||
body: [
|
||||
// Enough prose to push the links below the fold.
|
||||
for (var i = 0; i < 40; i++)
|
||||
ParagraphBlock([MewsText('Paragraph $i of the tall page.')]),
|
||||
const ParagraphBlock([
|
||||
MewsLink('https://example.com/deep.html', [MewsText('a link down below')]),
|
||||
]),
|
||||
],
|
||||
);
|
||||
|
||||
final scroll = ScrollController();
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: StringsScope(
|
||||
strings: await _strings(),
|
||||
child: MewsPageView(
|
||||
page: page,
|
||||
palette: paletteFor(
|
||||
pageVariant: MewsVariant.plain, readerVariant: 'site', dark: false),
|
||||
fontFamily: 'Atkinson Hyperlegible',
|
||||
fontScale: 1.0,
|
||||
visitedUrls: const {},
|
||||
onLinkTap: (_) {},
|
||||
onFormSubmit: (_, _) {},
|
||||
scrollController: scroll,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(scroll.offset, 0);
|
||||
|
||||
// Pressing the page puts the keyboard focus on it, and the walk carries
|
||||
// the viewport along to whichever link is highlighted.
|
||||
final body = tester.getRect(find.text('Paragraph 0 of the tall page.'));
|
||||
await tester.tapAt(body.center);
|
||||
await tester.pump();
|
||||
await tester.sendKeyDownEvent(LogicalKeyboardKey.tab);
|
||||
await tester.pump();
|
||||
await tester.sendKeyUpEvent(LogicalKeyboardKey.tab);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(scroll.offset, greaterThan(0));
|
||||
});
|
||||
|
||||
testWidgets('an image on a capsule arrives over the protocol', (tester) async {
|
||||
// A 1x1 PNG, small enough to spell out.
|
||||
final png = Uint8List.fromList(base64.decode(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue