Checklist
Une liste de premiers pas, ce qui coche vraiment un élément, son lien avec les visites, et les deux composants qui l'affichent.
Une checklist est une liste fixe de premiers pas présentée à côté de l’application. C’est une fonctionnalité distincte des visites, avec son propre provider.
Checklist et ChecklistItem
interface Checklist {
id: string;
items: ChecklistItem[];
}
interface ChecklistItem {
id: string;
title?: string;
titleKey?: string;
body?: string;
bodyKey?: string;
tourId?: string;
href?: string;
}
title et body sont du texte littéral ; titleKey et bodyKey passent par la
prop translate du provider, exactement comme une étape de visite.
tourId et href décrivent ce qui se passe quand l’élément est activé, et tourId
l’emporte si les deux sont fournis. Montez ChecklistProvider à l’intérieur de
GuideProvider pour qu’un élément portant un tourId puisse l’atteindre :
import {
ChecklistProvider,
GuideProvider,
createBrowserStorage,
type Checklist,
} from '@apollovisionlabs/guide-core';
import { ChecklistLauncher, GuideTour } from '@apollovisionlabs/guide-mui';
import { productTour } from './tours';
const storage = createBrowserStorage('my-app');
const onboarding: Checklist = {
id: 'onboarding',
items: [
{ id: 'tour', title: 'Suivre la visite du produit', tourId: 'product' },
{ id: 'projects', title: 'Ouvrir la page projets', href: '/projects' },
{ id: 'theme', title: 'Essayer le thème sombre' },
],
};
export function App({ navigate }: { navigate: (path: string) => void }) {
return (
<GuideProvider tours={[productTour]} navigate={navigate} storage={storage}>
<ChecklistProvider checklists={[onboarding]} navigate={navigate} storage={storage}>
<AppRoutes />
<GuideTour />
<ChecklistLauncher checklistId="onboarding" title="Premiers pas" />
</ChecklistProvider>
</GuideProvider>
);
}
Les deux providers partagent une même instance de stockage dans le cas normal : la
progression des visites vit sous tour:<id>, celle des checklists sous
checklist:<id>, donc une seule implémentation de GuideStorage sert les deux.
Ce qui coche un élément, et ce qui ne le coche pas
Trois choses cochent un élément : l’utilisateur qui clique sa case, votre appel à
complete, et la fin d’une visite dont l’élément porte l’identifiant.
Un élément href navigue, et rien d’autre. Arriver sur une page ne prouve pas que
quelqu’un y a fait quoi que ce soit : seule une coche délibérée le ferme. Attendre
que la navigation coche l’élément est la première surprise.
Un élément sans tourId ni href se coche en l’activant : cliquer sa ligne le
bascule, comme la case.
complete(itemId) est idempotente. Cocher un élément déjà coché ne change rien et
n’émet rien : l’appeler depuis un effet qui se déclenche souvent est sans risque.
toggle(itemId) décoche un élément déjà coché, et décocher n’émet aucun
événement : checklist:item-complete décrit une progression, pas chaque changement
d’état. checklist:complete est émis au moment où le dernier élément se coche, une
fois par franchissement.
Un élément dont le tourId ne désigne aucune visite du GuideProvider échoue en
silence. Le démarrage est rejeté, le provider le rattrape et avertit une fois avec
[guide] starting a tour for a checklist item failed, et rien d’autre ne se passe :
aucun événement, rien à l’écran, et l’élément reste décoché. Cet avertissement est
émis une fois par provider et non par clic : un second élément cassé après le
premier est donc muet. Gardez les tourId des checklists et les Tour.id dans le
même module, ou couverts par le même test.
useChecklist
import { useChecklist } from '@apollovisionlabs/guide-core';
function Progress() {
const {
items, // ResolvedChecklistItem[] : id, title, body, completed, tourId, href
completedCount,
total,
isComplete,
dismissed,
restored, // la lecture initiale de stockage de cette checklist est-elle arrivée ?
activate, // (itemId) => void : lance la visite, l'href, ou bascule la coche
toggle, // (itemId) => void
complete, // (itemId) => void, idempotente
dismiss, // () => void
reset, // () => void
} = useChecklist('onboarding');
if (!restored) return null;
if (dismissed) return null;
return <p>{`${completedCount} sur ${total}`}</p>;
}
Toutes les actions sont déjà liées à l’identifiant de la checklist : aucune ne le
redemande. items porte du texte déjà résolu par translate, donc un rendu
personnalisé ne touche jamais aux clés de traduction.
restored empêche une checklist d’afficher un instant son état initial vide avant
que le vrai n’arrive. Il vaut true immédiatement quand ChecklistProvider n’a
pas de prop storage, puisqu’il n’y a rien à attendre, et il devient true une
fois que la lecture propre à cette checklist s’est terminée, qu’elle ait abouti ou
échoué. Il se règle indépendamment par checklist : une lecture lente ou bloquée
pour une checklist d’un provider qui en tient plusieurs ne bloque jamais le
restored d’une autre, et un rendu personnalisé qui l’attend, comme le fait la
garde ci-dessus, n’affiche jamais le lanceur d’une checklist fermée pendant une
image avant qu’il ne disparaisse, ni le compte périmé d’une checklist en cours
avant qu’il ne saute à la bonne valeur.
useChecklist lève une erreur hors d’un ChecklistProvider, et pour un
identifiant que le provider ne possède pas. Contrairement aux visites, les
identifiants de checklists en double ne sont pas rejetés : la dernière déclarée
l’emporte, en silence.
Le lien avec les visites
Un élément portant un tourId lance cette visite quand il est activé, et terminer
la visite coche l’élément. Le lien se fait par identifiant et n’est pas exclusif :
tous les éléments de toutes les checklists qui nomment la visite terminée sont
cochés dans la même passe. Deux éléments qui pointent vers une même visite sont
donc fermés par un seul parcours, ce qui est en général une erreur de modélisation
à repérer.
Fermeture et remise à zéro
dismiss() masque la checklist et émet checklist:dismiss. L’état est persisté
avec les coches : une liste fermée le reste après rechargement. reset() efface les
deux : aucune coche, plus de fermeture. Il n’y a pas d’autre moyen de rouvrir la
liste que reset.
La progression restaurée depuis le stockage est fusionnée avec ce qui est à
l’écran, et non substituée, parce qu’une lecture lente ne doit pas défaire une coche
que l’utilisateur vient de poser. Cette fusion est une union et ne peut donc pas
retrancher : décocher un élément, ou appeler reset, pendant que la lecture
initiale est encore en vol sera annulé quand elle arrivera. La fenêtre se limite à
une lecture au montage.
Les deux composants
Checklist affiche la liste elle-même : une barre de progression, une ligne par
élément avec sa case, et un bouton de fermeture. Elle rend null tant que
restored n’est pas vrai, donc avec une prop storage une checklist fermée ou
partiellement complétée n’affiche jamais son état vide d’abord, et elle rend
null une fois la checklist fermée.
import { Checklist } from '@apollovisionlabs/guide-mui';
<Checklist
checklistId="onboarding"
title="Premiers pas"
onDismiss={() => console.info('fermée')}
onActivate={(item) => console.info(item.id)}
/>;
ChecklistLauncher enveloppe cette liste dans un bouton flottant entouré d’un
anneau de progression, et l’ouvre dans une bulle.
<ChecklistLauncher checklistId="onboarding" title="Premiers pas" placement="bottom-left" />
Il est positionné en fixed dans un coin de la fenêtre, à 24 pixels des bords,
selon placement (bottom-right par défaut). C’est ce qu’il faut savoir avant de
l’intégrer : il ne suit pas votre mise en page, il se pose par-dessus ce qui occupe
déjà ce coin, et il se dessine juste sous la couche modale, au-dessus d’une barre
d’application ou d’un tiroir et sous le projecteur des visites. Si vous avez déjà
quelque chose ancré dans ce coin, prenez l’autre coin, ou posez votre propre
Checklist dans la page.
La bulle reste ouverte pendant qu’on coche les éléments les uns après les autres. Elle ne se ferme que si l’élément activé passe la main à quelque chose qui a besoin de l’écran : une visite ou une navigation.
Traduire les textes propres aux composants
Les titres et les corps des éléments passent déjà par le translate du provider,
via titleKey et bodyKey. Les textes que les composants dessinent autour d’eux
sont à part, et se remplacent par labels, comme GuideTour accepte déjà
labels pour les boutons du popover.
ChecklistLabels couvre Checklist :
interface ChecklistLabels {
dismiss: string;
progress: (completedCount: number, total: number) => string;
markComplete: (itemTitle: string) => string;
markNotComplete: (itemTitle: string) => string;
}
ChecklistLauncherLabels l’étend avec les deux textes propres au lanceur :
interface ChecklistLauncherLabels extends ChecklistLabels {
fabLabel: (title: string, completedCount: number, total: number) => string;
dismissed: (title: string) => string;
}
Les entrées qui insèrent une valeur sont des fonctions et non des gabarits de chaîne. La place d’un compte ou d’un titre dans la phrase change d’une langue à l’autre, et une fonction vous laisse cet ordre des mots.
Les deux props acceptent un Partial : vous ne remplacez que ce dont vous avez
besoin, le reste garde sa valeur anglaise par défaut, soit Dismiss, 2 of 5,
Mark <titre> as complete, Mark <titre> as not complete,
<titre>, 2 of 5 complete et <titre> dismissed.
<ChecklistLauncher
checklistId="onboarding"
title="Premiers pas"
labels={{
dismiss: 'Masquer',
progress: (done, total) => `${done} sur ${total}`,
markComplete: (item) => `Marquer « ${item} » comme fait`,
markNotComplete: (item) => `Marquer « ${item} » comme non fait`,
fabLabel: (title, done, total) => `${title}, ${done} sur ${total} fait`,
dismissed: (title) => `${title} masquée`,
}}
/>;
Le lanceur transmet les libellés résolus à la Checklist de son popover : un seul
objet suffit pour les deux composants. Deux textes restent hors de labels : le
title que vous passez vous-même, et le repli Checklist employé comme nom
accessible du popover quand aucun title n’est fourni.