Skip to content

Conversation

@Sidnioulz
Copy link
Member

@Sidnioulz Sidnioulz commented Nov 25, 2025

What I did

Fixes an internal bug report, where a global type created with true and undefined values did not work well with Select:

  • The undefined value was filtered out
  • The true value caused a crash upon selection because type checking in defaultOptions was not defensive enough

The fix is performed by allowing a wider range of values in Select options and internally converting undefined to Symbol.for('undefined') so it does not conflict with the 'Reset' option.

Checklist for Contributors

Testing

The changes in this PR are covered in the following automated tests:

  • stories
  • unit tests
  • integration tests
  • end-to-end tests

Manual testing

@yannbf has a repo with a local addon using both true and undefined, to be tested against.

Anyone else: please run the new stories that should cover all potential use cases within Select.

Documentation

ø

Checklist for Maintainers

  • When this PR is ready for testing, make sure to add ci:normal, ci:merged or ci:daily GH label to it to run a specific set of sandboxes. The particular set of sandboxes can be found in code/lib/cli-storybook/src/sandbox-templates.ts

  • Make sure this PR contains one of the labels below:

    Available labels
    • bug: Internal changes that fixes incorrect behavior.
    • maintenance: User-facing maintenance tasks.
    • dependencies: Upgrading (sometimes downgrading) dependencies.
    • build: Internal-facing build tooling & test updates. Will not show up in release changelog.
    • cleanup: Minor cleanup style change. Will not show up in release changelog.
    • documentation: Documentation only changes. Will not show up in release changelog.
    • feature request: Introducing a new feature.
    • BREAKING CHANGE: Changes that break compatibility in some way with current major version.
    • other: Changes that don't fit in the above categories.

🦋 Canary release

This PR does not have a canary release associated. You can request a canary release of this pull request by mentioning the @storybookjs/core team here.

core team members can create a canary release here or locally with gh workflow run --repo storybookjs/storybook publish.yml --field pr=<PR_NUMBER>

Summary by CodeRabbit

  • New Features

    • Select now supports non-string option values (numbers, booleans, null, undefined) for single and multi-select; callbacks and defaults accept and emit these values.
  • Bug Fixes

    • Improved handling of undefined and other non-string default/reset cases for consistent select/deselect/reset behavior and aria-selected state.
  • Documentation

    • Added Storybook examples demonstrating selection, deselection, defaults, multi-select, accessibility, keyboard navigation, and reset interactions with non-string values.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 25, 2025

📝 Walkthrough

Walkthrough

Select and its helpers were modified to support non-string option values (number, boolean, null, undefined) via an internal representation (Symbol for undefined). Select public prop types and callbacks now use a Value union; stories and several callers were updated to exercise and new-typed behaviors.

Changes

Cohort / File(s) Summary
Select implementation
code/core/src/components/components/Select/Select.tsx
Changed public API types (defaultOptions, onSelect, onDeselect, onChange) to use Value/Value[]. Introduced InternalOption/ResetOption, internal value conversion, updated selection/navigation/focus logic, and callbacks that emit external values.
Select helpers / types
code/core/src/components/components/Select/helpers.tsx
Added Value/InternalValue unions, UNDEFINED_VALUE Symbol, PAGE_STEP_SIZE, InternalOption and ResetOption types, and conversion utilities: externalToValue, valueToExternal, optionToInternal, optionOrResetToInternal, isLiteralValue.
Storybook stories
code/core/src/components/components/Select/Select.stories.tsx
Added nonStringOptions and multiple new stories to validate single/multi-select, defaultOptions, reset, and aria-selected behavior for numbers, booleans, null, and undefined.
Toolbar integration
code/core/src/toolbar/components/ToolbarMenuSelect.tsx
Simplified item type guard, added 'Untitled' fallback for missing titles/values, and now passes defaultOptions as an array ([currentValue]).
Minor typing assertions in callers
code/core/src/backgrounds/components/Tool.tsx, code/core/src/viewport/components/Tool.tsx, code/core/src/manager/components/sidebar/RefIndicator.tsx, code/addons/pseudo-states/src/manager/PseudoStateTool.tsx
Narrowed select-handler typings by casting selected values (or reduce accumulator) to string; these are local type assertions without intended runtime behavioral changes.

Sequence Diagram(s)

sequenceDiagram
    participant User as User
    participant Select as Select Component
    participant Helpers as helpers.tsx
    participant Consumer as onSelect/onChange callbacks

    User->>Select: interact (open/select/deselect/reset)
    Select->>Helpers: externalToValue(externalValue)
    Note right of Helpers `#eef2ff`: maps undefined -> UNDEFINED_VALUE (Symbol)
    Helpers-->>Select: internalValue (InternalValue)
    Select->>Select: store/render InternalOption / ResetOption, update active/focus
    Select->>Helpers: valueToExternal(internalValue)
    Helpers-->>Select: externalValue
    Select->>Consumer: call onSelect/onDeselect/onChange with externalValue
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Areas to focus on:
    • UNDEFINED_VALUE Symbol conversions and edge cases in helpers.tsx
    • All callback emission sites in Select.tsx to ensure consistent external values
    • Keyboard navigation, active-option/focus management, and id/key generation after internalization
    • defaultOptions handling and interactions with callers (e.g., ToolbarMenuSelect.tsx)

Possibly related PRs

✨ Finishing touches
  • 📝 Generate docstrings

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (2)
code/core/src/components/components/Select/helpers.tsx (1)

28-37: isLiteralValue includes symbol check but Value type excludes symbols.

The function checks typeof value === 'symbol' (line 35), but the Value type on line 8 is string | number | null | boolean | undefined and doesn't include symbol. This makes the symbol check unreachable when the input is typed as Value | Value[] | undefined.

If the symbol check is intentional for internal use (e.g., checking if an InternalValue containing UNDEFINED_VALUE is a literal), consider either:

  1. Updating the function signature to accept InternalValue as well, or
  2. Removing the symbol check if it's truly unreachable.
 export function isLiteralValue(value: Value | Value[] | undefined): value is Value {
   return (
     value === undefined ||
     value === null ||
     typeof value === 'string' ||
     typeof value === 'number' ||
-    typeof value === 'boolean' ||
-    typeof value === 'symbol'
+    typeof value === 'boolean'
   );
 }
code/core/src/components/components/Select/Select.tsx (1)

272-285: Consider typing resetOption explicitly as ResetOption | undefined.

The resetOption memo returns an object literal with type: 'reset', but TypeScript may infer a wider type. Adding an explicit type annotation would ensure consistency with the ResetOption interface:

-    const resetOption = useMemo(
+    const resetOption = useMemo<ResetOption | undefined>(
       () =>
         onReset
           ? {
-              type: 'reset',
+              type: 'reset' as const,
               value: undefined,
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 358240c and fa6d6ef.

📒 Files selected for processing (4)
  • code/core/src/components/components/Select/Select.stories.tsx (1 hunks)
  • code/core/src/components/components/Select/Select.tsx (12 hunks)
  • code/core/src/components/components/Select/helpers.tsx (1 hunks)
  • code/core/src/toolbar/components/ToolbarMenuSelect.tsx (2 hunks)
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{ts,tsx,js,jsx,mjs}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use camelCase for variable and function names

Files:

  • code/core/src/components/components/Select/Select.stories.tsx
  • code/core/src/components/components/Select/helpers.tsx
  • code/core/src/components/components/Select/Select.tsx
  • code/core/src/toolbar/components/ToolbarMenuSelect.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{ts,tsx}: Enable TypeScript strict mode
Export functions from modules for testing purposes

Files:

  • code/core/src/components/components/Select/Select.stories.tsx
  • code/core/src/components/components/Select/helpers.tsx
  • code/core/src/components/components/Select/Select.tsx
  • code/core/src/toolbar/components/ToolbarMenuSelect.tsx
**/*.{ts,tsx,js,jsx,json,html,mjs}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{ts,tsx,js,jsx,json,html,mjs}: Use ESLint and Prettier for code style enforcement
Run 'yarn prettier --write ' to format code after making changes
Run 'yarn lint:js:cmd ' to check for ESLint issues after making changes

Files:

  • code/core/src/components/components/Select/Select.stories.tsx
  • code/core/src/components/components/Select/helpers.tsx
  • code/core/src/components/components/Select/Select.tsx
  • code/core/src/toolbar/components/ToolbarMenuSelect.tsx
code/**/!(*.test).{ts,tsx,js,mjs}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

code/**/!(*.test).{ts,tsx,js,mjs}: Use 'logger' from 'storybook/internal/node-logger' for server-side (Node.js) logging, not console.log/console.warn/console.error
Use 'logger' from 'storybook/internal/client-logger' for client-side (browser) logging, not console.log/console.warn/console.error
Do not use console.log, console.warn, or console.error directly unless in isolated files where importing loggers would significantly increase bundle size

Files:

  • code/core/src/components/components/Select/Select.stories.tsx
  • code/core/src/components/components/Select/helpers.tsx
  • code/core/src/components/components/Select/Select.tsx
  • code/core/src/toolbar/components/ToolbarMenuSelect.tsx
🧠 Learnings (3)
📓 Common learnings
Learnt from: Sidnioulz
Repo: storybookjs/storybook PR: 32458
File: code/core/src/components/components/Select/Select.tsx:200-204
Timestamp: 2025-11-05T09:38:47.712Z
Learning: Repo: storybookjs/storybook — Guidance: Until Storybook 11 is released, do not suggest using React.useId anywhere (e.g., in code/core/src/components/components/Select/Select.tsx) to maintain compatibility with React 17 runtimes. Prefer advising: accept a caller-provided props.id and, if needed, generate a client-only fallback id to minimize SSR hydration issues — but avoid useId. Resume prompting for useId after Storybook 11.
Learnt from: Sidnioulz
Repo: storybookjs/storybook PR: 32458
File: code/core/src/components/components/Tabs/Tabs.stories.tsx:222-227
Timestamp: 2025-11-05T09:36:55.944Z
Learning: Repo: storybookjs/storybook PR: 32458 — In code/core/src/components/components/Button/Button.tsx (React/TypeScript), ButtonProps includes ariaLabel?: string | false and the component maps it to the DOM aria-label. Convention: ariaLabel is mandatory on all Button usages — provide a descriptive string for icon-only buttons; set ariaLabel=false when the button’s children already serve as the accessible name. Do not suggest using a raw aria-label prop on Button call sites.
📚 Learning: 2025-11-05T09:38:47.712Z
Learnt from: Sidnioulz
Repo: storybookjs/storybook PR: 32458
File: code/core/src/components/components/Select/Select.tsx:200-204
Timestamp: 2025-11-05T09:38:47.712Z
Learning: Repo: storybookjs/storybook — Guidance: Until Storybook 11 is released, do not suggest using React.useId anywhere (e.g., in code/core/src/components/components/Select/Select.tsx) to maintain compatibility with React 17 runtimes. Prefer advising: accept a caller-provided props.id and, if needed, generate a client-only fallback id to minimize SSR hydration issues — but avoid useId. Resume prompting for useId after Storybook 11.

Applied to files:

  • code/core/src/components/components/Select/Select.stories.tsx
📚 Learning: 2025-11-05T09:36:55.944Z
Learnt from: Sidnioulz
Repo: storybookjs/storybook PR: 32458
File: code/core/src/components/components/Tabs/Tabs.stories.tsx:222-227
Timestamp: 2025-11-05T09:36:55.944Z
Learning: Repo: storybookjs/storybook PR: 32458 — In code/core/src/components/components/Button/Button.tsx (React/TypeScript), ButtonProps includes ariaLabel?: string | false and the component maps it to the DOM aria-label. Convention: ariaLabel is mandatory on all Button usages — provide a descriptive string for icon-only buttons; set ariaLabel=false when the button’s children already serve as the accessible name. Do not suggest using a raw aria-label prop on Button call sites.

Applied to files:

  • code/core/src/components/components/Select/Select.stories.tsx
🧬 Code graph analysis (1)
code/core/src/components/components/Select/Select.stories.tsx (4)
code/core/src/csf-tools/CsfFile.ts (1)
  • meta (964-966)
code/core/src/node-logger/logger/logger.ts (1)
  • step (197-202)
code/core/src/test/testing-library.ts (1)
  • userEvent (123-126)
code/renderers/react/template/stories/js-argtypes.stories.jsx (1)
  • args (44-44)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: normal
  • GitHub Check: Core Unit Tests, windows-latest
🔇 Additional comments (10)
code/core/src/toolbar/components/ToolbarMenuSelect.tsx (2)

59-60: LGTM! Clean type guard consolidation.

The single type guard filter with item is ToolbarItem is cleaner than the previous two-step filtering approach and provides better TypeScript type narrowing for the subsequent map operation.


94-95: Potential unintended selection when currentValue is undefined.

Wrapping currentValue in an array creates [undefined] when globals[id] is undefined. Per the new Select behavior (as demonstrated in DefaultOptionUndefinedInArrayWorks story), [undefined] will select an option with value: undefined if one exists, whereas previously a bare undefined was filtered out.

If toolbar items can have undefined as a valid option value, this change might unintentionally pre-select that option when no global is set. Consider whether this is the intended behavior or if you need to filter out undefined:

-        defaultOptions={[currentValue]}
+        defaultOptions={currentValue !== undefined ? [currentValue] : undefined}
code/core/src/components/components/Select/helpers.tsx (2)

5-6: Good use of Symbol.for for the undefined sentinel.

Using Symbol.for('undefined') ensures the same symbol is returned across different parts of the codebase, which is important for value comparisons. This cleanly separates the "undefined as a valid option value" case from the "reset/no selection" case.


39-68: LGTM! Clean conversion utilities.

The bidirectional conversion functions (optionToInternal, valueToExternal, externalToValue) correctly handle the mapping between external undefined and internal UNDEFINED_VALUE sentinel. The optionOrResetToInternal function appropriately preserves reset options while converting regular options.

code/core/src/components/components/Select/Select.stories.tsx (3)

1059-1067: Comprehensive test data covering all falsy and non-string values.

The nonStringOptions array covers all the edge cases that previously caused crashes: 0, false, null, undefined, along with truthy non-string values. This is excellent test coverage for the bug fix.


1234-1258: Good documentation of undefined handling semantics through stories.

These two stories clearly document the asymmetry: defaultOptions: undefined means "no default" while defaultOptions: [undefined] means "select the option with value undefined". This is valuable for maintainers understanding the API behavior.


1288-1333: Critical test for Reset vs Undefined option distinction.

This story validates that the UNDEFINED_VALUE symbol correctly separates the reset functionality from an actual undefined option value. The test flow (select → select undefined → reset → select undefined again) thoroughly exercises this edge case.

code/core/src/components/components/Select/Select.tsx (3)

91-106: LGTM! Clean handling of defaultOptions with proper internal conversion.

The function correctly:

  1. Returns empty array for bare undefined (no default selection)
  2. Uses isLiteralValue to distinguish single value from array
  3. Maps matched options through optionToInternal for internal state

232-269: Solid implementation of selection logic with type discrimination.

The option.type === 'reset' check cleanly handles reset options separately from regular options, and valueToExternal correctly converts internal values back to external Value types for callbacks.


533-586: Good dual-mapping approach for internal/external option handling.

Mapping to both option (internal) and externalOption allows proper internal state management while still passing the original option to setActiveOption via onFocus. This preserves the expected behavior while supporting the new value types.

@nx-cloud
Copy link

nx-cloud bot commented Nov 25, 2025

View your CI Pipeline Execution ↗ for commit 9aeb817

Command Status Duration Result
nx run-many -t build --parallel=3 ✅ Succeeded 43s View ↗

☁️ Nx Cloud last updated this comment at 2025-11-25 15:03:24 UTC

Copy link
Member

@yannbf yannbf left a comment

Choose a reason for hiding this comment

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

awesome!!

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
code/addons/pseudo-states/src/manager/PseudoStateTool.tsx (1)

37-41: Multi-select reduction correctly builds the pseudo-state map

Using curr as string inside reduce matches the fact that option values are the string keys from PSEUDO_STATES, so the resulting { [state]: true } map is type-safe and behaviorally unchanged.

If you ever care about tiny perf wins here, you could mutate the accumulator instead of recreating it on each iteration:

const next = selected.reduce<Record<string, boolean>>((acc, curr) => {
  acc[curr as string] = true;
  return acc;
}, {});
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fa6d6ef and f7d3304.

📒 Files selected for processing (5)
  • code/addons/pseudo-states/src/manager/PseudoStateTool.tsx (1 hunks)
  • code/core/src/backgrounds/components/Tool.tsx (1 hunks)
  • code/core/src/manager/components/sidebar/RefIndicator.tsx (1 hunks)
  • code/core/src/toolbar/components/ToolbarMenuSelect.tsx (2 hunks)
  • code/core/src/viewport/components/Tool.tsx (1 hunks)
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{ts,tsx,js,jsx,mjs}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use camelCase for variable and function names

Files:

  • code/core/src/toolbar/components/ToolbarMenuSelect.tsx
  • code/core/src/manager/components/sidebar/RefIndicator.tsx
  • code/core/src/viewport/components/Tool.tsx
  • code/core/src/backgrounds/components/Tool.tsx
  • code/addons/pseudo-states/src/manager/PseudoStateTool.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{ts,tsx}: Enable TypeScript strict mode
Export functions from modules for testing purposes

Files:

  • code/core/src/toolbar/components/ToolbarMenuSelect.tsx
  • code/core/src/manager/components/sidebar/RefIndicator.tsx
  • code/core/src/viewport/components/Tool.tsx
  • code/core/src/backgrounds/components/Tool.tsx
  • code/addons/pseudo-states/src/manager/PseudoStateTool.tsx
**/*.{ts,tsx,js,jsx,json,html,mjs}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{ts,tsx,js,jsx,json,html,mjs}: Use ESLint and Prettier for code style enforcement
Run 'yarn prettier --write ' to format code after making changes
Run 'yarn lint:js:cmd ' to check for ESLint issues after making changes

Files:

  • code/core/src/toolbar/components/ToolbarMenuSelect.tsx
  • code/core/src/manager/components/sidebar/RefIndicator.tsx
  • code/core/src/viewport/components/Tool.tsx
  • code/core/src/backgrounds/components/Tool.tsx
  • code/addons/pseudo-states/src/manager/PseudoStateTool.tsx
code/**/!(*.test).{ts,tsx,js,mjs}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

code/**/!(*.test).{ts,tsx,js,mjs}: Use 'logger' from 'storybook/internal/node-logger' for server-side (Node.js) logging, not console.log/console.warn/console.error
Use 'logger' from 'storybook/internal/client-logger' for client-side (browser) logging, not console.log/console.warn/console.error
Do not use console.log, console.warn, or console.error directly unless in isolated files where importing loggers would significantly increase bundle size

Files:

  • code/core/src/toolbar/components/ToolbarMenuSelect.tsx
  • code/core/src/manager/components/sidebar/RefIndicator.tsx
  • code/core/src/viewport/components/Tool.tsx
  • code/core/src/backgrounds/components/Tool.tsx
  • code/addons/pseudo-states/src/manager/PseudoStateTool.tsx
🧠 Learnings (4)
📓 Common learnings
Learnt from: Sidnioulz
Repo: storybookjs/storybook PR: 32458
File: code/core/src/components/components/Select/Select.tsx:200-204
Timestamp: 2025-11-05T09:38:47.712Z
Learning: Repo: storybookjs/storybook — Guidance: Until Storybook 11 is released, do not suggest using React.useId anywhere (e.g., in code/core/src/components/components/Select/Select.tsx) to maintain compatibility with React 17 runtimes. Prefer advising: accept a caller-provided props.id and, if needed, generate a client-only fallback id to minimize SSR hydration issues — but avoid useId. Resume prompting for useId after Storybook 11.
Learnt from: Sidnioulz
Repo: storybookjs/storybook PR: 32458
File: code/core/src/components/components/Tabs/Tabs.stories.tsx:222-227
Timestamp: 2025-11-05T09:36:55.944Z
Learning: Repo: storybookjs/storybook PR: 32458 — In code/core/src/components/components/Button/Button.tsx (React/TypeScript), ButtonProps includes ariaLabel?: string | false and the component maps it to the DOM aria-label. Convention: ariaLabel is mandatory on all Button usages — provide a descriptive string for icon-only buttons; set ariaLabel=false when the button’s children already serve as the accessible name. Do not suggest using a raw aria-label prop on Button call sites.
📚 Learning: 2025-10-03T07:55:42.639Z
Learnt from: Sidnioulz
Repo: storybookjs/storybook PR: 32458
File: code/core/src/manager/components/preview/Toolbar.tsx:102-105
Timestamp: 2025-10-03T07:55:42.639Z
Learning: In code/core/src/manager/components/preview/Toolbar.tsx, we intentionally do not add aria-label/aria-labelledby to StyledToolbar (AbstractToolbar) because the enclosing section is already labeled via an sr-only heading and the toolbar is the only content. Revisit only if real user testing indicates a need.

Applied to files:

  • code/core/src/toolbar/components/ToolbarMenuSelect.tsx
📚 Learning: 2025-09-18T20:51:06.618Z
Learnt from: Sidnioulz
Repo: storybookjs/storybook PR: 32458
File: code/core/src/viewport/components/Tool.tsx:38-39
Timestamp: 2025-09-18T20:51:06.618Z
Learning: In viewport tool code, when using the `useGlobals` hook from storybook/manager-api, the third returned value `storyGlobals` is guaranteed by TypeScript to be defined (not undefined/null), making the `in` operator safe to use without additional null checks.

Applied to files:

  • code/core/src/viewport/components/Tool.tsx
📚 Learning: 2025-11-05T09:37:25.920Z
Learnt from: Sidnioulz
Repo: storybookjs/storybook PR: 32458
File: code/core/src/components/components/tooltip/WithTooltip.tsx:54-96
Timestamp: 2025-11-05T09:37:25.920Z
Learning: Repo: storybookjs/storybook — In code/core/src/components/components/tooltip/WithTooltip.tsx, the legacy WithTooltip implementation is intentionally reintroduced for backward compatibility and is deprecated; maintainers (per Sidnioulz) do not want maintenance or improvements on it. Prefer WithTooltipNew/Popover; avoid suggesting changes to WithTooltip.* going forward.

Applied to files:

  • code/core/src/viewport/components/Tool.tsx
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: normal
  • GitHub Check: Core Unit Tests, windows-latest
🔇 Additional comments (4)
code/core/src/manager/components/sidebar/RefIndicator.tsx (1)

200-205: String cast in onSelect matches how options are constructed

options only ever use string ids as value, so treating item as a string to index ref.versions is consistent and safe, and the clarifying comment helps future readers.

code/core/src/backgrounds/components/Tool.tsx (1)

96-108: Select handler correctly narrows the selected value to a background name

Given that options use the background map keys (strings) as value, casting selected to string before storing it in globals is consistent with the existing data model and keeps behavior unchanged under the new Select typing.

code/core/src/viewport/components/Tool.tsx (1)

112-126: Viewport onSelect cast is consistent with viewport option keys

Since viewport options use the map keys (strings) as their value, casting selected to string before updating { value, isRotated: false } is a safe way to align with the updated Select typings without changing behavior.

code/core/src/toolbar/components/ToolbarMenuSelect.tsx (1)

57-63: Toolbar option narrowing, title fallback, and defaultOptions shape all look correct

  • The .filter((item): item is ToolbarItem => item.type === 'item') guard cleanly narrows to selectable items before mapping.
  • The itemTitle fallback to 'Untitled' makes the menu more resilient when a config omits both title and value.
  • Passing defaultOptions={[currentValue]} aligns with a Value | Value[] Select API and preserves whatever value type the toolbar uses for this global.

No issues from a correctness or UX standpoint.

Also applies to: 95-103

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
code/core/src/components/components/Select/Select.tsx (1)

81-83: Nullish coalescing after String() is ineffective (regression or not fixed).

String(value) always returns a string (including "undefined" for undefined), so ?? never triggers. For the reset option where value is undefined, this produces "select-...-opt-undefined" instead of the intended "select-...-opt-sb-reset".

A past review comment flagged this same issue and claimed it was addressed, but the current code still contains the bug.

Apply this diff to fix:

-  return `${parentId}-opt-${String(value) ?? 'sb-reset'}`;
+  return `${parentId}-opt-${value === undefined ? 'sb-reset' : String(value)}`;
🧹 Nitpick comments (1)
code/core/src/components/components/Select/Select.tsx (1)

17-26: Consider adding a comparison helper to the helpers module.

The comparison between external options (from the options array) and internal options (activeOption, selectedOptions) is repeated in multiple places and has subtle behavior for ResetOption. Consider adding a helper function to helpers.tsx:

export function compareOptionValues(
  externalOption: Option | ResetOption,
  internalOption: InternalOption | ResetOption
): boolean {
  if ('type' in externalOption && externalOption.type === 'reset') {
    return 'type' in internalOption && internalOption.type === 'reset';
  }
  return externalToValue(externalOption.value) === internalOption.value;
}

This would make the comparison logic explicit, reduce duplication, and prevent similar bugs.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f2b690a and 9aeb817.

📒 Files selected for processing (1)
  • code/core/src/components/components/Select/Select.tsx (12 hunks)
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{ts,tsx,js,jsx,mjs}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use camelCase for variable and function names

Files:

  • code/core/src/components/components/Select/Select.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{ts,tsx}: Enable TypeScript strict mode
Export functions from modules for testing purposes

Files:

  • code/core/src/components/components/Select/Select.tsx
**/*.{ts,tsx,js,jsx,json,html,mjs}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{ts,tsx,js,jsx,json,html,mjs}: Use ESLint and Prettier for code style enforcement
Run 'yarn prettier --write ' to format code after making changes
Run 'yarn lint:js:cmd ' to check for ESLint issues after making changes

Files:

  • code/core/src/components/components/Select/Select.tsx
code/**/!(*.test).{ts,tsx,js,mjs}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

code/**/!(*.test).{ts,tsx,js,mjs}: Use 'logger' from 'storybook/internal/node-logger' for server-side (Node.js) logging, not console.log/console.warn/console.error
Use 'logger' from 'storybook/internal/client-logger' for client-side (browser) logging, not console.log/console.warn/console.error
Do not use console.log, console.warn, or console.error directly unless in isolated files where importing loggers would significantly increase bundle size

Files:

  • code/core/src/components/components/Select/Select.tsx
🧠 Learnings (2)
📓 Common learnings
Learnt from: Sidnioulz
Repo: storybookjs/storybook PR: 32458
File: code/core/src/components/components/Select/Select.tsx:200-204
Timestamp: 2025-11-05T09:38:47.712Z
Learning: Repo: storybookjs/storybook — Guidance: Until Storybook 11 is released, do not suggest using React.useId anywhere (e.g., in code/core/src/components/components/Select/Select.tsx) to maintain compatibility with React 17 runtimes. Prefer advising: accept a caller-provided props.id and, if needed, generate a client-only fallback id to minimize SSR hydration issues — but avoid useId. Resume prompting for useId after Storybook 11.
Learnt from: Sidnioulz
Repo: storybookjs/storybook PR: 32458
File: code/core/src/components/components/Tabs/Tabs.stories.tsx:222-227
Timestamp: 2025-11-05T09:36:55.944Z
Learning: Repo: storybookjs/storybook PR: 32458 — In code/core/src/components/components/Button/Button.tsx (React/TypeScript), ButtonProps includes ariaLabel?: string | false and the component maps it to the DOM aria-label. Convention: ariaLabel is mandatory on all Button usages — provide a descriptive string for icon-only buttons; set ariaLabel=false when the button’s children already serve as the accessible name. Do not suggest using a raw aria-label prop on Button call sites.
📚 Learning: 2025-11-05T09:38:47.712Z
Learnt from: Sidnioulz
Repo: storybookjs/storybook PR: 32458
File: code/core/src/components/components/Select/Select.tsx:200-204
Timestamp: 2025-11-05T09:38:47.712Z
Learning: Repo: storybookjs/storybook — Guidance: Until Storybook 11 is released, do not suggest using React.useId anywhere (e.g., in code/core/src/components/components/Select/Select.tsx) to maintain compatibility with React 17 runtimes. Prefer advising: accept a caller-provided props.id and, if needed, generate a client-only fallback id to minimize SSR hydration issues — but avoid useId. Resume prompting for useId after Storybook 11.

Applied to files:

  • code/core/src/components/components/Select/Select.tsx
🧬 Code graph analysis (1)
code/core/src/components/components/Select/Select.tsx (1)
code/core/src/components/components/Select/helpers.tsx (8)
  • Value (8-8)
  • InternalOption (19-22)
  • ResetOption (23-26)
  • isLiteralValue (28-37)
  • optionToInternal (39-45)
  • valueToExternal (56-61)
  • optionOrResetToInternal (47-54)
  • externalToValue (63-68)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: normal
  • GitHub Check: Core Unit Tests, windows-latest
🔇 Additional comments (4)
code/core/src/components/components/Select/Select.tsx (4)

91-106: LGTM!

The function correctly handles both single values and arrays for defaultOptions, properly converts them to the internal representation, and appropriately handles undefined as a valid value.


227-269: LGTM!

The state management and selection handling correctly:

  • Uses internal representation for state
  • Converts to external values when invoking callbacks
  • Properly discriminates reset options using the type field
  • Handles both single-select and multi-select modes appropriately

370-414: LGTM!

The keyboard handler correctly determines the initial focus position. The comparison at line 381 works properly because it compares internal representations on both sides (after externalToValue conversion), and the reset option is never in selectedOptions.


533-586: Rendering logic is sound, but depends on fixing valueToId.

The rendering correctly:

  • Maps options to internal representation for consistent comparison
  • Uses the proper pattern for the key prop (line 546) that was fixed from a past review
  • Compares internal values when determining selection state (line 540-541)
  • Passes the external option to onFocus for proper re-conversion

However, the id generation at line 557 depends on valueToId (lines 81-83), which has the bug flagged earlier. Once that's fixed, this rendering logic will work correctly.

Comment on lines +328 to +330
const currentIndex = options.findIndex(
(option) => externalToValue(option.value) === activeOption.value
);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Comparison fails for active reset option, breaking keyboard navigation.

The comparison externalToValue(option.value) === activeOption.value doesn't work when activeOption is the reset option. Here's why:

  • ResetOption keeps value: undefined (not converted to a symbol)
  • InternalOption converts undefined to UNDEFINED_VALUE symbol
  • When comparing: externalToValue(undefined) returns UNDEFINED_VALUE symbol, but activeOption.value is undefined
  • Result: UNDEFINED_VALUE === undefined is false, so findIndex returns -1
  • This causes incorrect keyboard navigation behavior when the reset option is active

Apply this diff to fix the comparison:

-        const currentIndex = options.findIndex(
-          (option) => externalToValue(option.value) === activeOption.value
-        );
+        const currentIndex = options.findIndex((option) => {
+          if ('type' in option && option.type === 'reset') {
+            return 'type' in activeOption && activeOption.type === 'reset';
+          }
+          return externalToValue(option.value) === activeOption.value;
+        });
🤖 Prompt for AI Agents
In code/core/src/components/components/Select/Select.tsx around lines 328 to
330, the findIndex comparison compares externalToValue(option.value) to
activeOption.value which fails when activeOption is the reset option (undefined
vs UNDEFINED_VALUE). Replace the comparison so both sides are normalized via
externalToValue: use externalToValue(option.value) ===
externalToValue(activeOption.value) so the reset option matches correctly and
keyboard navigation works.

Comment on lines +352 to +354
const currentIndex = options.findIndex(
(option) => externalToValue(option.value) === activeOption.value
);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Same comparison bug as moveActiveOptionDown.

This has the identical issue flagged in moveActiveOptionDown (lines 328-330) where the comparison fails when activeOption is the reset option.

Apply the same fix:

-        const currentIndex = options.findIndex(
-          (option) => externalToValue(option.value) === activeOption.value
-        );
+        const currentIndex = options.findIndex((option) => {
+          if ('type' in option && option.type === 'reset') {
+            return 'type' in activeOption && activeOption.type === 'reset';
+          }
+          return externalToValue(option.value) === activeOption.value;
+        });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const currentIndex = options.findIndex(
(option) => externalToValue(option.value) === activeOption.value
);
const currentIndex = options.findIndex((option) => {
if ('type' in option && option.type === 'reset') {
return 'type' in activeOption && activeOption.type === 'reset';
}
return externalToValue(option.value) === activeOption.value;
});
🤖 Prompt for AI Agents
In code/core/src/components/components/Select/Select.tsx around lines 352-354,
the findIndex comparison fails when activeOption is the reset option; update the
comparison to normalize both sides (apply externalToValue to activeOption.value
as well) and guard against activeOption being undefined/null so the equality
check uses externalToValue(option.value) ===
externalToValue(activeOption?.value).

@yannbf yannbf merged commit 2d27199 into next Nov 25, 2025
67 checks passed
@yannbf yannbf deleted the bug/globaltypes-select-issue branch November 25, 2025 16:14
@github-actions github-actions bot mentioned this pull request Nov 25, 2025
13 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants