feat(remix)!: add RemixLink and rebuild FortalLink on it - #151
Merged
Conversation
RemixLink and FortalLink are built on NakedLink, which is unreleased and lives on conceptadev/naked_ui#65. Isolated in its own commit so it reverts in one step once that PR ships and the naked_ui floor can be raised instead. The Fortal parity contract fails while this override is in place, by design: tool/fortal_parity/check.dart guards that the workspace resolves a hosted naked_ui. That guard is the merge blocker for this branch.
Adds a first-class Link component to `remix`: LinkSpec, the generated LinkStyler, and RemixLink built on naked_ui's NakedLink. A link publishes the Link role and activates on Enter, where a button publishes the Button role and also takes Space. FortalLink now delegates to RemixLink instead of wrapping NakedButton in a hand-built Semantics node. That deletes the mirrored focus field, the duplicated inert widget path, and the excludeSemantics workaround the old recipe needed because NakedButton's ExcludeSemantics sits inside its focus node. fortalLinkStyle absorbs _fortalInteractiveLinkStyle and now returns the complete style, so the widget no longer branches on actionability. A link with no onPressed renders as prose rather than through NakedLink: the primitive folds `enabled` and `onPressed` into one effective-enabled flag and publishes hasEnabledState for both, which would announce ordinary body text as unavailable. Both paths resolve their style against their own widget states, so one LinkStyler behaves the same whether or not the link is actionable. BREAKING CHANGE: FortalLink no longer activates on Space. Space belongs to the Button role; a link takes Enter and Numpad Enter, matching an anchor on the web. Use FortalButton where Space should activate. BREAKING CHANGE: fortalLinkStyle returns LinkStyler instead of BadgeStyler, and its `hovered` and `focused` parameters are gone. They were the only raw widget-state parameters on any Fortal recipe and existed solely to build the variant snapshots the recipe now assembles itself. Pass `actionable` and let the returned style resolve its own states. BREAKING CHANGE: a disabled FortalLink no longer publishes the Link role. NakedLink gates the role on effective-enabled, so `enabled: false` with a callback announces as unavailable text rather than an unavailable link.
This reverts commit 807fbcd.
NakedLink shipped in naked_ui 1.0.0-beta.11, so RemixLink and FortalLink resolve it from pub.dev instead of the branch override reverted in the previous commit. The two package constraints and the parity contract's pinned version move together on purpose: tool/fortal_parity/check.dart asserts that both pubspecs name the same constraint and that the lockfile resolves that exact version from a hosted source, so bumping fewer than all three keeps the guard red.
RemixLink special-cased `onPressed: null` into its own render path so an inert link would announce no enabled state, on the reasoning that HTML's `<a>` without an href is not a link. That reasoning imported the wrong model. Flutter's convention is the opposite and universal: a null callback disables a control, on ElevatedButton, IconButton, NakedButton, and the rest. NakedLink follows that convention, and choosing the link widget at all is what declares intent, so a link with no callback is a disabled link. Deleting the special case removes the second render path, the empty WidgetStateProvider that path needed, and the fork that let one LinkStyler resolve two different ways. That state fork was not hypothetical: it was the source of the ancestor-hover leak this branch previously found and patched. With one path there is nothing to leak, and the regression test now passes for both spellings of disabled. No consumer relied on the old behaviour. Every callback-less call site in the workspace was a gallery tile demonstrating the inert state itself, and those now name the state honestly. The playground's accent row was the one real casualty: it used a callback-less link to show accent colour and would have silently rendered the disabled treatment, so it gets a callback. BREAKING CHANGE: a FortalLink or RemixLink with no `onPressed` now reports `hasEnabledState` and announces as unavailable, where it previously published a plain text node with no interactive metadata. Use ordinary text such as `FortalText(accent: true)` for prose that was never meant to navigate.
Adopts the `@MixableSpec(target:)` generation that landed in #152. The hand-written `RemixLinkStylerRemixHelpers` extension is deleted; the generator now emits an identical `LinkStyler.call` into link.g.dart, so the constructor and its callable form can no longer drift apart. link_style.dart held nothing but that extension, so it goes entirely, the way badge's and button's did. The style files that survive #152 keep other helpers besides `call`. Adds LinkStyler to the generated-call test's non-generic group. It needs a label because RemixLink asserts on having a name.
A link has no fill, border, or padding, so its container slot exists for one reason: hosting the focus-visible ring. That made the box look like dead weight, and nothing in the suite disagreed — deleting RemixBoxWithEffects from RemixLink kept all 378 Fortal typography tests green, including the two named for focus. Those tests read the resolved LinkSpec out of its StyleSpecProvider, so they prove the ring is computed, never that it is drawn. Without the box the effects resolve exactly as before and paint nothing. This asserts the resolved effects arrive at the widget that paints them, and fails if the box is removed. It reaches past the barrel for the internal painter type deliberately; that wiring is the thing under test.
5 tasks
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
Adds a first-class Link component to
remix—LinkSpec, the generatedLinkStyler, andRemixLinkbuilt on naked_ui'sNakedLink— and rebuildsFortalLinkon top of it, replacing the hand-rolledNakedButton+ outerSemantics(link:)workaround. A link publishes the Link role and activates on Enter and Numpad Enter, where a button publishes the Button role and also takes Space.onPressed: nullandenabled: falsemean the same thing, matching Flutter's universal convention that a null callback disables a control; for text that was never meant to navigate, use ordinary text rather than a link.fortalLinkStyleabsorbs the old_fortalInteractiveLinkStyleand returns the complete style, soFortalLinksheds its mirrored focus field, its duplicated inert widget path, and itsexcludeSemanticsworkaround, andbuildno longer branches on actionability.Requires
NakedLink, which shipped in naked_ui 1.0.0-beta.11 (conceptadev/naked_ui#65). The temporary branch override used during development was reverted in9435057; thenaked_uifloor is raised to the released version in both package pubspecs and in the parity contract, which move together by design.Related Issues
Closes #137
Closes #143
Checklist
Note: Updating the
pubspec.yamlandCHANGELOG.mdis not required. These are handled automatically during the release process.///).Breaking Change
Does this PR require users of the package to manually update their code?
Four breaks, all in
remix_fortal:FortalLinkno longer activates on Space (FortalLink activates on Space; links must only activate on Enter #143). UseFortalButtonwhere Space should activate.fortalLinkStylereturnsLinkStylerinstead ofBadgeStyler, and itshoveredandfocusedparameters are gone — they were the only raw widget-state parameters on any Fortal recipe and existed solely to build the variant snapshots the recipe now assembles itself. Passactionableand let the returned style resolve its own states.FortalLinkno longer publishes the Link role, becauseNakedLinkgates the role on effective-enabled. This now coversonPressed: nullas well asenabled: false— a callback-less link reportshasEnabledStateand announces as unavailable, where it previously published a plain text node with no interactive metadata.FortalLinkis no longer reachable underNavigationMode.directional. The oldNakedButton-backed recipe left disabled controls traversable on directional platforms;NakedLinkdoes not.Verification
melos run cipasses end to end:toolchain:check,mix:consumer:check,material:check,generate:check,docs:check,fortal:parity:check(verifying hosted Naked 1.0.0-beta.11), andtest:flutter— remix 2614, remix_fortal 378, dashboard 48, demo 1.flutter analyzeanddart formatclean.