a11y: Setup observers for element roles (#4469) * a11y: Setup observers for element roles * Reuse a function * Add error handling
Signed| @@ -84,7 +84,7 @@ | |||
| 84 | </div> | 84 | </div> |
| 85 | 85 | ||
| 86 | <script src="lib/jquery-3.5.1.min.js"></script> | 86 | <script src="lib/jquery-3.5.1.min.js"></script> |
| 87 | <script src="scripts/login.js"></script> | 87 | <script src="scripts/login.js" type="module"></script> |
| 88 | </body> | 88 | </body> |
| 89 | 89 | ||
| 90 | </html> | 90 | </html> |
| @@ -266,6 +266,7 @@ import { initDataMaid } from './scripts/data-maid.js'; | |||
| 266 | import { clearItemizedPrompts, deleteItemizedPrompts, findItemizedPromptSet, initItemizedPrompts, itemizedParams, itemizedPrompts, loadItemizedPrompts, promptItemize, replaceItemizedPromptText, saveItemizedPrompts } from './scripts/itemized-prompts.js'; | 266 | import { clearItemizedPrompts, deleteItemizedPrompts, findItemizedPromptSet, initItemizedPrompts, itemizedParams, itemizedPrompts, loadItemizedPrompts, promptItemize, replaceItemizedPromptText, saveItemizedPrompts } from './scripts/itemized-prompts.js'; |
| 267 | import { getSystemMessageByType, initSystemMessages, SAFETY_CHAT, sendSystemMessage, system_message_types, system_messages } from './scripts/system-messages.js'; | 267 | import { getSystemMessageByType, initSystemMessages, SAFETY_CHAT, sendSystemMessage, system_message_types, system_messages } from './scripts/system-messages.js'; |
| 268 | import { event_types, eventSource } from './scripts/events.js'; | 268 | import { event_types, eventSource } from './scripts/events.js'; |
| 269 | import { initAccessibility } from './scripts/a11y.js'; | ||
| 269 | 270 | ||
| 270 | // API OBJECT FOR EXTERNAL WIRING | 271 | // API OBJECT FOR EXTERNAL WIRING |
| 271 | globalThis.SillyTavern = { | 272 | globalThis.SillyTavern = { |
| @@ -687,6 +688,7 @@ async function firstLoadInit() { | |||
| 687 | initCustomSelectedSamplers(); | 688 | initCustomSelectedSamplers(); |
| 688 | initDataMaid(); | 689 | initDataMaid(); |
| 689 | initItemizedPrompts(); | 690 | initItemizedPrompts(); |
| 691 | initAccessibility(); | ||
| 690 | addDebugFunctions(); | 692 | addDebugFunctions(); |
| 691 | doDailyExtensionUpdatesCheck(); | 693 | doDailyExtensionUpdatesCheck(); |
| 692 | await hideLoader(); | 694 | await hideLoader(); |
| @@ -0,0 +1,104 @@ | |||
| 1 | /** | ||
| 2 | * Shared module between login and main app. | ||
| 3 | * Be careful what you import! | ||
| 4 | */ | ||
| 5 | |||
| 6 | const buttonSelectors = [ | ||
| 7 | '.menu_button', | ||
| 8 | '.right_menu_button', | ||
| 9 | '.mes_button', | ||
| 10 | '.drawer-icon', | ||
| 11 | '.inline-drawer-icon', | ||
| 12 | '.swipe_left', | ||
| 13 | '.swipe_right', | ||
| 14 | '.character_select', | ||
| 15 | '.tags .tag', | ||
| 16 | ].join(', '); | ||
| 17 | |||
| 18 | const listSelectors = [ | ||
| 19 | '.options-content', | ||
| 20 | '.list-group', | ||
| 21 | '#rm_print_characters_block', | ||
| 22 | '#rm_group_members', | ||
| 23 | '#rm_group_add_members', | ||
| 24 | '.tag_view_list_tags', | ||
| 25 | '.secretKeyManagerList', | ||
| 26 | '.recentChatList', | ||
| 27 | '.dataMaidCategoryContent', | ||
| 28 | '#userList', | ||
| 29 | ].join(', '); | ||
| 30 | |||
| 31 | const listItemSelectors = [ | ||
| 32 | '.options-content .interactable', | ||
| 33 | '.list-group .list-group-item', | ||
| 34 | '#rm_print_characters_block .entity_block', | ||
| 35 | '#rm_group_members .group_member', | ||
| 36 | '#rm_group_add_members .group_member', | ||
| 37 | '.tag_view_list_tags .tag_view_item', | ||
| 38 | '.secretKeyManagerList .secretKeyManagerItem', | ||
| 39 | '.recentChatList .recentChat', | ||
| 40 | '.dataMaidCategoryContent .dataMaidItem', | ||
| 41 | '#userList .userSelect', | ||
| 42 | ].join(', '); | ||
| 43 | |||
| 44 | /** @type {Record<string, (element: Element) => void>} */ | ||
| 45 | const a11yRules = { | ||
| 46 | [buttonSelectors]: (element) => { | ||
| 47 | element.setAttribute('role', 'button'); | ||
| 48 | }, | ||
| 49 | [listSelectors]: (element) => { | ||
| 50 | element.setAttribute('role', 'list'); | ||
| 51 | }, | ||
| 52 | [listItemSelectors]: (element) => { | ||
| 53 | element.setAttribute('role', 'listitem'); | ||
| 54 | }, | ||
| 55 | '#toast-container .toast-message': (element) => { | ||
| 56 | element.setAttribute('role', 'alert'); | ||
| 57 | }, | ||
| 58 | }; | ||
| 59 | |||
| 60 | /** | ||
| 61 | * Apply accessibility rules to an element. | ||
| 62 | * @param {Element} element Element to process. | ||
| 63 | */ | ||
| 64 | function applyA11yRules(element) { | ||
| 65 | try { | ||
| 66 | for (const [selector, rule] of Object.entries(a11yRules)) { | ||
| 67 | // Apply if the element directly matches the selector | ||
| 68 | if (element.matches(selector)) { | ||
| 69 | rule(element); | ||
| 70 | } | ||
| 71 | // Apply the rule to descendants | ||
| 72 | element.querySelectorAll(selector).forEach(rule); | ||
| 73 | } | ||
| 74 | } catch (error) { | ||
| 75 | console.error('Error applying accessibility rules to element:', element, error); | ||
| 76 | } | ||
| 77 | } | ||
| 78 | |||
| 79 | function setAccessibilityObserver() { | ||
| 80 | // Apply for existing elements | ||
| 81 | applyA11yRules(document.body); | ||
| 82 | |||
| 83 | // Setup observer for dynamic content | ||
| 84 | const observer = new MutationObserver((mutationsList) => { | ||
| 85 | for (const mutation of mutationsList) { | ||
| 86 | if (mutation.type === 'childList') { | ||
| 87 | for (const addedNode of mutation.addedNodes) { | ||
| 88 | if (addedNode instanceof Element && addedNode.nodeType === Node.ELEMENT_NODE) { | ||
| 89 | applyA11yRules(addedNode); | ||
| 90 | } | ||
| 91 | } | ||
| 92 | } | ||
| 93 | } | ||
| 94 | }); | ||
| 95 | |||
| 96 | observer.observe(document.body, { | ||
| 97 | childList: true, | ||
| 98 | subtree: true, | ||
| 99 | }); | ||
| 100 | } | ||
| 101 | |||
| 102 | export function initAccessibility() { | ||
| 103 | setAccessibilityObserver(); | ||
| 104 | } | ||
| @@ -1,3 +1,5 @@ | |||
| 1 | import { initAccessibility } from './a11y.js'; | ||
| 2 | |||
| 1 | /** | 3 | /** |
| 2 | * CRSF token for requests. | 4 | * CRSF token for requests. |
| 3 | */ | 5 | */ |
| @@ -265,6 +267,8 @@ function configureDiscreetLogin() { | |||
| 265 | } | 267 | } |
| 266 | 268 | ||
| 267 | (async function () { | 269 | (async function () { |
| 270 | initAccessibility(); | ||
| 271 | |||
| 268 | csrfToken = await getCsrfToken(); | 272 | csrfToken = await getCsrfToken(); |
| 269 | const userList = await getUserList(); | 273 | const userList = await getUserList(); |
| 270 | 274 | ||