Semantic HTML: home page card grid — <ul>/<li>/<figure>/<nav> refactor

Replace presentational divs in index.php and main.css with elements that
carry correct semantic meaning, fixing multiple WCAG 2.1 AA issues:

index.php:
- <div class="cards-container"> → <ul class="cards-container"> (list of navigable items)
- <a class="card-link"><div class="card">…</div></a> → <li class="card"><a> (block link
  is the <a>, <li> is the container; removes the redundant .card div wrapper)
- <div class="card__media"> → <figure class="card__media"> when wrapping an <img>;
  gradient placeholder stays as <div> (presentational, aria-hidden)
- Improved alt text: "Couverture — [title] par [authors]" instead of bare title
- Removed <div class="card__info"> wrapper; caption is now a bare <p class="card__caption">
  directly inside the <a>
- <div class="filter-info"> → <p class="filter-info" role="status"> (live-region
  semantics; announces filter state to screen readers)
- ✕ symbol in clear-filter link wrapped in <span aria-hidden="true">
- Gradient placeholder div gets aria-hidden="true" (decorative; caption below carries text)
- Empty-state <p style="…"> → <li class="cards-empty"> (removes inline style)
- <div class="pagination-wrap"> → <nav class="pagination-wrap" aria-label="Pagination">
  with <ul>/<li> children; page-info <span> → <li aria-current="page">

main.css:
- .cards-container: add list-style:none; margin:0; padding:0 (reset <ul> defaults)
- Remove .card-link rule; replace with .card > a (block flex link, no separate class)
- .card__media: add margin:0 to reset <figure> default margin
- Remove .card__info rules; rename .authors to .card__caption with same styles
- Add .cards-empty rule (removes last inline style from index.php)
- .pagination-wrap: restructured for <nav>/<ul>; inner <ul> carries the flex layout
- prefers-reduced-motion: add .card__media--gradient guard

WCAG criteria addressed: 1.1.1 (alt text), 1.3.1 (info & relationships via semantic
list/figure), 2.4.1 (filter-info now live region), role="status" on filter banner.
This commit is contained in:
Pontoporeia
2026-03-29 16:13:02 +02:00
parent c352a392a1
commit ac872c1fe0
3 changed files with 114 additions and 94 deletions

View File

@@ -22,6 +22,9 @@
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 0;
list-style: none;
margin: 0;
padding: 0;
}
@media (min-width: 1400px) {
@@ -36,13 +39,7 @@
}
}
/* Each card = media thumbnail + text below */
.card-link {
text-decoration: none;
color: inherit;
display: block;
}
/* Each card = list item containing a block <a> link */
.card {
display: flex;
flex-direction: column;
@@ -52,12 +49,22 @@
overflow: hidden;
}
.card > a {
text-decoration: none;
color: inherit;
display: flex;
flex-direction: column;
flex: 1;
}
/* card__media is a <figure> (with image) or <div> (gradient placeholder) */
.card__media {
width: 100%;
aspect-ratio: 4/3;
overflow: hidden;
background: #e8e8e8;
position: relative;
margin: 0; /* reset <figure> default margin */
}
.card__media img,
@@ -120,26 +127,22 @@
font-style: italic;
}
.card__info {
/* Card caption — <p> directly inside the <a>, below the media */
.card__caption {
padding: 0.55rem 0.5rem 0.65rem;
font-size: 0.88rem;
line-height: 1.35;
color: var(--black);
}
.card__info .authors {
margin: 0;
font-weight: 400;
}
.card__info .title {
margin: 0;
font-weight: 400;
color: var(--text-muted);
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
/* Empty-state message shown when no cards exist */
.cards-empty {
padding: 2rem;
color: #6b6b6b;
font-size: 0.9rem;
list-style: none;
}
/* Filter info */
@@ -169,14 +172,19 @@
/* Pagination */
.pagination-wrap {
border-top: 1px solid var(--border-color);
background: var(--white);
flex-shrink: 0;
}
.pagination-wrap ul {
display: flex;
justify-content: center;
align-items: center;
gap: 0.5rem;
padding: 1rem;
border-top: 1px solid var(--border-color);
background: var(--white);
flex-shrink: 0;
list-style: none;
margin: 0;
}
.pagination-btn {
@@ -226,4 +234,7 @@
.card:hover .card__media video {
transform: none;
}
.card__media--gradient {
transition: none;
}
}

View File

@@ -69,89 +69,98 @@ $extraCss = ['assets/main.css'];
<?php include APP_ROOT . '/templates/search-bar.php'; ?>
<?php if ($year): ?>
<div class="filter-info">
<p class="filter-info" role="status">
Année : <?= htmlspecialchars($year) ?>
<a href="index.php" class="clear-filter"> Réinitialiser</a>
</div>
<a href="index.php" class="clear-filter"><span aria-hidden="true">✕</span> Réinitialiser</a>
</p>
<?php elseif ($isDefaultView && !empty($latestYear)): ?>
<div class="filter-info home-latest-label">
<p class="filter-info home-latest-label" role="status">
Découvrez les TFE de <?= (int)$latestYear ?> — sélection aléatoire
</div>
</p>
<?php endif; ?>
<main class="home-main" id="main-content">
<div class="cards-container">
<ul class="cards-container">
<?php foreach ($itemsToLoad as $item): ?>
<a href="tfe.php?id=<?= (int)$item["id"] ?>" class="card-link">
<div class="card">
<div class="card__media">
<?php
// Resolve thumbnail: banner_path → cover file → gradient placeholder
$thumb = null;
<li class="card">
<a href="tfe.php?id=<?= (int)$item["id"] ?>">
<?php
// Resolve thumbnail: banner_path → cover file → gradient placeholder
$thumb = null;
// 1. Banner path (dedicated home thumbnail)
if (!empty($item['banner_path'])) {
$thumb = $item['banner_path'];
}
// 1. Banner path (dedicated home thumbnail)
if (!empty($item['banner_path'])) {
$thumb = $item['banner_path'];
}
// 2. Cover image from thesis_files (batch-loaded above)
if (!$thumb && isset($coverMap[$item['id']])) {
$thumb = $coverMap[$item['id']];
}
// 2. Cover image from thesis_files (batch-loaded above)
if (!$thumb && isset($coverMap[$item['id']])) {
$thumb = $coverMap[$item['id']];
}
// 3. Fall through to gradient
?>
<?php if ($thumb): ?>
// 3. Fall through to gradient
?>
<?php if ($thumb): ?>
<figure class="card__media">
<img src="/media.php?path=<?= urlencode($thumb) ?>"
alt="<?= htmlspecialchars($item['title']) ?>"
alt="Couverture — <?= htmlspecialchars($item['title']) ?> par <?= htmlspecialchars($item['authors'] ?? '') ?>"
loading="lazy">
<?php else: ?>
<?php
$hue = ($item['id'] * 47 + 160) % 360;
$hue2 = ($hue + 40) % 360;
?>
<div class="card__media--gradient"
style="background:linear-gradient(135deg,hsl(<?= $hue ?>,55%,40%),hsl(<?= $hue2 ?>,50%,28%));">
<span class="card__gradient-author"><?= htmlspecialchars($item['authors'] ?? '') ?></span>
<span class="card__gradient-title"><?= htmlspecialchars($item['title']) ?></span>
</div>
<?php endif; ?>
</div>
<div class="card__info">
<p class="authors"><?= htmlspecialchars($item["authors"] ?? '') ?><?php if (!empty($item['authors']) && !empty($item['title'])): ?> <?php endif; ?><?= htmlspecialchars($item["title"]) ?></p>
</div>
</div>
</a>
</figure>
<?php else: ?>
<?php
$hue = ($item['id'] * 47 + 160) % 360;
$hue2 = ($hue + 40) % 360;
?>
<div class="card__media card__media--gradient"
style="background:linear-gradient(135deg,hsl(<?= $hue ?>,55%,40%),hsl(<?= $hue2 ?>,50%,28%));"
aria-hidden="true">
<span class="card__gradient-author"><?= htmlspecialchars($item['authors'] ?? '') ?></span>
<span class="card__gradient-title"><?= htmlspecialchars($item['title']) ?></span>
</div>
<?php endif; ?>
<p class="card__caption"><?= htmlspecialchars($item["authors"] ?? '') ?><?php if (!empty($item['authors']) && !empty($item['title'])): ?> <?php endif; ?><?= htmlspecialchars($item["title"]) ?></p>
</a>
</li>
<?php endforeach; ?>
<?php if (empty($itemsToLoad)): ?>
<p style="padding:2rem;color:#666;">Aucun mémoire trouvé.</p>
<li class="cards-empty">Aucun mémoire trouvé.</li>
<?php endif; ?>
</div>
</ul>
<?php if ($totalPages > 1): ?>
<div class="pagination-wrap">
<nav class="pagination-wrap" aria-label="Pagination">
<?php $yearParam = $year ? '&year=' . (int)$year : ''; ?>
<a href="?page=1<?= $yearParam ?>"
class="pagination-btn <?= $page <= 1 ? 'disabled' : '' ?>"
<?= $page <= 1 ? 'aria-disabled="true" tabindex="-1"' : '' ?>
aria-label="Première page">«</a>
<a href="?page=<?= max(1, $page - 1) . $yearParam ?>"
class="pagination-btn <?= $page <= 1 ? 'disabled' : '' ?>"
<?= $page <= 1 ? 'aria-disabled="true" tabindex="-1"' : '' ?>
aria-label="Page précédente"></a>
<span class="pagination-info">
<span class="page-current"><?= $page ?></span> / <?= $totalPages ?>
</span>
<a href="?page=<?= min($totalPages, $page + 1) . $yearParam ?>"
class="pagination-btn <?= $page >= $totalPages ? 'disabled' : '' ?>"
<?= $page >= $totalPages ? 'aria-disabled="true" tabindex="-1"' : '' ?>
aria-label="Page suivante"></a>
<a href="?page=<?= $totalPages . $yearParam ?>"
class="pagination-btn <?= $page >= $totalPages ? 'disabled' : '' ?>"
<?= $page >= $totalPages ? 'aria-disabled="true" tabindex="-1"' : '' ?>
aria-label="Dernière page">»</a>
</div>
<ul>
<li>
<a href="?page=1<?= $yearParam ?>"
class="pagination-btn <?= $page <= 1 ? 'disabled' : '' ?>"
<?= $page <= 1 ? 'aria-disabled="true" tabindex="-1"' : '' ?>
aria-label="Première page">«</a>
</li>
<li>
<a href="?page=<?= max(1, $page - 1) . $yearParam ?>"
class="pagination-btn <?= $page <= 1 ? 'disabled' : '' ?>"
<?= $page <= 1 ? 'aria-disabled="true" tabindex="-1"' : '' ?>
aria-label="Page précédente"></a>
</li>
<li class="pagination-info" aria-current="page">
<span class="page-current"><?= $page ?></span> / <?= $totalPages ?>
</li>
<li>
<a href="?page=<?= min($totalPages, $page + 1) . $yearParam ?>"
class="pagination-btn <?= $page >= $totalPages ? 'disabled' : '' ?>"
<?= $page >= $totalPages ? 'aria-disabled="true" tabindex="-1"' : '' ?>
aria-label="Page suivante"></a>
</li>
<li>
<a href="?page=<?= $totalPages . $yearParam ?>"
class="pagination-btn <?= $page >= $totalPages ? 'disabled' : '' ?>"
<?= $page >= $totalPages ? 'aria-disabled="true" tabindex="-1"' : '' ?>
aria-label="Dernière page">»</a>
</li>
</ul>
</nav>
<?php endif; ?>
</main>