Skip to content

feat: Add ref forwarding via nativeAttributes - #5049

Open
mgmolisani wants to merge 7 commits into
cloudscape-design:mainfrom
mgmolisani:feat/native-element-refs
Open

mgmolisani wants to merge 7 commits into
cloudscape-design:mainfrom
mgmolisani:feat/native-element-refs

Conversation

@mgmolisani

@mgmolisani mgmolisani commented Sep 25, 2026 •

Copy link
Copy Markdown

Description

Components accepting nativeAttributes now accept an optional ref alongside the attributes, merged with the component's own forwarded ref, so consumers can reach the underlying native element.

nativeAttributes lets you set attributes on the native element but gives no way to obtain the element itself. The only access today is whatever a component vends explicitly — typically focus() plus zero to two component-specific methods — which leaves text selection (selectionStart, setSelectionRange), imperative scrolling, measurement and observation (getBoundingClientRect, ResizeObserver), containment checks for focus and click-outside handling, and any third-party library that takes an element ref out of reach. The workarounds are a data-* attribute plus a DOM query, or an extra wrapper element — both brittle and coupled to internal structure.

The merge happens in the shared with-native-attributes wrapper, so every component already accepting nativeAttributes gains the ref at once. The component's own exposed ref is unaffected and continues to represent imperative actions on the composite component.

const inputRef = useRef<HTMLInputElement>(null);

<Input
  value={value}
  onChange={({ detail }) => setValue(detail.value)}
  nativeInputAttributes={{ ref: inputRef }}
/>

The ref is typed React.Ref<ET>, so object refs, callback refs and null all work.

Related links, issue #, if available: tracked internally (AWSUI ticket and approved contribution kick-off doc); no public issue.

How has this been tested?

Three new unit tests in src/internal/utils/__tests__/with-native-attributes.test.tsx cover an object ref, a callback ref, and merging a consumer ref with the component's internal ref; that suite passes 13/13.

@mgmolisani
mgmolisani requested a review from a team as a code owner September 25, 2026 00:47
@mgmolisani
mgmolisani requested review from amanabiy and removed request for a team September 25, 2026 00:47
@github-actions
github-actions Bot temporarily deployed to fork-dev-pages-react18 September 25, 2026 09:18 Inactive
@codecov

codecov Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.83%. Comparing base (fb5485b) to head (a9f0d02).
⚠️ Report is 35 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #5049      +/-   ##
==========================================
- Coverage   97.70%   93.83%   -3.87%     
==========================================
  Files         987      989       +2     
  Lines       31742    27557    -4185     
  Branches    11722     9495    -2227     
==========================================
- Hits        31013    25859    -5154     
- Misses        722      786      +64     
- Partials        7      912     +905     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions
github-actions Bot temporarily deployed to fork-dev-pages-react16 September 25, 2026 09:18 Inactive
@pan-kot
pan-kot requested a balanced review from Copilot September 28, 2026 06:29

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.

Copilot review overview

🟡 Changes recommended

The exported type introduces an unnecessary additional compatibility break, and the new public behavior remains undocumented.

Review effort: Balanced
Findings: 1 Medium severity · 1 Low severity

Open (2)
What changed in this PR

Adds native-element ref forwarding through existing native-attribute props while preserving component-level refs.

Changes:

  • Extends NativeAttributes with typed React refs.
  • Merges consumer and internal refs in the shared wrapper.
  • Adds unit coverage, snapshots, and a manual demonstration page.
File Description
src/​types/​native-attributes.ts Adds the typed native ref API.
src/​toggle/​interfaces.ts Types the toggle input element.
src/​textarea/​interfaces.ts Types the textarea element.
src/​status-indicator/​interfaces.ts Updates native attribute typing.
src/​spinner/​interfaces.ts Updates native attribute typing.
src/​space-between/​interfaces.ts Types the root div element.
src/​radio-button/​interfaces.ts Types the radio input element.
src/​prompt-input/​interfaces.ts Types the prompt textarea element.
src/​link/​interfaces.ts Types the anchor element.
src/​item-card/​internal.tsx Adapts to the helper signature.
src/​item-card/​interfaces.ts Types the root div element.
src/​internal/​utils/​with-native-attributes.tsx Merges internal and consumer refs.
src/​internal/​utils/​__tests__/​with-native-attributes.test.tsx Tests object, callback, and merged refs.
src/​internal/​components/​masked-input/​index.tsx Adapts native attribute processing.
src/​internal/​components/​autosuggest-input/​index.tsx Adapts native attribute processing.
src/​input/​interfaces.ts Types the input element.
src/​icon/​interfaces.ts Updates native attribute typing.
src/​divider/​interfaces.ts Updates native attribute typing.
src/​checkbox/​interfaces.ts Types the checkbox input element.
src/​button/​interfaces.ts Types button and anchor elements.
src/​button-dropdown/​interfaces.ts Types trigger and action elements.
src/​box/​interfaces.ts Updates native attribute typing.
src/​badge/​interfaces.ts Updates native attribute typing.
src/​action-card/​interfaces.ts Types button and anchor elements.
src/​__tests__/​snapshot-tests/​__snapshots__/​documenter.test.ts.snap Updates generated API snapshots.
pages/​native-refs/​default.page.tsx Demonstrates native ref use cases.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/types/native-attributes.ts Outdated
Comment thread src/input/interfaces.ts Outdated
@mgmolisani
mgmolisani force-pushed the feat/native-element-refs branch from d6f57e1 to 7875e93 Compare September 28, 2026 19:20
@mgmolisani

Copy link
Copy Markdown
Author

cc @pan-kot, who processed the intake request internally.

@pan-kot
pan-kot requested a balanced review from Copilot September 29, 2026 09:26
Comment thread pages/native-refs/default.page.tsx Outdated
export default function NativeRefsPage() {
return (
<Box tagOverride="article" padding={`m`}>
<h1>Native Element Refs</h1>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit: for test pages you can use our SimplePage helper - it has slots for title and more

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.

Copilot review overview

🟡 Changes recommended

The public API descriptions still do not document the newly supported ref behavior.

Review effort: Balanced
Findings: 1 Low severity

Open (1)
Resolved since last review (1)

Comment thread src/prompt-input/interfaces.ts Outdated
Comment thread pages/native-refs/default.page.tsx Outdated
Comment thread pages/native-refs/default.page.tsx Outdated
Comment thread pages/native-refs/default.page.tsx
Mike Molisani added 4 commits October 2, 2026 11:32
Components accepting nativeAttributes now accept an optional ref
alongside the attributes, merged with the component's own forwarded ref,
so consumers can reach the underlying native element for text selection,
imperative scrolling, measurement and observation, containment checks,
and integration with libraries that take an element ref.

The merge happens in the shared nativeAttributes wrapper, so every
component already accepting nativeAttributes gains the ref at once. The
component's own exposed ref is unaffected and continues to represent
imperative actions on the composite component.

The ref is typed React.Ref, so object refs, callback refs and null all
work.

BREAKING CHANGE: NativeAttributes takes the element as a required first
type parameter, e.g. NativeAttributes<HTMLButtonElement,
React.ButtonHTMLAttributes<HTMLButtonElement>>. It affects only
consumers who name the type explicitly; object literals passed to
component props are unchanged, and no compiled behaviour changes.
Every prop accepting nativeAttributes now states that a ref is accepted
and what it is for. Components that expose imperative ref methods also
carry a note that operating on the native element directly can bypass
the component's own behavior; display and layout components have no such
methods, so they carry only the first sentence.
The page now exercises a single Textarea carrying both the component ref
and a native ref: focus through each, an API reached only through the
native element, and observers attached to that element. The chained
native onChange reports inputType, data and selectionStart, none of
which the component's change detail carries.

Section headings are h2 to follow the page title's h1. Skipping to h3
fails the axe heading-order rule.
These props carry computed attributes injected by the existing Table's
td-element and th-element. They are now typed as the element's own
props rather than through the NativeAttributes generic, keeping the
previous shape of omitted children plus data-* attributes.
@mgmolisani
mgmolisani force-pushed the feat/native-element-refs branch from 8ffc0fc to f557cc5 Compare October 2, 2026 13:05
Comment thread src/table-body-cell/internal.tsx Outdated
@github-actions
github-actions Bot temporarily deployed to fork-dev-pages-react16 October 5, 2026 10:16 Inactive
@github-actions
github-actions Bot temporarily deployed to fork-dev-pages-react18 October 5, 2026 10:16 Inactive
Comment thread src/space-between/interfaces.ts Outdated
@github-actions
github-actions Bot temporarily deployed to fork-dev-pages-react16 October 6, 2026 16:37 Inactive
@github-actions
github-actions Bot temporarily deployed to fork-dev-pages-react18 October 6, 2026 16:37 Inactive
Comment thread src/types/native-attributes.ts Outdated
| undefined;
// The element type is recoverable from the attributes type: `React.DOMAttributes<E>` carries `E` in every
// event handler's `currentTarget`. Falls back to `HTMLElement` for attribute types that target a non-HTML element.
export type NativeElement<T> =

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Consider never as the fallback instead of HTMLElement. There's no mechanical advantage to lying about a type even if its case should never appear in practice — I swapped both fallback branches to never locally and tsc is clean across the repo, so nothing currently relies on it.

Separately, is there a reason NativeElement is exported? Nothing re-exports or consumes it, so it's only reachable by deep import.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Good call - updated.

Comment thread src/table-body-cell/internal.tsx Outdated
// Non-base native attributes injected by internal callers (the existing Table's td-element): role and
// sizing. Base props (className/id/data-*) flow directly and are read via getBaseProps.
nativeAttributes?: NativeAttributes<React.ThHTMLAttributes<HTMLTableCellElement>>;
nativeAttributes?: NativeAttributes<

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

These cells never go through the native attributes util, so none of this type's behavior applies to them — nothing honors the ref it declares, and the merge/chain semantics aren't implemented here either. They're raw element buckets, so typing them as such would leave this type for the actual util. Same in the header cell.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Agree - updated.

@mgmolisani

Copy link
Copy Markdown
Author

This is where I started. I abandoned it and posted the breaking change instead, since that version was more complete and a better basis for discussing the tradeoffs. Fine by me as the direction.

The tradeoff is that you're locked in if the element and the attributes type ever need to diverge, though that shouldn't come up with standard element types, and a cast or declaration merge covers it in a pinch.

@pan-kot

pan-kot commented Oct 7, 2026

Copy link
Copy Markdown
Member

This is where I started. I abandoned it and posted the breaking change instead, since that version was more complete and a better basis for discussing the tradeoffs. Fine by me as the direction.

The tradeoff is that you're locked in if the element and the attributes type ever need to diverge, though that shouldn't come up with standard element types, and a cast or declaration merge covers it in a pinch.

If we'd need to change types in an existing component - that is going to be a breaking change anyways. For new implementations, if the need emerges - we should be always able to introduce an alternative NativeAttributes version with two generic args.

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.

🟡 Changes recommended

ItemCard’s native ref overrides its internal root ref, disconnecting metadata and other root-dependent hooks.

2 open findings
1 resolved since last review

🧠 Review effort: Balanced

Comment thread src/item-card/internal.tsx
Comment thread src/types/native-attributes.ts

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.

🟢 Approval recommended

No blocking defects remain; feedback is limited to documentation accuracy and demo screenshot setup.

0 open findings

2 resolved since last review
Previously missed (2)

In code that hasn't changed since last review

Medium severity Add screenshotArea prop to SimplePage

pages/​native-refs/​default.page.tsx:134

Non-blocking: Add screenshotArea={{}}, as required by .github/instructions/dev-pages.instructions.md:17–19. Without it, SimplePage renders the content without a ScreenshotArea (pages/app/templates.tsx:39–45). The prop provides an isolated screenshot target while keeping the page heading outside it.

Low severity Correct textarea ref guidance for text selection

src/​textarea/​interfaces.ts:64

Non-blocking: TextareaProps.Ref exposes only focus() (lines 89–94), so this guidance directs consumers to a selection method that does not exist. Recommend the component ref for focus and the native ref for text selection, then regenerate the corresponding documentation snapshot.

🧠 Review effort: Balanced


Give feedback about Copilot approvals in this survey to enter a drawing for a $150 gift card.

// Non-base native attributes injected by internal callers (the existing Table's td-element): role and
// sizing. Base props (className/id/data-*) flow directly and are read via getBaseProps.
nativeAttributes?: NativeAttributes<React.ThHTMLAttributes<HTMLTableCellElement>>;
nativeAttributes?: Omit<

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why the bare Omit<...> here rather than NativeAttributes<> like the rest of the PR? Is this expected?

// Non-base native attributes injected by internal callers (the existing Table's th-element): colSpan,
// scope, role, aria-sort. Base props (className/id/data-*) flow directly and are read via getBaseProps.
nativeAttributes?: NativeAttributes<React.ThHTMLAttributes<HTMLTableCellElement>>;
nativeAttributes?: Omit<React.ThHTMLAttributes<HTMLTableCellElement>, 'children'>;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Same question, Why the bare Omit<...> here rather than NativeAttributes<> like the rest of the PR? Is this expected?

@mgmolisani mgmolisani Oct 8, 2026 •

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Left comments on this before. It's not using any of the native attributes ref features or semantics. This is the true type for this, not just a convenient usage of some other type. If the native attributes type changes because the utils for native attributes changed, this should not.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Though previously I believe my implementation used ComponentPropsWithoutRef which is slightly easier to reason about IMO.


return (
<Tag {...processedAttributes} ref={ref}>
<Tag {...processedAttributes} ref={mergedRef}>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Can we filter out ref in processAttributes so it doesn't get duplicated with mergedRef? Right now the ref from nativeAttributes falls into the else branch and gets copied into the returned object, so it ends up spread onto here. It works because the explicit ref={mergedRef} comes last, but that leaves correctness depending on the attribute order. Stripping ref out of the processedAttributes would help us avoid the dependency on order.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

The attribute order is intentional, not random and it should be tested behavior. It's a feature of the language. You can pull it out but its order dependency is the same as any objects keys.

Comment thread pages/native-refs/default.page.tsx

This branch was successfully deployed

2 active deployments
fork-dev-pages-react16 — a9f0d021 Deployed Oct 8, 2026 by github-actions[bot]
fork-dev-pages-react18 — a9f0d021 Deployed Oct 8, 2026 by github-actions[bot]
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.

4 participants