Skip to content

Conversation

@lindseywild
Copy link
Contributor

@lindseywild lindseywild commented Dec 9, 2025

Related to https://github.com/github/primer/issues/5937.

Related PVC PR

Below is a video showcasing that TextArea and TextField components have an optional characterLimit that can be passed in as a prop. When a user types, the field is updated with how many characters are left / how many are over. When a user exceeds the limit, the character count text changes to red and an error icon prepends the text.

There is also an aria-live region that updates after a slight delay (500ms) when a user finishes typing. This accurately tells screen reader users how many characters they have left / are over, as well as when the input is invalid if they have typed too many characters.

We are also including sr-only text that is associated with the input so that when a user focuses on the input, they hear "You can enter up to X character(s)". This was added because associating the "X character(s) remaining" message was causing duplicate announcements in NVDA and this is the recommended approach.

Components._.Textarea._.Features.-.With.Character.Limit.Storybook.-.19.December.2025.mp4

Changelog

New

  • Adds characterLimit prop to TextArea
  • Adds characterLimit prop to TextInput
  • Adds shared character_counter.ts functionality

Changed

Removed

Rollout strategy

  • Patch release
  • Minor release
  • Major release; if selected, include a written rollout or migration plan
  • None; if selected, include a brief description as to why

Testing & Reviewing

Merge checklist

@changeset-bot
Copy link

changeset-bot bot commented Dec 9, 2025

🦋 Changeset detected

Latest commit: dbf0f21

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

This PR includes changesets to release 1 package
Name Type
@primer/react Minor

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

@github-actions
Copy link
Contributor

github-actions bot commented Dec 9, 2025

👋 Hi, this pull request contains changes to the source code that github/github-ui depends on. If you are GitHub staff, test these changes with github/github-ui using the integration workflow. Or, apply the integration-tests: skipped manually label to skip these checks.

@github-actions github-actions bot added the integration-tests: recommended This change needs to be tested for breaking changes. See https://arc.net/l/quote/tdmpakpm label Dec 9, 2025
@github-actions github-actions bot temporarily deployed to storybook-preview-7293 December 9, 2025 18:11 Inactive
@github-actions github-actions bot requested a deployment to storybook-preview-7293 December 18, 2025 15:49 Abandoned
@github-actions github-actions bot temporarily deployed to storybook-preview-7293 December 18, 2025 16:27 Inactive
@lindseywild lindseywild added the update snapshots 🤖 Command that updates VRT snapshots on the pull request label Dec 18, 2025
@github-actions github-actions bot removed the update snapshots 🤖 Command that updates VRT snapshots on the pull request label Dec 18, 2025
{...rest}
aria-describedby={
characterLimit
? [characterCountStaticMessageId, rest['aria-describedby']].filter(Boolean).join(' ') || undefined
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We needed to add the characterCountStaticMessageId as an aria-describedby ID to ensure that the user is getting the information about the character limit.

<VisuallyHidden id={characterCountLiveRegionId} aria-live="polite" role="status">
{screenReaderMessage}
</VisuallyHidden>
<VisuallyHidden id={characterCountStaticMessageId}>
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is hidden as we want to include it for screen reader users, but including the actual character counter span that updates when a user types was causing double announcements.

<UnstyledTextInput
// @ts-expect-error it needs a non nullable ref
ref={inputRef}
<>
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I just wrapped the <TextInputWrapper> in an empty <> since we're adding another sibling component. I'll call out specific changes within this part of the diff!

@github-actions github-actions bot temporarily deployed to storybook-preview-7293 December 19, 2025 17:32 Inactive
@github-actions github-actions bot requested a deployment to storybook-preview-7293 December 19, 2025 17:42 Abandoned
@github-actions github-actions bot requested a deployment to storybook-preview-7293 December 19, 2025 17:49 Abandoned
Copy link
Member

@siddharthkp siddharthkp left a comment

Choose a reason for hiding this comment

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

Trust you with the implementation overall. Left a couple small nits.

Next steps:

  1. Add e2e tests + add update snapshots label
  2. Run integration tests

const [isInputFocused, setIsInputFocused] = useState<boolean>(false)
const inputRef = useProvidedRefOrCreate(ref as React.RefObject<HTMLInputElement | null>)
const [characterCount, setCharacterCount] = useState<string>('')
const [isOverLimit, setIsOverLimit] = useState<boolean>(false)
Copy link
Member

Choose a reason for hiding this comment

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

nit: Can this be inferred instead of stored in state? Might save us a re-render?

const isOverLimit = characterCount > characterLimit

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I tried storing it both as a ref and a constant and updating it in a useEffect, but since it needs to update on every keystroke it wasn't quite working for me. Do you have another suggestion or do you think it's ok as-is? I see you added it's a "nit" so wanted to check!

Copy link
Member

Choose a reason for hiding this comment

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

yep, called it a nit because I assumed you must have had a reason.

but since it needs to update on every keystroke it wasn't quite working for me.

that's odd, I assumed setCharacterCount would cause a re-render which can be used to calculate isOverLimit.

anywho, not ideal but also not blocking

@lindseywild lindseywild added the update snapshots 🤖 Command that updates VRT snapshots on the pull request label Dec 19, 2025
@github-actions github-actions bot temporarily deployed to storybook-preview-7293 December 19, 2025 19:01 Inactive
@primer-integration
Copy link

👋 Hi from github/github-ui! Your integration PR is ready: https://github.com/github/github-ui/pull/9302

@primer-integration
Copy link

🔬 github-ui Integration Test Results

Check Status Details
CI ⏳ Pending Waiting for workflow to complete
Projects (Memex) ✅ Passed View run
VRT ✅ Passed View run

@siddharthkp siddharthkp added the integration-tests: skipped manually Changes in this PR do not require an integration test label Dec 20, 2025
@siddharthkp
Copy link
Member

I see that one of the checks is still pending here but completed in the integration branch 🤔

Going to add integration-tests: skipped manually because I confirmed it passed on the integration PR, it's just reporting correctly.

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

Labels

integration-tests: recommended This change needs to be tested for breaking changes. See https://arc.net/l/quote/tdmpakpm integration-tests: skipped manually Changes in this PR do not require an integration test

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants