Repeater: TUI Accordion/Tabs layouts, ConfirmDialog, reordering, icon actions - #93
Open
juliacanzani wants to merge 22 commits into
Open
Repeater: TUI Accordion/Tabs layouts, ConfirmDialog, reordering, icon actions#93juliacanzani wants to merge 22 commits into
juliacanzani wants to merge 22 commits into
Conversation
The repeater's remove / remove-all / bulk-delete confirms used the legacy react-aria ModalTrigger with "Confirmation" titles and "Are you sure…" body copy. Replace them with a shared ConfirmDialog built on TUI Modal. - ConfirmDialog (controlled) and ConfirmTrigger (button + dialog) in base - Title/body wired as the dialog's accessible name and description - Initial focus lands on Cancel, the least destructive action - Escape and backdrop click both cancel - Confirm button repeats the trigger label unless confirmText is given - usePortalContainer hook extracted from DynamicFieldSettings so every TUI Modal we portal gets the tf-context wrapper; DynamicFieldSettings now uses it too, and only mounts the container while open - Lift --tui-layer-modal on .tf-interface so TUI modals clear the WP admin bar (the legacy .tf-modal did this for itself only) - Repeater strings gain removeAll and confirm title/description copy so the dialog text can be overridden like the other labels - Storybook stories for the dialog; repeater tests target .tf-confirm-dialog ModalTrigger stays for the conditional panel and the `modal` element. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Edit/Close, Clone and Delete now use TUI's link variant at xs, laid out as a flex row with a gap (link buttons have no padding of their own). The hover-only reveal is gone: the actions are always visible and always in the tab order, which the display:none toggle never allowed. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…strings) Follow-up to 38f14ca from the a11y and code reviews. - Focus restore on close was broken for every close path: the Modal was unmounted instead of re-rendered with open=false, which is the only render in which TUI restores focus to the trigger. The same gate in usePortalContainer regressed DynamicFieldSettings. The hook now creates its wrapper on first activation and keeps it until unmount, and the dialog gates only on the container, not on open - usePortalContainer: useLayoutEffect, el.remove(), fallback wrapper includes tf-interface, docblock states the real reasons for the wrapper - Set --tui-modal-z on .tf-interface instead of lifting the shared --tui-layer-modal token that Sidebar and is-layer-modal also read - Confirm copy follows the same per-layout override as the verb it echoes ("Delete this item?" in advanced), and bulk-action copy moves into strings.ts so every repeater confirm is overridable the same way - string(name, params) fills {placeholders}; replaces the inline %d replace - ModalTrigger marked @deprecated in favour of ConfirmTrigger - New confirmDialog tests: name/description wiring, initial focus on Cancel, focus restore after cancel and confirm, Escape as cancel, confirmText override, one wrapper per trigger removed on unmount Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Replaces the hand-rolled collapse (index state, span-as-button chevron,
data-open styling) with a flush single-open TUI Accordion.
- One Accordion.Item per row, keyed by item.key: deleting a row above the
open one no longer switches which row is open
- The chevron is a TUI IconButton (with tooltip) wrapped by
Accordion.Trigger asChild, so it carries aria-expanded/aria-controls,
Enter/Space, and Arrow/Home/End between rows. Labels are row-specific
("Open item 2" / "Close item 2") and overridable via strings, as are
Edit and Close
- Double-click on the overview row toggles it, except on its own controls;
a mousedown guard stops the double-click selecting text
- Closed rows still render no fields (the field-group tests pin this)
- Flush accordion goes square between the column-label header and the
footer actions, which keep the outer radius; TUI owns item borders,
hover and open state
- Repeater/Layouts stories for advanced (with and without bulk), block,
table and tab, rendered through the public Field entry
- header.tsx docblock named the prop headerConfig; it is headerFields
- Tests: trigger wiring and labels, exactly-once toggle on Space/Enter,
single-open, double-click on overview vs controls, open row survives
removing the row above
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
From the a11y audit and code review of 18312ce. - Rows hydrated from a saved value never received a key, so keyed open state only held for rows added in-session and the index fallback hid it. initDispatcher now assigns a key to any row without one; the layouts key on item.key alone - Focus handoff after delete: the confirm dialog restores focus to its trigger, which a delete destroys with the row. Repeater now hands focus to the row occupying the removed position (its toggle when the layout has one, else its first control), or to the footer actions when the list is empty. Same for Remove all - Edit/Close link carries aria-expanded and a row-specific name ("Edit item 2" / "Close item 2") like the chevron. aria-controls waits on TUI exposing the panel id - Double-click guard leaves header values selectable; only row chrome suppresses selection - SCSS: panel spacing token actually zeroed (the previous comment claimed spacers that the padding token had already removed); drop the width that duplicated IconButton's own size; drop a dead is-size-sm - Drop the new Repeater.stories.tsx: the existing "(Legacy)" per-layout stories cover more (headerFields, callbacks, maxlength, before/afterRow) - Tests assert against the panel via aria-controls and getByDisplayValue, cover aria-hidden on closed panels, Arrow/End navigation, Edit/Close state, key assignment on hydration, and focus after delete/remove all; the assertion pinning a double-click on Edit as open+close is gone Upstream asks sent to TUI: asChild trigger paint class, native-button asChild activation, controlled single value as string | null, exposing item ids, and a MoveHandle mode with index and arrows but no drag grip. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
sortable: true (PHP: sortable) swaps the plain row index for a TUI
MoveHandle with the index badge and up/down moves. No drag library: the
grip TUI swaps in on hover is suppressed by CSS and the index stays put,
until TUI ships an index-plus-arrows mode (requested).
- dispatcher: move action (from, to), bounds-checked
- Repeater: renderMoveHandle(i) for layouts, row-specific labels
("Reorder item 2", "Move item 2 up"), a polite live region announcing
"Item moved to position 2 of 3", and focus kept on the moved row's
same-direction arrow (or the opposite one at a boundary)
- Advanced: index cell hosts the handle when sortable; sortable is only
honoured when the repeater is repeatable
- Legacy Advanced story gains a Sortable variant
- Tests: plain index when not sortable, labelled handles with boundary
arrows disabled, move + announcement + focus, focus fallback at the
end, open row stays open across a move
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…, unstyled trigger TUI 0.2.20 ships the changes requested from this branch (TUI PRs #98 and #99), so the interim workarounds go. - ConfirmDialog: role="alertdialog"; backdrop click no longer cancels (TUI's default for the role), Escape still does. The docblock note waiting on TUI is gone - Modal.Foot is an end-justified actions row by default: the hand-rolled flex rules on the confirm and dynamic-settings footers are removed - MoveHandle mode="arrows" with index: the CSS that hid the drag grip and pinned the badge on hover is removed - Accordion.Trigger asChild unstyled: the specificity reset against the trigger paint class is removed; our data-state rotation rule stays - Controlled single accordion takes null for "nothing open" instead of '' - Edit/Close moves into a ToggleLink that reads useAccordionItem() for aria-controls and toggle package-lock also picks up the 2026.8.19 version that was already stale. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Unsortable rows show a 32px muted badge with the same radius, colour and type as the MoveHandle index, so sortable and unsortable repeaters read as one family. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…MoveHandle Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ry Add item - Header, rows, accordion and footer borders/backgrounds/radius/spacing move from SCSS variables to TUI tokens: --tui-color-border, --tui-color-bg-surface / -muted, --tui-radius-md, --tui-spacing-xs/md, --tui-border-width; open-row and hover tint on --tui-theme-primary-subtlest; the accent bar and first column on --tui-theme-primary-base - Hover/open wash transitions background-color on --tui-motion-duration and --tui-motion-timing; TUI zeroes the duration under prefers-reduced-motion, so the token carries the opt-out - Add item is a primary outline button (Julia) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
ExpandablePanel becomes a thin single-item TUI Accordion, and its row is exposed as PanelItem so the Block layout renders one card-variant accordion of PanelItems. The Accordion field keeps its API. - Title is the real trigger: TUI's own full-width button with the built-in chevron, so aria-expanded/aria-controls, Enter/Space and Arrow/Home/End come for free. The bulk checkbox and switch sit beside the trigger, not inside a button. The header row still toggles on click for pointer users, ignoring clicks on its own controls - Block keys open state by item.key (first row open by default as before) and passes PanelItem an always-visible footer for Clone / Edit-Close / Remove; Edit-Close is the shared ToggleLink (moved from Advanced) with aria-expanded, aria-controls and a row-specific name - Block supports sortable: the MoveHandle sits in the header's left slot - behavior='hide' keeps content mounted and lets TUI hide it; 'remove' (default) unmounts closed content as before - Panel and block SCSS on TUI tokens: item border/radius/surface/gap from the accordion, footer border and spacing from tokens; the hand-rolled chevron, data-status show/hide rules and section mixin are gone. Beaver Builder theme drops selectors for the old button header - focusAfterRemove prefers any [data-tui-accordion-trigger] in the row - Tests: block rows open/close from trigger, header row and footer link; header controls do not toggle; open row survives removal above; move handle in the header when sortable. Existing accordion-field and dependent-title tests pass against the new markup Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Table layout, following the design reference:
- One bordered TUI-token surface holding header row, hairline-divided
rows and the footer; borders, radius and spacing from tokens
- Sortable rows lead with a MoveHandle (arrows only, no index badge:
renderMoveHandle gained an { index } option)
- Bulk checkbox column with a per-row label ("Select item 2"); visually
hidden header labels for the order, select and actions columns
- Row actions are outline icon buttons: Clone (copy icon, primary) and
Remove (minus icon, danger). Actions column only when repeatable
Block layout: actionsPosition="header" (PHP: actions_position) puts Clone
and Remove in the trigger row as ghost icon buttons and drops the footer;
Edit/Close is redundant with the trigger there. Default stays 'footer'.
Button: contentVisuallyHidden now renders the label visually hidden
inside the button instead of moving it to aria-label, so icon-only
actions keep their text for find-in-page, translation and tests.
Shared iconAction() helper in repeater/common; strings for the new
labels; Sortable table story and HeaderActions block story; tests reach
buttons through their visually hidden labels; block header-actions test.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- Shared isInteractiveTarget() in utils/interactive, used by the Block
header click and the Advanced double-click; covers role=group so a
click on a MoveHandle's own chrome never toggles a row. PanelItem also
treats its side slots as control zones in their entirety
- Gesture divergence recorded where it lives: Block headers toggle on a
single click (card convention), Advanced overview rows on double-click
(dense, selectable values)
- Layouts receive `repeatable` instead of decoding maxLength === undefined
- Clone and Remove triggers carry row-specific names ("Clone item 2",
"Remove item 2"; "Duplicate"/"Delete" in advanced) alongside their
visible labels; bulk checkbox and switch labels go through string()
- PanelItem accepts headingLevel (not yet used by the layouts: the row
title as a heading is a call for later)
- ExpandablePanel: the .tf-panel wrapper with data-status is documented
as the public theming hook; the isOpen sync rule is spelled out
- Accordion field drops the stopPropagation wrapper the header guard
made redundant; its tests no longer mute console.warn
- Elementor theme targets the new trigger instead of the old button
header; Beaver Builder drops a duplicate footer border
- Tests: uncontrolled accordion content stays mounted but hidden while
closed, controlled content is removed; Block arrow-key navigation,
focus handoff after delete, empty left slot, per-row action names
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…econdary
- Fields Button gains an `icon` prop: it renders a TUI IconButton with the
label as accessible name and tooltip (showTooltip on by default), so the
repeater's renderAction plumbing is unchanged. Link variant falls back to
ghost, which IconButton supports
- iconAction() now yields { icon, theme, variant }; the table's row actions
and Block's header actions use it. Duplicate is secondary, Remove danger
- Table drops the CSS that squared the old text buttons
- Tests find row actions by accessible name ("Clone item 2") via a shared
actionButtons() helper, since icon buttons carry no text; Block header
test asserts IconButton classes and themes
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Child combinators on .tf-repeater-table-grid keep the header, row and cell rules off tables that fields render inside a cell, such as the date picker's calendar. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The base Tabs compound (Container / Header / Title / Content / Row…) now renders TUI Tabs in the underline variant: a real tablist with roving focus and arrow keys, aria-selected / aria-controls on each tab, and panels that TUI hides with aria-hidden + inert. The compound API changes from isOpen/onPress/isActive per part to a `value` per Title and Content with the active value and change handler on the Container, which also names the tablist. - Tab field: same behaviour, named after its label; uncontrolled mode keeps inactive panels mounted (behavior=hide) so hidden inputs submit - Tab repeater: active tab keyed by item.key, so removing the active item shows its neighbour instead of shifting by index; Clone and Remove are ghost IconButtons with tooltips beside the Add link; the tablist is named "Items" (overridable). No sortable support here: tabs have no natural place for a move handle - Styles on TUI tokens: the header draws one divider across list and actions, TUI's underline indicator overlaps it; the hand-rolled active bar and data-open styling are gone - Tests target role="tab" / aria-selected and find icon actions by name Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A border on the header clipped the indicator's 1px overhang under the header's overflow. The list keeps TUI's divider and the actions continue the same line, unbroken, to the header's end. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- Rows keyed by item.key; Remove only when repeatable, as a ghost danger IconButton (trash) with the row-specific name and tooltip - Sortable: an arrows-only MoveHandle leads the row - Gaps on TUI tokens; fields flex to fill the row - Shared repeater styles: footer actions on tokens, and the legacy rule that recoloured the last action button is gone (Remove all is already a danger button); block margins live in block/index.scss Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- The repeatable:false common test compared text, which icon-only actions
no longer have, so it could not fail; it now asserts by accessible name
across all layouts
- Tabs compound: Container provides the active value and tablist label
through a context; Header names the list from it and Content decides
what to mount from it, so consumers declare each once. The dead
data-tf-tablist-label attribute is gone; activationMode is forwarded
- Tab repeater: a missing active key is handled explicitly instead of via
Math.max, the item actions hide when the list is empty (Clone could
otherwise clone undefined into a keyed empty row), titles and panels are
both sliced by maxlength, and the tablist is named after the field
("Speakers items") when it has a label
- Tab field drops the `false` sentinel so the Tabs stay controlled
- renderAction: clone accepts buttonProps like delete; a caller-provided
aria-label wins over the row-specific default; the comment about
repeatable now states the actual rule (Repeater owns the action, layouts
own the chrome). Delete accepts restoreFocus:false for layouts whose
trigger survives the removal; the tab layout uses it, since the shared
handoff assumed rows and only happened to be a no-op there
- Button: the icon branch warns when it has no name; stale comment fixed
- Tests: neighbour selection after removing a middle / last / only tab,
focus staying on the tab Remove trigger, tablist naming; bare layout
gets its own file (icon Remove per row, repeatable gate, move handle)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
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.
Summary
Rebuilds the repeater layouts on
@tangible/ui0.2.20 and replaces the remaining hand-rolled interaction patterns with TUI components.Shared
role="alertdialog", Cancel focused first, Escape cancels, focus restored to the trigger, or handed to the next row when a delete destroys the trigger). Replaces the react-ariaModalTriggerconfirms;ModalTriggeris deprecated but kept for the conditional panel andmodalelement.usePortalContainerhook shared by TUI modals so they land inside the fields context wrapper; modal z-index token lifted above the WP admin bar.sortable/ PHPsortable): TUI MoveHandle in arrows mode,movedispatcher action, live-region announcements, focus kept on the moved row. No drag library.Buttongains aniconprop rendering TUI IconButton with tooltip; Clone and Remove carry row-specific names ("Remove item 2"). All repeater copy is overridable throughstrings.alertdialog,Modal.Footlayout,MoveHandle mode="arrows"with index,Accordion.Trigger unstyled, nullable controlled accordion value,useAccordionItem.Layouts
PanelItems (ExpandablePanel rebuilt on the same); title is the trigger, checkbox/switch beside it;actionsPosition="header"(PHPactions_position) moves Clone/Remove into the trigger row as icon buttons.Verification
tsc --noEmit: no new errors versusmain(three pre-existing ones removed).Follow-ups (not in this PR)
headingLevelon accordion rows (PanelItem accepts it; not enabled).tabIndex=0on every active panel; exportuseTabsContext.beforeRow/afterRowper field (pre-existing).🤖 Generated with Claude Code