Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -583,6 +583,20 @@ Added the missing viewport meta, so phones get a real phone layout instead of a

---

### ✅ 3.24 Ledger Accessibility Follow-ups
**Status:** ✅ Complete
**Changelog:** `[Unreleased]` (part of 3.21's new interface)
**Priority:** Low
**Files Modified:** `static/ledger.js`, `static/ledger.css`, `static/themes.css`, `templates/base.html`, `templates/_belt_ladder.html`, `templates/active_session.html`, `templates/start_session.html`, `templates/settings.html`, `tests/test_routes.py`, `tests/test_theme_contrast.py`

Four small gaps from 3.21's final review, closed before release:
- **Theme menu:** its group labels are `role="presentation"`, since a menu may hold only groups and items, and its rail button has `aria-haspopup="menu"`.
- **Preset stops:** the selected stop is marked `aria-current="true"` instead of `aria-pressed`, so screen readers announce the current preset rather than a toggle.
- **History:** after a row is deleted, focus moves to the neighbouring row's Delete, or to Edit when none is left, instead of falling to the page (WCAG 2.4.3).
- **Settings:** Add profile renders open, so it works without JavaScript; the page's script folds it behind its button on load.

---

## Stats & Motivation

### 4.1 Estimated Time to Distance Goal
Expand Down
6 changes: 3 additions & 3 deletions static/ledger.css
Original file line number Diff line number Diff line change
Expand Up @@ -188,15 +188,15 @@ kbd { font-family: inherit; font-size: 11px; font-weight: 700; padding: 0 5px; b
background: var(--surface); color: var(--text); display: grid; justify-items: start; text-align: left; line-height: 16px; white-space: nowrap; }
.stop::before { content: ""; position: absolute; top: -13px; left: var(--tick, 50%); width: 1px; height: 9px; background: var(--muted); }
.stop:hover { background: var(--raised); }
.stop[aria-pressed="true"] { background: var(--primary); color: var(--on-primary); border-color: var(--primary); }
.stop[aria-pressed="true"] small { opacity: 1; }
.stop[aria-current="true"] { background: var(--primary); color: var(--on-primary); border-color: var(--primary); }
.stop[aria-current="true"] small { opacity: 1; }
.stop small { font-size: 12px; color: inherit; opacity: .8; font-variant-numeric: tabular-nums; display: flex; gap: 6px; align-items: center; }
.stop:disabled, .step:disabled { opacity: .45; }
.is-parked .ladder { pointer-events: none; }
.is-parked .track .fill { background: var(--chart-muted); }
.is-parked .stop { border-style: dashed; }
.is-parked .stop:disabled { opacity: 1; }
.is-parked .stop[aria-pressed="true"] { background: var(--raised); color: var(--text); border-color: var(--control-quiet); }
.is-parked .stop[aria-current="true"] { background: var(--raised); color: var(--text); border-color: var(--control-quiet); }
.kv { margin: 0; display: grid; grid-template-columns: max-content 1fr; gap: 0 32px; background: var(--raised); border-radius: var(--radius-card); padding: 4px 16px; }
.kv dt, .kv dd { margin: 0; padding: 8px 0; border-top: 1px solid var(--rule); }
.kv dt:first-of-type, .kv dd:first-of-type { border-top: 0; }
Expand Down
1 change: 1 addition & 0 deletions static/ledger.js
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ function renderThemeFx() {
var head = document.createElement('p');
head.className = 'wd-menu-label';
head.id = 'theme-group-' + label.toLowerCase();
head.setAttribute('role', 'presentation'); // a menu may only hold groups and items; the label names its group
head.textContent = label;
var box = document.createElement('div');
box.setAttribute('role', 'group');
Expand Down
2 changes: 1 addition & 1 deletion static/themes.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions templates/_belt_ladder.html
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@
('max_speed', 'Max', max_mph, 'M', 1, '--tx:-100%;--tick:100%')] %}
{% set pressed = (speed - mph) | abs < 0.05 %}
{% if parked %}
<button type="button" class="stop" aria-label="{{ label }}, {{ '%.1f'|format(mph) }} mph" style="--at:{{ '%.3f'|format(at) }};{{ extra }}" aria-pressed="{{ 'true' if pressed else 'false' }}" disabled>{{ label }}<small>{{ '%.1f'|format(mph) }} mph</small></button>
<button type="button" class="stop" aria-label="{{ label }}, {{ '%.1f'|format(mph) }} mph" style="--at:{{ '%.3f'|format(at) }};{{ extra }}" {% if pressed %} aria-current="true"{% endif %} disabled>{{ label }}<small>{{ '%.1f'|format(mph) }} mph</small></button>
{% else %}
<form method="post" action="{{ url_for(route) }}" style="display:contents">
<button type="submit" class="stop" aria-label="{{ label }}, {{ '%.1f'|format(mph) }} mph" style="--at:{{ '%.3f'|format(at) }};{{ extra }}" data-mph="{{ '%.1f'|format(mph) }}" aria-pressed="{{ 'true' if pressed else 'false' }}"{{ hotkey(keys) }}{{ ' disabled' if off }}>{{ label }}<small>{{ '%.1f'|format(mph) }} mph{% if keyboard_shortcuts_enabled %} <kbd aria-hidden="true">{{ keys }}</kbd>{% endif %}</small></button>
<button type="submit" class="stop" aria-label="{{ label }}, {{ '%.1f'|format(mph) }} mph" style="--at:{{ '%.3f'|format(at) }};{{ extra }}" data-mph="{{ '%.1f'|format(mph) }}" {% if pressed %} aria-current="true"{% endif %}{{ hotkey(keys) }}{{ ' disabled' if off }}>{{ label }}<small>{{ '%.1f'|format(mph) }} mph{% if keyboard_shortcuts_enabled %} <kbd aria-hidden="true">{{ keys }}</kbd>{% endif %}</small></button>
</form>
{% endif %}
{% endfor %}
Expand Down
2 changes: 1 addition & 1 deletion templates/active_session.html
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ <h2 id="speed-heading">Belt speed</h2>
ladder.setAttribute('aria-valuetext', text + ' mph');
ladder.style.setProperty('--speed-frac', Math.min(1, Math.max(0, (mph - minMph) / ((maxMph - minMph) || 1))).toFixed(3));
document.querySelectorAll('.stop[data-mph]').forEach(function(s) {
s.setAttribute('aria-pressed', Math.abs(parseFloat(s.dataset.mph) - mph) < 0.05 ? 'true' : 'false');
if (Math.abs(parseFloat(s.dataset.mph) - mph) < 0.05) s.setAttribute('aria-current', 'true'); else s.removeAttribute('aria-current');
});
}

Expand Down
2 changes: 1 addition & 1 deletion templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@
{% set on_settings = request.endpoint == 'settings_page' %}
<a class="ri" href="{{ url_for('root') }}" aria-label="Walk"{% if not on_settings %} aria-current="page"{% endif %}><i class="bi bi-person-walking" aria-hidden="true"></i><span class="lbl" aria-hidden="true">Walk</span><span class="tip" aria-hidden="true">Walk</span></a>
<hr>
<button type="button" class="ri" id="color-theme-toggle" popovertarget="color-theme-menu" aria-label="Color theme"><i class="bi bi-palette" aria-hidden="true"></i><span class="lbl" aria-hidden="true">Theme</span><span class="tip" aria-hidden="true">Color theme</span></button>
<button type="button" class="ri" id="color-theme-toggle" popovertarget="color-theme-menu" aria-haspopup="menu" aria-label="Color theme"><i class="bi bi-palette" aria-hidden="true"></i><span class="lbl" aria-hidden="true">Theme</span><span class="tip" aria-hidden="true">Color theme</span></button>
<div id="color-theme-menu" popover class="wd-theme-menu" role="menu" aria-label="Color theme"></div>
<button type="button" class="ri" id="theme-toggle" aria-label="Appearance: System"><i class="bi bi-display" aria-hidden="true"></i><span class="lbl" aria-hidden="true">System</span><span class="tip" aria-hidden="true">Appearance</span></button>
{% if keyboard_shortcuts_enabled %}
Expand Down
13 changes: 8 additions & 5 deletions templates/settings.html
Original file line number Diff line number Diff line change
Expand Up @@ -57,16 +57,17 @@ <h2 id="profiles-heading">Profiles</h2>
{% endfor %}
{% set adding = profile_form | length > 0 %}
<div>
<button type="button" class="wd-btn wd-btn-sm" id="add-profile-btn" aria-controls="add-profile" aria-expanded="{{ 'true' if adding else 'false' }}"{% if adding %} hidden{% endif %}>
<button type="button" class="wd-btn wd-btn-sm" id="add-profile-btn" aria-controls="add-profile" aria-expanded="true" hidden>
<i class="bi bi-plus-lg" aria-hidden="true"></i>Add profile
</button>
</div>
<div id="add-profile"{% if not adding %} hidden{% endif %}>
{# Rendered open so it works without JavaScript; the script below folds it behind the button. #}
<div id="add-profile" data-open="{{ 'true' if adding else 'false' }}">
<form method="post" action="{{ url_for('profile_create') }}" aria-label="Add profile" class="wd-profile">
{{ profile_fields(profile_form, 'new') }}
<div class="wd-btn-row">
<button type="submit" class="wd-btn wd-btn-primary wd-btn-sm">Save profile</button>
<button type="button" class="wd-btn wd-btn-sm" id="add-profile-cancel">Cancel</button>
<button type="button" class="wd-btn wd-btn-sm" id="add-profile-cancel" hidden>Cancel</button>
</div>
</form>
</div>
Expand Down Expand Up @@ -219,12 +220,14 @@ <h2 id="profiles-heading">Profiles</h2>

var addBtn = document.getElementById('add-profile-btn');
var addBox = document.getElementById('add-profile');
function setAdding(on) {
function setAdding(on, quiet) {
addBox.hidden = !on;
addBtn.hidden = on;
addBtn.setAttribute('aria-expanded', on ? 'true' : 'false');
(on ? document.getElementById('name-new') : addBtn).focus();
if (!quiet) (on ? document.getElementById('name-new') : addBtn).focus();
}
document.getElementById('add-profile-cancel').hidden = false;
setAdding(addBox.dataset.open === 'true', true);
addBtn.addEventListener('click', function() { setAdding(true); });
document.getElementById('add-profile-cancel').addEventListener('click', function() { setAdding(false); });

Expand Down
4 changes: 4 additions & 0 deletions templates/start_session.html
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,11 @@ <h2 id="history-heading">{{ history_heading }}</h2>
var row = btn.closest('tr');
var banner = document.getElementById('health-export-banner');
if (banner && banner.dataset.sessionId === row.dataset.sessionId) banner.remove();
// Focus would fall to <body> with the removed button (WCAG 2.4.3): move it to a neighbour's Delete, else Edit.
var near = row.nextElementSibling || row.previousElementSibling;
var next = near && near.querySelector('[data-delete-url]');
row.remove();
(next || editBtn).focus();
});
});
});
Expand Down
25 changes: 20 additions & 5 deletions tests/test_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,20 @@ def test_active_belt_ladder_is_a_slider(client, running, monkeypatch):
assert 'aria-valuenow="2.8"' in ladder and 'aria-valuetext="2.8 mph"' in ladder


def test_selected_preset_is_current_not_pressed(client, running, monkeypatch):
monkeypatch.setattr(app, "SLOW_WALK_SPEED_KMH", 4.5)
monkeypatch.setattr(app, "current_speed_kmh", 4.5)
html = client.get("/").get_data(as_text=True)
stops = re.findall(r'<button [^>]*class="stop"[^>]*>', html)
assert len(stops) == 3 and not any("aria-pressed" in b for b in stops)
assert [("Moderate" in b, 'aria-current="true"' in b) for b in stops] == [(False, False), (True, True), (False, False)]


def test_theme_menu_button_announces_a_menu(client):
html = client.get("/").get_data(as_text=True)
assert re.search(r'<button [^>]*id="color-theme-toggle"[^>]*aria-haspopup="menu"', html)


def test_active_controls_start_disabled_while_transitioning(client, running, monkeypatch):
monkeypatch.setattr(app, "_belt_transitioning", True)
html = client.get("/").get_data(as_text=True)
Expand Down Expand Up @@ -1234,10 +1248,12 @@ def test_export_csv_has_no_walker_column_without_profiles(client):
assert header == "date,start_time,end_time,duration_seconds,distance_km,distance_mi,steps,calories,avg_speed_kmh,avg_speed_mph,id,has_samples"


def test_add_profile_reveal_without_bootstrap(client):
def test_add_profile_reachable_without_js(client):
# The form renders open, so it works with JavaScript off; ledger.js folds it behind the button on load.
html = client.get("/settings").get_data(as_text=True)
assert 'aria-controls="add-profile" aria-expanded="false"' in html
assert '<div id="add-profile" hidden>' in html
assert '<div id="add-profile" data-open="false">' in html
assert re.search(r'<button [^>]*id="add-profile-btn"[^>]*aria-controls="add-profile"[^>]*hidden>', html)
assert re.search(r'<button [^>]*id="add-profile-cancel"[^>]*hidden>', html)
assert "data-bs-" not in html
assert "confirm(" not in html

Expand All @@ -1262,8 +1278,7 @@ def test_settings_labels_name_their_inputs(client):

def test_add_profile_form_open_after_create_error(client):
html = client.post("/profiles", data=profile_form(lb="49")).get_data(as_text=True)
assert 'aria-controls="add-profile" aria-expanded="true"' in html
assert '<div id="add-profile">' in html
assert '<div id="add-profile" data-open="true">' in html



Expand Down
2 changes: 1 addition & 1 deletion tests/test_theme_contrast.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,4 +100,4 @@ def test_theme_contrast(tid, mode):

def test_vt_dark_selected_stop_has_a_ring():
css = (STATIC / "themes.css").read_text()
assert re.search(r':root\[data-color-theme="vt"\]\[data-theme="dark"\] \.stop\[aria-pressed="true"\] \{[^}]*box-shadow: 0 0 0 2px #e5751f', css)
assert re.search(r':root\[data-color-theme="vt"\]\[data-theme="dark"\] \.stop\[aria-current="true"\] \{[^}]*box-shadow: 0 0 0 2px #e5751f', css)
Loading