Skip to content

Commit f57be64

Browse files
Merge pull request #177 from JLG-WOCFR-DEV/codex/update-html-template-and-focus-management
Enhance command palette accessibility
2 parents 84a22da + a3ab1bc commit f57be64

File tree

1 file changed

+138
-14
lines changed
  • supersede-css-jlg-enhanced/assets/js

1 file changed

+138
-14
lines changed

supersede-css-jlg-enhanced/assets/js/ux.js

Lines changed: 138 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -199,8 +199,8 @@
199199
// --- Command Palette (⌘K) ---
200200
const cmdkButton = $('#ssc-cmdk');
201201
const cmdkPanelHtml = `
202-
<div id="ssc-cmdp">
203-
<div class="panel">
202+
<div id="ssc-cmdp" role="dialog" aria-modal="true" aria-hidden="true" aria-label="Palette de commandes Supersede CSS" tabindex="-1">
203+
<div class="panel" role="document">
204204
<input type="text" id="ssc-cmdp-search" placeholder="Naviguer ou lancer une action..." style="width: 100%; padding: 12px; border: none; border-bottom: 1px solid var(--ssc-border); font-size: 16px;">
205205
<ul id="ssc-cmdp-results"></ul>
206206
</div>
@@ -210,7 +210,96 @@
210210
const cmdp = $('#ssc-cmdp');
211211
const searchInput = $('#ssc-cmdp-search');
212212
const resultsList = $('#ssc-cmdp-results');
213+
const backgroundElementsSelector = 'body > *:not(#ssc-cmdp)';
213214
let commands = [];
215+
let previouslyFocusedCommandElement = null;
216+
let paletteFocusableElements = $();
217+
let isCommandPaletteOpen = false;
218+
219+
const updatePaletteFocusableElements = () => {
220+
const focusable = cmdp.find(focusableSelectors).filter(':visible');
221+
paletteFocusableElements = focusable.length ? focusable : cmdp;
222+
};
223+
224+
const setBackgroundTreeState = (hidden) => {
225+
$(backgroundElementsSelector).each(function() {
226+
const $element = $(this);
227+
228+
if (hidden) {
229+
if (typeof $element.data('sscCmdpOriginalAriaHidden') === 'undefined') {
230+
const originalAriaHidden = $element.attr('aria-hidden');
231+
$element.data('sscCmdpOriginalAriaHidden', typeof originalAriaHidden === 'undefined' ? null : originalAriaHidden);
232+
}
233+
234+
if (typeof $element.data('sscCmdpOriginalInert') === 'undefined') {
235+
$element.data('sscCmdpOriginalInert', this.hasAttribute('inert'));
236+
}
237+
238+
$element.attr('aria-hidden', 'true');
239+
this.setAttribute('inert', '');
240+
} else {
241+
if (typeof $element.data('sscCmdpOriginalAriaHidden') !== 'undefined') {
242+
const originalAriaHidden = $element.data('sscCmdpOriginalAriaHidden');
243+
if (originalAriaHidden === null) {
244+
$element.removeAttr('aria-hidden');
245+
} else {
246+
$element.attr('aria-hidden', originalAriaHidden);
247+
}
248+
$element.removeData('sscCmdpOriginalAriaHidden');
249+
}
250+
251+
if (typeof $element.data('sscCmdpOriginalInert') !== 'undefined') {
252+
if ($element.data('sscCmdpOriginalInert')) {
253+
this.setAttribute('inert', '');
254+
} else {
255+
this.removeAttribute('inert');
256+
}
257+
$element.removeData('sscCmdpOriginalInert');
258+
}
259+
}
260+
});
261+
};
262+
263+
const openCommandPalette = () => {
264+
if (isCommandPaletteOpen) {
265+
searchInput.trigger('focus');
266+
return;
267+
}
268+
269+
previouslyFocusedCommandElement = document.activeElement instanceof HTMLElement ? document.activeElement : null;
270+
isCommandPaletteOpen = true;
271+
cmdp.addClass('active');
272+
cmdp.attr('aria-hidden', 'false');
273+
cmdkButton.attr('aria-expanded', 'true');
274+
setBackgroundTreeState(true);
275+
renderResults();
276+
searchInput.val('').trigger('focus');
277+
updatePaletteFocusableElements();
278+
};
279+
280+
const closeCommandPalette = () => {
281+
if (!isCommandPaletteOpen) {
282+
return;
283+
}
284+
285+
isCommandPaletteOpen = false;
286+
cmdp.removeClass('active');
287+
cmdp.attr('aria-hidden', 'true');
288+
cmdkButton.attr('aria-expanded', 'false');
289+
setBackgroundTreeState(false);
290+
paletteFocusableElements = $();
291+
if (cmdkButton.length) {
292+
cmdkButton.trigger('focus');
293+
} else if (previouslyFocusedCommandElement) {
294+
$(previouslyFocusedCommandElement).trigger('focus');
295+
}
296+
previouslyFocusedCommandElement = null;
297+
};
298+
299+
cmdkButton.attr({
300+
'aria-haspopup': 'dialog',
301+
'aria-expanded': 'false'
302+
});
214303

215304
// Collect navigation links
216305
$('.ssc-sidebar a').each(function() {
@@ -243,47 +332,73 @@
243332
const link = $(`<a href="#">${c.name}</a>`);
244333
link.on('click', (e) => {
245334
e.preventDefault();
335+
closeCommandPalette();
246336
if (c.type === 'link') {
247337
window.location.href = c.handler;
248338
} else {
249339
c.handler();
250340
}
251-
cmdp.removeClass('active');
252341
});
253342
resultsList.append($('<li></li>').append(link));
254343
});
344+
updatePaletteFocusableElements();
255345
}
256346

257347
cmdkButton.on('click', () => {
258-
cmdp.addClass('active');
259-
renderResults();
260-
searchInput.val('').focus();
348+
openCommandPalette();
261349
});
262350

263351
cmdp.on('click', function(e) {
264352
if ($(e.target).is(cmdp)) {
265-
cmdp.removeClass('active');
353+
closeCommandPalette();
266354
}
267355
});
268-
356+
269357
searchInput.on('input', () => renderResults(searchInput.val()));
270358

271359
$(document).on('keydown', function(e) {
272-
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
360+
const key = typeof e.key === 'string' ? e.key.toLowerCase() : e.key;
361+
362+
if ((e.metaKey || e.ctrlKey) && key === 'k') {
273363
e.preventDefault();
274-
cmdkButton.click();
364+
openCommandPalette();
275365
}
276366

277-
if (e.key === 'Escape') {
278-
if (cmdp.hasClass('active')) {
279-
cmdp.removeClass('active');
367+
if (key === 'escape') {
368+
if (isCommandPaletteOpen) {
369+
e.preventDefault();
370+
closeCommandPalette();
280371
}
281372
if (shell.hasClass('ssc-shell--menu-open')) {
282373
closeMobileMenu();
283374
}
284375
}
285376

286-
if (shell.hasClass('ssc-shell--menu-open') && e.key === 'Tab') {
377+
if (isCommandPaletteOpen && key === 'tab') {
378+
const focusable = paletteFocusableElements;
379+
const elements = focusable.toArray();
380+
381+
if (!elements.length) {
382+
e.preventDefault();
383+
cmdp.trigger('focus');
384+
return;
385+
}
386+
387+
const first = elements[0];
388+
const last = elements[elements.length - 1];
389+
const activeElement = document.activeElement;
390+
391+
if (!e.shiftKey && activeElement === last) {
392+
e.preventDefault();
393+
$(first).trigger('focus');
394+
} else if (e.shiftKey && activeElement === first) {
395+
e.preventDefault();
396+
$(last).trigger('focus');
397+
}
398+
return;
399+
}
400+
401+
if (shell.hasClass('ssc-shell--menu-open') && key === 'tab') {
287402
const focusable = sidebar.find(focusableSelectors).filter(':visible');
288403
if (!focusable.length) {
289404
return;
@@ -304,6 +419,15 @@
304419
});
305420

306421
$(document).on('focusin', function(e) {
422+
if (isCommandPaletteOpen && $(e.target).closest('#ssc-cmdp').length === 0) {
423+
if (paletteFocusableElements.length) {
424+
$(paletteFocusableElements.get(0)).trigger('focus');
425+
} else {
426+
cmdp.trigger('focus');
427+
}
428+
return;
429+
}
430+
307431
if (!shell.hasClass('ssc-shell--menu-open') || !isMobileViewport()) {
308432
return;
309433
}

0 commit comments

Comments
 (0)