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

feat: add keyboard navigation to svelte inspector and improve a11y #438

Merged
merged 7 commits into from Sep 17, 2022
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/friendly-avocados-tie.md
@@ -0,0 +1,5 @@
---
'@sveltejs/vite-plugin-svelte': minor
---

Add next/prev sibling navigation and improve a11y
14 changes: 10 additions & 4 deletions docs/config.md
Expand Up @@ -306,12 +306,18 @@ export default {
toggleKeyCombo?: string;

/**
* define keys to drill from the active element (up selects parent, down selects child).
* @default {up: 'ArrowUp',down: 'ArrowDown'}
* define keys to select elements with via keyboard
* @default {parent: 'ArrowUp', child: 'ArrowDown', next: 'ArrowRight', prev: 'ArrowLeft' }
*
* This is useful when components wrap another one without providing any hoverable area between them
* improves accessibility and also helps when you want to select elements that do not have a hoverable surface area
* due to tight wrapping
*
* parent: select closest parent
* child: select first child (or grandchild)
* next: next sibling (or parent if no next sibling exists)
* prev: previous sibling (or parent if no prev sibling exists)
*/
drillKeys?: { up: string; down: string };
navKeys?: { parent: string; child: string; next: string; prev: string };

/**
* inspector is automatically disabled when releasing toggleKeyCombo after holding it for a longpress
Expand Down
94 changes: 73 additions & 21 deletions packages/vite-plugin-svelte/src/ui/inspector/Inspector.svelte
Expand Up @@ -4,7 +4,7 @@
// eslint-disable-next-line node/no-missing-import
import options from 'virtual:svelte-inspector-options';
const toggle_combo = options.toggleKeyCombo?.toLowerCase().split('-');

const nav_keys = Object.values(options.navKeys).map((k) => k.toLowerCase());
let enabled = false;

const icon = `data:image/svg+xml;base64,${btoa(
Expand Down Expand Up @@ -37,26 +37,63 @@
y = event.y;
}

function find_parent_with_meta(el) {
while (el) {
if (has_meta(el)) {
function find_selectable_parent(el) {
do {
el = el.parentNode;
if (is_selectable(el)) {
return el;
}
el = el.parentNode;
}
} while (el);
}

function find_child_with_meta(el) {
return [...el.querySelectorAll('*')].find(has_meta);
function find_selectable_child(el) {
return [...el.querySelectorAll('*')].find(is_selectable);
}

function find_selectable_sibling(el, prev = false) {
do {
el = prev ? el.previousElementSibling : el.nextElementSibling;
if (is_selectable(el)) {
return el;
}
} while (el);
}

function find_selectable_for_nav(key) {
const el = active_el;
if (!el) {
return find_selectable_child(document?.body);
}
switch (key) {
case options.navKeys.parent:
return find_selectable_parent(el);
case options.navKeys.child:
return find_selectable_child(el);
case options.navKeys.next:
return find_selectable_sibling(el) || find_selectable_parent(el);
case options.navKeys.prev:
return find_selectable_sibling(el, true) || find_selectable_parent(el);
default:
return;
}
}

function has_meta(el) {
const file = el.__svelte_meta?.loc?.file;
return el !== toggle_el && file && !file.includes('node_modules/');
function is_selectable(el) {
if (el === toggle_el) {
return false; // toggle is our own
}
const file = el?.__svelte_meta?.loc?.file;
if (!file || file.includes('node_modules/')) {
return false; // no file or 3rd party
}
if (['svelte-announcer', 'svelte-inspector-announcer'].includes(el.getAttribute('id'))) {
return false; // ignore some elements by id that would be selectable from keyboard nav otherwise
}
return true;
}

function mouseover(event) {
const el = find_parent_with_meta(event.target);
const el = find_selectable_parent(event.target);
activate(el);
}

Expand Down Expand Up @@ -104,6 +141,10 @@
return toggle_combo?.every((key) => is_key_active(key, event));
}

function is_nav(event) {
return nav_keys?.some((key) => is_key_active(key, event));
}

function is_holding() {
return enabled_ts && Date.now() - enabled_ts > 250;
}
Expand All @@ -124,14 +165,8 @@
if (options.holdMode && enabled) {
enabled_ts = Date.now();
}
} else if (event.key === options.drillKeys.up && active_el) {
const el = find_parent_with_meta(active_el.parentNode);
if (el) {
activate(el);
stop(event);
}
} else if (event.key === options.drillKeys.down && active_el) {
const el = find_child_with_meta(active_el);
} else if (is_nav(event)) {
const el = find_selectable_for_nav(event.key);
if (el) {
activate(el);
stop(event);
Expand Down Expand Up @@ -222,9 +257,11 @@
.join('')}`}
on:click={() => toggle()}
bind:this={toggle_el}
aria-label={`${enabled ? 'disable' : 'enable'} svelte-inspector`}
dominikg marked this conversation as resolved.
Show resolved Hide resolved
/>
{/if}
{#if enabled && file_loc}
{#if enabled && active_el && file_loc}
{@const loc = active_el.__svelte_meta.loc}
<div
class="svelte-inspector-overlay"
style:left="{Math.min(x + 3, document.body.clientWidth - w - 10)}px"
Expand All @@ -233,6 +270,9 @@
>
&lt;{active_el.tagName.toLowerCase()}&gt;&nbsp;{file_loc}
</div>
<div id="svelte-inspector-announcer" aria-live="assertive" aria-atomic="true">
{active_el.tagName.toLowerCase()} in file {loc.file} on line {loc.line} column {loc.column}
</div>
{/if}

<style>
Expand Down Expand Up @@ -264,6 +304,18 @@
cursor: pointer;
}

#svelte-inspector-announcer {
position: absolute;
left: 0px;
top: 0px;
clip: rect(0px, 0px, 0px, 0px);
clip-path: inset(50%);
overflow: hidden;
white-space: nowrap;
width: 1px;
height: 1px;
}

.svelte-inspector-toggle:not(.enabled) {
filter: grayscale(1);
}
Expand Down
2 changes: 1 addition & 1 deletion packages/vite-plugin-svelte/src/ui/inspector/plugin.ts
Expand Up @@ -8,7 +8,7 @@ import { idToFile } from './utils';

const defaultInspectorOptions: InspectorOptions = {
toggleKeyCombo: process.platform === 'win32' ? 'control-shift' : 'meta-shift',
drillKeys: { up: 'ArrowUp', down: 'ArrowDown' },
navKeys: { parent: 'ArrowUp', child: 'ArrowDown', next: 'ArrowRight', prev: 'ArrowLeft' },
holdMode: false,
showToggleButton: 'active',
toggleButtonPos: 'top-right',
Expand Down
14 changes: 10 additions & 4 deletions packages/vite-plugin-svelte/src/utils/options.ts
Expand Up @@ -644,12 +644,18 @@ export interface InspectorOptions {
toggleKeyCombo?: string;

/**
* define keys to drill from the active element (up selects parent, down selects child).
* @default {up: 'ArrowUp',down: 'ArrowDown'}
* define keys to select elements with via keyboard
* @default {parent: 'ArrowUp', child: 'ArrowDown', next: 'ArrowRight', prev: 'ArrowLeft' }
*
* This is useful when components wrap another one without providing any hoverable area between them
* improves accessibility and also helps when you want to select elements that do not have a hoverable surface area
* due to tight wrapping
*
* parent: select closest parent
* child: select first child (or grandchild)
* next: next sibling (or parent if no next sibling exists)
* prev: previous sibling (or parent if no prev sibling exists)
*/
drillKeys?: { up: string; down: string };
navKeys?: { parent: string; child: string; next: string; prev: string };

/**
* inspector is automatically disabled when releasing toggleKeyCombo after holding it for a longpress
Expand Down