Skip to content

Commit 1e169fd

Browse files
committed
Code kata for focus ring zoomies.
1 parent 5d98a69 commit 1e169fd

3 files changed

Lines changed: 233 additions & 0 deletions

File tree

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+
* [Animating DOM Rectangles Over Focused Elements In JavaScript](https://bennadel.github.io/JavaScript-Demos/demos/focus-box)
1314
* [Linking To A Disclosure (Details) Element](https://bennadel.github.io/JavaScript-Demos/demos/scroll-to-details)
1415
* [Opening The Dialog Element As A Fly-out Sidebar](https://bennadel.github.io/JavaScript-Demos/demos/dialog-element-sidebar)
1516
* [Exploring The Dialog Element In HTML](https://bennadel.github.io/JavaScript-Demos/demos/dialog-element)

demos/focus-box/index.htm

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8" />
5+
<title>
6+
Animating DOM Rectangles Over Focused Elements In JavaScript
7+
</title>
8+
<link rel="stylesheet" type="text/css" href="./main.css" />
9+
</head>
10+
<body>
11+
12+
<h1>
13+
Animating DOM Rectangles Over Focused Elements In JavaScript
14+
</h1>
15+
16+
<p>
17+
Before demo <a href>text block</a> (to take focus from demo block and to contrast
18+
the normal focus-outline behavior).
19+
</p>
20+
21+
<p class="textBlock">
22+
<mark hidden class="box">
23+
<!--
24+
I will scurry across the viewport and highlight focused areas within this
25+
paragraph. Since focused text may wrap across line-breaks, I'm providing
26+
several internal segments for compound highlighting rectangles.
27+
-->
28+
<span></span>
29+
<span></span>
30+
<span></span>
31+
</mark>
32+
33+
<span data-repeat="10">
34+
Lorem ipsum dolor <a href>sit amet</a> interrogo communis flumen.
35+
<a href>Mille iuvenis</a>, umquam ante cohors, adhibeo citus fortis provincia.
36+
Hic posco ego, quis frequens tenebrae. Aliquis turbo is epistula. <a href>Hic
37+
experior quidam voluntas</a> nam aliquis vinculum. Noster puto paulo sum post
38+
saevus natura. Diversus ventus, quasi cum for meus, scribo dexter prior
39+
astrum. Idem condo qua ictus quoque nemo iter. Mortalis frater, denique in
40+
puer, tollo nullus quattuor vestigium. <a href>Iste dico ipse voluntas an sui
41+
vitium meus desino quisquis, qua gratus ira tu opto nemo praemium gens rideo
42+
diversus error</a>.
43+
</span>
44+
</p>
45+
46+
<style type="text/css">
47+
48+
mark.box span {
49+
background-color: transparent ;
50+
border-radius: 3px ;
51+
border: 1px solid tomato ;
52+
box-shadow: inset 0 0 0 3px #ffff04aa ;
53+
opacity: 0 ;
54+
pointer-events: none ;
55+
position: absolute ;
56+
transition: all 100ms ease-out ;
57+
z-index: 9999 ;
58+
}
59+
60+
</style>
61+
<script type="text/javascript">
62+
63+
var box = document.querySelector( "mark.box" );
64+
var boxSegments = Array.from( box.children );
65+
66+
// When a new element is focused within the document, we want zoom the marker
67+
// box(es) over to the focused element to emphasize focus.
68+
document.addEventListener( "focusin", ( event ) => {
69+
70+
var target = event.target;
71+
72+
// If an element OUTSIDE of the demo text block was focused, hide the marker -
73+
// we only want that to show the crazy boxy inside the demo block.
74+
if ( ! target.closest( ".textBlock" ) ) {
75+
76+
box.hidden = true;
77+
return;
78+
79+
}
80+
81+
// We're about to animate the box segments into place, mark the box as active
82+
// (and visible) in the DOM.
83+
box.hidden = false;
84+
85+
// Instead of using the `.getBoundingClientRect()`, which will give us the box
86+
// that encompasses ALL areas of the focused text, we're going to use the
87+
// `.getClientRects()` to get individual DOMRect readings for each part of the
88+
// text if it wraps across lines.
89+
// --
90+
// Note: if the text doesn't wrap, the two get-rect methods are equivalent.
91+
var rects = Array.from( target.getClientRects() );
92+
var offsetBlock = window.scrollY;
93+
var offsetInline = window.scrollX;
94+
var padBlock = 1;
95+
var padInline = 6;
96+
97+
boxSegments.forEach(
98+
( segment, i ) => {
99+
100+
var rect = rects[ i ];
101+
// In order to make the animations smooth, we want to position every
102+
// segment even if they aren't going to be rendered. This way, when
103+
// they do get rendered, it looks like everything originates from the
104+
// same location within the viewport. To that end, if we don't have a
105+
// matching DOMRect for this segment, just use the first one and mark
106+
// the segment as translucent.
107+
if ( rect ) {
108+
109+
segment.style.opacity = 1; // Show.
110+
111+
} else {
112+
113+
segment.style.opacity = 0; // Hide.
114+
rect = rects[ 0 ];
115+
116+
}
117+
118+
segment.style.left = `${ rect.left - padInline + offsetInline }px`;
119+
segment.style.top = `${ rect.top - padBlock + offsetBlock }px`;
120+
segment.style.width = `${ rect.width + ( padInline * 2 ) }px`;
121+
segment.style.height = `${ rect.height + ( padBlock * 2 ) }px`;
122+
123+
}
124+
);
125+
126+
});
127+
128+
// When the window loses focus, hide the marker box.
129+
window.addEventListener( "blur", ( event ) => {
130+
131+
box.hidden = true;
132+
133+
});
134+
135+
// --------------------------------------------------------------------------- //
136+
// --------------------------------------------------------------------------- //
137+
// Flesh-out lots of text for the demo so we can force window scrolling.
138+
// --------------------------------------------------------------------------- //
139+
// --------------------------------------------------------------------------- //
140+
141+
var templateNode = document.querySelector( "[data-repeat]" );
142+
var count = Number( templateNode.dataset.repeat );
143+
144+
for ( var i = 1 ; i <= count ; i++ ) {
145+
146+
templateNode.after( templateNode.cloneNode( true ) );
147+
148+
}
149+
150+
// Don't honor any anchor links. I want the `<a>` tags to have something to focus;
151+
// but I don't want the clicks to actually navigate away from the page or offset.
152+
document.addEventListener( "click", ( event ) => {
153+
154+
if ( event.target.closest( "a" ) ) {
155+
156+
event.preventDefault();
157+
158+
}
159+
160+
});
161+
162+
</script>
163+
164+
</body>
165+
</html>

demos/focus-box/main.css

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
2+
:where( html ) {
3+
box-sizing: border-box ;
4+
5+
& *,
6+
& *:before,
7+
& *:after {
8+
box-sizing: inherit ;
9+
}
10+
}
11+
12+
:where( * ) {
13+
&:focus,
14+
&:focus-visible {
15+
animation-duration: 200ms ;
16+
animation-fill-mode: forwards ;
17+
animation-iteration-count: 1 ;
18+
animation-name: outlineEnter ;
19+
animation-timing-function: ease-out ;
20+
outline-color: hotpink ;
21+
outline-offset: 4px ;
22+
outline-width: 2px ;
23+
}
24+
}
25+
26+
@keyframes outlineEnter {
27+
from {
28+
outline-offset: 8px ;
29+
}
30+
to {
31+
outline-offset: 4px ;
32+
}
33+
}
34+
35+
.textBlock * {
36+
&:focus,
37+
&:focus-visible {
38+
animation-name: none !important ;
39+
outline: none !important ;
40+
}
41+
}
42+
43+
44+
body {
45+
font-family: Avenir, Montserrat, Corbel, URW Gothic, source-sans-pro, sans-serif ;
46+
font-size: 18px ;
47+
line-height: 1.4 ;
48+
}
49+
50+
button,
51+
input:where([type="text"]),
52+
select,
53+
textarea {
54+
color: inherit ;
55+
font-family: inherit ;
56+
font-size: 20px ;
57+
line-height: inherit ;
58+
padding: 5px 10px ;
59+
}
60+
61+
button {
62+
padding: 5px 15px ;
63+
}
64+
65+
a {
66+
color: red ;
67+
}

0 commit comments

Comments
 (0)