Skip to content

ref: growing input is no more #86553

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 15 commits into from
Jun 18, 2025
Merged

ref: growing input is no more #86553

merged 15 commits into from
Jun 18, 2025

Conversation

JonasBa
Copy link
Member

@JonasBa JonasBa commented Mar 6, 2025

All bow to useAutosizeInput and <Input autosize/>

I have opted to use <Input autosize as opposed to requiring everyone to call the hook, as this allows us to ensure that the hook is correctly called (value is required depending on if the input is controlled or not), meaning that we can reduce the possible bug surface.

CleanShot.2025-03-07.at.09.48.15.mp4

The PR is missing tests, I had started to write some and realized mid way that I will have to mock pretty much all of the window styles methods, defeating the purpose of the tests (I will give that another go)

The GrowingInput component was also hiding a bug that caused react to not throw warnings in development when only the value prop was provided as it was providing its own onchange handler and then calling props.onChange?.() which was always falsy

Below is what I originally typed into the draft PR and explains the reasoning for ^ decision more in depth.

The question now is, do we expose this via hook, or do we build it into the Input via autosize prop like <Input autosize/>? I think I am leaning towards autosize prop as I believe it is more intuitive than consuming a hook (which could still be exposed).

If we require users to call the hook, then the API would look something like

function Component() {
  const ref = useAutosizeInput({value})
  return <Input ref={ref}/>
}

The awkward part is that exposing it like this could that the DOM structure could depend on the autosize prop (provided would use the CSS only approach that @natemoo-re shared, which requires a wrapper component). An alternative could be to add autosize to InputGroup and require the prop to be set there, but that feels weird, as the autosize input should be set on the input itself, and making InputGroup a requirement for autosize to work feels sub-optimal.

function Component(){
  return (
   <InputGroup autosize> // <- this feels weird, but also means that standalone inputs dont support autosize?
     <InputGroup.Input/>
  </InputGroup>
}

Finally, and the version that I am most leaning towards is to expose this as an autosize bool prop on Input, and use the current JS handler to measure resizing. This way, we can also ensure that the hook is correctly called with the value props if the component is controlled, and hide that last bit of complexity from the user (provided we dont use the pure CSS solution for now)

function Component() {
  return <Input autosize/>
}

@natemoo-re @TkDodo @evanpurkhiser or anyone else reading this, feedback and opinions would be much appreciated

@github-actions github-actions bot added the Scope: Frontend Automatically applied to PRs that change frontend components label Mar 6, 2025
Comment on lines 62 to 66
const computedStyles = window.getComputedStyle(input);

const sizingDiv = createSizingDiv(computedStyles);
sizingDiv.innerText = input.value || input.placeholder;
document.body.appendChild(sizingDiv);
Copy link
Member Author

Choose a reason for hiding this comment

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

Seems pretty wasteful to append a new child per input, but I'm holding off on making wider changes here.

Copy link
Member

Choose a reason for hiding this comment

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

this is creating a new child per input per resize though, right? could be a performance nightmare

Copy link
Member Author

Choose a reason for hiding this comment

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

Yup, exactly! It's been doing that before too, so that'd be a great case to go for the pure CSS solution you shared!

document.body.appendChild(sizingDiv);

const newTotalInputSize =
sizingDiv.offsetWidth +
Copy link
Member Author

Choose a reason for hiding this comment

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

I wonder if we can just use calc(${n}ch + padding instead of querying the width. If we can, then we might be able to remove other parts of the sizing functionality as well

Copy link
Member Author

Choose a reason for hiding this comment

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

Update: we cant because ch unit is apparently based on the the 0 character. When I tested this, I was seeing a drift between the input and width which grew with the size of the input

Copy link

codecov bot commented Mar 6, 2025

❌ 1 Tests Failed:

Tests completed Failed Passed Skipped
9665 1 9664 10
View the top 1 failed test(s) by shortest run time
ComboBox can click to select an option
Stack Traces | 0.261s run time
Error: Expected test not to call console.error().

If the error is expected, test for it explicitly by mocking it out using jest.spyOn(console, 'error').mockImplementation() and test that the warning occurs.

Warning: You provided a `value` prop to a form field without an `onChange` handler. This will render a read-only field. If the field should be mutable use `defaultValue`. Otherwise, set either `onChange` or `readOnly`.
    at input
    at .../core/input/index.tsx:29:13
    at .../sentry/sentry/node_modules/@.../react/dist/emotion-element-25f9958c.browser.cjs.js:56:23
    at div
    at .../sentry/sentry/node_modules/@.../react/dist/emotion-element-25f9958c.browser.cjs.js:56:23
    at ComboBoxInner (.../tokenizedInput/token/comboBox.tsx:23:3)
    at ComboBoxWrapper
    at GlobalDrawer (.../components/globalDrawer/index.tsx:25:3)
    at $4068a0fae83b6d84$export$323e4fc2fa4753fb (.../sentry/sentry/node_modules/@.../utils/dist/packages/@.../utils/src/openLink.tsx:41:8)
    at ProvideAriaRouter (.../app/utils/provideAriaRouter.tsx:17:3)
    at QueryClientProvider (.../sentry/sentry/node_modules/@.../react-query/src/QueryClientProvider.tsx:30:3)
    at ThemeProvider (.../sentry/sentry/node_modules/@.../react/dist/emotion-element-25f9958c.browser.cjs.js:83:32)
    at .../js/sentry-test/reactTestingLibrary.tsx:114:5
    at routeContext (.../sentry/sentry/node_modules/react-router/lib/hooks.tsx:658:26)
    at RenderErrorBoundary (.../sentry/sentry/node_modules/react-router/lib/hooks.tsx:584:5)
    at routes (.../sentry/sentry/node_modules/react-router-dom/index.tsx:760:3)
    at basenameProp (.../sentry/sentry/node_modules/react-router/lib/components.tsx:413:13)
    at fallbackElement (.../sentry/sentry/node_modules/react-router-dom/index.tsx:487:3)
    at console.captureMessage [as error] (.../sentry/sentry/node_modules/jest-fail-on-console/index.js:83:25)
    at printWarning (.../sentry/sentry/node_modules/react-dom/cjs/react-dom.development.js:86:30)
    at error (.../sentry/sentry/node_modules/react-dom/cjs/react-dom.development.js:60:7)
    at checkControlledValueProps (.../sentry/sentry/node_modules/react-dom/cjs/react-dom.development.js:1592:7)
    at initWrapperState (.../sentry/sentry/node_modules/react-dom/cjs/react-dom.development.js:1775:5)
    at setInitialProperties (.../sentry/sentry/node_modules/react-dom/cjs/react-dom.development.js:9888:7)
    at finalizeInitialChildren (.../sentry/sentry/node_modules/react-dom/cjs/react-dom.development.js:10950:3)
    at completeWork (.../sentry/sentry/node_modules/react-dom/cjs/react-dom.development.js:22232:17)
    at completeUnitOfWork (.../sentry/sentry/node_modules/react-dom/cjs/react-dom.development.js:26632:16)
    at performUnitOfWork (.../sentry/sentry/node_modules/react-dom/cjs/react-dom.development.js:26607:5)
    at workLoopSync (.../sentry/sentry/node_modules/react-dom/cjs/react-dom.development.js:26505:5)
    at renderRootSync (.../sentry/sentry/node_modules/react-dom/cjs/react-dom.development.js:26473:7)
    at performConcurrentWorkOnRoot (.../sentry/sentry/node_modules/react-dom/cjs/react-dom.development.js:25777:74)
    at flushActQueue (.../sentry/sentry/node_modules/react/cjs/react.development.js:2667:24)
    at act (.../sentry/sentry/node_modules/react/cjs/react.development.js:2582:11)
    at .../sentry/sentry/node_modules/@.../react/dist/act-compat.js:47:25
    at renderRoot (.../sentry/sentry/node_modules/@.../react/dist/pure.js:188:26)
    at Object.render (.../sentry/sentry/node_modules/@.../react/dist/pure.js:287:10)
    at render (.../js/sentry-test/reactTestingLibrary.tsx:272:28)
    at Object.<anonymous> (.../tokenizedInput/token/comboBox.spec.tsx:21:37)
    at Promise.then.completed (.../sentry/sentry/node_modules/jest-circus/build/utils.js:298:28)
    at new Promise (<anonymous>)
    at callAsyncCircusFn (.../sentry/sentry/node_modules/jest-circus/build/utils.js:231:10)
    at _callCircusTest (.../sentry/sentry/node_modules/jest-circus/build/run.js:316:40)
    at processTicksAndRejections (node:internal/process/task_queues:105:5)
    at _runTest (.../sentry/sentry/node_modules/jest-circus/build/run.js:252:3)
    at _runTestsForDescribeBlock (.../sentry/sentry/node_modules/jest-circus/build/run.js:126:9)
    at _runTestsForDescribeBlock (.../sentry/sentry/node_modules/jest-circus/build/run.js:121:9)
    at run (.../sentry/sentry/node_modules/jest-circus/build/run.js:71:3)
    at runAndTransformResultsToJestFormat (.../sentry/sentry/node_modules/jest-circus/build/legacy-code-todo-rewrite/jestAdapterInit.js:122:21)
    at jestAdapter (.../sentry/sentry/node_modules/jest-circus/build/legacy-code-todo-rewrite/jestAdapter.js:79:19)
    at runTestInternal (.../sentry/sentry/node_modules/jest-runner/build/runTest.js:367:16)
    at runTest (.../sentry/sentry/node_modules/jest-runner/build/runTest.js:444:34)
    at Object.worker (.../sentry/sentry/node_modules/jest-runner/build/testWorker.js:106:12)
    at flushUnexpectedConsoleCalls (.../sentry/sentry/node_modules/jest-fail-on-console/index.js:48:13)
    at Object.<anonymous> (.../sentry/sentry/node_modules/jest-fail-on-console/index.js:139:7)
    at Promise.then.completed (.../sentry/sentry/node_modules/jest-circus/build/utils.js:298:28)
    at new Promise (<anonymous>)
    at callAsyncCircusFn (.../sentry/sentry/node_modules/jest-circus/build/utils.js:231:10)
    at _callCircusHook (.../sentry/sentry/node_modules/jest-circus/build/run.js:281:40)
    at _runTest (.../sentry/sentry/node_modules/jest-circus/build/run.js:254:5)
    at _runTestsForDescribeBlock (.../sentry/sentry/node_modules/jest-circus/build/run.js:126:9)
    at _runTestsForDescribeBlock (.../sentry/sentry/node_modules/jest-circus/build/run.js:121:9)
    at run (.../sentry/sentry/node_modules/jest-circus/build/run.js:71:3)
    at runAndTransformResultsToJestFormat (.../sentry/sentry/node_modules/jest-circus/build/legacy-code-todo-rewrite/jestAdapterInit.js:122:21)
    at jestAdapter (.../sentry/sentry/node_modules/jest-circus/build/legacy-code-todo-rewrite/jestAdapter.js:79:19)
    at runTestInternal (.../sentry/sentry/node_modules/jest-runner/build/runTest.js:367:16)
    at runTest (.../sentry/sentry/node_modules/jest-runner/build/runTest.js:444:34)
    at Object.worker (.../sentry/sentry/node_modules/jest-runner/build/testWorker.js:106:12)

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

@JonasBa JonasBa force-pushed the jb/components/growing-input branch from c715e8c to b2dc249 Compare March 7, 2025 14:38
@JonasBa JonasBa marked this pull request as ready for review March 7, 2025 14:42
@JonasBa JonasBa requested review from a team as code owners March 7, 2025 14:42
@@ -89,6 +89,7 @@ export default storyBook('ComboBox', story => {
});

story('With growing input', () => {
const [value, setValue] = useState('opt_one');
Copy link
Member Author

Choose a reason for hiding this comment

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

No value here meant that selecting an option in the story result in a weird flash and the combobox to not contain the clicked value

Comment on lines 14 to 17
options: {
disabled?: boolean;
value?: React.InputHTMLAttributes<HTMLInputElement>['value'] | undefined;
} = {}
Copy link
Member

Choose a reason for hiding this comment

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

nit: move the types to a interface Options or something

@JonasBa
Copy link
Member Author

JonasBa commented Apr 16, 2025

CleanShot 2025-04-16 at 10 12 41@2x

I need to figure out what broke here before I can merge this

@getsantry
Copy link
Contributor

getsantry bot commented Jun 6, 2025

This issue has gone three weeks without activity. In another week, I will close it.

But! If you comment or otherwise update it, I will reset the clock, and if you remove the label Waiting for: Community, I will leave it alone ... forever!


"A weed is but an unloved flower." ― Ella Wheeler Wilcox 🥀

@getsantry getsantry bot added Stale and removed Stale labels Jun 6, 2025
@armenzg armenzg removed the request for review from a team June 12, 2025 12:43
@JonasBa JonasBa requested review from scttcper and malwilley June 17, 2025 18:21
@JonasBa JonasBa marked this pull request as ready for review June 17, 2025 18:21
@JonasBa JonasBa requested a review from a team as a code owner June 17, 2025 18:21
@JonasBa
Copy link
Member Author

JonasBa commented Jun 17, 2025

I finally got back to fixing this, but I just missed attaching autosize to those inputs that broke in the screenshot above. Tested and things look good. @malwilley and @scttcper, I tagged you two because the change is primarily to the combobox searchbar, and that is a critical component. I tested things locally, and everything seems to work as it used to

Copy link
Member

@malwilley malwilley left a comment

Choose a reason for hiding this comment

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

Search component looks good to me! Can't spot any regressions

Copy link
Member

@scttcper scttcper left a comment

Choose a reason for hiding this comment

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

issues comments look good

@JonasBa JonasBa merged commit 2d6bf0d into master Jun 18, 2025
45 checks passed
@JonasBa JonasBa deleted the jb/components/growing-input branch June 18, 2025 14:13
@JonasBa
Copy link
Member Author

JonasBa commented Jun 18, 2025

Thank you for the reviews 🙇🏼

andrewshie-sentry pushed a commit that referenced this pull request Jun 19, 2025
All bow to useAutosizeInput and `<Input autosize/>`

I have opted to use <Input autosize as opposed to requiring everyone to
call the hook, as this allows us to ensure that the hook is correctly
called (value is required depending on if the input is controlled or
not), meaning that we can reduce the possible bug surface.


https://github.com/user-attachments/assets/a321da42-e648-42c7-ad94-e885ba99bb12

The PR is missing tests, I had started to write some and realized mid
way that I will have to mock pretty much all of the window styles
methods, defeating the purpose of the tests (I will give that another
go)

The GrowingInput component was also hiding a bug that caused react to
not throw warnings in development when only the value prop was provided
as it was providing its own onchange handler and then calling
`props.onChange?.()` which was always falsy
 
**Below** is what I originally typed into the draft PR and explains the
reasoning for ^ decision more in depth.

The question now is, do we expose this via hook, or do we build it into
the Input via autosize prop like `<Input autosize/>`? I think I am
leaning towards autosize prop as I believe it is more intuitive than
consuming a hook (which could still be exposed).

If we require users to call the hook, then the API would look something
like
```tsx
function Component() {
  const ref = useAutosizeInput({value})
  return <Input ref={ref}/>
}
```

The awkward part is that exposing it like this could that the DOM
structure could depend on the autosize prop (provided would use the CSS
only approach that @natemoo-re shared, which requires a wrapper
component). An alternative could be to add autosize to InputGroup and
require the prop to be set there, but that feels weird, as the autosize
input should be set on the input itself, and making InputGroup a
requirement for autosize to work feels sub-optimal.

```tsx
function Component(){
  return (
   <InputGroup autosize> // <- this feels weird, but also means that standalone inputs dont support autosize?
     <InputGroup.Input/>
  </InputGroup>
}
```

Finally, and the version that I am most leaning towards is to expose
this as an autosize bool prop on Input, and use the current JS handler
to measure resizing. This way, we can also ensure that the hook is
correctly called with the value props if the component is controlled,
and hide that last bit of complexity from the user (provided we dont use
the pure CSS solution for now)

```tsx
function Component() {
  return <Input autosize/>
}
```

@natemoo-re @TkDodo @evanpurkhiser or anyone else reading this, feedback
and opinions would be much appreciated
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Scope: Frontend Automatically applied to PRs that change frontend components
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants