Repository navigation
feat(registry)!: adopt the new Remix APIs in the Fortal preset - #346
Draft
leoafarias wants to merge 17 commits into
Draft
leoafarias wants to merge 17 commits into
leoafarias wants to merge 17 commits into
Conversation
Toggle, toggle-group item, tab, disclosure and accordion triggers drew their focus ring as an inside border, which resized the part and shifted its neighbors by twice the ring width on focus. They now draw it through containerEffects (triggerEffects on accordion and disclosure), which paints over the box without taking layout space. Tabs ring in focus-8 at outline-offset -2, as base-tab-list.css:126-129 does. The Fortal-only parts keep their focus-a8 color and share the same inset -2 offset, so the clip of a toggle group or a panel never crops the ring. Every ring still reads the focus ring width token. A sidebar destination keeps its border ring, because RemixSidebar paints a destination's container but not its containerEffects. fortalFocusRing and its two extensions are replaced by fortalInsetFocusOutline. The size and paint tests drive each part with the keyboard, in light and dark, at the default and an overridden ring width.
…button The data-table kebab passed a styled Box to RemixMenuTrigger.builder, which fakes a button: it is not focusable on its own and has no button semantics. It is now a FortalIconButton through RemixMenuTrigger.control, given the handle's focusNode and toggle, so the menu keeps one tab stop, the expanded state merges onto the button, and Escape returns focus to it. FortalActionMenu takes the RemixMenuTrigger itself instead of a label and a widget. The sidebar account row and the top-bar avatar stay visual-content triggers.
The radio demo composed a Row with a Text beside each radio, and the switch demo named itself with semanticLabel only. Both pass label now, so the text is part of the control's tap, hover and focus target and its accessible name.
Radix gives the select content `min-width: var(--radix-select-trigger-width)` (select.css:45), so a short option list never draws a panel narrower than its trigger. Fortal set no width, so the panel was as wide as its widest option. The content styler now reads remixAnchorWidth() as its minimum width. A longer option still widens the panel past the trigger. Fortal's content has no minimum of its own, so the token goes straight on the content box.
Radix Themes 3.3.0 draws nothing on a focused table header: table.css has no focus rule and its Table has no sortable header. A sortable header is a Fortal extension and a focusable control, so it needs a visible focus indicator. It rings in focus-8 at the focus ring width token, inside its cell like a tab. The ring is a foreground border that is present and transparent at rest, and focus changes only its color. A decoration that appeared on focus would swap the cell's root widget and rebuild a stateful custom header on every focus change. A non-sortable header resolves against its own empty widget-state scope, so a focused Pressable or disclosure trigger around the table no longer lights it. The row divider paints over the ring's last pixel and tints it gray-a5; the inside stroke does not avoid that, so it is recorded in the data_table flutterExceptions of the parity manifest and the catalog. The parity test that counted row dividers now selects them by their bottom-only border, so the header ring is not counted.
RemixSidebar destinations now paint containerEffects, so a destination takes the outline ring of the toggle it derives from. The inside border that stood in for it is gone. Left in place it would have painted over the same pixels as the outline, doubling the ring's alpha, and it inset the destination's content by the ring width on focus. The sidebar test now focuses a destination from the keyboard and checks that its icon does not move, and that the ring is present on all four edges and nothing paints outside it, in light and dark, expanded and collapsed. The sidebar is shorter than its destinations and the focused one sits flush against the bottom of the scroll region, so the inset ring is shown not to be clipped there.
The ring was a foreground border on the header cell, transparent at rest. The row divider, a foreground border on the outer row region, painted over its bottom pixel and tinted it, and a zero focus ring width drew a hairline. Each header cell resolves its own row region under its own widget-state scope, so the ring now goes on that region: the focused cell rings on all four sides, and its neighbours do not. The region already paints the divider at rest, so focus only changes a decoration that is always there and a stateful custom header still keeps its state. A width of zero leaves the divider alone. While focused, the ring replaces the divider on the cell's bottom edge. A focusable control nested in a header keeps the ring lit while it holds focus; that is recorded in the parity manifest, which no longer mentions a divider tint. The parity test that counted dividers is back to its original form.
…e helper Every ring in the pinned Radix CSS is solid focus-8 except the soft variants, and every other Fortal ring is focus-8 or the accent. The toggle, toggle-group item, accordion and disclosure triggers, and the sidebar destination, which derives from the toggle, rang in focus-a8. They now share the tab's ring: fortalTabFocusOutline is focus-8 at outline-offset -2 and replaces both fortalInsetFocusOutline and the tab's own copy of the same call. The name says whose ring it is instead of how far inset it sits, which is not unique to it. The control_styles and fortal.md descriptions called every offset except the token one a Radix literal, and listed -1 for only some of its parts. They now say that classic and solid buttons, checkboxes, radios, switches, and links draw outward at the offset token, name every part at -1 (including the ghost, outline, and surface buttons, the icon buttons, and the segmented control), and say that the toggle-like parts borrow the tab ring rather than a Radix literal.
The sidebar account row and the top-bar avatar were menu triggers that took keyboard focus and drew nothing, so a keyboard user could not see where they were. The account row stays a visual-content trigger, since it is a row and not a button. Its content resolves under the menu's widget-state scope, so it now draws the focus-8 ring as a foreground border on focus. The top-bar avatar becomes a real icon button through RemixMenuTrigger.control, like its toolbar neighbors, so it gets their hover and focus treatment and its own button semantics. The apps' toolbar test now counts four square icon buttons. fortalFocusRingDecoration builds the ring for boxes that take no box effects. The data-table header row uses it too, so the zero-width case lives in one place: a zero-width BorderSide is a hairline, so the ring is null there. A pixel test for each trigger takes keyboard focus and checks the ring on all four edges, and that the kebab, which already rings through the ghost icon button, keeps doing so. The avatar test also pins one named, expandable button.
…th rule Add an Unreleased entry to the Fortal changelog for the installed-source changes in this stack: focus rings painted as outlines in focus-8, with fortalFocusRing, FortalFocusRing, and FortalFocusRingFlexBox removed and fortalTabFocusOutline as the replacement; FortalActionMenu taking a RemixMenuTrigger and fortalDataTableActionsTrigger becoming a function; the select panel's minimum width. It names the items to overwrite together and the remix and naked_ui releases they need. The select panel is now at least as wide as its trigger, which replaces a minimum a caller sets on the content. The fortalSelectStyle doc comment and a new Select section in the Fortal skill reference say so, and that a maxWidth below the trigger must also set minWidth because a minimum above a maximum is invalid. A test pins that the documented override caps the panel.
The generator copies fortalSelectStyle's doc comment onto FortalSelect. The previous commit added the select width note to that comment but not to the generated class, so generate:check reported select.g.dart as changed.
The sidebar account row drew its focus ring as a foreground border that existed only while focused. Mix's Container inserts a DecoratedBox when a foreground decoration appears, so focusing rebuilt the row's subtree and the avatar became a new element. The ring was also a square, unpadded band painted inside the row, over the avatar's left 2px, and it grew inward at larger ring widths. The row content is now wrapped in a RemixCard with the row's radius, which has an effects slot, and draws fortalFocusOutline in containerEffects on focus-visible, as the Vanilla account row does. The ring sits at the focus ring offset outside the row, so it covers none of the content, keeps the content's state, follows the ring tokens, and draws nothing at a width of zero. fortalFocusRingDecoration now has a single caller, the data-table header row, so it becomes that recipe's private helper again. The top-bar avatar button passes a null icon, since its iconBuilder draws the avatar and ignores it. The row tests host the row in a sidebar footer and check, in light and dark, that the ring paints around the row and leaves the avatar's pixels alone, that the avatar element survives focus, and that a width of zero paints nothing.
…ix two stale notes The changelog said FortalFocusRingFlexBox was removed but not what replaces it. It rang a plain FlexBoxStyler, which has no effects slot, so there is no helper: the entry now says to wrap the content in a RemixCard and use fortalFocusOutline in its containerEffects, or to draw a foreground border. The Fortal skill reference's ring colour sentence omitted the gray-8 ring of a disabled field and the error-8 ring of an errored one. Eval 13 still expected builder triggers without a ring for the avatar, profile row, and kebab; it now expects real buttons through RemixMenuTrigger.control for the avatar and kebab, and a builder that draws its own ring for the profile row.
This branch has not been deployed
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.
Description
Draft, stacked on #339. Do not merge until remix 1.0.0-beta.14 and naked_ui 1.3.0 are on pub.dev.
Fortal adopts the new Remix APIs.
containerEffectsortriggerEffects.outline: 2px solid var(--focus-8); outline-offset: -2px. The Fortal-only parts borrow that ring infocus-8through one helper,fortalTabFocusOutline.focus-8ring inside its own cell, like a tab.Pressableor disclosure trigger (fix(remix): keep outside widget state out of table headers, panel content, and sidebar slots #340).focusRingWidth: 0nothing is drawn. A focusable control nested in a header keeps the ring lit while it holds focus.remixAnchorWidth(), as Radix'smin-width: var(--radix-select-trigger-width)does (select.css:45). AmaxWidthbelow the trigger width must also setminWidth.RemixMenuTrigger.control. Each has one tab stop and expanded semantics on the button, and Escape returns focus to it. The visible change is focus and hover feedback.RemixCardwithcontainerEffects, outside the row content, so focus neither covers the avatar nor rebuilds the row.label.Validation
registry_source: 2330 tests pass.apps/dashboard: 84 pass.apps/demo: 64 pass.New tests cover:
maxWidthoverride;The regression tests fail before their changes.
dart analyze,dart format,open-code:registry:build,build_registry --check,open-code:apps:check,melos run ci:checks(includingfortal:parity:checkanddocs:check),melos run generate:check, anddocs:catalog --check: pass.check_open_code --preset fortal --source checkout: pass.Merge note
Merge after #342. Both PRs add tests to
registry_source/test/dashboard/dashboard_demo_test.dartat the same place, so rebasing this PR onto amainwith #342 conflicts there: keep both test sets, and keep the unrestrictedpackage:flutter/services.dartimport. The full-stack check that merges #339, #342, #343, and this PR shows the result. Once rebased, point the Vanilla ring tests at this PR's sharedtest/helpers/pixels.dartinstead of #342's private copy.Related Issues
Stacked on #339. Builds on #320, #328, #331, and #340. Follows up the Fortal focus-ring finding tracked in the PR D plan.
Checklist
///).Breaking Change
These apply to installed Fortal source and need remix 1.0.0-beta.14 and naked_ui 1.3.0. Overwrite
theme,toggle,toggle_group,tabs,accordion,disclosure,data_table,select, anddashboard_demotogether.sidebarneeds no overwrite.fortalFocusRing,FortalFocusRing, andFortalFocusRingFlexBoxare removed. PutfortalTabFocusOutline(), orfortalFocusOutline(color, offset:), incontainerEffectsortriggerEffects.focus-8instead offocus-a8, and focusing no longer resizes or moves them.FortalActionMenutakes aRemixMenuTriggerinstead ofsemanticLabelandtrigger: Widget.fortalDataTableActionsTriggeris now a function of the accessible label that returns aRemixMenuTrigger. The top-bar account menu is now an icon button.maxWidthbelow the trigger width must also setminWidth.No package API changes. No registry floor or package version is bumped.