Skip to content

feat: add naked link - #65

Draft
leoafarias wants to merge 16 commits into
mainfrom
feat/naked-link
Draft

feat: add naked link#65
leoafarias wants to merge 16 commits into
mainfrom
feat/naked-link

Conversation

@leoafarias

@leoafarias leoafarias commented Jul 13, 2026

Copy link
Copy Markdown
Member

Description

Adds the headless NakedLink primitive from the Phase 2 contract. The Link exposes Link rather than Button semantics, optional URL metadata, effective-disabled behavior, observable hover/focus/press state, caller-owned focus-node support, primary/semantic activation, and Enter/Numpad Enter keyboard activation. Space and secondary click remain unclaimed.

This PR is ready for code review, but Phase 2 closure is explicitly blocked on the evidence listed below. Do not merge without maintainer authorization.

1. Contract summary

  • Adds NakedLink and immutable NakedLinkState.
  • Effective enabled state is enabled && onPressed != null.
  • Primary tap, Enter, Numpad Enter, and semantic tap call the consumer callback once.
  • Canceled primary gestures clear pressed state; Space and secondary click do not activate.
  • Semantics expose Link, optional linkUrl, name/hint, enabled/focus state, and tap only while enabled; Button semantics are absent.
  • Styling, routing/launching, visited history, localized copy, modifier-click policy, Context Menu, and Hover Card behavior remain consumer-owned.

2. Compatibility statement

This is an additive, non-breaking API. No existing public API, source behavior, semantics, keyboard mapping, or focus behavior changes. Consumers adopting NakedLink should replace text-styled navigation buttons only when Link semantics and Enter-without-Space activation are intended.

3. Implementation notes

  • Reuses WidgetStatesMixin, NakedStateScopeBuilder, and NakedFocusableDetector.
  • Uses a Link-private intent mapped only from Enter and Numpad Enter, avoiding Flutter's ambient Space-to-ActivateIntent behavior.
  • Keeps one outer Link semantics node and excludes gesture-generated semantics.
  • Borrows external focus nodes and never disposes caller resources.
  • Owns no router, launcher, overlay, controller, timer, product copy, or visual style.

4. Requirement traceability

Requirement ID Requirement Automated test Platform proof Screenshot/manual AT Result
LINK-API-01 API invariant, immutable state/scope, URL equality/hash naked_link_test.dart; hash contract flutter-tester + macOS N/A Pass
LINK-ACT-01 Primary once; cancel/secondary do not activate activation widget tests; Link integration flutter-tester + macOS pointer state in fixture Pass
LINK-KEY-01 Enter/Numpad activate; Space unclaimed keyboard widget test; Link integration flutter-tester + macOS + pinned web keyboard-focus macOS reviewed Pass
LINK-STATE-01 Effective enabled controls activation/traversal/feedback/cursor widget and integration transitions flutter-tester + macOS + API 34 Android disabled reviewed Pass
LINK-STATE-02 Hover/focus/press callbacks and state snapshots exact widget and integration state tests flutter-tester + macOS focus screenshot reviewed Pass
LINK-LIFE-01 Focus ownership/replacement/disposal and callback removal lifecycle widget tests; aggregate teardown flutter-tester + macOS N/A Pass
LINK-SEM-01 Link/URL/name/hint/enabled/focus/action exact; Button absent naked_link_semantics_test.dart automated semantics + hosted web behavior manual AT blocked Partial
LINK-SEM-02 Disabled action absent; label/icon/exclusion/Arabic RTL exact semantics and fixture tests flutter-tester + macOS manual AT blocked Partial
LINK-COMP-01 Primary/secondary/hover/keyboard compose without conflict secondary path covered now real composition unavailable requires Phases 5 and 7 Blocked
LINK-VIS-01 Inline, focus, disabled, external, 200% text, RTL appearance fixture/guideline/golden harness Ubuntu + macOS + Android reviewed required web captures unsupported Partial

Test-first proof included missing-API compilation failures, missing-Link semantics failures, and targeted failing mutations for Space activation, secondary activation, Button semantics, target size/contrast, inline height, and standalone width. All temporary mutations were restored.

5. Screenshot manifest

Exact implementation/evidence head: 24460f0a94b657854c95d5dc900e5ef7215d9604; hosted merge ref 09e62c8dc29b424a1d00e5e7de8cfc4a99cd124f. Local macOS captures use Flutter 3.41.2, 800×600 logical pixels, DPR 1, en-US/LTR, and disabled animations.

Required artifact Status SHA-256 / blocker
link__default_inline__macos__reference.png Reviewed; hosted/local bytes match 02f2565b397780d8ea50d2a8c98c754fcab85464d13abe76c8b962097fd3a6a7
link__keyboard_focus__macos__reference.png Reviewed; hosted/local bytes match 18683fbc41e260fa54622db65893c164bd4bdb29b06cbdea32754a098c9d9095
link__long_text_200__macos__reference.png Reviewed; hosted/local bytes match 4bf12ea4007dadc972a74bb71fa9a376f85dc823aca53658d381a70a68085cda
link__disabled__android__reference.png Reviewed on hosted API 34 db651e38a3e4b5f53e4ae07cd059e02a235b29563ea222806ce96795b5636b75
link__hover__web__reference.png Blocked Flutter 3.41.2 web capture remains unsupported
link__external_hint__web__reference.png Blocked Flutter 3.41.2 web capture remains unsupported
link__rtl__web__reference.png Blocked Flutter 3.41.2 web capture remains unsupported

Visual review: exact manifest metadata and file dimensions verified; default inline layout, visible keyboard focus, standalone target sizing, disabled styling, 200% wrapping, clipping/overflow, and Android safe-area placement were reviewed. The accepted Ubuntu golden is 800×600 with pinned Roboto and Material Icons, SHA-256 88f39adcc2a5916f370d2ed5fdd8ff897e9c6cc4f00a5e3a9d71152ccf22086e; the same hosted job subsequently compared the checked-in file unchanged. No overlay behavior applies to Link.

6. Accessibility evidence

  • Exact semantics tests cover Link true, Button false, URL, label, hint, enabled/focused flags, action presence/removal, visible versus overriding names, decorative icon exclusion, Arabic RTL, and full exclusion.
  • Label and contrast guidelines pass for the inline fixture.
  • The separate standalone fixture passes labeled-target, Android target, iOS target, and text-contrast guidelines without inflating the inline link to a 48px line box.
  • Required VoiceOver, TalkBack, Chrome accessibility-tree, and release-iOS VoiceOver sessions are unavailable and block closure; automated semantics are not substituted for them.

7. Platform evidence

Local Flutter 3.41.2 evidence before publication:

  • dart format --set-exit-if-changed . — pass.
  • flutter analyze — pass.
  • Package suite — 597 pass, 3 documented external-integration skips.
  • Example suite — 24 pass, 2 host-specific pixel-comparison skips on macOS.
  • Link focused package proof — 39 pass.
  • Link integration on flutter-tester — 8 pass.
  • Fast aggregate on flutter-tester — 96 pass, 1 documented Tooltip skip.
  • Link integration on real macOS — 8 pass.
  • Aggregate on real macOS — 96 pass, 1 documented Tooltip skip.
  • macOS screenshot driver — 6 behavior scenarios pass with complete manifest.

All seven hosted checks passed on implementation head 24460f0 / merge ref 09e62c8:

Final documentation-only head: 261455521a27fb822307f7ff83bd1082725c5ecc; final test merge ref: fab2edadfeee84e07fd44df2a91f8884df7451c7. All exact-head checks are green:

8. Known limitations and blockers

  • Flutter 3.41.2 cannot produce the three stable required web screenshots in this repository; a web behavior log is not treated as a substitute.
  • Manual VoiceOver, TalkBack, Chrome accessibility-tree, and release-iOS evidence is unavailable.
  • Real Context Menu and Hover Card composition cannot exist until the Phase 5 and Phase 7 primitives land; placeholder wrappers are intentionally not used.
  • URL launching, routing, visited history, browser downloads, modifier-click/window management, and styling are explicit non-goals.

9. Documentation

Includes API dartdoc and example, root/package README usage, registry entry, changelog, executable Phase 2 plan, status board, test map, evidence names, and compatibility/non-goal notes. No migration is required because the API is additive.

10. Consumer note

Remix should provide routing or launching in onPressed, pass linkUrl only as semantic/DOM metadata, localize labels and hints, and style the builder's immutable states. Remix must still verify its router behavior, modifier-click policy, visited appearance, standalone versus inline target sizing, and later Context Menu/Hover Card composition.

Checklist

  • My PR includes unit or integration tests for all changed behaviors.
  • I have updated or added relevant documentation.
  • I am prepared to follow up on review comments in a timely manner.
  • All required platform, screenshot, and manual accessibility closure evidence is present (blocked as documented).
  • No, this is not a breaking change.

@github-actions

Copy link
Copy Markdown
Contributor

LCOV of commit 2565a27 during Flutter CI #229

	(use "lcov --ignore-errors deprecated,deprecated ..." to suppress this warning)
Reading tracefile /tmp/__zgosalvez_github-actions-report-lcov/lcov.info.
Summary coverage rate:
  source files: 23
  lines.......: 90.5% (2094 of 2315 lines)
  functions...: no data found
  branches....: no data found
Message summary:
  1 warning message:
    deprecated: 1

Files changed coverage rate:
  	(use "lcov --ignore-errors deprecated,deprecated ..." to suppress this warning)
                                                                   |Lines       |Functions  |Branches    
  Filename                                                         |Rate     Num|Rate    Num|Rate     Num
  packages/naked_ui/lib/src/naked_link.dart                        |91.4%     70|    -     0|    -      0
  packages/naked_ui/lib/src/utilities/intents.dart                 |95.6%    114|    -     0|    -      0

@github-actions

Copy link
Copy Markdown
Contributor

LCOV of commit 24460f0 during Flutter CI #230

	(use "lcov --ignore-errors deprecated,deprecated ..." to suppress this warning)
Reading tracefile /tmp/__zgosalvez_github-actions-report-lcov/lcov.info.
Summary coverage rate:
  source files: 23
  lines.......: 90.5% (2094 of 2315 lines)
  functions...: no data found
  branches....: no data found
Message summary:
  1 warning message:
    deprecated: 1

Files changed coverage rate:
  	(use "lcov --ignore-errors deprecated,deprecated ..." to suppress this warning)
                                                                   |Lines       |Functions  |Branches    
  Filename                                                         |Rate     Num|Rate    Num|Rate     Num
  packages/naked_ui/lib/src/naked_link.dart                        |91.4%     70|    -     0|    -      0
  packages/naked_ui/lib/src/utilities/intents.dart                 |95.6%    114|    -     0|    -      0

@github-actions

Copy link
Copy Markdown
Contributor

LCOV of commit 2614555 during Flutter CI #231

	(use "lcov --ignore-errors deprecated,deprecated ..." to suppress this warning)
Reading tracefile /tmp/__zgosalvez_github-actions-report-lcov/lcov.info.
Summary coverage rate:
  source files: 23
  lines.......: 90.5% (2094 of 2315 lines)
  functions...: no data found
  branches....: no data found
Message summary:
  1 warning message:
    deprecated: 1

Files changed coverage rate:
  	(use "lcov --ignore-errors deprecated,deprecated ..." to suppress this warning)
                                                                   |Lines       |Functions  |Branches    
  Filename                                                         |Rate     Num|Rate    Num|Rate     Num
  packages/naked_ui/lib/src/naked_link.dart                        |91.4%     70|    -     0|    -      0
  packages/naked_ui/lib/src/utilities/intents.dart                 |95.6%    114|    -     0|    -      0

@github-actions

Copy link
Copy Markdown
Contributor

LCOV of commit 8084ecf during Flutter CI #234

	(use "lcov --ignore-errors deprecated,deprecated ..." to suppress this warning)
Reading tracefile /tmp/__zgosalvez_github-actions-report-lcov/lcov.info.
Summary coverage rate:
  source files: 23
  lines.......: 90.4% (2139 of 2365 lines)
  functions...: no data found
  branches....: no data found
Message summary:
  1 warning message:
    deprecated: 1

Files changed coverage rate:
  	(use "lcov --ignore-errors deprecated,deprecated ..." to suppress this warning)
                                                                   |Lines       |Functions  |Branches    
  Filename                                                         |Rate     Num|Rate    Num|Rate     Num
  packages/naked_ui/lib/src/mixins/naked_mixins.dart               | 100%     78|    -     0|    -      0
  packages/naked_ui/lib/src/naked_link.dart                        |90.0%     90|    -     0|    -      0
  packages/naked_ui/lib/src/utilities/intents.dart                 |95.6%    114|    -     0|    -      0
  packages/naked_ui/lib/src/utilities/naked_focusable_detector.dart|97.4%     76|    -     0|    -      0

# 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
@github-actions

Copy link
Copy Markdown
Contributor

LCOV of commit 3bd5b38 during Flutter CI #236

	(use "lcov --ignore-errors deprecated,deprecated ..." to suppress this warning)
Reading tracefile /tmp/__zgosalvez_github-actions-report-lcov/lcov.info.
Summary coverage rate:
  source files: 23
  lines.......: 90.2% (2209 of 2449 lines)
  functions...: no data found
  branches....: no data found
Message summary:
  1 warning message:
    deprecated: 1

Files changed coverage rate:
  	(use "lcov --ignore-errors deprecated,deprecated ..." to suppress this warning)
                                                                   |Lines       |Functions  |Branches    
  Filename                                                         |Rate     Num|Rate    Num|Rate     Num
  packages/naked_ui/lib/src/mixins/naked_mixins.dart               | 100%     78|    -     0|    -      0
  packages/naked_ui/lib/src/naked_link.dart                        |84.1%    126|    -     0|    -      0
  packages/naked_ui/lib/src/utilities/intents.dart                 |95.6%    114|    -     0|    -      0
  packages/naked_ui/lib/src/utilities/naked_focusable_detector.dart|97.5%     81|    -     0|    -      0

@github-actions

Copy link
Copy Markdown
Contributor

LCOV of commit 52d9c97 during Flutter CI #237

	(use "lcov --ignore-errors deprecated,deprecated ..." to suppress this warning)
Reading tracefile /tmp/__zgosalvez_github-actions-report-lcov/lcov.info.
Summary coverage rate:
  source files: 23
  lines.......: 90.2% (2209 of 2449 lines)
  functions...: no data found
  branches....: no data found
Message summary:
  1 warning message:
    deprecated: 1

Files changed coverage rate:
  	(use "lcov --ignore-errors deprecated,deprecated ..." to suppress this warning)
                                                                   |Lines       |Functions  |Branches    
  Filename                                                         |Rate     Num|Rate    Num|Rate     Num
  packages/naked_ui/lib/src/mixins/naked_mixins.dart               | 100%     78|    -     0|    -      0
  packages/naked_ui/lib/src/naked_link.dart                        |84.1%    126|    -     0|    -      0
  packages/naked_ui/lib/src/utilities/intents.dart                 |95.6%    114|    -     0|    -      0
  packages/naked_ui/lib/src/utilities/naked_focusable_detector.dart|97.5%     81|    -     0|    -      0

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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, and NakedLinkResolver with Link semantics and Enter-only keyboard activation.
  • Extends NakedFocusableDetector to 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.

Comment on lines +248 to +252
if (kIsWeb && widget.linkUrl.hasScheme) {
unawaited(_launchWebExternalLink());
return;
}
unawaited(followLink());
Comment on lines +283 to +291
void _handlePointerActivation(launcher.FollowLink followLink) {
final modified = _modifiedPointerActivation || _hasPointerModifier;
_modifiedPointerActivation = false;
if (modified) {
unawaited(followLink());
return;
}
_handleOrdinaryActivation(followLink);
}
@leoafarias
leoafarias marked this pull request as draft July 14, 2026 00:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants