|
199 | 199 | // --- Command Palette (⌘K) --- |
200 | 200 | const cmdkButton = $('#ssc-cmdk'); |
201 | 201 | 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"> |
204 | 204 | <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;"> |
205 | 205 | <ul id="ssc-cmdp-results"></ul> |
206 | 206 | </div> |
|
210 | 210 | const cmdp = $('#ssc-cmdp'); |
211 | 211 | const searchInput = $('#ssc-cmdp-search'); |
212 | 212 | const resultsList = $('#ssc-cmdp-results'); |
| 213 | + const backgroundElementsSelector = 'body > *:not(#ssc-cmdp)'; |
213 | 214 | 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 | + }); |
214 | 303 |
|
215 | 304 | // Collect navigation links |
216 | 305 | $('.ssc-sidebar a').each(function() { |
|
243 | 332 | const link = $(`<a href="#">${c.name}</a>`); |
244 | 333 | link.on('click', (e) => { |
245 | 334 | e.preventDefault(); |
| 335 | + closeCommandPalette(); |
246 | 336 | if (c.type === 'link') { |
247 | 337 | window.location.href = c.handler; |
248 | 338 | } else { |
249 | 339 | c.handler(); |
250 | 340 | } |
251 | | - cmdp.removeClass('active'); |
252 | 341 | }); |
253 | 342 | resultsList.append($('<li></li>').append(link)); |
254 | 343 | }); |
| 344 | + updatePaletteFocusableElements(); |
255 | 345 | } |
256 | 346 |
|
257 | 347 | cmdkButton.on('click', () => { |
258 | | - cmdp.addClass('active'); |
259 | | - renderResults(); |
260 | | - searchInput.val('').focus(); |
| 348 | + openCommandPalette(); |
261 | 349 | }); |
262 | 350 |
|
263 | 351 | cmdp.on('click', function(e) { |
264 | 352 | if ($(e.target).is(cmdp)) { |
265 | | - cmdp.removeClass('active'); |
| 353 | + closeCommandPalette(); |
266 | 354 | } |
267 | 355 | }); |
268 | | - |
| 356 | + |
269 | 357 | searchInput.on('input', () => renderResults(searchInput.val())); |
270 | 358 |
|
271 | 359 | $(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') { |
273 | 363 | e.preventDefault(); |
274 | | - cmdkButton.click(); |
| 364 | + openCommandPalette(); |
275 | 365 | } |
276 | 366 |
|
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(); |
280 | 371 | } |
281 | 372 | if (shell.hasClass('ssc-shell--menu-open')) { |
282 | 373 | closeMobileMenu(); |
283 | 374 | } |
284 | 375 | } |
285 | 376 |
|
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') { |
287 | 402 | const focusable = sidebar.find(focusableSelectors).filter(':visible'); |
288 | 403 | if (!focusable.length) { |
289 | 404 | return; |
|
304 | 419 | }); |
305 | 420 |
|
306 | 421 | $(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 | + |
307 | 431 | if (!shell.hasClass('ssc-shell--menu-open') || !isMobileViewport()) { |
308 | 432 | return; |
309 | 433 | } |
|
0 commit comments