Skip to content

Commit a2d488b

Browse files
authored
Merge pull request #6 from geospatialem/geospatialem/feat-consistent-focus
feat: add consistent focus demo
2 parents a711bb7 + f70de41 commit a2d488b

File tree

6 files changed

+311
-5
lines changed

6 files changed

+311
-5
lines changed

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ Presented at the 2025 Esri Developer Summit by Kelly Hutchins and Kitty Hurley o
1111
1. [Map Description and Live Regions](demos/description-region/index.html)
1212
- Provide context to when the map has loaded and include a description when the map is in focus to further WCAG's [1.3.1: Info and Relationships](https://www.w3.org/WAI/WCAG22/Understanding/info-and-relationships.html) Success Criterion.
1313
2. [Expand Component Focus Trap Disabled](demos/expand-component/index.html)
14-
- Provide users the ability to not be trapped when interacting with `arcgis-expand` to further WCAG's [2.1.2: No Keyboard Trap](https://www.w3.org/WAI/WCAG21/Understanding/no-keyboard-trap.html) Success Criterion. Also includes support with Calcite's focus for [1.4.3: Contrast (Minimum)](https://www.w3.org/WAI/WCAG22/Understanding/contrast-minimum.html) and keyboard support with [2.4.3: Focus Order](https://www.w3.org/WAI/WCAG22/Understanding/focus-order.html) and [3.2.3: Consistent Navigation](https://www.w3.org/WAI/WCAG22/Understanding/consistent-navigation.html) Success Criterion.
15-
3. Consistent Focus 🚧
16-
- Provide a consistent focus order throughout your UI supporting WCAG's [2.4.3: Focus Order](https://www.w3.org/WAI/WCAG22/Understanding/focus-order.html) and [3.2.3: Consistent Navigation](https://www.w3.org/WAI/WCAG22/Understanding/consistent-navigation.html) Success Criterion.
14+
- Provide users the ability to not be trapped when interacting with `arcgis-expand` to further WCAG's [2.1.2: No Keyboard Trap](https://www.w3.org/WAI/WCAG21/Understanding/no-keyboard-trap.html) Success Criterion. Also includes support with Calcite's focus for [1.4.3: Contrast (Minimum)](https://www.w3.org/WAI/WCAG22/Understanding/contrast-minimum.html) and keyboard support with the [2.4.3: Focus Order](https://www.w3.org/WAI/WCAG22/Understanding/focus-order.html) Success Criterion.
15+
3. [Consistent Focus](demos/consistent-focus/index.html)
16+
- Provide a consistent focus order throughout your UI supporting WCAG's [2.4.3: Focus Order](https://www.w3.org/WAI/WCAG22/Understanding/focus-order.html) and [2.1.1: Keyboard](https://www.w3.org/WAI/WCAG22/Understanding/keyboard.html) Success Criterion.
1717
4. [High Contrast](demos/high-contrast/index.html)
1818
- Explore contrast with your data, altering the basemap and layer effects to support the map's purpose when a user has enabled high contrast on their operating system, also supporting WCAG's [1.4.3: Contrast Minimum](https://www.w3.org/WAI/WCAG22/Understanding/contrast-minimum) Success Criterion.
1919
5. [Reduced Motion](demos/reduced-motion/index.html)

demos/consistent-focus/app.js

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
require(["esri/core/reactiveUtils"], (reactiveUtils) => {
2+
3+
const toggleModeEl = document.getElementById("toggle-mode");
4+
const mapEl = document.querySelector("arcgis-map");
5+
const darkModeCss = document.getElementById("jsapi-mode-dark");
6+
const lightModeCss = document.getElementById("jsapi-mode-light");
7+
const actionBarEl = document.getElementById("custom-action-bar");
8+
const searchEl = document.getElementById("search-el");
9+
const expandEl = document.getElementById("expand-el");
10+
11+
let mode = "light";
12+
13+
toggleModeEl.addEventListener("click", handleModeChange);
14+
15+
function handleModeChange() {
16+
mode = mode === "dark" ? "light" : "dark";
17+
const isDarkMode = mode === "dark";
18+
darkModeCss.disabled = !isDarkMode;
19+
lightModeCss.disabled = isDarkMode;
20+
toggleModeEl.icon = isDarkMode ? "moon" : "brightness";
21+
document.body.className = isDarkMode ? "calcite-mode-dark" : "";
22+
mapEl.basemap = isDarkMode ? "dark-gray" : "gray";
23+
24+
document.querySelectorAll(`.calcite-mode-${isDarkMode ? "light" : "dark"}`).forEach(node =>
25+
node.classList.replace(`calcite-mode-${isDarkMode ? "light" : "dark"}`, `calcite-mode-${mode}`)
26+
);
27+
}
28+
29+
// reactiveUtils to watch for when the popup is opened and closed
30+
// Resource: https://developers.arcgis.com/javascript/latest/watch-for-changes/#watch-for-changes-in-the-api
31+
mapEl.addEventListener("arcgisViewReadyChange", () => {
32+
// Initial layer effect
33+
updateBloom(bloomIntensity, bloomRadius, bloomThreshold);
34+
reactiveUtils.watch(() => mapEl.view.popup.visible, (visible) => {
35+
if (mapEl.view.popup.visible) {
36+
setTimeout(() => {
37+
mapEl.view.popup.focus();
38+
}, 100);
39+
} else {
40+
searchEl.focusSearch();
41+
}
42+
}
43+
);
44+
});
45+
46+
// Initialize the mutation observer
47+
// Resource: https://developers.arcgis.com/javascript/latest/watch-for-changes/#using-a-mutation-observer
48+
const observer = new MutationObserver((mutations, observer) => {
49+
for (let mutation of mutations) {
50+
// Set focus on the arcgis-search if the component is expanded
51+
// Else set focus on the arcgis-expand
52+
if (mutation.target[mutation.attributeName] == true) {
53+
searchEl.focusSearch();
54+
} else {
55+
const expandEls = document.querySelectorAll(".esri-expand__toggle > calcite-action");
56+
expandEls[0].setFocus();
57+
}
58+
}
59+
});
60+
61+
// Start observing the arcgis-expand's "expanded" attribute
62+
observer.observe(expandEl, {
63+
attributeFilter: ["expanded"]
64+
});
65+
66+
// Active widget
67+
let activeWidget = "";
68+
69+
// Layer effects
70+
const effectBlockSectionEl = document.getElementById("bloom-effect");
71+
const shadowBlockSectionEl = document.getElementById("shadow-effect");
72+
73+
let bloomIntensity = 1.5;
74+
let bloomRadius = 0.5;
75+
let bloomThreshold = 0.1;
76+
77+
let shadowLength = 3;
78+
let shadowDepth = 1;
79+
let shadowOutline = 3;
80+
81+
// Bloom effects
82+
effectBlockSectionEl.addEventListener("calciteBlockSectionToggle", (evt) => {
83+
if (evt.target.open) {
84+
shadowBlockSectionEl.open = false;
85+
updateBloom(bloomIntensity, bloomRadius, bloomThreshold);
86+
} else {
87+
updateBloom(0, 0, 0);
88+
}
89+
});
90+
91+
// Drop shadow effects
92+
shadowBlockSectionEl.addEventListener("calciteBlockSectionToggle", (evt) => {
93+
if (evt.target.open) {
94+
effectBlockSectionEl.open = false;
95+
updateShadow(shadowLength, shadowDepth, shadowOutline);
96+
} else {
97+
updateShadow(0, 0, 0);
98+
}
99+
});
100+
101+
// Layer effect values
102+
const sliderEls = document.querySelectorAll("calcite-slider");
103+
sliderEls?.forEach((sliderEl) => {
104+
sliderEl.addEventListener("calciteSliderInput", () => {
105+
const sliderElId = sliderEl.id;
106+
// Bloom effects
107+
if (sliderElId.includes("bloom")) {
108+
bloomIntensity = document.getElementById("bloom-intensity").value;
109+
bloomRadius = document.getElementById("bloom-radius").value;
110+
bloomThreshold = document.getElementById("bloom-threshold").value;
111+
if (effectBlockSectionEl.open) {
112+
updateBloom(bloomIntensity, bloomRadius, bloomThreshold);
113+
}
114+
// Drop shadow effects
115+
} else if (sliderElId.includes("shadow")) {
116+
shadowLength = document.getElementById("shadow-length").value;
117+
shadowDepth = document.getElementById("shadow-depth").value;
118+
shadowOutline = document.getElementById("shadow-outline").value;
119+
if (shadowBlockSectionEl.open) {
120+
updateShadow(shadowLength, shadowDepth, shadowOutline);
121+
}
122+
}
123+
});
124+
});
125+
126+
// Bloom effect
127+
function updateBloom(bloomIntensity, bloomRadius, bloomThreshold) {
128+
mapEl.map.layers._items[0].effect = `bloom(${bloomIntensity}, ${bloomRadius}px, ${bloomThreshold})`;
129+
}
130+
131+
// Drop shadow effect
132+
function updateShadow(shadowLength, shadowDepth, shadowOutline) {
133+
mapEl.map.layers.items[0].effect = `drop-shadow(${shadowLength}px, ${shadowDepth}px, ${shadowOutline}px)`;
134+
}
135+
136+
// Active action
137+
const handleActionBarClick = ({ target }) => {
138+
if (target.tagName !== "CALCITE-ACTION") {
139+
return;
140+
}
141+
142+
if (activeWidget) {
143+
activeActionEl = document
144+
.querySelector(`[data-action-id=${activeWidget}]`)
145+
.removeAttribute("active");
146+
activePanelEl = document.querySelector(
147+
`[data-panel-id=${activeWidget}]`
148+
).closed = true;
149+
}
150+
151+
const nextWidget = target.dataset.actionId;
152+
if (nextWidget !== activeWidget) {
153+
document.querySelector(`[data-action-id=${nextWidget}]`).active = true;
154+
document.querySelector(`[data-panel-id=${nextWidget}]`).closed = false;
155+
activeWidget = nextWidget;
156+
document.querySelector(`[data-panel-id=${nextWidget}]`).setFocus();
157+
} else {
158+
activeWidget = null;
159+
}
160+
};
161+
162+
actionBarEl.addEventListener("click", handleActionBarClick);
163+
164+
// Panel interaction
165+
const panelEls = document.querySelectorAll("calcite-panel");
166+
for (let i = 0; i < panelEls.length; i++) {
167+
panelEls[i].addEventListener("calcitePanelClose", () => {
168+
document.querySelector(`[data-action-id=${activeWidget}]`).closed = true;
169+
document.querySelector(`[data-action-id=${activeWidget}]`).active = false;
170+
document.querySelector(`[data-action-id=${activeWidget}]`).setFocus();
171+
activeWidget = null;
172+
});
173+
}
174+
175+
});

demos/consistent-focus/index.html

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
<html lang="en">
2+
3+
<head>
4+
<meta charset="utf-8" />
5+
<meta name="viewport" />
6+
<title>2025 Esri Developer & Technology Summit - Consistent Focus</title>
7+
8+
<!-- Calcite imports -->
9+
<script type="module" src="https://js.arcgis.com/calcite-components/3.0.3/calcite.esm.js"></script>
10+
11+
12+
<!-- ArcGIS Maps SDK for JavaScript imports -->
13+
<script src="https://js.arcgis.com/4.32/"></script>
14+
<link id="jsapi-mode-light" rel="stylesheet" href="https://js.arcgis.com/4.32/esri/themes/light/main.css" />
15+
<link disabled id="jsapi-mode-dark" rel="stylesheet" href="https://js.arcgis.com/4.32/esri/themes/dark/main.css" />
16+
<script type="module" src="https://js.arcgis.com/map-components/4.32/arcgis-map-components.esm.js"></script>
17+
18+
<!-- Demo imports -->
19+
<script src="./app.js" defer></script>
20+
<link rel="stylesheet" href="./style.css" />
21+
</head>
22+
23+
<body>
24+
<calcite-shell content-behind>
25+
<calcite-navigation slot="header" id="nav">
26+
<calcite-navigation-logo icon="accessibility" alt="Application logo" slot="logo"
27+
heading="Consistent Focus"
28+
description="Esri Developer & Technology Summit 2025"></calcite-navigation-logo>
29+
<calcite-action-pad layout="horizontal" expand-disabled slot="content-end">
30+
<calcite-action id="toggle-mode" text="Toggle mode" icon="brightness"></calcite-action>
31+
</calcite-action-pad>
32+
<calcite-tooltip placement="bottom" reference-element="toggle-mode" slot="content-end">Toggle
33+
mode</calcite-tooltip>
34+
</calcite-navigation>
35+
36+
<calcite-shell-panel slot="panel-start" display-mode="float-content">
37+
<calcite-action-bar id="custom-action-bar" slot="action-bar">
38+
<calcite-action data-action-id="layer-effects" icon="effects" text="Layer effects"></calcite-action>
39+
<calcite-action data-action-id="legend" icon="legend" text="Legend"></calcite-action>
40+
</calcite-action-bar>
41+
<!-- Layer effects -->
42+
<calcite-panel heading="Layer effects" height-scale="l" data-panel-id="layer-effects" closable closed>
43+
<!-- Bloom effect -->
44+
<calcite-block open heading="Bloom" description="Apply a neon-like glow">
45+
<!-- Bloom enabled -->
46+
<calcite-block-section id="bloom-effect" open text="Apply bloom effect" toggle-display="switch">
47+
<!-- Intensity -->
48+
<calcite-label for="bloom-intensity">Bloom intensity</calcite-label>
49+
<calcite-slider id="bloom-intensity" value="1.5" min="0" max="10" step="0.5" label-ticks ticks="2"></calcite-slider>
50+
<!-- Radius-->
51+
<calcite-label for="bloom-radius">Bloom radius</calcite-label>
52+
<calcite-slider id="bloom-radius" value="0.5" min="0" max="1" step="0.5" label-ticks ticks="0.25"></calcite-slider>
53+
<!-- Threshold -->
54+
<calcite-label for="bloom-threshold">Bloom threshold</calcite-label>
55+
<calcite-slider id="bloom-threshold" value="0.1" min="0" max="0.5" step="0.1" label-ticks ticks="0.25"></calcite-slider>
56+
</calcite-block-section>
57+
</calcite-block>
58+
<!-- Drop shadow effect -->
59+
<calcite-block open heading="Drop shadow" description="Apply a drop shadow">
60+
<!-- Shadow enabled -->
61+
<calcite-block-section id="shadow-effect" text="Apply drop shadow effect" toggle-display="switch">
62+
<!-- Shadow length -->
63+
<calcite-label for="shadow-length">Shadow length</calcite-label>
64+
<calcite-slider id="shadow-length" value="3" min="0" max="4" step="1" label-ticks ticks="1"></calcite-slider>
65+
<!-- Shadow depth-->
66+
<calcite-label for="shadow-depth">Shadow depth</calcite-label>
67+
<calcite-slider id="shadow-depth" value="1" min="0" max="4" step="1" label-ticks ticks="1"></calcite-slider>
68+
<!-- Shadow outline -->
69+
<calcite-label for="shadow-outline">Shadow outline</calcite-label>
70+
<calcite-slider id="shadow-outline" value="3" min="0" max="4" step="1" label-ticks ticks="1"></calcite-slider>
71+
</calcite-block-section>
72+
</calcite-block>
73+
</calcite-panel>
74+
<!-- Legend Panel -->
75+
<calcite-panel heading="Legend" height-scale="l" data-panel-id="legend" closable closed>
76+
<arcgis-legend reference-element="arcgis-map" position="bottom-right"></arcgis-legend>
77+
</calcite-panel>
78+
</calcite-shell-panel>
79+
80+
<arcgis-map item-id="c2a3444863f2466aaad9efa6e65063e1" id="mapEl" basemap="gray">
81+
<arcgis-home position="top-right"></arcgis-home>
82+
<arcgis-expand id="expand-el" focus-trap-enabled="false" close-on-esc position="top-right" mode="floating">
83+
<arcgis-search id="search-el"></arcgis-search>
84+
</arcgis-expand>
85+
<arcgis-zoom position="bottom-right"></arcgis-zoom>
86+
</arcgis-map>
87+
</calcite-shell>
88+
89+
</body>
90+
91+
</html>

demos/consistent-focus/style.css

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/* Esri Developer & Technology Summit Demo Template */
2+
/* Demo template theming example */
3+
4+
body calcite-navigation-logo{
5+
--calcite-color-text-2:#737373;
6+
}
7+
body.calcite-mode-dark calcite-navigation-logo{
8+
--calcite-icon-color:#8FD0FF;
9+
--calcite-color-text-2:#C7C7C7;
10+
}
11+
12+
html,
13+
body {
14+
margin: 0;
15+
height: 100%;
16+
}
17+
18+
#mapEl {
19+
padding-left: 49px;
20+
}
21+
22+
calcite-shell-panel[slot="panel-start"] calcite-panel {
23+
border-top: 0;
24+
}
25+
26+
body.calcite-mode-dark,
27+
.calcite-mode-dark :focus {
28+
--calcite-color-brand: #d67229;
29+
--calcite-color-brand-hover: #d67229bf;
30+
--calcite-color-brand-press: #d67229e6;
31+
--calcite-color-focus: var(--calcite-color-brand);
32+
}
33+
34+
body,
35+
:focus {
36+
--calcite-color-brand: #000;
37+
--calcite-color-brand-hover: #000000bf;
38+
--calcite-color-brand-press: #000000e6;
39+
--calcite-color-focus: var(--calcite-color-brand);
40+
}

demos/expand-component/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
<body>
2424
<calcite-shell content-behind>
2525
<calcite-navigation slot="header" id="nav">
26-
<calcite-navigation-logo href="#" icon="accessibility" alt="Application logo" slot="logo"
26+
<calcite-navigation-logo icon="accessibility" alt="Application logo" slot="logo"
2727
heading="Expand Component Focus Trap Disabled"
2828
description="Esri Developer & Technology Summit 2025"></calcite-navigation-logo>
2929
<calcite-action-pad layout="horizontal" expand-disabled slot="content-end">

demos/high-contrast/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
<body>
2424
<calcite-shell content-behind>
2525
<calcite-navigation slot="header" id="nav">
26-
<calcite-navigation-logo href="#" icon="accessibility" alt="Application logo" slot="logo"
26+
<calcite-navigation-logo icon="accessibility" alt="Application logo" slot="logo"
2727
heading="High contrast"
2828
description="Esri Developer & Technology Summit 2025"></calcite-navigation-logo>
2929
<calcite-action-pad layout="horizontal" expand-disabled slot="content-end">

0 commit comments

Comments
 (0)