Skip to content

feat(textfield, number-field): add tooltip for a11y on truncated input text#6038

Open
rise-erpelding wants to merge 24 commits intomainfrom
rise-erpelding/swc-1183-text-spacing-cutoff
Open

feat(textfield, number-field): add tooltip for a11y on truncated input text#6038
rise-erpelding wants to merge 24 commits intomainfrom
rise-erpelding/swc-1183-text-spacing-cutoff

Conversation

@rise-erpelding
Copy link
Collaborator

@rise-erpelding rise-erpelding commented Feb 19, 2026

Description

Adds truncated-value tooltip behavior to single-line text inputs in sp-textfield and sp-number-field.

  • Detects when the visible value is clipped and conditionally renders a tooltip with the full value.
  • Applies behavior to both valid and invalid values (when truncated).
  • Includes updates to tests, stories, docs, and changeset coverage.

Updates (2/27/26):

  • Does not show tooltip for password and removes truncation for password fields
  • Adds describeTrigger property to overlay to prevent overlay from setting aria-describedby, thus preventing screen reader double announcement
  • Breaks tooltip feature out into a mixin which lazy-loads tooltip and overlay
  • Although it worked in textfield, number-field was not reflecting real-time edits within the tooltip, so a fix was made to keep truncated tooltip in sync with edits
  • In number-fields whose content went from untruncated to truncated, the tooltip was not showing up - this was fixed
  • Tooltip is now maintained during focus (while typing), it doesn't suddenly disappear when content is small enough to not need truncation

Motivation and context

Long values in narrow fields can be difficult to review, especially while correcting invalid input.

This change improves usability by exposing the full value on hover/focus only when truncation occurs, while keeping default behavior unchanged when content fits.

Related issue(s)

  • fixes SWC-1183

Screenshots (if appropriate)

image image

Author's checklist

  • I have read the CONTRIBUTING and PULL_REQUESTS documents.
  • I have reviewed at the Accessibility Practices for this feature, see: Aria Practices
  • I have added automated tests to cover my changes.
  • I have included a well-written changeset if my change needs to be published.
  • I have included updated documentation if my change required it.

Reviewer's checklist

  • Includes a Github Issue with appropriate flag or Jira ticket number without a link
  • Includes thoughtfully written changeset if changes suggested include patch, minor, or major features
  • Automated tests cover all use cases and follow best practices for writing
  • Validated on all supported browsers
  • All VRTs are approved before the author can update Golden Hash

Manual review test cases

  • Textfield shows tooltip only when visually truncated

    1. Open Textfield / truncatedValueTooltip story.
    2. Hover and keyboard-focus the truncated example.
    3. Verify full value is shown in tooltip.
    4. Verify non-truncated example does not show tooltip.
  • Invalid truncated textfield still exposes full value

    1. Use the invalid truncated textfield story/example.
    2. Hover/focus the field.
    3. Verify tooltip appears with the full value even while invalid.
  • Number-field truncation behavior works with and without stepper

    1. Open Number Field / truncatedValueTooltip story.
    2. Verify tooltip appears for truncated examples (stepper visible and hidden).
    3. Verify formatted currency text appears in tooltip.
  • Regression checks

    1. Verify multiline textfield behavior is unchanged (no new truncation tooltip behavior introduced there).
    2. Verify no regressions in validation icon/help-text rendering.

Updates (2/27/26):

  • Check password field in Textfield truncation stories, confirm that this does not have truncation ellipses and does not show the password in a tooltip
  • Use screen-reader on truncated textfield to determine that the text input is not double announced
  • Use screen-reader on truncated numberfield to determine that the number input is not double announced
  • Check truncated number-fields, when editing truncated content it should update the value in real time (Note: it doesn't re-place the commas in real-time... yet)
  • Type in a number long enough (such as 12345678) to truncate in the number-field story to confirm that the truncation tooltip appears in number-fields that are not initially truncated
  • On that same number-field story with a number that is long enough to truncate (such as 12345678), remove characters from the input so that it is short enough to not need truncation (for instance, change the input from 12345678 to 12345). Verify that the tooltip does not disappear until you mouse/key focus somewhere else, and when you focus on the number-field again (with the shorter number inputted), the tooltip does not reappear.
  • Check a textfield story for the same. If text input truncates, you should see the tooltip while typing, and if the text becomes shortened enough to not need truncation, the tooltip still appears until the next time the input is focused.

Device review

  • Did it pass in Desktop?
  • Did it pass in (emulated) Mobile?
  • Did it pass in (emulated) iPad?

Accessibility testing checklist

Required: Complete each applicable item and document your testing steps (replace the placeholders with your component-specific instructions).

  • Keyboard (required — document steps below) — What to test for: Focus order is logical; Tab reaches the component and all interactive descendants; Enter/Space activate where appropriate; arrow keys work for tabs, menus, sliders, etc.; no focus traps; Escape dismisses when applicable; focus indicator is visible.

    1. Go to truncated numberfield stories
    2. Tab to the first number-field input
    3. When focused, the tooltip displaying the full value should be visible
  • Screen reader (required — document steps below) — What to test for: Role and name are announced correctly; state changes (e.g. expanded, selected) are announced; labels and relationships are clear; no unnecessary or duplicate announcements.

    1. Go truncated textfield stories
    2. Turn on VoiceOver
    3. Expect truncated input values to be read - they actually are being read twice due to the overlay setting aria-describedby; addressing this felt a bit out-of-scope

@rise-erpelding rise-erpelding self-assigned this Feb 19, 2026
@changeset-bot
Copy link

changeset-bot bot commented Feb 19, 2026

🦋 Changeset detected

Latest commit: edee920

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 83 packages
Name Type
@spectrum-web-components/overlay Patch
@spectrum-web-components/textfield Patch
@spectrum-web-components/number-field Patch
@spectrum-web-components/action-menu Patch
@spectrum-web-components/combobox Patch
@spectrum-web-components/contextual-help Patch
@spectrum-web-components/menu Patch
@spectrum-web-components/picker Patch
@spectrum-web-components/popover Patch
@spectrum-web-components/tooltip Patch
@spectrum-web-components/story-decorator Patch
@spectrum-web-components/bundle Patch
@spectrum-web-components/truncated Patch
@spectrum-web-components/color-field Patch
@spectrum-web-components/search Patch
@spectrum-web-components/slider Patch
@spectrum-web-components/breadcrumbs Patch
@spectrum-web-components/custom-vars-viewer Patch
@spectrum-web-components/action-bar Patch
@spectrum-web-components/card Patch
@spectrum-web-components/coachmark Patch
documentation Patch
@spectrum-web-components/vrt-compare Patch
@spectrum-web-components/accordion Patch
@spectrum-web-components/action-button Patch
@spectrum-web-components/action-group Patch
@spectrum-web-components/alert-banner Patch
@spectrum-web-components/alert-dialog Patch
@spectrum-web-components/asset Patch
@spectrum-web-components/avatar Patch
@spectrum-web-components/badge Patch
@spectrum-web-components/button-group Patch
@spectrum-web-components/button Patch
@spectrum-web-components/checkbox Patch
@spectrum-web-components/clear-button Patch
@spectrum-web-components/close-button Patch
@spectrum-web-components/color-area Patch
@spectrum-web-components/color-handle Patch
@spectrum-web-components/color-loupe Patch
@spectrum-web-components/color-slider Patch
@spectrum-web-components/color-wheel Patch
@spectrum-web-components/dialog Patch
@spectrum-web-components/divider Patch
@spectrum-web-components/dropzone Patch
@spectrum-web-components/field-group Patch
@spectrum-web-components/field-label Patch
@spectrum-web-components/help-text Patch
@spectrum-web-components/icon Patch
@spectrum-web-components/icons-ui Patch
@spectrum-web-components/icons-workflow Patch
@spectrum-web-components/icons Patch
@spectrum-web-components/iconset Patch
@spectrum-web-components/illustrated-message Patch
@spectrum-web-components/infield-button Patch
@spectrum-web-components/link Patch
@spectrum-web-components/meter Patch
@spectrum-web-components/modal Patch
@spectrum-web-components/picker-button Patch
@spectrum-web-components/progress-bar Patch
@spectrum-web-components/progress-circle Patch
@spectrum-web-components/radio Patch
@spectrum-web-components/sidenav Patch
@spectrum-web-components/split-view Patch
@spectrum-web-components/status-light Patch
@spectrum-web-components/swatch Patch
@spectrum-web-components/switch Patch
@spectrum-web-components/table Patch
@spectrum-web-components/tabs Patch
@spectrum-web-components/tags Patch
@spectrum-web-components/thumbnail Patch
@spectrum-web-components/toast Patch
@spectrum-web-components/top-nav Patch
@spectrum-web-components/tray Patch
@spectrum-web-components/underlay Patch
@spectrum-web-components/base Patch
@spectrum-web-components/grid Patch
@spectrum-web-components/opacity-checkerboard Patch
@spectrum-web-components/reactive-controllers Patch
@spectrum-web-components/shared Patch
@spectrum-web-components/styles Patch
@spectrum-web-components/theme Patch
@spectrum-web-components/eslint-plugin Patch
@spectrum-web-components/stylelint-header-plugin Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@rise-erpelding rise-erpelding added the Status:WIP PR is a work in progress or draft label Feb 19, 2026
@rise-erpelding rise-erpelding force-pushed the rise-erpelding/swc-1183-text-spacing-cutoff branch 2 times, most recently from a03db36 to c46b5e3 Compare February 19, 2026 02:45
@github-actions
Copy link
Contributor

github-actions bot commented Feb 19, 2026

📚 Branch Preview Links

🔍 First Generation Visual Regression Test Results

When a visual regression test fails (or has previously failed while working on this branch), its results can be found in the following URLs:

Deployed to Azure Blob Storage: pr-6038

If the changes are expected, update the current_golden_images_cache hash in the circleci config to accept the new images. Instructions are included in that file.
If the changes are unexpected, you can investigate the cause of the differences and update the code accordingly.

@rise-erpelding rise-erpelding force-pushed the rise-erpelding/swc-1183-text-spacing-cutoff branch 2 times, most recently from c6a7d36 to 7d06234 Compare February 19, 2026 22:03
@rise-erpelding rise-erpelding marked this pull request as ready for review February 19, 2026 22:18
@rise-erpelding rise-erpelding requested a review from a team as a code owner February 19, 2026 22:18
@rise-erpelding rise-erpelding added Status:Ready for review PR ready for review or re-review. Component:Textfield a11y Issues or PRs related to accessibility Component:Number field and removed Status:WIP PR is a work in progress or draft labels Feb 19, 2026
@rise-erpelding rise-erpelding removed their assignment Feb 19, 2026
el.value = 45;
expect(el.value).to.equal(45);
el.focus();
await sendKeys({ type: '7' }); // Visible text: EUR 45.007
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Just linting fixes in this file below this point

Copy link
Contributor

@Rajdeepc Rajdeepc left a comment

Choose a reason for hiding this comment

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

Liked the approach here. Most of the direction looks sound to me except for a few areas which needs attention. One major blocker is the the password field security issue and the added dependencies on the textfield is a bundle size concern for me, we can discuss this with the team and the eager ResizeObserver allocation will hinder performance. Try to optimize and think of a more performant approach.
Most of the other things looks solid and thanks for handling this a11y compliance. Happy to work with you in resolving the blocker.

@rise-erpelding rise-erpelding added Status:Addressing feedback PR owner is addressing review comments and will change label back to "Ready for review" when ready. and removed Status:Ready for review PR ready for review or re-review. labels Feb 20, 2026
Copy link
Contributor

@nikkimk nikkimk left a comment

Choose a reason for hiding this comment

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

I think we might need to discuss our options with this one given the a11y concerns.

Copy link
Contributor

@5t3ph 5t3ph left a comment

Choose a reason for hiding this comment

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

In addition to being concerned about the issue for screen readers, I noticed that for number field the tooltip does not reflect real-time edits to the value (deletions or insertions).

The value does update real-time for textfield, but still with a bit of oddness that the tooltip is removed if the value decreases below the threshold (which is a little jarring, possible cognitive issue ex "wait, what just happened? did I do something wrong?"). But, it doesn't re-appear when the value increases above the threshold. Not reappearing might be ok, since the user is actively in the field and their cursor position is still allowing them to see the relevant part that they're editing.

@rise-erpelding rise-erpelding added Status:Blocked PR is blocked for some reason and removed Status:Blocked PR is blocked for some reason labels Feb 23, 2026
@rise-erpelding rise-erpelding force-pushed the rise-erpelding/swc-1183-text-spacing-cutoff branch 3 times, most recently from 7ccfd84 to aebf001 Compare February 27, 2026 16:35
Copy link
Contributor

@caseyisonit caseyisonit left a comment

Choose a reason for hiding this comment

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

great job

- change name of resize observer so it doesn't clash
- add guard for inputElement
- also add aria-hidden to tooltip
- Do not reflect tooltip-placement
- Trigger refreshTruncationState from update() only on value change
- Run observer setup and refreshTruncationState only when !multiline
- Test import remains from '../'
@rise-erpelding rise-erpelding force-pushed the rise-erpelding/swc-1183-text-spacing-cutoff branch from bca6b86 to fbaeb28 Compare March 10, 2026 21:11
this._resizeObserver.disconnect();
this._resizeObserver = null;
}
this._observerInitialized = false;
Copy link
Contributor

Choose a reason for hiding this comment

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

If the host is disconnected and then reconnected, the observer won't be recreated until hostUpdated() runs. This seems fine for most cases, but only if the host actually triggers an update cycle after reconnection. If it doesn't (e.g. no property changes), the observer won't be re-attached.

this.truncatedValueTooltipController.refresh();
if (justBecameTruncated) {
this.updateComplete.then(() => {
this.truncatedValueTooltipController.syncTooltipText(
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: if it justBecameTruncated, we'd be syncing twice (it's already happening above on line 549).

this.refreshTruncationState();
});
this._resizeObserver.observe(this.host as unknown as Element);
if (host.inputElement) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Are we sure this is guaranteed to exist on hostUpdated? If host.inputElement is null on the first hostUpdated() call, only the host element gets observed. On subsequent renders, _observerInitialized is already true so the early return skips observing inputElement even once it exists.

Comment on lines +135 to +137
const overlayWithDescribe = this.overlay as {
describeTrigger?: 'auto' | 'none';
};
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: couldn't we use the Overlay type directly?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

a11y Issues or PRs related to accessibility Component:Number field Component:Textfield Status:Ready for re-review PR has had its feedback addressed and is once again ready for review.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants