From 380b898d0d90da1d73db776243a24f0f4ac2a654 Mon Sep 17 00:00:00 2001 From: diondiondion Date: Wed, 18 Mar 2026 17:08:23 +0100 Subject: [PATCH] Improve accessibility of server rules list in sign-up flow (#38257) --- app/javascript/entrypoints/public.tsx | 60 +++++++++++++--- app/javascript/styles/mastodon/about.scss | 70 +++++++++++-------- app/javascript/styles/mastodon/reset.scss | 2 +- app/views/auth/registrations/rules.html.haml | 2 +- .../_rule_translation.html.haml | 10 ++- config/locales/en.yml | 1 + 6 files changed, 103 insertions(+), 42 deletions(-) diff --git a/app/javascript/entrypoints/public.tsx b/app/javascript/entrypoints/public.tsx index f891410a3c..4089575e41 100644 --- a/app/javascript/entrypoints/public.tsx +++ b/app/javascript/entrypoints/public.tsx @@ -149,6 +149,8 @@ function loaded() { document.querySelector('#user_settings_attributes_default_privacy'), ); + truncateRuleHints(); + const reactComponents = document.querySelectorAll('[data-component]'); if (reactComponents.length > 0) { @@ -425,21 +427,61 @@ on('submit', '#registration_new_user,#new_user', () => { }); }); +// Truncate long rule hints + +const MAX_RULE_HINT_LENGTH = 100; + +function truncateRuleHints() { + const ruleListItems = + document.querySelectorAll('.rules-list li'); + if (!ruleListItems.length) return; + + ruleListItems.forEach((item) => { + toggleRuleHint(item, true); + }); +} + +function toggleRuleHint(listItem: HTMLLIElement, isInitialSetup?: boolean) { + const hint = listItem.querySelector( + '.rules-list__hint-text', + ); + if (!hint) return; + + const hintText = hint.innerHTML; + const hintToggleButton = listItem.querySelector('button'); + + if (hintText.length > MAX_RULE_HINT_LENGTH) { + // Store full hint in a data attribute, then truncate it with an '…' + hint.dataset.fullHint = hintText; + hint.innerHTML = `${hintText.slice(0, MAX_RULE_HINT_LENGTH - 1).trim()}…`; + + if (hintToggleButton) { + // Reveal toggle button if needed + hintToggleButton.removeAttribute('hidden'); + hintToggleButton.setAttribute('aria-expanded', 'false'); + } + } else if (!isInitialSetup) { + const { fullHint } = hint.dataset; + if (fullHint) { + // Restore full hint from data attribute, then delete attribute + hint.innerHTML = fullHint; + delete hint.dataset.fullHint; + + hintToggleButton?.setAttribute('aria-expanded', 'true'); + hint.parentElement?.focus(); + } + } +} + on('click', '.rules-list button', ({ target }) => { if (!(target instanceof HTMLElement)) { return; } - const button = target.closest('button'); + const listItem = target.closest('li'); - if (!button) { - return; - } - - if (button.ariaExpanded === 'true') { - button.ariaExpanded = 'false'; - } else { - button.ariaExpanded = 'true'; + if (listItem) { + toggleRuleHint(listItem); } }); diff --git a/app/javascript/styles/mastodon/about.scss b/app/javascript/styles/mastodon/about.scss index 0bb2c8c9eb..cb4885a7e8 100644 --- a/app/javascript/styles/mastodon/about.scss +++ b/app/javascript/styles/mastodon/about.scss @@ -34,34 +34,6 @@ $fluid-breakpoint: $maximum-width + 20px; counter-increment: list-counter; min-height: 4ch; - button { - background: transparent; - border: 0; - padding: 0; - margin: 0; - text-align: start; - font: inherit; - - &:hover, - &:focus, - &:active { - background: transparent; - } - - &[aria-expanded='false'] .rules-list__hint { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - - @supports (-webkit-line-clamp: 2) { - display: -webkit-box; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; - white-space: normal; - } - } - } - &::before { content: counter(list-counter); position: absolute; @@ -91,6 +63,48 @@ $fluid-breakpoint: $maximum-width + 20px; font-size: 14px; font-weight: 400; color: var(--color-text-secondary); + + // Giving this a focus outline as the hint + // will be focused when toggling the full hint + &:focus-visible { + outline: var(--outline-focus-default); + outline-offset: 2px; + } + } + + &__toggle-button { + position: relative; + display: inline-flex; + vertical-align: -0.25em; + border: none; + border-radius: 4px; + color: var(--color-text-primary); + background: var(--color-bg-secondary); + + &[hidden] { + display: none; + } + + .icon { + width: 1lh; + height: 1lh; + } + + &:hover { + background: var(--color-bg-tertiary); + } + + &:focus-visible { + outline: var(--outline-focus-default); + outline-offset: 2px; + } + + &::before { + // Increase clickable size + content: ''; + position: absolute; + inset: -12px; + } } } diff --git a/app/javascript/styles/mastodon/reset.scss b/app/javascript/styles/mastodon/reset.scss index 2c3efbddc4..b6b5837136 100644 --- a/app/javascript/styles/mastodon/reset.scss +++ b/app/javascript/styles/mastodon/reset.scss @@ -35,7 +35,7 @@ body { } ol, ul { - list-style: none; + list-style-type: none; } blockquote, q { diff --git a/app/views/auth/registrations/rules.html.haml b/app/views/auth/registrations/rules.html.haml index 1e3d934c37..54c21e7798 100644 --- a/app/views/auth/registrations/rules.html.haml +++ b/app/views/auth/registrations/rules.html.haml @@ -16,7 +16,7 @@ %h1.title= t('auth.rules.title') %p.lead= t('auth.rules.preamble', domain: site_hostname) - %ol.rules-list + %ol.rules-list{ role: 'list' } = render collection: @rule_translations, partial: 'auth/rule_translations/rule_translation' .stacked-actions diff --git a/app/views/auth/rule_translations/_rule_translation.html.haml b/app/views/auth/rule_translations/_rule_translation.html.haml index 32b9cc28af..a8c307270f 100644 --- a/app/views/auth/rule_translations/_rule_translation.html.haml +++ b/app/views/auth/rule_translations/_rule_translation.html.haml @@ -1,4 +1,8 @@ %li - %button{ type: 'button', aria: { expanded: 'false' } } - .rules-list__text= rule_translation.text - .rules-list__hint= rule_translation.hint + .rules-list__text= rule_translation.text + - if rule_translation.hint? + .rules-list__hint{ tabIndex: -1 } + %span.rules-list__hint-text= rule_translation.hint + %button.rules-list__toggle-button{ type: 'button', hidden: true, 'aria-expanded': 'false' } + = material_symbol('more_horiz') + %span.sr-only= t('auth.rules.read_more') diff --git a/config/locales/en.yml b/config/locales/en.yml index 5db5384a47..fc20a1da8c 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1295,6 +1295,7 @@ en: invited_by: 'You can join %{domain} thanks to the invitation you have received from:' preamble: These are set and enforced by the %{domain} moderators. preamble_invited: Before you proceed, please consider the ground rules set by the moderators of %{domain}. + read_more: Read more title: Some ground rules. title_invited: You've been invited. security: Security