Skip to content

refactor: remove select dual states#8821

Open
araujogui wants to merge 1 commit intonodejs:mainfrom
araujogui:refactor/select-defaultvalue
Open

refactor: remove select dual states#8821
araujogui wants to merge 1 commit intonodejs:mainfrom
araujogui:refactor/select-defaultvalue

Conversation

@araujogui
Copy link
Copy Markdown
Member

@araujogui araujogui commented Apr 14, 2026

Description

Refactor selects with dual controlled state to just one

Validation

Try changing selects

Check List

  • I have read the Contributing Guidelines and made commit messages that follow the guideline.
  • I have run pnpm format to ensure the code follows the style guide.
  • I have run pnpm test to check if all tests are passing.
  • I have run pnpm build to check if the website builds without errors.
  • I've covered new added functionality with unit tests if necessary.

@araujogui araujogui requested a review from a team as a code owner April 14, 2026 15:45
Copilot AI review requested due to automatic review settings April 14, 2026 15:45
@vercel
Copy link
Copy Markdown

vercel bot commented Apr 14, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
nodejs-org Ready Ready Preview Apr 14, 2026 3:59pm

Request Review

@cursor
Copy link
Copy Markdown

cursor bot commented Apr 14, 2026

PR Summary

Medium Risk
This changes the public Select API from defaultValue to value and removes internal state, which can break existing callers and subtly change selection behavior if parents don’t keep value in sync.

Overview
Refactors the shared Select component to be fully controlled by replacing defaultValue with value and removing its internal state/effect syncing.

Updates download page dropdowns (OS/platform/install method/package manager) to pass value instead of defaultValue, and adjusts the noscript fallback (NoScriptSelect/StatelessSelect) types so only the stateless variant uses defaultValue. Storybook is updated to drive value via useArgs so interactions update the selected option.

Reviewed by Cursor Bugbot for commit 208a19d. Bugbot is set up for automated code reviews on this repo. Configure here.

@github-actions
Copy link
Copy Markdown
Contributor

👋 Codeowner Review Request

The following codeowners have been identified for the changed files:

Team reviewers: @nodejs/nodejs-website

Please review the changes when you have a chance. Thank you! 🙏

@codecov
Copy link
Copy Markdown

codecov bot commented Apr 14, 2026

❌ 1 Tests Failed:

Tests completed Failed Passed Skipped
97 1 96 0
View the top 1 failed test(s) by shortest run time
test::renders the default value when provided
Stack Traces | 0.0931s run time
Error [ERR_TEST_FAILURE]: Unable to find an element with the text: Option 1. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible.

Ignored nodes: comments, script, style
�[36m<body>�[39m
  �[36m<div>�[39m
    �[36m<span�[39m
      �[33mclass�[39m=�[32m""�[39m
    �[36m>�[39m
      �[36m<label�[39m
        �[33mfor�[39m=�[32m"_r_6_"�[39m
      �[36m>�[39m
        �[0mOption label�[0m
      �[36m</label>�[39m
      �[36m<button�[39m
        �[33maria-autocomplete�[39m=�[32m"none"�[39m
        �[33maria-controls�[39m=�[32m"radix-_r_7_"�[39m
        �[33maria-expanded�[39m=�[32m"false"�[39m
        �[33mclass�[39m=�[32m""�[39m
        �[33mdata-placeholder�[39m=�[32m""�[39m
        �[33mdata-state�[39m=�[32m"closed"�[39m
        �[33mdir�[39m=�[32m"ltr"�[39m
        �[33mid�[39m=�[32m"_r_6_"�[39m
        �[33mrole�[39m=�[32m"combobox"�[39m
        �[33mtype�[39m=�[32m"button"�[39m
      �[36m>�[39m
        �[36m<span�[39m
          �[33mstyle�[39m=�[32m"pointer-events: none;"�[39m
        �[36m>�[39m
          �[0mSelect an option�[0m
        �[36m</span>�[39m
        �[36m<svg�[39m
          �[33maria-hidden�[39m=�[32m"true"�[39m
          �[33mdata-slot�[39m=�[32m"icon"�[39m
          �[33mfill�[39m=�[32m"none"�[39m
          �[33mstroke�[39m=�[32m"currentColor"�[39m
          �[33mstroke-width�[39m=�[32m"1.5"�[39m
          �[33mviewBox�[39m=�[32m"0 0 24 24"�[39m
          �[33mxmlns�[39m=�[32m"http://www.w3.org/2000/svg"�[39m
        �[36m>�[39m
          �[36m<path�[39m
            �[33md�[39m=�[32m"m19.5 8.25-7.5 7.5-7.5-7.5"�[39m
            �[33mstroke-linecap�[39m=�[32m"round"�[39m
            �[33mstroke-linejoin�[39m=�[32m"round"�[39m
          �[36m/>�[39m
        �[36m</svg>�[39m
      �[36m</button>�[39m
    �[36m</span>�[39m
  �[36m</div>�[39m
�[36m</body>�[39m
    at async Promise.all (index 0) {
  code: 'ERR_TEST_FAILURE',
  failureType: 'testCodeFailure',
  cause: Error [TestingLibraryElementError]: Unable to find an element with the text: Option 1. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible.
  
  Ignored nodes: comments, script, style
  �[36m<body>�[39m
    �[36m<div>�[39m
      �[36m<span�[39m
        �[33mclass�[39m=�[32m""�[39m
      �[36m>�[39m
        �[36m<label�[39m
          �[33mfor�[39m=�[32m"_r_6_"�[39m
        �[36m>�[39m
          �[0mOption label�[0m
        �[36m</label>�[39m
        �[36m<button�[39m
          �[33maria-autocomplete�[39m=�[32m"none"�[39m
          �[33maria-controls�[39m=�[32m"radix-_r_7_"�[39m
          �[33maria-expanded�[39m=�[32m"false"�[39m
          �[33mclass�[39m=�[32m""�[39m
          �[33mdata-placeholder�[39m=�[32m""�[39m
          �[33mdata-state�[39m=�[32m"closed"�[39m
          �[33mdir�[39m=�[32m"ltr"�[39m
          �[33mid�[39m=�[32m"_r_6_"�[39m
          �[33mrole�[39m=�[32m"combobox"�[39m
          �[33mtype�[39m=�[32m"button"�[39m
        �[36m>�[39m
          �[36m<span�[39m
            �[33mstyle�[39m=�[32m"pointer-events: none;"�[39m
          �[36m>�[39m
            �[0mSelect an option�[0m
          �[36m</span>�[39m
          �[36m<svg�[39m
            �[33maria-hidden�[39m=�[32m"true"�[39m
            �[33mdata-slot�[39m=�[32m"icon"�[39m
            �[33mfill�[39m=�[32m"none"�[39m
            �[33mstroke�[39m=�[32m"currentColor"�[39m
            �[33mstroke-width�[39m=�[32m"1.5"�[39m
            �[33mviewBox�[39m=�[32m"0 0 24 24"�[39m
            �[33mxmlns�[39m=�[32m"http://www.w3.org/2000/svg"�[39m
          �[36m>�[39m
            �[36m<path�[39m
              �[33md�[39m=�[32m"m19.5 8.25-7.5 7.5-7.5-7.5"�[39m
              �[33mstroke-linecap�[39m=�[32m"round"�[39m
              �[33mstroke-linejoin�[39m=�[32m"round"�[39m
            �[36m/>�[39m
          �[36m</svg>�[39m
        �[36m</button>�[39m
      �[36m</span>�[39m
    �[36m</div>�[39m
  �[36m</body>�[39m
      at Object.getElementError (.../runner/work/nodejs.org/nodejs.org/node_modules/.pnpm/@testing-library+dom@10.4.0/node_modules/@.../dom/dist/config.js:37:19)
      at .../runner/work/nodejs.org/nodejs.org/node_modules/.pnpm/@testing-library+dom@10.4.0/node_modules/@.../dom/dist/query-helpers.js:76:38
      at .../runner/work/nodejs.org/nodejs.org/node_modules/.pnpm/@testing-library+dom@10.4.0/node_modules/@.../dom/dist/query-helpers.js:52:17
      at .../runner/work/nodejs.org/nodejs.org/node_modules/.pnpm/@testing-library+dom@10.4.0/node_modules/@.../dom/dist/query-helpers.js:95:19
      at TestContext.<anonymous> (.../runner/work/nodejs.org/nodejs..../Select/__tests__/index.test.jsx:50:12)
      at Test.runInAsyncScope (node:async_hooks:228:14)
      at Test.run (node:internal/test_runner/test:1118:25)
      at Suite.processPendingSubtests (node:internal/test_runner/test:787:18)
      at Test.postRun (node:internal/test_runner/test:1247:19)
      at Test.run (node:internal/test_runner/test:1175: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.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Refactors the shared Select UI component to remove its internal “dual state” behavior and shift to an externally controlled value API, then updates downstream call sites (site download dropdowns + Storybook) to match.

Changes:

  • Replace defaultValue with value in SelectProps and remove internal state syncing from Select.
  • Update Storybook stories to keep value in sync via useArgs.
  • Update Downloads “Release” dropdown components to pass value instead of defaultValue, and adjust NoScript/Stateless select typings/wiring.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
packages/ui-components/src/Common/Select/index.tsx Removes internal state and changes public API from defaultValuevalue.
packages/ui-components/src/Common/Select/index.stories.tsx Updates stories to use value and keeps Storybook args controlled via useArgs.
packages/ui-components/src/Common/Select/StatelessSelect/index.tsx Adjusts props typing to reintroduce defaultValue semantics for StatelessSelect.
packages/ui-components/src/Common/Select/NoScriptSelect/index.tsx Threads defaultValue into JS Select as value and into noscript select as defaultValue.
apps/site/components/Downloads/Release/PlatformDropdown.tsx Migrates Select usage to value.
apps/site/components/Downloads/Release/PackageManagerDropdown.tsx Migrates Select usage to value.
apps/site/components/Downloads/Release/OperatingSystemDropdown.tsx Migrates Select usage to value.
apps/site/components/Downloads/Release/InstallationMethodDropdown.tsx Migrates Select usage to value.
Comments suppressed due to low confidence (2)

packages/ui-components/src/Common/Select/index.tsx:115

  • handleChange no longer updates any internal value. Because the trigger rendering (currentItem) is derived solely from the value prop, the Select won't visually update when used without a controlled value (or when callers still pass the now-ignored defaultValue prop). This is likely to break existing uncontrolled usage (and the current Select unit tests that pass defaultValue). Consider either (a) making value required and documenting Select as controlled-only, or (b) restoring an uncontrolled path (e.g., keep internal state only when value is undefined / support a defaultValue prop and avoid overriding SelectPrimitive.Value content in uncontrolled mode).
  const handleChange = (value: T) => {
    if (typeof onChange === 'function') {
      onChange(value);
    }
  };

packages/ui-components/src/Common/Select/index.tsx:35

  • This change removes the public defaultValue prop from SelectProps and replaces it with value, which is a breaking API change for any consumers using Select uncontrolled/with an initial default. If this package is consumed outside this repo, consider keeping defaultValue as a deprecated alias (or doing a major version bump + migration) to avoid silent runtime regressions (e.g., defaultValue currently becomes an ignored prop).
export type SelectProps<T extends string> = {
  values: Array<SelectGroup<T>> | Array<T> | Array<SelectValue<T>>;
  value?: T;
  placeholder?: string;
  label?: string;
  inline?: boolean;
  onChange?: (value: T) => void;

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 8 to 22
const WithNoScriptSelect = <T extends string>({
as,
defaultValue,
...props
}: StatelessSelectProps<T>) => {
const id = useId();
const selectId = `select-${id.replace(/[^a-zA-Z0-9]/g, '')}`;

return (
<>
<Select {...props} fallbackClass={selectId} />
<Select {...props} value={defaultValue} fallbackClass={selectId} />
<noscript>
<style>{`.${selectId} { display: none!important; }`}</style>
<StatelessSelect {...props} as={as} />
<StatelessSelect {...props} defaultValue={defaultValue} as={as} />
</noscript>
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

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

WithNoScriptSelect accepts a prop named defaultValue, but it is now passed through to the interactive <Select> as a controlled value. If a consumer provides a static defaultValue without updating it on onChange, the selection will appear “stuck” (no internal updates). To avoid this API trap, consider renaming this prop to value (and/or adding a wrapper state for the JS-enabled Select while keeping defaultValue semantics for the noscript version).

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit a1bd464. Configure here.

const Select = <T extends string>({
values = [],
defaultValue,
value,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Tests pass obsolete defaultValue prop to refactored Select

Medium Severity

The Select component's prop was renamed from defaultValue to value, but the existing test file (__tests__/index.test.jsx) was not updated. Because the test file is .jsx (not TypeScript), passing the now-unrecognized defaultValue prop won't produce a type error — it's silently ignored. This means Select renders with value={undefined}, so currentItem is undefined, and the "renders the default value when provided" test either fails or passes for the wrong reason (matching text in dropdown items rather than the selected trigger value).

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit a1bd464. Configure here.

@canerakdas
Copy link
Copy Markdown
Member

I think this reduces complexity and is a better naming choice. Applying this prop naming to StatelessSelect and NoScriptSelect as well would be great for consistency 🙌

It would also be worth checking doc-kit and learn to see whether these components/props are actually being used. I took a quick look and it doesn't seem like they are 👀

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.

3 participants