FSHSP-160 feat(editor): ui-editor, éditeur de texte riche sans moteur tiers - #92
Merged
Conversation
Rich-text field built on the ui-field shell in multiline mode, with no third-party editing engine: the formatting commands rely on the browser's native editing API, confined to ui-editor-commands so a future engine only has to replace that file. The kit keeps tslib as its sole runtime dependency. - Configurable toolbar (tools, toolbarPosition) with a single tab stop: roving tabindex, aria-pressed on the toggles only, Escape returns focus to the editing area. Clicking a tool preserves the selection. - Value is HTML, sanitised through DomSanitizer and reduced to a tag whitelist on paste; script/style are dropped with their content and insertHTML's own decoration is stripped back off in place. - maxlength and showCount measure the text, never the markup. - 27 --ui-editor-* hooks, documented for the generated theming table. Adds `text-decoration` to the recognised property names in component-vars.build.mjs (same shape as cursor / object-fit) so the link decoration hook conforms to the naming convention.
- codeBlock toggles the block under the caret between <pre> and a plain
paragraph, and reports its state through aria-pressed like the other
toggles. No syntax highlighting: it would pull in a third-party dependency.
- fontFamily renders a dropdown rather than a button, since it picks among
values instead of toggling one. The list is closed to the three system
families: a free picker would write an arbitrary font into the value,
outside the --fontfamily-* tokens.
The family is stored as a class, never as an inline style, so a project
rebinding --fontfamily-title moves the content already typed with it. Those
classes are the only ones the paste whitelist lets through, alongside the
new pre/code tags.
execCommand('fontName') emits obsolete <font face> elements, so the command
runs against a marker that is immediately rewritten into a span.class. The
dropdown is also the only toolbar control that takes the focus, so the
caret is remembered and restored before the command applies.
The single "font" dropdown conflated two different things: what a block IS and how it looks. They are now separate. - blockFormat (Normal, Titre 1-3) sets the block itself. A heading carries document structure, which a screen reader navigates; applying the title face to a paragraph never did. It reports no value at all when the caret sits in a block the list does not offer (code block, list item), rather than claiming "Normal". - fontFamily now lists the REAL face each token resolves to — Inter, DM Sans, Roboto Mono — read from the computed style instead of hardcoded: a project rebinding --fontfamily-base would otherwise be shown a menu naming a font it no longer uses. - fontSize (Petit -> Très grand) exists but is deliberately NOT in the default bar: on screen it duplicates blockFormat, since both make text bigger, yet only one carries structure. Offering both side by side invites headings that are merely large text. Opt in through `tools`. Family and size are written as classes bound to the tokens, never as an inline style, so rebinding a token moves the content already typed. h1-h3 join pre/code in the paste whitelist. The dropdowns are the only toolbar controls that take the focus, so the caret is remembered and restored before their command runs.
The closed dropdowns advertised their own name ("Niveau", "Police"), which
is where the current formatting should be readable instead. They now show
what the text actually uses: "Normal" and "Inter" at rest.
The family is read from the RENDERED font rather than inferred from the
absence of a class. A heading carries --fontfamily-title from its own
styling, so the list reports "DM Sans" there — claiming the base family
would have named a font the reader cannot see.
blockFormat keeps a blank state, and only it: in a code block or a list
item there is no honest level to display, and showing "Normal" would
invite a click that reformats the block. The spelled-out names stay as
aria-label, so nothing is lost for screen readers.
Every dropdown now shares one fixed width. Sizing them to their content
made the whole toolbar jump whenever a longer value was picked.
- The toolbar's role="toolbar" container carries the keyboard handler while focus stays on its children (roving tabindex) — same ARIA pattern already used by ui-menu, disabled the same way there. - Removed the useless `null` initializer on `next` in onToolbarKeydown: every switch branch either assigns it or returns.
…n stories
Lint:
- The toolbar's role="toolbar" container carries the keyboard handler while
focus stays on its children (roving tabindex) — same ARIA pattern already
used by ui-menu, disabled the same way there.
- Removed the useless `null` initializer on `next` in onToolbarKeydown:
every switch branch either assigns it or returns.
Dropdown width: replaced the measured 132px with 13ch (same precedent as
ui-empty-state's max-width). It now scales with $select-font-size instead
of a guessed pixel count for the longest label ("Roboto Mono").
Stories: widened the ui-editor demo wrapper (420px -> 640px) so the toolbar
and its dropdowns have visible room — scoped to this component's stories only.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Contexte
FSHSP-160 — le kit s'arrêtait à
ui-textarea: dès qu'un projet avait besoin de gras, de listes ou d'un lien dans un champ libre, il installait Quill ou TipTap et repartait avec un champ qui ne ressemble à aucun autre du formulaire.ui-editorcouvre ce besoin sans ajouter une seule dépendance runtime : les commandes de mise en forme s'appuient sur l'API d'édition native du navigateur, isolée dansui-editor-commands.tspour qu'un futur moteur ne se substitue qu'à ce fichier. Le kit gardetslibcomme unique dépendance.Bâti sur le shell
ui-fielden mode multiligne, il partage libellé, boîte, niveaux, helper et validation avec les huit autres champs (BaseFormField, donc[(ngModel)], reactive forms et Signal Forms sans code supplémentaire).Ce que contient la PR
Barre d'outils configurable (
tools,toolbarPosition) formant un seul arrêt de tabulation : roving tabindex,aria-pressedsur les bascules uniquement,Échaprend le focus à la saisie. Cliquer un outil ne déplace pas le focus et préserve la sélection.Deux listes déroulantes par défaut,
blockFormat(Normal, Titre 1-3) etfontFamily. Elles affichent la valeur en vigueur — « Normal », « Inter » — et non leur propre nom. La police est lue sur le rendu effectif : dans un titre, qui porte--fontfamily-title, la liste affiche « DM Sans ». Largeur fixe et commune, sinon la barre saute à chaque changement de valeur.fontSizeexiste mais est volontairement hors de la barre par défaut : à l'écran il ferait doublon avecblockFormat— les deux grossissent le texte — alors que seul le niveau porte la structure du document. Le proposer à côté invite à fabriquer des titres qui n'en sont pas. Il s'active viatools.Sécurité de la valeur. Le HTML passe par
DomSanitizeravant tout affichage, et le collage est réduit à une liste blanche de balises et de classes :script/stylesont supprimés avec leur contenu,javascript:retiré des liens, styles en ligne et balises étrangères écartés. La décoration queinsertHTMLréinjecte après coup est nettoyée sur place.Tokens uniquement. Police et taille sont écrites en classe liée aux tokens, jamais en style en ligne : rebinder
--fontfamily-titledéplace tout le contenu déjà saisi. 37 hooks--ui-editor-*documentés alimentent la table Theming générée.Vérifications
Tout a été observé dans Storybook, pas seulement compilé :
<b>posé,aria-pressed=true, focus conservétabindex=0; ⭢ /Home/End/Échapconformesp → pre → p,aria-pressedsuit le curseurh1→ « Titre 1 » + « DM Sans » ; paragraphe → « Normal » + « Inter »script/stylesupprimés,javascript:retiré, bon lien et gras conservésui-kit:build,docs:config:check(0 variable sans///),components:checket Prettier sont verts.Points à noter pour la revue
text-decorationa été ajouté aux propriétés reconnues descripts/component-vars.build.mjs(même forme quecursor/object-fit), pour que le hook de décoration des liens respecte la convention de nommage.::ng-deepcantonné à la zone de saisie (même précédent queui-card/ui-image).aria-labelplutôt qu'en<label for>: uncontenteditablen'est pas un contrôle de formulaire.linkutilisewindow.prompten v1.node-idFigma de la story pointe encore la racine du UI Kit, à mettre à jour quand le composant existera dans Figma.🤖 Generated with Claude Code