Skip to content

Commit 1d85481

Browse files
committed
Add tab/arrow-key navigation demo.
1 parent 92e0693 commit 1d85481

File tree

4 files changed

+217
-0
lines changed

4 files changed

+217
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ with.
1010

1111
## My JavaScript Demos - I Love JavaScript!
1212

13+
* [Using Both Tab And Arrow Keys For Keyboard Navigation](https://bennadel.github.io/JavaScript-Demos/demos/tab-group)
1314
* [Using Margins With Four-Sided Positioning In CSS](https://bennadel.github.io/JavaScript-Demos/demos/four-sided-margins)
1415
* [Nesting The pointer-events Property In CSS](https://bennadel.github.io/JavaScript-Demos/demos/nested-pointer-events-css)
1516
* [CSS Open Props Playground](https://bennadel.github.io/JavaScript-Demos/demos/open-props)

demos/tab-group/index.htm

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1" />
6+
<title>
7+
Using Both Tab And Arrow Keys For Keyboard Navigation
8+
</title>
9+
<link rel="stylesheet" type="text/css" href="./main.css" />
10+
</head>
11+
<body>
12+
13+
<h1>
14+
Using Both Tab And Arrow Keys For Keyboard Navigation
15+
</h1>
16+
17+
<p>
18+
<code>Tab</code> key moves focus from row-to-row.
19+
<code>Arrow</code> keys move focus from button-to-button.
20+
</p>
21+
22+
<!-- --------------------------------------------------------------------------- -->
23+
<!-- --------------------------------------------------------------------------- -->
24+
25+
<p
26+
x-data="tabGroup"
27+
@keydown.arrow-left="moveToPrevButton( $event )"
28+
@keydown.arrow-right="moveToNextButton( $event )"
29+
@click="moveTabIndexToButton( $event )">
30+
<!--
31+
The first button has a tabIndex of "0" so that it can be focused via the Tab
32+
key. All other keys in this group can be focused via the Arrow keys.
33+
-->
34+
<button tabindex="0"> Button A </button>
35+
<button tabindex="-1"> Button B </button>
36+
<button tabindex="-1"> Button C </button>
37+
<button tabindex="-1"> Button D </button>
38+
<button tabindex="-1"> Button E </button>
39+
</p>
40+
41+
<p
42+
x-data="tabGroup"
43+
@keydown.arrow-left="moveToPrevButton( $event )"
44+
@keydown.arrow-right="moveToNextButton( $event )"
45+
@click="moveTabIndexToButton( $event )">
46+
<!--
47+
The first button has a tabIndex of "0" so that it can be focused via the Tab
48+
key. All other keys in this group can be focused via the Arrow keys.
49+
-->
50+
<button tabindex="0"> Button A </button>
51+
<button tabindex="-1"> Button B </button>
52+
<button tabindex="-1"> Button C </button>
53+
<button tabindex="-1"> Button D </button>
54+
<button tabindex="-1"> Button E </button>
55+
</p>
56+
57+
<!-- --------------------------------------------------------------------------- -->
58+
<!-- --------------------------------------------------------------------------- -->
59+
60+
<!--
61+
For the DEMO, I'm catching the last focus on the page and using it to move the
62+
focus back to the FIRST tabbable button. This way, I can tab-forward through the
63+
two rows of buttons without having to tab-backwards.
64+
-->
65+
<button
66+
x-data
67+
@focus="document.querySelector( 'button[tabindex=\'0\']' ).focus();"
68+
style="position: fixed ; left: -300px ; top: -300px ;">
69+
Back to First Button
70+
</button>
71+
72+
<script type="text/javascript" src="./main.js" defer></script>
73+
<script type="text/javascript" src="../../vendor/alpine/3.13.5/alpine.3.13.5.min.js" defer></script>
74+
75+
</body>
76+
</html>

demos/tab-group/main.css

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
2+
html {
3+
box-sizing: border-box ;
4+
}
5+
html *,
6+
html *:before,
7+
html *:after {
8+
box-sizing: inherit ;
9+
}
10+
11+
body {
12+
font-family: monospace ;
13+
font-size: 18px ;
14+
line-height: 1.4 ;
15+
}
16+
17+
button {
18+
background-color: #f0f0f0 ;
19+
border: 1px solid #cccccc ;
20+
border-radius: 4px ;
21+
cursor: pointer ;
22+
font-family: inherit ;
23+
font-size: inherit ;
24+
padding: 8px 14px ;
25+
}
26+
27+
button:focus {
28+
outline: 3px solid blue ;
29+
outline-offset: 3px ;
30+
}

demos/tab-group/main.js

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
2+
function tabGroup() {
3+
4+
var host = this.$el;
5+
6+
// Return the public API of the component scope.
7+
return {
8+
moveTabIndexToButton: moveTabIndexToButton,
9+
moveToNextButton: moveToNextButton,
10+
moveToPrevButton: moveToPrevButton
11+
};
12+
13+
// ---
14+
// PUBLIC METHODS.
15+
// ---
16+
17+
/**
18+
* I move the active tabIndex (0) to the target button. This way, when the user clicks
19+
* a button, this becomes the button that can also be activated via the Tab key.
20+
*/
21+
function moveTabIndexToButton( event ) {
22+
23+
var targetButton = event.target.closest( "button" );
24+
25+
// Since we're using event-delegation on the host, it's possible that the click
26+
// event isn't targeting a button. In that case, ignore the event.
27+
if ( ! targetButton ) {
28+
29+
return;
30+
31+
}
32+
33+
for ( var button of getAllButtons() ) {
34+
35+
button.tabIndex = -1;
36+
37+
}
38+
39+
targetButton.tabIndex = 0;
40+
41+
}
42+
43+
/**
44+
* I move the focus and active tabIndex (0) to the next button in the set of buttons
45+
* contained within the host element.
46+
*/
47+
function moveToNextButton( event ) {
48+
49+
// Prevent any default browser behaviors (such as scrolling the viewport).
50+
event.preventDefault();
51+
52+
// Note: Technically, we're using event-delegation for the arrow keys. However,
53+
// since no other elements (other than our demo buttons) can be focused within the
54+
// host element, we can be confident that this was triggered by a button.
55+
var targetButton = event.target.closest( "button" );
56+
var allButtons = getAllButtons();
57+
var currentIndex = allButtons.indexOf( targetButton );
58+
// Get the NEXT button; or, loop around to the front of the collection.
59+
var futureButton = (
60+
allButtons[ currentIndex + 1 ] ||
61+
allButtons[ 0 ]
62+
);
63+
64+
targetButton.tabIndex = -1;
65+
futureButton.tabIndex = 0;
66+
futureButton.focus();
67+
68+
}
69+
70+
/**
71+
* I move the focus and active tabIndex (0) to the previous button in the set of
72+
* buttons contained within the host element.
73+
*/
74+
function moveToPrevButton( event ) {
75+
76+
// Prevent any default browser behaviors (such as scrolling the viewport).
77+
event.preventDefault();
78+
79+
// Note: Technically, we're using event-delegation for the arrow keys. However,
80+
// since no other elements (other than our demo buttons) can be focused within the
81+
// host element, we can be confident that this was triggered by a button.
82+
var targetButton = event.target.closest( "button" );
83+
var allButtons = getAllButtons();
84+
var currentIndex = allButtons.indexOf( targetButton );
85+
// Get the PREVIOUS button; or, loop around to the back of the collection.
86+
var futureButton = (
87+
allButtons[ currentIndex - 1 ] ||
88+
allButtons[ allButtons.length - 1 ]
89+
);
90+
91+
targetButton.tabIndex = -1;
92+
futureButton.tabIndex = 0;
93+
futureButton.focus();
94+
95+
}
96+
97+
// ---
98+
// PRIVATE METHODS.
99+
// ---
100+
101+
/**
102+
* I get all the buttons in the host element (as a proper array).
103+
*/
104+
function getAllButtons() {
105+
106+
return Array.from( host.querySelectorAll( "button" ) );
107+
108+
}
109+
110+
}

0 commit comments

Comments
 (0)