feat(a11y): skip-link (#75), FAB login-card relocation (#178), reading mode (#83) #185

Merged
jmiller merged 3 commits from feature/fab-a11y-mokoai-parity into dev 2026-07-21 21:30:48 +00:00
7 changed files with 108 additions and 2 deletions
+7
View File
@@ -15,6 +15,13 @@
# Changelog — MokoOnyx (VERSION: 02.33.04)
## [Unreleased]
### Added
- Accessibility toolbar: **Reading mode** toggle — hides sidebars/TOC, narrows the content to a comfortable measure, and relaxes line-height/letter/word spacing for easier reading (cognitive-accessibility / dyslexia aid). Persisted in `localStorage`; toggled via new `a11y_reading_mode` template param. (#83)
- Login view: the theme FAB (with its accessibility toggle) now relocates into the login card as a flush bottom bar with a theme-aware separator, matching the MokoAI widget behavior. Uses a `MutationObserver` so it works even if the login form renders late. (#178)
### Fixed
- Skip-to-content link (WCAG 2.4.1 Bypass Blocks): the main template had no skip link (only the offline page did), and the `.skip-link:focus` rule had a typo (`.skip-l ink`) that broke the on-focus reveal. Added a "Skip to content" anchor as the first focusable element (targets `#maincontent`) and corrected the focus style. (#75)
## [02.33.00] --- 2026-07-20
### Fixed
+3
View File
@@ -57,6 +57,7 @@ $params_a11y_contrast = $this->params->get('a11y_high_contrast', 1);
$params_a11y_links = $this->params->get('a11y_highlight_links', 1);
$params_a11y_font = $this->params->get('a11y_readable_font', 1);
$params_a11y_animations = $this->params->get('a11y_pause_animations', 1);
$params_a11y_reading = $this->params->get('a11y_reading_mode', 1);
$params_a11y_pos = 'br';
// Detecting Active Variables
@@ -333,6 +334,7 @@ $wa->useScript('user.js'); // js/user.js
data-a11y-links="<?php echo $params_a11y_links ? '1' : '0'; ?>"
data-a11y-font="<?php echo $params_a11y_font ? '1' : '0'; ?>"
data-a11y-animations="<?php echo $params_a11y_animations ? '1' : '0'; ?>"
data-a11y-reading="<?php echo $params_a11y_reading ? '1' : '0'; ?>"
data-a11y-pos="<?php echo htmlspecialchars($params_a11y_pos, ENT_QUOTES, 'UTF-8'); ?>"
class="site <?php
echo $option . ' ' . $wrapper
@@ -344,6 +346,7 @@ $wa->useScript('user.js'); // js/user.js
. $hasClass
. ($this->direction == 'rtl' ? ' rtl' : '');
?>">
<a class="skip-link" href="#maincontent"><?php echo Text::_('JSKIP_TO_CONTENT') ?: 'Skip to content'; ?></a>
<?php if (!empty($params_visitordetection) && (!empty($params_googletagmanager) || !empty($params_googleanalytics))) :
$user = Factory::getUser();
$visitorType = $user->guest ? 'guest' : 'logged_in';
+2
View File
@@ -129,6 +129,8 @@ TPL_MOKO_A11Y_READABLE_FONT="Readable font"
TPL_MOKO_A11Y_READABLE_FONT_DESC="Allow visitors to switch to a clean system font optimised for readability."
TPL_MOKO_A11Y_PAUSE_ANIMATIONS="Pause animations"
TPL_MOKO_A11Y_PAUSE_ANIMATIONS_DESC="Allow visitors to stop all CSS animations and transitions."
TPL_MOKO_A11Y_READING_MODE="Reading mode"
TPL_MOKO_A11Y_READING_MODE_DESC="Allow visitors to strip sidebars, narrow the content column, and relax typography for easier reading (cognitive-accessibility / dyslexia aid)."
; ===== CSS Variables tab =====
TPL_MOKOONYX_CSS_VARS_FIELDSET_LABEL="CSS Variables"
+2
View File
@@ -129,6 +129,8 @@ TPL_MOKO_A11Y_READABLE_FONT="Readable font"
TPL_MOKO_A11Y_READABLE_FONT_DESC="Allow visitors to switch to a clean system font optimized for readability."
TPL_MOKO_A11Y_PAUSE_ANIMATIONS="Pause animations"
TPL_MOKO_A11Y_PAUSE_ANIMATIONS_DESC="Allow visitors to stop all CSS animations and transitions."
TPL_MOKO_A11Y_READING_MODE="Reading mode"
TPL_MOKO_A11Y_READING_MODE_DESC="Allow visitors to strip sidebars, narrow the content column, and relax typography for easier reading (cognitive-accessibility / dyslexia aid)."
; ===== CSS Variables tab =====
TPL_MOKOONYX_CSS_VARS_FIELDSET_LABEL="CSS Variables"
+45 -1
View File
@@ -17230,6 +17230,24 @@ body.has-gtranslate #mokoThemeFab.pos-bl {
top: 1rem;
}
/* Login view: FAB relocated into the login card as a flush bottom bar (parity with
MokoAI). Stretches to the card edges (assumes ~1rem card padding), drops its float
positioning, and uses a theme-aware separator instead of a shadow. */
#mokoThemeFab.in-card {
position: static;
transform: none;
inset: auto;
width: auto;
margin: 1.25rem -1rem -1rem;
padding: .875rem;
border: 0;
border-top: 1px solid var(--border-color, #dfe3e7);
border-radius: 0;
box-shadow: none;
background: transparent;
justify-content: center;
}
/* Sun/Moon theme toggle button */
.theme-icon-btn {
display: flex;
@@ -17477,11 +17495,15 @@ body.site.error-page {
overflow: hidden;
}
.skip-l ink:focus {
.skip-link:focus {
position: static;
width: auto;
height: auto;
padding: .5rem 1rem;
z-index: 2000;
background: var(--color-primary, #112855);
color: #fff;
border-radius: 0 0 .25rem 0;
}
.btn {
@@ -17554,6 +17576,28 @@ html.a11y-pause-animations *::after {
scroll-behavior: auto !important;
}
/* Reading mode: cognitive-accessibility / dyslexia aid hide sidebars + TOC,
narrow the content to a comfortable measure, and relax typography. */
html.a11y-reading-mode .container-sidebar-left,
html.a11y-reading-mode .container-sidebar-right,
html.a11y-reading-mode #toc,
html.a11y-reading-mode .container-toc {
display: none !important;
}
html.a11y-reading-mode .container-component {
max-width: 72ch;
margin-inline: auto;
float: none;
width: auto;
}
html.a11y-reading-mode #maincontent,
html.a11y-reading-mode #maincontent p,
html.a11y-reading-mode #maincontent li {
line-height: 1.9;
letter-spacing: 0.02em;
word-spacing: 0.08em;
}
/* Toolbar container */
#mokoA11yToolbar {
position: fixed;
+42 -1
View File
@@ -209,7 +209,7 @@
var raw = localStorage.getItem(a11yStorageKey);
if (raw) return JSON.parse(raw);
} catch (e) {}
return { fontStep: defaultStep, inverted: false, contrast: false, links: false, font: false, paused: false };
return { fontStep: defaultStep, inverted: false, contrast: false, links: false, font: false, paused: false, reading: false };
}
function saveA11yPrefs(prefs) {
@@ -256,6 +256,10 @@
root.classList.toggle("a11y-pause-animations", on);
}
function applyReadingMode(on) {
root.classList.toggle("a11y-reading-mode", on);
}
/** Create a Font Awesome icon element (safe DOM, no innerHTML). */
function faIcon(classes) {
var i = doc.createElement("i");
@@ -274,6 +278,7 @@
var showLinks = body.getAttribute("data-a11y-links") === "1";
var showFont = body.getAttribute("data-a11y-font") === "1";
var showAnimations = body.getAttribute("data-a11y-animations") === "1";
var showReading = body.getAttribute("data-a11y-reading") === "1";
var pos = (body.getAttribute("data-a11y-pos") || "tl").toLowerCase();
if (!/^(br|bl|tr|tl)$/.test(pos)) pos = "tl";
@@ -417,6 +422,7 @@
addSwitchOption(showLinks, "links", "fa-solid fa-link", "Highlight links", applyLinks);
addSwitchOption(showFont, "font", "fa-solid fa-font", "Readable font", applyFont);
addSwitchOption(showAnimations, "paused", "fa-solid fa-pause", "Pause animations", applyPaused);
addSwitchOption(showReading, "reading", "fa-solid fa-book-open-reader", "Reading mode", applyReadingMode);
// Apply saved preferences on load
if (prefs.fontStep !== defaultStep) applyFontSize(prefs.fontStep);
@@ -425,6 +431,7 @@
if (prefs.links) applyLinks(true);
if (prefs.font) applyFont(true);
if (prefs.paused) applyPaused(true);
if (prefs.reading) applyReadingMode(true);
// If theme FAB is present, mount a11y toggle inside it; otherwise standalone
var fabSlot = doc.getElementById("mokoA11ySlot");
@@ -477,6 +484,37 @@
}
}
/**
* Relocate the theme FAB into the login card as a flush bottom bar (parity with
* MokoAI). Only on the com_users login view. The FAB (with its a11y toggle) becomes
* part of the card via the `in-card` class. Uses a MutationObserver in case the
* login form renders late; Joomla's login container is `.com-users-login`, with
* fallbacks in case a template override changes the wrapper.
*/
function relocateFabToLoginCard() {
if (!doc.body.classList.contains("view-login")) return;
function move() {
var fab = doc.getElementById("mokoThemeFab");
if (!fab) return false;
var card = doc.querySelector(".com-users-login, .login .card, main#maincontent .card, .login-max");
if (!card) return false;
fab.classList.add("in-card");
card.appendChild(fab);
return true;
}
if (move()) return;
if (typeof win.MutationObserver === "function") {
var obs = new win.MutationObserver(function () {
if (move()) obs.disconnect();
});
obs.observe(doc.body, { childList: true, subtree: true });
win.setTimeout(function () { obs.disconnect(); move(); }, 3000);
}
}
// ========================================================================
// TEMPLATE UTILITIES
// ========================================================================
@@ -940,6 +978,9 @@
buildA11yToolbar();
}
// On the login view, relocate the FAB into the login card as a flush bottom bar
relocateFabToLoginCard();
// Sticky header behavior
handleScroll();
win.addEventListener("scroll", handleScroll);
+7
View File
@@ -303,6 +303,13 @@
<option value="0">JNO</option>
<option value="1">JYES</option>
</field>
<field name="a11y_reading_mode" type="radio" default="1"
label="TPL_MOKO_A11Y_READING_MODE" description="TPL_MOKO_A11Y_READING_MODE_DESC"
layout="joomla.form.field.radio.switcher" filter="boolean"
showon="a11y_toolbar_enabled:1">
<option value="0">JNO</option>
<option value="1">JYES</option>
</field>
<!-- Position forced to bottom-right in index.php -->
<field name="a11y_toolbar_pos" type="hidden" default="br"
label="TPL_MOKO_A11Y_TOOLBAR_POS" description="TPL_MOKO_A11Y_TOOLBAR_POS_DESC"