Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
---
name: cds-deprecation-v9-to-v10-select-to-alpha-select-web
description: Migrate deprecated web Select usage from `packages/web/src/controls/Select.tsx` to the alpha Select in `packages/web/src/alpha/select/Select.tsx`. Use this whenever a repo is cleaning up CDS v9 deprecations before v10 and the code imports the old web Select or `SelectOption`, uses child-based Select options, or needs help converting `valueLabel`, `SelectOption` children, and legacy Select props into the alpha options-array API.
---

# Web Select To Alpha Select

Use this skill when migrating deprecated web `Select` usage to the alpha web `Select`.

This migration is not just an import rename. The old component is a trigger plus `SelectOption` children model. The alpha component is an options-driven API with richer customization and slightly different value semantics.

## What Changed

- Old web `Select` takes `children` and usually renders many `SelectOption` elements.
- Alpha web `Select` takes an `options` array.
- Old `SelectOption.title` becomes alpha option `label`.
- Old `SelectOption.description`, `media`, `accessory`, `end`, and `disabled` map naturally onto alpha option objects.
- Old `valueLabel` usually disappears because alpha `Select` can derive the displayed label from the selected option object.
- Old `value` is commonly `string | undefined`; alpha `Select` prefers `string | null` for single select.
- Old `width` and `disablePortal` are not alpha `Select` props.

If you need exact mappings or examples, read `references/api-mapping.md` and the files in `examples/`.

## Migration Workflow

1. Find imports of the deprecated web `Select` and `SelectOption`.
2. Replace them with the alpha `Select` import.
3. Convert child `SelectOption` nodes into an `options` array.
4. Normalize state from `string | undefined` to `string | null` when needed.
5. Remove props that no longer exist, especially `valueLabel`, `width`, `disablePortal`, and `onClick`.
6. Preserve supported props that still exist, such as `label`, `helperText`, `placeholder`, `compact`, `labelVariant`, `startNode`, `endNode`, `variant`, and `disabled`.
7. If layout depended on `width`, move that concern to `style`, `className`, or a parent layout component.
8. Re-check behavior for empty selection, focus/open behavior, and accessibility labels.

## Import Rewrite

Old pattern:

```tsx
import { Select } from '@coinbase/cds-web/controls';
import { SelectOption } from '@coinbase/cds-web/controls';
```

Preferred new pattern:

```tsx
import { Select } from '@coinbase/cds-web/alpha/select';
```

If the local import style is relative, keep it consistent with the surrounding file.

## Core Rewrite Rule

Convert this:

```tsx
<Select value={value} onChange={setValue}>
<SelectOption title="Option 1" value="1" />
<SelectOption description="BTC" title="Option 2" value="2" />
</Select>
```

Into this:

```tsx
const options = [
{ value: '1', label: 'Option 1' },
{ value: '2', label: 'Option 2', description: 'BTC' },
];

<Select onChange={setValue} options={options} value={value} />;
```

## Mapping Rules

- `SelectOption.title` -> `option.label`
- `SelectOption.description` -> `option.description`
- `SelectOption.media` -> `option.media`
- `SelectOption.accessory` -> `option.accessory`
- `SelectOption.end` -> `option.end`
- `SelectOption.disabled` -> `option.disabled`
- `SelectOption.value` -> `option.value`
- `SelectOption.Component` does not exist on the old API; if custom option rendering existed through wrappers, model it with alpha option `Component` or `SelectOptionComponent`

## State Rules

- Prefer `const [value, setValue] = useState<string | null>(null)` for single select.
- If old code uses `''` or `undefined` for "no selection", migrate carefully to `null`.
- If the old options intentionally supported clearing the selection, include a nullable option like `{ value: null, label: 'Remove selection' }`.

## Props To Remove Or Rework

- Remove `valueLabel`. Alpha `Select` displays the selected option label from `options`.
- Remove `width`. Use `style`, `className`, or a parent layout wrapper instead.
- Remove `disablePortal`. Alpha `Select` does not expose that prop.
- Remove `onClick` on the old root `Select`. If custom open behavior is required, use alpha `open` and `setOpen`.
- Old accessibility props like `accessibilityLabelledBy` and `accessibilityHint` do not map directly. Keep `accessibilityLabel` and add `controlAccessibilityLabel` when the visible label is not enough.

## When To Pause And Be Careful

Pause and reason more carefully if any of these are true:

- `Select` children are not a flat list of `SelectOption` elements.
- Options are conditionally rendered or built from nested React trees.
- The old component relies on `valueLabel` to show text that does not match the option label.
- The file depended on `disableCloseOnOptionChange` behavior from old option internals.
- The migration should become multi-select. This is not a one-to-one rewrite and needs alpha `type="multi"` plus array state.

## Output Expectations

When performing this migration, produce:

- The rewritten import(s)
- A local `options` array or memoized options list
- Updated single-select state typed as `string | null` when appropriate
- Removal or adaptation of unsupported old props
- A short note calling out anything that still needs manual product review

## Validation Checklist

- The file no longer imports the deprecated web `Select` or `SelectOption`.
- The alpha `Select` receives an `options` prop.
- The selected value type matches alpha expectations.
- The displayed selected label still matches product intent.
- Placeholder, helper text, and start/end visuals still render correctly.
- Layout still looks correct without the old `width` prop.

## Examples

- Basic child-to-options rewrite: `examples/basic.before.tsx` -> `examples/basic.after.tsx`
- Asset-style example with `valueLabel` removal and media mapping: `examples/asset.before.tsx` -> `examples/asset.after.tsx`
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { useMemo, useState } from 'react';

import { DotSymbol } from '../../dots';
import { Box } from '../../layout/Box';
import { RemoteImage } from '../../media';
import { Select } from '../../alpha/select';

const assets = [
{ value: 'btc', label: 'Bitcoin', imageUrl: '/btc.png' },
{ value: 'eth', label: 'Ethereum', imageUrl: '/eth.png' },
];

export function AssetExample() {
const [value, setValue] = useState<string | null>('btc');
const selectedAsset = assets.find((asset) => asset.value === value) ?? assets[0];

const options = useMemo(
() =>
assets.map((asset) => ({
value: asset.value,
label: asset.label,
description: 'Asset',
media: <RemoteImage shape="circle" size="l" source={asset.imageUrl} />,
})),
[],
);

return (
<Select
label="Select asset"
onChange={setValue}
options={options}
startNode={
<Box paddingX={2}>
<DotSymbol overlap="circular" pin="bottom-end" size="s" source="/eth.png">
<RemoteImage shape="circle" size="l" source={selectedAsset.imageUrl} />
</DotSymbol>
</Box>
}
value={value}
/>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { useState } from 'react';

import { DotSymbol } from '../../dots';
import { Box } from '../../layout/Box';
import { RemoteImage } from '../../media';
import { Select } from '../../controls/Select';
import { SelectOption } from '../../controls/SelectOption';

const assets = [
{ value: 'btc', label: 'Bitcoin', imageUrl: '/btc.png' },
{ value: 'eth', label: 'Ethereum', imageUrl: '/eth.png' },
];

export function AssetExample() {
const [value, setValue] = useState<string | undefined>('btc');
const selectedAsset = assets.find((asset) => asset.value === value) ?? assets[0];

return (
<Select
label="Select asset"
onChange={setValue}
startNode={
<Box paddingX={2}>
<DotSymbol overlap="circular" pin="bottom-end" size="s" source="/eth.png">
<RemoteImage shape="circle" size="l" source={selectedAsset.imageUrl} />
</DotSymbol>
</Box>
}
value={value}
valueLabel={selectedAsset.label}
>
{assets.map((asset) => (
<SelectOption
key={asset.value}
description="Asset"
media={<RemoteImage shape="circle" size="l" source={asset.imageUrl} />}
title={asset.label}
value={asset.value}
/>
))}
</Select>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { useState } from 'react';

import { Select } from '../../alpha/select';

const options = [
{ value: 'option-1', label: 'Option 1', description: 'BTC' },
{ value: 'option-2', label: 'Option 2', description: 'ETH' },
];

export function Example() {
const [value, setValue] = useState<string | null>(null);

return (
<Select
helperText="You can only choose one option"
label="How many would you like?"
onChange={setValue}
options={options}
placeholder="Choose an amount"
value={value}
/>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { useState } from 'react';

import { Select } from '../../controls/Select';
import { SelectOption } from '../../controls/SelectOption';

export function Example() {
const [value, setValue] = useState<string | undefined>('');

return (
<Select
helperText="You can only choose one option"
label="How many would you like?"
onChange={setValue}
placeholder="Choose an amount"
value={value}
>
<SelectOption description="BTC" title="Option 1" value="option-1" />
<SelectOption description="ETH" title="Option 2" value="option-2" />
</Select>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# API Mapping

This reference supports the `cds-deprecation-v9-to-v10-select-to-alpha-select-web` skill.

## Component Model

Legacy web `Select`:

- Import path is in the old `controls` area.
- Renders options as `children`.
- Commonly uses `SelectOption` elements.
- Stores the displayed string on the control with `value` and sometimes `valueLabel`.

Alpha web `Select`:

- Import path is in `alpha/select`.
- Renders from an `options` array.
- Supports single and multi select.
- Derives selected display content from the selected option and the default control renderer.

## Prop Mapping

| Legacy | Alpha | Notes |
| --------------------------------------- | ---------------------------------------------- | --------------------------------------------------------------- |
| `children` | `options` | Convert `SelectOption` nodes to plain objects |
| `value?: string` | `value: string \| null` | Prefer `null` for empty value |
| `onChange?: (newValue: string) => void` | `onChange: (newValue: string \| null) => void` | Widen state when empty selection is possible |
| `valueLabel` | remove | Usually redundant once `options` has the correct `label` |
| `label` | `label` | Same concept |
| `helperText` | `helperText` | Same concept |
| `placeholder` | `placeholder` | Same concept |
| `compact` | `compact` | Same concept |
| `labelVariant` | `labelVariant` | Same concept |
| `startNode` | `startNode` | Same concept |
| `variant` | `variant` | Same concept |
| `disabled` | `disabled` | Same concept |
| `width` | `style` or parent layout | Alpha `Select` does not expose `width` directly |
| `disablePortal` | remove | No direct alpha equivalent |
| `onClick` | `open` / `setOpen` if needed | Only keep if product logic actually needs controlled open state |
| `accessibilityLabel` | `accessibilityLabel` | Still supported |
| `accessibilityLabelledBy` | manual review | No direct alpha prop |
| `accessibilityHint` | manual review | No direct alpha prop |

## SelectOption Mapping

| Legacy `SelectOption` prop | Alpha option field |
| -------------------------- | ------------------ |
| `title` | `label` |
| `description` | `description` |
| `value` | `value` |
| `disabled` | `disabled` |
| `media` | `media` |
| `accessory` | `accessory` |
| `end` | `end` |

Ignore old option-only behavior that was tied to legacy dropdown internals unless the product still needs it.

## Common Rewrite Pattern

Before:

```tsx
<Select value={value} onChange={setValue}>
<SelectOption title="Option 1" value="1" />
<SelectOption description="BTC" title="Option 2" value="2" />
</Select>
```

After:

```tsx
const options = [
{ value: '1', label: 'Option 1' },
{ value: '2', label: 'Option 2', description: 'BTC' },
];

<Select onChange={setValue} options={options} value={value} />;
```

## Review Traps

- Old code may have used `valueLabel` to display a human-readable label while storing an opaque ID.
- Old code may initialize state with `''` instead of `null`.
- Old code may rely on `width="100%"`; alpha uses standard root styling instead.
- Old code may contain conditionally rendered options that should become a `useMemo`-generated `options` array.