summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHiDeoo2023-07-11 12:26:27 +0200
committerGitHub2023-07-11 12:26:27 +0200
commitfb15a9b65252ac5fa32304096fbdb49ecdd6009b (patch)
tree14530df00e6656f45b7e7e5c25ac5b583b51ccb4
parentdc42569bddfae2c48ea60c0dd5cc70643a129a68 (diff)
downloadIT.starlight-fb15a9b65252ac5fa32304096fbdb49ecdd6009b.tar.gz
IT.starlight-fb15a9b65252ac5fa32304096fbdb49ecdd6009b.tar.bz2
IT.starlight-fb15a9b65252ac5fa32304096fbdb49ecdd6009b.zip
Improve `<Tabs>` component keyboard interactions (#297)
Co-authored-by: Chris Swithinbank <swithinbank@gmail.com>
-rw-r--r--.changeset/curvy-points-juggle.md5
-rw-r--r--packages/starlight/package.json1
-rw-r--r--packages/starlight/user-components/Tabs.astro19
-rw-r--r--packages/starlight/user-components/rehype-tabs.ts30
-rw-r--r--pnpm-lock.yaml47
5 files changed, 87 insertions, 15 deletions
diff --git a/.changeset/curvy-points-juggle.md b/.changeset/curvy-points-juggle.md
new file mode 100644
index 00000000..9829d1ff
--- /dev/null
+++ b/.changeset/curvy-points-juggle.md
@@ -0,0 +1,5 @@
+---
+"@astrojs/starlight": minor
+---
+
+Improve `<Tabs>` component keyboard interactions
diff --git a/packages/starlight/package.json b/packages/starlight/package.json
index 5027131d..e41d7d6c 100644
--- a/packages/starlight/package.json
+++ b/packages/starlight/package.json
@@ -41,6 +41,7 @@
"@types/mdast": "^3.0.11",
"bcp-47": "^2.1.0",
"execa": "^7.1.1",
+ "hast-util-select": "^5.0.5",
"hastscript": "^7.2.0",
"pagefind": "^1.0.0-alpha.5",
"rehype": "^12.0.1",
diff --git a/packages/starlight/user-components/Tabs.astro b/packages/starlight/user-components/Tabs.astro
index b0a2c5a5..8013a35a 100644
--- a/packages/starlight/user-components/Tabs.astro
+++ b/packages/starlight/user-components/Tabs.astro
@@ -98,23 +98,20 @@ const { html, panels } = processPanels(panelHtml);
const index = this.tabs.indexOf(e.currentTarget as any);
// Work out which key the user is pressing and
// Calculate the new tab's index where appropriate
- const dir =
+ const nextIndex =
e.key === 'ArrowLeft'
? index - 1
: e.key === 'ArrowRight'
? index + 1
- : e.key === 'ArrowDown'
- ? 'down'
+ : e.key === 'Home'
+ ? 0
+ : e.key === 'End'
+ ? this.tabs.length - 1
: null;
- if (dir === null) return;
- // If the down key is pressed, move focus to the open panel,
- // otherwise switch to the adjacent tab
- if (dir === 'down') {
+ if (nextIndex === null) return;
+ if (this.tabs[nextIndex]) {
e.preventDefault();
- this.panels[i]?.focus();
- } else if (this.tabs[dir]) {
- e.preventDefault();
- this.switchTab(this.tabs[dir], dir);
+ this.switchTab(this.tabs[nextIndex], nextIndex);
}
});
});
diff --git a/packages/starlight/user-components/rehype-tabs.ts b/packages/starlight/user-components/rehype-tabs.ts
index e3421856..dc69b9e4 100644
--- a/packages/starlight/user-components/rehype-tabs.ts
+++ b/packages/starlight/user-components/rehype-tabs.ts
@@ -1,3 +1,4 @@
+import { select } from 'hast-util-select';
import { rehype } from 'rehype';
import { CONTINUE, SKIP, visit } from 'unist-util-visit';
@@ -15,6 +16,26 @@ declare module 'vfile' {
export const TabItemTagname = 'starlight-tab-item';
+// https://github.com/adobe/react-spectrum/blob/99ca82e87ba2d7fdd54f5b49326fd242320b4b51/packages/%40react-aria/focus/src/FocusScope.tsx#L256-L275
+const focusableElementSelectors = [
+ 'input:not([disabled]):not([type=hidden])',
+ 'select:not([disabled])',
+ 'textarea:not([disabled])',
+ 'button:not([disabled])',
+ 'a[href]',
+ 'area[href]',
+ 'summary',
+ 'iframe',
+ 'object',
+ 'embed',
+ 'audio[controls]',
+ 'video[controls]',
+ '[contenteditable]',
+ '[tabindex]:not([disabled])',
+]
+ .map((selector) => `${selector}:not([hidden]):not([tabindex="-1"])`)
+ .join(',');
+
let count = 0;
const getIDs = () => {
const id = count++;
@@ -51,7 +72,14 @@ const tabsProcessor = rehype()
node.properties.id = ids.panelId;
node.properties['aria-labelledby'] = ids.tabId;
node.properties.role = 'tabpanel';
- node.properties.tabindex = -1;
+
+ const focusableChild = select(focusableElementSelectors, node);
+ // If the panel does not contain any focusable elements, include it in
+ // the tab sequence of the page.
+ if (!focusableChild) {
+ node.properties.tabindex = 0;
+ }
+
// Hide all panels except the first
// TODO: make initially visible tab configurable
if (isFirst) {
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index c5cd1e4a..e4cb870e 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -116,6 +116,9 @@ importers:
execa:
specifier: ^7.1.1
version: 7.1.1
+ hast-util-select:
+ specifier: ^5.0.5
+ version: 5.0.5
hastscript:
specifier: ^7.2.0
version: 7.2.0
@@ -1578,6 +1581,10 @@ packages:
/base64-js@1.5.1:
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
+ /bcp-47-match@2.0.3:
+ resolution: {integrity: sha512-JtTezzbAibu8G0R9op9zb3vcWZd9JF6M0xOYGPn0fNCd7wOpRB1mU2mH9T8gaBGbAAyIIVgB2G7xG0GP98zMAQ==}
+ dev: false
+
/bcp-47@2.1.0:
resolution: {integrity: sha512-9IIS3UPrvIa1Ej+lVDdDwO7zLehjqsaByECw0bu2RRGP73jALm6FYbzI5gWbgHLvNdkvfXB5YrSbocZdOS0c0w==}
dependencies:
@@ -1627,7 +1634,6 @@ packages:
/boolbase@1.0.0:
resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==}
- dev: true
/boxen@6.2.1:
resolution: {integrity: sha512-H4PEsJXfFI/Pt8sjDWbHlQPx4zL/bvSQjcilJmaulGt5mLDorHOHpmdXAJcBcmru7PhYSp/cDMWRko4ZUMFkSw==}
@@ -1944,6 +1950,10 @@ packages:
nth-check: 2.1.1
dev: true
+ /css-selector-parser@1.4.1:
+ resolution: {integrity: sha512-HYPSb7y/Z7BNDCOrakL4raGO2zltZkbeXyAd6Tg9obzix6QhzxCotdBl6VT0Dv4vZfJGVz3WL/xaEI9Ly3ul0g==}
+ dev: false
+
/css-what@6.1.0:
resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==}
engines: {node: '>= 6'}
@@ -2080,6 +2090,11 @@ packages:
path-type: 4.0.0
dev: true
+ /direction@2.0.1:
+ resolution: {integrity: sha512-9S6m9Sukh1cZNknO1CWAr2QAWsbKLafQiyM5gZ7VgXHeuaoUwffKN4q6NC4A/Mf9iiPlOXQEKW/Mv/mh9/3YFA==}
+ hasBin: true
+ dev: false
+
/dlv@1.1.3:
resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==}
@@ -2993,6 +3008,10 @@ packages:
vfile-location: 4.1.0
web-namespaces: 2.0.1
+ /hast-util-has-property@2.0.1:
+ resolution: {integrity: sha512-X2+RwZIMTMKpXUzlotatPzWj8bspCymtXH3cfG3iQKV+wPF53Vgaqxi/eLqGck0wKq1kS9nvoB1wchbCPEL8sg==}
+ dev: false
+
/hast-util-parse-selector@3.1.1:
resolution: {integrity: sha512-jdlwBjEexy1oGz0aJ2f4GKMaVKkA9jwjr4MjAAI22E5fM/TXVZHuS5OpONtdeIkRKqAaryQ2E9xNQxijoThSZA==}
dependencies:
@@ -3013,6 +3032,26 @@ packages:
web-namespaces: 2.0.1
zwitch: 2.0.4
+ /hast-util-select@5.0.5:
+ resolution: {integrity: sha512-QQhWMhgTFRhCaQdgTKzZ5g31GLQ9qRb1hZtDPMqQaOhpLBziWcshUS0uCR5IJ0U1jrK/mxg35fmcq+Dp/Cy2Aw==}
+ dependencies:
+ '@types/hast': 2.3.4
+ '@types/unist': 2.0.6
+ bcp-47-match: 2.0.3
+ comma-separated-tokens: 2.0.3
+ css-selector-parser: 1.4.1
+ direction: 2.0.1
+ hast-util-has-property: 2.0.1
+ hast-util-to-string: 2.0.0
+ hast-util-whitespace: 2.0.1
+ not: 0.1.0
+ nth-check: 2.1.1
+ property-information: 6.2.0
+ space-separated-tokens: 2.0.2
+ unist-util-visit: 4.1.2
+ zwitch: 2.0.4
+ dev: false
+
/hast-util-to-estree@2.3.2:
resolution: {integrity: sha512-YYDwATNdnvZi3Qi84iatPIl1lWpXba1MeNrNbDfJfVzEBZL8uUmtR7mt7bxKBC8kuAuvb0bkojXYZzsNHyHCLg==}
dependencies:
@@ -3064,7 +3103,6 @@ packages:
resolution: {integrity: sha512-02AQ3vLhuH3FisaMM+i/9sm4OXGSq1UhOOCpTLLQtHdL3tZt7qil69r8M8iDkZYyC0HCFylcYoP+8IO7ddta1A==}
dependencies:
'@types/hast': 2.3.4
- dev: true
/hast-util-whitespace@2.0.1:
resolution: {integrity: sha512-nAxA0v8+vXSBDt3AnRUNjyRIQ0rD+ntpbAp4LnPkumc5M9yUbSMa4XDU9Q6etY4f1Wp4bNgvc1yjiZtsTTrSng==}
@@ -4291,6 +4329,10 @@ packages:
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
engines: {node: '>=0.10.0'}
+ /not@0.1.0:
+ resolution: {integrity: sha512-5PDmaAsVfnWUgTUbJ3ERwn7u79Z0dYxN9ErxCpVJJqe2RK0PJ3z+iFUxuqjwtlDDegXvtWoxD/3Fzxox7tFGWA==}
+ dev: false
+
/npm-run-path@4.0.1:
resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==}
engines: {node: '>=8'}
@@ -4308,7 +4350,6 @@ packages:
resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
dependencies:
boolbase: 1.0.0
- dev: true
/object-assign@4.1.1:
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}