Skip to content
Open
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
54 changes: 54 additions & 0 deletions client-admin/docs/a11y-audit-week-13-14.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Admin a11y — snabb audit (Vecka 13–14 stretch)

**Datum:** 2026-08-03
**Branch:** `feature/a11y-admin-stretch`
**App:** `client-admin` (serveras som static via `file-server` / nginx)

## Hur man kör

- Docker: byggd in i `file-server` — öppna `https://localhost/` (admin lander) efter `make DETACH=true rebuild-web`
- Lokalt: `cd client-admin && npm start` (webpack-dev-server)
- Testkonton (dev): `admin@polis.test` / `Te$tP@ssw0rd*`

## Kritiska vyer

| Vy | Fil | Kommentar |
|----|-----|-----------|
| Sign in | `landers/SignIn.js` | En knapp `#signinButton` → OIDC |
| Conversation list | `Conversations.js` | Create + list; superadmin-filter |
| Account | `Account.js` | Endast text, få interaktiva fält |
| Conversation admin | `conversation-admin/*` | Moderation, stats, invite — större yta |

## Fynd (prioritet)

| ID | Severity | Vy | Problem | Föreslagen fix |
|----|----------|-----|---------|----------------|
| A1 | P1 | Conversations (All / superadmin) | Filter-`input`/`select` har bara `placeholder`, ingen `<label>` / `aria-label` | Synliga labels eller `aria-label` på varje kontroll |
| A2 | P1 | Lander layout | Ingen `<main>` landmark i `lander-layout.js` / `MainLayout` | Wrappa content i `<main>` |
| A3 | P2 | Globalt | Ingen tydlig `:focus-visible`-stil i admin theme (jämfört med alpha) | Global focus-visible i theme/CSS |
| A4 | P2 | Sign in | OK knapptext; OIDC-form utanför vår kontroll | Ingen fix i admin (OIDC simulator) |

## Scope för stretch (max 1–3 fixer)

Rekommenderat: **A1 + A2** (labels på filter + `<main>`). A3 om tid finns.

### Åtgärdat (2026-08-03)

- **A1:** Synliga `<label>` + `htmlFor`/`id` på alla superadmin-filter i `Conversations.js`
- **A2:** `<main>` i `lander-layout.js`; `<nav aria-label="Admin">` + `<main>` i `MainLayout.js`
- **A3:** Ej gjort (P2, kvar som rekommendation)

## Metod

Kodgranskning + jämförelse med alpha P0-mönster.

## Verify (2026-08-03)

| Check | Resultat |
|-------|----------|
| `npm run lint` (client-admin) | Pass |
| Jest (`SignIn`/`home`, Docker Node 22) | Pass |
| Kodgranskning A1/A2 | Pass — labels + landmarks på plats |
| Manuell Tab i browser | Rekommenderas efter `rebuild-web` / `npm start` |

**Slutsats:** Stretch-leveransen är lint-ren, Jest grön i Docker, och kodmässigt klar. Full UI-Tab efter rebuild av file-server.
39 changes: 39 additions & 0 deletions client-admin/docs/a11y-delivery-week-13-14.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Admin a11y — leverans Vecka 13–14 (stretch)

**Branch:** `feature/a11y-admin-stretch`
**Datum:** 2026-08-03
**Audit:** [a11y-audit-week-13-14.md](./a11y-audit-week-13-14.md)

## Vad som levereras

Begränsad a11y-förbättring i `client-admin` (P1), i linje med alpha-mönster från tidigare veckor.

| ID | Fix | Filer |
|----|-----|-------|
| A1 | Synliga labels + `htmlFor`/`id` på superadmin-filter | `Conversations.js` |
| A2 | Landmarks: `<main>`, `<nav aria-label="Admin">` | `lander-layout.js`, `MainLayout.js` |

## Medvetet utanför scope

- **A3** global `:focus-visible` (P2)
- Conversation-admin moderation/stats/invite (större yta)
- OIDC sign-in-formulär (extern simulator)

## PR-text (kopiera)

**Titel:** `Vecka 13-14: Admin a11y stretch (P1)`

```markdown
## Summary
- Lägger synliga labels på superadmin conversation-filter (`Conversations.js`)
- Lägger till `<main>` / `<nav>` landmarks i lander- och MainLayout

## Test plan
- [ ] `cd client-admin && npm run lint`
- [ ] Manuell: logga in som admin, öppna All Conversations (superadmin) och Tab genom filterfält
- [ ] Manuell: kontrollera att lander/signin och inloggad app har en `main`-landmark (devtools)

## Notes
- Stretch-scope; se `client-admin/docs/a11y-audit-week-13-14.md`
- Base: `feature/a11y-manual-i18n` (eller aktuell a11y-stack-tip)
```
3 changes: 3 additions & 0 deletions client-admin/src/components/MainLayout.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ const MainLayout = () => {
}}>
{/* Navigation Sidebar - stacks vertically on mobile/tablet, sidebar on desktop */}
<Box
as="nav"
aria-label="Admin"
sx={{
py: [2, 2, 4],
px: [2, 3, 4],
Expand Down Expand Up @@ -48,6 +50,7 @@ const MainLayout = () => {
</Box>
{/* Main Content Area */}
<Box
as="main"
sx={{
p: [2, 3, 4],
flex: '1 1 auto',
Expand Down
169 changes: 104 additions & 65 deletions client-admin/src/components/conversations-and-account/Conversations.js
Original file line number Diff line number Diff line change
Expand Up @@ -195,73 +195,112 @@ const Conversations = () => {

const renderAllControls = () => {
if (!(activeView === 'all' && superAdmin)) return null
const fieldSx = {
display: 'flex',
flexDirection: 'column',
gap: 1,
fontSize: 1,
fontWeight: 600
}
return (
<Box sx={{ mb: [3] }}>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 3, alignItems: 'center' }}>
<input
type="text"
placeholder="Owner email contains..."
value={filters.owner_email}
onChange={(e) => setFilters((f) => ({ ...f, owner_email: e.target.value }))}
/>
<select
value={filters.is_active}
onChange={(e) =>
setFilters((f) => ({
...f,
is_active: e.target.value === '' ? '' : e.target.value === 'true'
}))
}>
<option value="">All statuses</option>
<option value="true">Active</option>
<option value="false">Inactive</option>
</select>
<input
type="number"
min="0"
placeholder="Min comments"
value={filters.min_comment_count}
onChange={(e) => setFilters((f) => ({ ...f, min_comment_count: e.target.value }))}
style={{ width: 120 }}
/>
<input
type="number"
min="0"
placeholder="Min participants"
value={filters.min_participant_count}
onChange={(e) => setFilters((f) => ({ ...f, min_participant_count: e.target.value }))}
style={{ width: 140 }}
/>
<input
type="number"
min="0"
placeholder="Updated in last N days"
value={filters.recently_updated_days}
onChange={(e) => setFilters((f) => ({ ...f, recently_updated_days: e.target.value }))}
style={{ width: 200 }}
/>
<input
type="number"
min="0"
placeholder="Created in last N days"
value={filters.recently_created_days}
onChange={(e) => setFilters((f) => ({ ...f, recently_created_days: e.target.value }))}
style={{ width: 200 }}
/>
<select
value={sort.sort_by}
onChange={(e) => setSort((s) => ({ ...s, sort_by: e.target.value }))}>
<option value="updated">Sort: Updated</option>
<option value="created">Sort: Created</option>
<option value="participant_count">Sort: Participants</option>
<option value="comment_count">Sort: Comments</option>
</select>
<select
value={sort.sort_dir}
onChange={(e) => setSort((s) => ({ ...s, sort_dir: e.target.value }))}>
<option value="desc">Desc</option>
<option value="asc">Asc</option>
</select>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 3, alignItems: 'flex-end' }}>
<Box as="label" htmlFor="filter-owner-email" sx={fieldSx}>
Owner email
<input
id="filter-owner-email"
type="text"
placeholder="Owner email contains..."
value={filters.owner_email}
onChange={(e) => setFilters((f) => ({ ...f, owner_email: e.target.value }))}
/>
</Box>
<Box as="label" htmlFor="filter-is-active" sx={fieldSx}>
Status
<select
id="filter-is-active"
value={filters.is_active}
onChange={(e) =>
setFilters((f) => ({
...f,
is_active: e.target.value === '' ? '' : e.target.value === 'true'
}))
}>
<option value="">All statuses</option>
<option value="true">Active</option>
<option value="false">Inactive</option>
</select>
</Box>
<Box as="label" htmlFor="filter-min-comments" sx={fieldSx}>
Min comments
<input
id="filter-min-comments"
type="number"
min="0"
placeholder="Min comments"
value={filters.min_comment_count}
onChange={(e) => setFilters((f) => ({ ...f, min_comment_count: e.target.value }))}
style={{ width: 120 }}
/>
</Box>
<Box as="label" htmlFor="filter-min-participants" sx={fieldSx}>
Min participants
<input
id="filter-min-participants"
type="number"
min="0"
placeholder="Min participants"
value={filters.min_participant_count}
onChange={(e) => setFilters((f) => ({ ...f, min_participant_count: e.target.value }))}
style={{ width: 140 }}
/>
</Box>
<Box as="label" htmlFor="filter-updated-days" sx={fieldSx}>
Updated in last N days
<input
id="filter-updated-days"
type="number"
min="0"
placeholder="Updated in last N days"
value={filters.recently_updated_days}
onChange={(e) => setFilters((f) => ({ ...f, recently_updated_days: e.target.value }))}
style={{ width: 200 }}
/>
</Box>
<Box as="label" htmlFor="filter-created-days" sx={fieldSx}>
Created in last N days
<input
id="filter-created-days"
type="number"
min="0"
placeholder="Created in last N days"
value={filters.recently_created_days}
onChange={(e) => setFilters((f) => ({ ...f, recently_created_days: e.target.value }))}
style={{ width: 200 }}
/>
</Box>
<Box as="label" htmlFor="filter-sort-by" sx={fieldSx}>
Sort by
<select
id="filter-sort-by"
value={sort.sort_by}
onChange={(e) => setSort((s) => ({ ...s, sort_by: e.target.value }))}>
<option value="updated">Sort: Updated</option>
<option value="created">Sort: Created</option>
<option value="participant_count">Sort: Participants</option>
<option value="comment_count">Sort: Comments</option>
</select>
</Box>
<Box as="label" htmlFor="filter-sort-dir" sx={fieldSx}>
Sort direction
<select
id="filter-sort-dir"
value={sort.sort_dir}
onChange={(e) => setSort((s) => ({ ...s, sort_dir: e.target.value }))}>
<option value="desc">Desc</option>
<option value="asc">Asc</option>
</select>
</Box>
<Button
onClick={() => {
setAllConversations(null)
Expand Down
2 changes: 1 addition & 1 deletion client-admin/src/components/landers/lander-layout.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const Layout = ({ children }) => {
padding: ['0 1rem 1rem', '0 1.5rem 1.45rem', '0 1.0875rem 1.45rem']
}}>
<Header />
<Box>{children}</Box>
<Box as="main">{children}</Box>
<Footer />
</Box>
)
Expand Down
Loading