feat: add naked link - #65
Conversation
LCOV of commit
|
LCOV of commit
|
LCOV of commit
|
LCOV of commit
|
# Conflicts: # .github/workflows/ci.yml # .github/workflows/integration-tests.yml # packages/example/integration_test/screenshot_smoke.dart # packages/example/lib/registry.dart # packages/example/test/accessibility_guidelines_test.dart # packages/example/test/screenshot_evidence_test.dart # packages/naked_ui/CHANGELOG.md # tool/run_android_integration.sh
LCOV of commit
|
LCOV of commit
|
There was a problem hiding this comment.
Pull request overview
Adds a new headless NakedLink primitive to naked_ui, plus supporting intents, hover/focus utilities, documentation, examples, tests, and CI/integration evidence updates.
Changes:
- Introduces
NakedLink,NakedLinkState, andNakedLinkResolverwith Link semantics and Enter-only keyboard activation. - Extends
NakedFocusableDetectorto optionally restore hover when re-enabled under a stationary pointer, and stabilizes Actions/Shortcuts wrappers across enable/disable changes. - Adds extensive unit/semantics/integration/golden evidence and updates CI workflows to validate Link behavior and artifacts.
Reviewed changes
Copilot reviewed 30 out of 32 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| tool/run_android_integration.sh | Verifies new Android Link screenshot evidence artifacts exist after the run. |
| README.md | Documents the new NakedLink API and resolver usage at the workspace level. |
| pubspec.lock | Adds url_launcher transitive resolution and updates recorded Dart SDK minimum. |
| packages/naked_ui/test/utilities/naked_focusable_detector_test.dart | Adds coverage for hover restoration behavior across disable/enable transitions. |
| packages/naked_ui/test/src/naked_link_test.dart | New widget tests for link activation, modifiers, lifecycle, focus, state, and platform-default navigation. |
| packages/naked_ui/test/semantics/naked_link_semantics_test.dart | New semantics contract tests ensuring Link role/URL/actions match expectations and Button semantics are absent. |
| packages/naked_ui/test/hashcode_contract_test.dart | Extends hashCode contract tests to include NakedLinkState. |
| packages/naked_ui/README.md | Package-level documentation updates for NakedLink and resolver behavior. |
| packages/naked_ui/pubspec.yaml | Adds url_launcher dependency needed for native/web Link coordination. |
| packages/naked_ui/lib/src/utilities/naked_focusable_detector.dart | Adds opt-in hover restoration and stabilizes Shortcuts/Actions wrappers when disabled. |
| packages/naked_ui/lib/src/utilities/intents.dart | Introduces Link-specific intent/shortcut mapping for Enter/Numpad Enter without Space. |
| packages/naked_ui/lib/src/naked_widgets.dart | Exports naked_link.dart from the public widgets barrel. |
| packages/naked_ui/lib/src/naked_link.dart | New NakedLink implementation with semantics, resolver routing, and url_launcher-backed navigation. |
| packages/naked_ui/lib/src/mixins/naked_mixins.dart | Adds optional rebuild control for state synchronization during framework lifecycle callbacks. |
| packages/naked_ui/CHANGELOG.md | Changelog entry describing the new Link primitive and related behavior updates. |
| packages/example/test/screenshot_evidence_test.dart | Ensures Link evidence artifact names remain stable/deterministic. |
| packages/example/test/naked_link_example_test.dart | Tests the example fixture behavior/state determinism and semantics expectations. |
| packages/example/test/goldens/golden_test_harness.dart | Updates golden font loading to include pinned Material Icons for deterministic rendering. |
| packages/example/test/goldens/components/naked_link_golden_test.dart | Adds a Link golden test (keyboard focus baseline on pinned Linux host). |
| packages/example/test/accessibility_guidelines_test.dart | Adds accessibility guideline checks for inline and standalone Link fixtures. |
| packages/example/test_driver/link_browser_driver.dart | Adds a web-driver test to verify trusted-event browser ownership and navigation behavior. |
| packages/example/pubspec.yaml | Adds dev deps for flutter_driver, webdriver, and web to support the new browser driver flow. |
| packages/example/lib/registry.dart | Registers the new Link demo in the example demo registry. |
| packages/example/lib/api/naked_link.0.dart | Adds the Link example fixture(s) used by tests, goldens, screenshots, and guidelines. |
| packages/example/integration_test/screenshot_smoke.dart | Adds Link screenshot capture scenarios (inline, focus, disabled, long text) and surface checks. |
| packages/example/integration_test/link_browser_driver_app.dart | Adds a minimal app target for flutter drive browser-ownership verification. |
| packages/example/integration_test/components/naked_link_integration.dart | Adds integration tests for focus/activation, Space non-activation, semantics, disabled behavior, and RTL/long-text robustness. |
| packages/example/integration_test/all_tests.dart | Registers Link integration tests into the suite runner. |
| .github/workflows/integration-web.yml | Runs and verifies the new web flutter drive browser ownership log evidence. |
| .github/workflows/integration-tests.yml | Verifies new macOS screenshot evidence artifacts exist in CI. |
| .github/workflows/ci.yml | Improves golden failure diagnostics by generating/uploading candidate images only on the pinned-host failure path. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if (kIsWeb && widget.linkUrl.hasScheme) { | ||
| unawaited(_launchWebExternalLink()); | ||
| return; | ||
| } | ||
| unawaited(followLink()); |
| void _handlePointerActivation(launcher.FollowLink followLink) { | ||
| final modified = _modifiedPointerActivation || _hasPointerModifier; | ||
| _modifiedPointerActivation = false; | ||
| if (modified) { | ||
| unawaited(followLink()); | ||
| return; | ||
| } | ||
| _handleOrdinaryActivation(followLink); | ||
| } |
# Conflicts: # packages/example/integration_test/screenshot_smoke.dart # packages/example/test/accessibility_guidelines_test.dart
|
Downstream feedback from Remix, where we hit the exact gap this PR fills. Two parts: evidence that the primitive is worth having, and a boundary concern about how it is currently packaged. The primitive is needed — here is the proof from downstream
// intents.dart
SingleActivator(LogicalKeyboardKey.enter): ActivateIntent(),
SingleActivator(LogicalKeyboardKey.space): ActivateIntent(), // button-onlySo our link activates on Space, which links must not do — Enter activates a link, Space scrolls. Measured: We cannot fix this downstream by styling or by wrapping semantics; the keyboard contract lives in the primitive. Filed as conceptadev/remix#143. This is the strongest argument for The concern:
|
LCOV of commit
|
LCOV of commit
|
Adds a first-class Link component to `remix` — `LinkSpec`, the generated `LinkStyler`, and `RemixLink` built on naked_ui's `NakedLink` — and rebuilds `FortalLink` on top of it, replacing the hand-rolled `NakedButton` plus outer `Semantics(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: null` and `enabled: false` mean the same thing, matching Flutter's convention that a null callback disables a control; ordinary text is the right tool for prose that was never meant to navigate. `fortalLinkStyle` absorbs the old `_fortalInteractiveLinkStyle` and returns the complete style, so `FortalLink` sheds its mirrored focus field, its duplicated inert widget path, and its `excludeSemantics` workaround, and no longer branches on actionability. Requires `NakedLink`, released in naked_ui 1.0.0-beta.11 (conceptadev/naked_ui#65). The floor is raised in both package pubspecs and in the Fortal parity contract, which move together by design. The styler adopts the `@MixableSpec(target:)` generation from #152, so `LinkStyler.call` is generated rather than hand-written. Closes #137 Closes #143 BREAKING CHANGE: four changes to `remix_fortal` link behaviour. 1. `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. 2. `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. 3. A disabled `FortalLink` no longer publishes the Link role, because `NakedLink` gates the role on effective-enabled. This covers `onPressed: null` as well as `enabled: false`: a callback-less link now reports `hasEnabledState` and announces as unavailable, where it previously published a plain text node with no interactive metadata. 4. A disabled `FortalLink` is no longer reachable under `NavigationMode.directional`. The old `NakedButton`-backed recipe left disabled controls traversable on directional platforms; `NakedLink` does not.
Description
Adds a dependency-free
NakedLinkprimitive for navigation surfaces that need Link—not Button—behavior. It gives downstream design systems a reusable Link role, Enter-only keyboard activation, focus handling, and observable interaction state without coupling the core package to a router or launcher.This directly addresses the downstream
FortalLinkgaps documented in conceptadev/remix#137 and conceptadev/remix#143: the current Button-based workaround mirrors focus state, hand-builds Link semantics, and incorrectly activates on Space.Behavior
NakedLinkStatethrough the builder and state scope, including hover, focus, press, disabled state, and optional destination metadata.enabled && onPressed != null; inert Links have no Link role, URL, tap action, or focus stop.linkUrlsemantics metadata. Flutter web maps it to an anchorhref, so callback-owned navigation examples intentionally omit it to avoid two navigation owners.Documentation
hrefownership caveat.Visual Evidence
Desktop activation and hover feedback:
Inert/disabled behavior at a 390 px viewport:
Preview Deployment
The repository's hosted example deploys from
main. PR branches do not receive an isolated preview deployment, so the hosted example will includeNakedLinkonly after this PR merges.Validation
dart fix --apply— no fixes available.dart format --output=none --set-exit-if-changed .— 154 files checked, 0 changed.flutter analyze— no issues found.Related Issues
Related to conceptadev/remix#137 and conceptadev/remix#143.
Checklist
///).Breaking Change
Does this PR require users of the package to manually update their code?