major
#29659
TL Views: Popover-Element mit beliebigem Inhalt an einer auslösenden Schaltfläche
Teil von #29650 (TopLogic UI on MUI), Richtung 1: Komponenten, die TL fehlen.
Ziel
Ein Element für .view.xml, das an einer Schaltfläche (bzw. einem anderen Auslöser) ein nicht-modales Popover mit beliebigem View-Inhalt öffnet: kleine Formulare, Filter, Detailinfos, Farblegenden (MUI-Gegenstück: Popover, Popper).
Heute gibt es schwebende Oberflächen nur intern (Menüs, Dropdown der Auswahl, Tooltip) und als modalen Dialog (open-dialog). Ein leichter, am Auslöser verankerter Inhalt fehlt.
Anforderungen
- Auslöser: Schaltfläche/Kommando oder ein beliebiges Kind-Element; Öffnen per Klick (optional per Hover).
- Inhalt: beliebige View-Elemente, lazy beim ersten Öffnen erzeugt, mit Kanälen der umgebenden View.
- Platzierung relativ zum Auslöser (oben/unten/links/rechts) mit Ausweichen am Viewportrand; schließt bei Escape und Außenklick (gemeinsame Behandlung mit Menüs und Dialogen, focus-trap/`outside-press`).
- Serverseitig gehaltener Offen-Zustand; Zustandsvertrag in state.proto (siehe #29643).
Lösung
Element `<popover>`
{{{#!xml <popover label="Filter" image="css:bi bi-funnel" placement="bottom-start" open-on="click">
<trigger>
<theme-icon image="css:bi bi-funnel"/>
<text label="Filter"/>
</trigger>
<content>
<inset>
<value-input type="tl.core:String" value="filterText" label="Text"/>
<button>
<action class="com.top_logic.layout.view.command.GenericViewCommand" label="Anwenden">
<close-popover/>
</action>
</button>
</inset>
</content>
</popover> }}}
- Auslöser: Die Schaltfläche zeigt label und image (Bild allein, mit der Bezeichnung als zugänglichem Namen, sonst Bezeichnung mit Pfeil) oder den optionalen Inhalt von <trigger>. Dieser Inhalt ist rein darstellend (Text, Symbol, Abzeichen): Die Schaltfläche selbst ist der Auslöser, eine Schaltfläche darin wäre eine Schaltfläche in einer Schaltfläche. Die Schaltfläche meldet aria-haspopup="dialog" und aria-expanded.
- Öffnen (open-on): click (Vorgabe) oder hover. Bei hover öffnet das Popover verzögert beim Verweilen des Mauszeigers und sofort bei Tastaturfokus auf dem Auslöser; es bleibt offen, während der Zeiger vom Auslöser in die Fläche wechselt, und schließt verzögert, wenn der Zeiger beide verlässt (nicht, solange der Fokus in der Fläche steht).
- Inhalt (<content>): beliebige View-Elemente, erst beim ersten Öffnen erzeugt. Der Inhalt teilt die Kanäle der umgebenden View, es entsteht kein isolierter Kanalraum wie bei open-dialog. Beim Schließen wird der Inhalt abgehängt (er beobachtet keine Kanäle mehr), aber nicht verworfen; beim erneuten Öffnen wird er wieder angehängt. Soll ein Element im Inhalt angezeigt werden (Reveal), öffnet sich das Popover.
- Platzierung (placement): top, bottom, left, right, jeweils auch mit -start/`-end` (Vorgabe bottom-start); am Viewportrand weicht die Fläche auf die Gegenseite aus bzw. verschiebt sich. Die Fläche hat keinen eigenen Innenabstand (der Inhalt bringt ihn mit, z.B. <inset>) und ist in der Breite begrenzt, sodass Fließtext umbricht.
- Schließen: Escape, Außenklick, erneuter Klick auf den Auslöser, sowie die Aktion <close-popover/>, die aus dem Inhalt heraus das innerste umschließende Popover schließt (außerhalb eines Popovers wirkungslos).
- Fokus: nicht-modal. Öffnen per Klick oder Tastatur setzt den Fokus auf das erste Formularfeld der Fläche (sonst das erste fokussierbare Element), Schließen gibt ihn an den Auslöser zurück, sofern der Benutzer ihn nicht inzwischen woanders hingesetzt hat. Öffnen per Mauszeiger lässt den Fokus, wo er ist.
Control und Zustandsvertrag
ReactPopoverControl (Komponente TLPopover) hält den Offen-Zustand serverseitig und besitzt Auslöser- und Inhalts-Control. Der Zustand ist als PopoverState in state.proto beschrieben (open, placement, mode, label, icon, trigger, content) und durch TestControlStateSchema abgesichert; die Komponente ist damit wie TLMenu/`TLDialog` per replace ersetzbar. Platzierung und Modus sind die aus state.proto generierten Enums, die auch das Element direkt konfiguriert.
Korrekturen an gemeinsamen Mechanismen
- usePopover (Platzierung aller schwebenden Oberflächen) kannte nur bottom-start, bottom-end und top-start; es unterstützt nun alle Platzierungen und optional das Öffnen per Hover.
- Außenklick-Behandlung (useCloseOnOutsidePress): Ein Klick in eine aus der Oberfläche heraus geöffnete, in document.body portalte Unteroberfläche (Dropdown einer Auswahl, Farb-/Symbolauswahl, Menü) galt als Außenklick und schloss die Oberfläche. Verankerte Overlays (data-tl-anchored-overlay) oberhalb einer Oberfläche zählen nun als innen, wie es die Fokusfalle bereits tut. Das betrifft auch Menüs, Schubladen und Dialoge.
- Farb- und Symbolauswahl (ColorPopup, IconSelectPopup) werden in document.body portalt und als verankerte Overlays markiert, damit sie in einem Popover nicht abgeschnitten werden.
- Der Part MenuTrigger bekommt die Art des geöffneten Elements (haspopup, Vorgabe menu), damit derselbe Part auch Popover auslöst (auch im MUI-Adapter).
- Die Suche nach dem ersten Formularfeld (Fokus beim Öffnen von Fenstern und Popovers) erkennt auch Felder, die als fokussierbares Element mit Feld-Rolle gezeichnet sind (z.B. die Dropdown-Auswahl, role="combobox").
- useFocusTrap bekommt die Option restore: 'from-inside': Der Fokus wird beim Schließen nur zurückgegeben, wenn er noch in der Oberfläche stand.
Nicht Bestandteil: Die Vereinheitlichung der drei bestehenden Escape-Mechanismen (Menüs, Tastatur-Scopes, Tooltips).
Demo
tl-demo-react, Demos → "Popover": ein Filter als Popover (Eingaben an Kanälen der View mit Auswahlfeld, Schaltfläche "Anwenden" mit <close-popover/>, Anzeige der Werte außerhalb), ein Info-Popover mit Hover-Auslöser, die vier Platzierungen und ein Popover in einem Dialog.