Files
mabibli/MaBibli.Client/Composants/ArbreSerie.razor
T
mathieuandClaude Opus 5 c168959e12 Fait dire aux boutons repliables s'ils sont ouverts ou fermés
« aria-expanded="@_ouvert" » ne rend pas « true » ou « false » : Blazor y voit
un attribut de présence, comme disabled. Vrai donnait aria-expanded="", faux
retirait l'attribut — dans les deux cas, un lecteur d'écran n'apprenait rien,
et un bouton replié ne s'annonçait plus comme repliable.

Le défaut valait pour les six boutons de l'application. Une fonction nommée
remplace le booléen nu, qui se relisait comme correct.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 01:39:28 +02:00

134 lines
5.4 KiB
Plaintext

@using MaBibli.Shared.Dtos
@*
L'arbre des séries : rien d'autre que la descendance.
⚠️ Il dépliait auparavant la liste complète des tomes de chaque série, carte de livre
comprise. Un cycle de quatre séries occupait alors plusieurs écrans, et l'on ne voyait
justement plus ce qu'un arbre sert à voir — qui contient quoi. Les tomes ont leur écran,
celui de la série ; ici on ne montre que le titre, l'avancement, et le lien qui y mène.
Corollaire : tout est DÉPLIÉ par défaut, et l'on replie ce dont on ne veut pas. L'inverse
(tout replié) obligeait à ouvrir chaque nœud pour découvrir s'il contenait quelque chose,
c'est-à-dire à faire à la main le travail que l'arbre est censé faire.
*@
<li class="arbre-noeud" @key="Serie.Id">
<div class="arbre-ligne">
@if (Enfants.Count > 0)
{
<button type="button" class="arbre-plier" aria-expanded="@Aria.Etat(Deplie)"
aria-label="@($"{(Deplie ? "Replier" : "Déplier")} « {Serie.Titre} »")"
@onclick="Basculer">
<span aria-hidden="true">@(Deplie ? "▾" : "▸")</span>
</button>
}
else
{
@* Une puce à la place de la bascule : sans elle, les titres sans descendance ne
s'aligneraient pas sur ceux qui en ont. *@
<span class="arbre-puce" aria-hidden="true">•</span>
}
<a class="arbre-titre" href="@($"series/{Serie.Id}")">@Serie.Titre</a>
<span class="arbre-compte">@Avancement()</span>
@if (ActionsActives)
{
<span class="arbre-actions">
<button type="button" class="bouton bouton-compact"
disabled="@(_index <= 0)" title="@MotifBlocage"
aria-label="@($"Monter « {Serie.Titre} »")"
@onclick="Monter">▲</button>
<button type="button" class="bouton bouton-compact"
disabled="@(_index < 0 || _index == Soeurs.Count - 1)" title="@MotifBlocage"
aria-label="@($"Descendre « {Serie.Titre} »")"
@onclick="Descendre">▼</button>
</span>
}
</div>
@* Le chemin déjà rendu coupe la récursion : une boucle résiduelle en base ferait sinon
boucler le rendu Razor à l'infini. Le garde-fou serveur, lui, reste borné par le
nombre de séries. *@
@if (Deplie && Enfants.Count > 0)
{
<ul class="arbre-enfants">
@foreach (var enfant in Enfants.Where(e => !CheminAvecSerie.Contains(e.Id)))
{
<ArbreSerie Serie="enfant" Toutes="Toutes"
Totaux="Totaux" Chemin="@CheminAvecSerie"
ActionsActives="ActionsActives"
MotifBlocage="MotifBlocage"
Replies="Replies"
OnDeplacer="OnDeplacer" />
}
</ul>
}
</li>
@code {
[Parameter, EditorRequired] public SerieDto Serie { get; set; } = default!;
[Parameter, EditorRequired] public IReadOnlyList<SerieDto> Toutes { get; set; } = [];
[Parameter, EditorRequired] public IReadOnlyDictionary<int, (int Possedes, int Total)> Totaux { get; set; } = new Dictionary<int, (int, int)>();
[Parameter] public IReadOnlySet<int> Chemin { get; set; } = new HashSet<int>();
/// <summary>
/// Nœuds REPLIÉS, et non dépliés : le défaut est de tout montrer.
/// </summary>
/// <remarks>
/// État local de l'écran, volontairement non persistant : c'est un confort de lecture,
/// pas une propriété de la série.
/// </remarks>
[Parameter] public ISet<int> Replies { get; set; } = new HashSet<int>();
[Parameter] public bool ActionsActives { get; set; }
[Parameter] public string? MotifBlocage { get; set; }
[Parameter] public EventCallback<DeplacementSerie> OnDeplacer { get; set; }
private List<SerieDto> Soeurs => Toutes
.Where(s => s.SerieParenteId == Serie.SerieParenteId)
.OrderBy(s => s.Position)
.ThenBy(s => s.Id)
.ToList();
private List<SerieDto> Enfants => Toutes
.Where(s => s.SerieParenteId == Serie.Id)
.OrderBy(s => s.Position)
.ThenBy(s => s.Id)
.ToList();
private int _index => Soeurs.FindIndex(s => s.Id == Serie.Id);
private bool Deplie => !Replies.Contains(Serie.Id);
private IReadOnlySet<int> CheminAvecSerie => new HashSet<int>(Chemin) { Serie.Id };
/// <summary>
/// « 3 sur 7 tomes », en comptant les descendants — c'est ce qu'on vient chercher.
/// </summary>
private string Avancement()
{
var total = Totaux.TryGetValue(Serie.Id, out var valeur)
? valeur
: (Possedes: Serie.NombrePossedes, Total: Serie.Elements.Count);
return total.Total == 0
? "aucun tome recensé"
: $"{total.Possedes} sur {total.Total} tome{(total.Total > 1 ? "s" : "")}";
}
private void Basculer()
{
if (!Replies.Add(Serie.Id))
{
Replies.Remove(Serie.Id);
}
}
private Task Monter() => Deplacer(-1);
private Task Descendre() => Deplacer(1);
private Task Deplacer(int delta) =>
Serie.SerieParenteId is { } parent
? OnDeplacer.InvokeAsync(new DeplacementSerie(Serie.Id, parent, delta))
: Task.CompletedTask;
}