diff --git a/.github/workflows/debug-workflow.yml b/.github/workflows/debug-workflow.yml
index b11f07e652..93154a991c 100644
--- a/.github/workflows/debug-workflow.yml
+++ b/.github/workflows/debug-workflow.yml
@@ -20,34 +20,11 @@ on:
# Manual trigger for testing
workflow_dispatch:
-concurrency:
- group: Visreg-Mobile-${{ github.ref_name }}-${{ github.event.pull_request.number || github.sha }}
- cancel-in-progress: ${{ github.event_name == 'pull_request' }}
-
-permissions:
- contents: read
- actions: read
- pull-requests: write
-
-env:
- CI: true
-
jobs:
- ios:
- name: Visreg iOS
- runs-on: macos-latest
- environment: production
- outputs:
- percy_url: ${{ steps.percy-upload.outputs.percy_url }}
+ test-local:
+ runs-on: [small, default-config]
steps:
- - name: Harden the runner (Audit all outbound calls)
- uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
- with:
- egress-policy: audit
- - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- with:
- fetch-depth: 1
- ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
+ - uses: actions/checkout@v4
- uses: ./.github/actions/setup
diff --git a/apps/docs/docs/components/navigation/TabIndicator/mobileMetadata.json b/apps/docs/docs/components/navigation/TabIndicator/mobileMetadata.json
index 2ed5f0b36d..248d5d185e 100644
--- a/apps/docs/docs/components/navigation/TabIndicator/mobileMetadata.json
+++ b/apps/docs/docs/components/navigation/TabIndicator/mobileMetadata.json
@@ -2,6 +2,7 @@
"import": "import { TabIndicator } from '@coinbase/cds-mobile/tabs/TabIndicator'",
"source": "https://github.com/coinbase/cds/blob/master/packages/mobile/src/tabs/TabIndicator.tsx",
"description": "A visual indicator that shows the active tab position.",
+ "warning": "This component is deprecated along with the TabNavigation component. Please use the Tabs component and DefaultTabsActiveIndicator instead.",
"relatedComponents": [
{
"label": "TabNavigation",
diff --git a/apps/docs/docs/components/navigation/TabIndicator/webMetadata.json b/apps/docs/docs/components/navigation/TabIndicator/webMetadata.json
index 26caadaa9e..023854ed94 100644
--- a/apps/docs/docs/components/navigation/TabIndicator/webMetadata.json
+++ b/apps/docs/docs/components/navigation/TabIndicator/webMetadata.json
@@ -2,6 +2,7 @@
"import": "import { TabIndicator } from '@coinbase/cds-web/tabs/TabIndicator'",
"source": "https://github.com/coinbase/cds/blob/master/packages/web/src/tabs/TabIndicator.tsx",
"storybook": "https://cds-storybook.coinbase.com/?path=/story/components-tabs-tabindicator--default",
+ "warning": "This component is deprecated along with the TabNavigation component. Please use the Tabs component and DefaultTabsActiveIndicator instead.",
"description": "A visual indicator that shows the active tab position.",
"relatedComponents": [
{
diff --git a/apps/docs/docs/components/navigation/TabLabel/mobileMetadata.json b/apps/docs/docs/components/navigation/TabLabel/mobileMetadata.json
index 749f4f45f4..a19864c6e1 100644
--- a/apps/docs/docs/components/navigation/TabLabel/mobileMetadata.json
+++ b/apps/docs/docs/components/navigation/TabLabel/mobileMetadata.json
@@ -2,6 +2,7 @@
"import": "import { TabLabel } from '@coinbase/cds-mobile/tabs/TabLabel'",
"source": "https://github.com/coinbase/cds/blob/master/packages/mobile/src/tabs/TabLabel.tsx",
"description": "A text label component used within tab navigation.",
+ "warning": "This component is deprecated along with the TabNavigation component. Please use the Tabs component and DefaultTab instead.",
"relatedComponents": [
{
"label": "TabNavigation",
diff --git a/apps/docs/docs/components/navigation/TabLabel/webMetadata.json b/apps/docs/docs/components/navigation/TabLabel/webMetadata.json
index 0cfca5a265..2020c5bd4c 100644
--- a/apps/docs/docs/components/navigation/TabLabel/webMetadata.json
+++ b/apps/docs/docs/components/navigation/TabLabel/webMetadata.json
@@ -3,6 +3,7 @@
"source": "https://github.com/coinbase/cds/blob/master/packages/web/src/tabs/TabLabel.tsx",
"storybook": "https://cds-storybook.coinbase.com/?path=/story/components-tabs-tablabel--default",
"description": "A text label component used within tab navigation.",
+ "warning": "This component is deprecated along with the TabNavigation component. Please use the Tabs component and DefaultTab instead.",
"relatedComponents": [
{
"label": "TabNavigation",
diff --git a/apps/docs/docs/components/navigation/Tabs/_mobileExamples.mdx b/apps/docs/docs/components/navigation/Tabs/_mobileExamples.mdx
index 83d8764979..71db0dade9 100644
--- a/apps/docs/docs/components/navigation/Tabs/_mobileExamples.mdx
+++ b/apps/docs/docs/components/navigation/Tabs/_mobileExamples.mdx
@@ -1,10 +1,8 @@
-Tabs is a low-level primitive for building custom tab interfaces. It requires a `TabComponent` and `TabsActiveIndicatorComponent` to render. For a ready-to-use tab experience, see [SegmentedTabs](/components/navigation/SegmentedTabs).
+Tabs manages which tab is active and positions the animated indicator. For the common **underline** pattern, pass **`TabsActiveIndicatorComponent={DefaultTabsActiveIndicator}`** and rely on the default **`TabComponent` (`DefaultTab`)**. Use a custom **`TabComponent`** when you need layout or content beyond what `DefaultTab` provides. For **pill / segmented** controls, use [SegmentedTabs](/components/navigation/SegmentedTabs/) instead.
## Basics
-### Initial Value
-
-Use `useTabsContext` inside your `TabComponent` to access the active tab state. Pair with [TabLabel](/components/navigation/TabLabel) for consistent label styling and [TabsActiveIndicator](/components/navigation/TabIndicator) for the animated indicator.
+Out of the box, **`Tabs`** uses **`DefaultTab`** for each row (headline text, optional [DotCount](/components/other/DotCount/) via `count` / `max` on each tab) and **`DefaultTabsActiveIndicator`** for the animated underline. **`activeBackground`** sets the **underline color** (it is forwarded to the indicator as its `background` token).
```jsx
function Example() {
@@ -13,44 +11,28 @@ function Example() {
{ id: 'tab2', label: 'Tab 2' },
{ id: 'tab3', label: 'Tab 3' },
];
-
- const TabComponent = useCallback(({ id, label, disabled }) => {
- const { activeTab, updateActiveTab } = useTabsContext();
- const isActive = activeTab?.id === id;
- return (
- updateActiveTab(id)}
- disabled={disabled}
- accessibilityRole="tab"
- accessibilityState={{ selected: isActive, disabled }}
- >
-
- {label}
-
-
- );
- }, []);
-
- const ActiveIndicator = useCallback(
- (props) => ,
- [],
- );
-
const [activeTab, setActiveTab] = useState(tabs[0]);
return (
);
}
```
-Tabs can also start with no active selection by passing `null`.
+You can omit `TabComponent` explicitly: **`Tabs`** defaults it to **`DefaultTab`**.
+
+### No initial selection
```jsx
function Example() {
@@ -59,38 +41,39 @@ function Example() {
{ id: 'tab2', label: 'Tab 2' },
{ id: 'tab3', label: 'Tab 3' },
];
-
- const TabComponent = useCallback(({ id, label, disabled }) => {
- const { activeTab, updateActiveTab } = useTabsContext();
- const isActive = activeTab?.id === id;
- return (
- updateActiveTab(id)}
- disabled={disabled}
- accessibilityRole="tab"
- accessibilityState={{ selected: isActive, disabled }}
- >
-
- {label}
-
-
- );
- }, []);
-
- const ActiveIndicator = useCallback(
- (props) => ,
- [],
- );
-
const [activeTab, setActiveTab] = useState(null);
return (
+ );
+}
+```
+
+### Dot counts
+
+```jsx
+function Example() {
+ const tabs = [
+ { id: 'inbox', label: 'Inbox', count: 3, max: 99 },
+ { id: 'sent', label: 'Sent' },
+ ];
+ const [activeTab, setActiveTab] = useState(tabs[0]);
+ return (
+
);
}
@@ -98,60 +81,37 @@ function Example() {
### Disabled
-The entire component can be disabled with the `disabled` prop.
-
```jsx
function Example() {
const tabs = [
{ id: 'tab1', label: 'Tab 1' },
- { id: 'tab2', label: 'Tab 2' },
+ { id: 'tab2', label: 'Tab 2', disabled: true },
{ id: 'tab3', label: 'Tab 3' },
];
-
- const TabComponent = useCallback(({ id, label, disabled }) => {
- const { activeTab, updateActiveTab } = useTabsContext();
- const isActive = activeTab?.id === id;
- return (
- updateActiveTab(id)}
- disabled={disabled}
- accessibilityRole="tab"
- accessibilityState={{ selected: isActive, disabled }}
- >
-
- {label}
-
-
- );
- }, []);
-
- const ActiveIndicator = useCallback(
- (props) => ,
- [],
- );
-
const [activeTab, setActiveTab] = useState(tabs[0]);
return (
);
}
```
-Individual tabs can also be disabled while keeping others interactive.
+## Custom `TabComponent`
+
+Use **`useTabsContext`** with your own **`Pressable`** and **`Text`** for labels (and a custom **`TabsActiveIndicatorComponent`** if needed) when you need more control than `DefaultTab`.
```jsx
function Example() {
const tabs = [
{ id: 'tab1', label: 'Tab 1' },
- { id: 'tab2', label: 'Tab 2', disabled: true },
+ { id: 'tab2', label: 'Tab 2' },
{ id: 'tab3', label: 'Tab 3' },
];
@@ -165,9 +125,9 @@ function Example() {
accessibilityRole="tab"
accessibilityState={{ selected: isActive, disabled }}
>
-
+
{label}
-
+
);
}, []);
@@ -191,11 +151,7 @@ function Example() {
}
```
-## Custom Components
-
-### Tab
-
-Pass additional data through the tab definitions and access it in your `TabComponent` to render custom content like icons.
+### Custom label content
```jsx
function Example() {
@@ -217,9 +173,9 @@ function Example() {
>
-
+
{label}
-
+
);
@@ -243,3 +199,7 @@ function Example() {
);
}
```
+
+## Accessibility
+
+Set **`accessibilityLabel`** on **`Tabs`**. **`DefaultTab`** wires `accessibilityRole="tab"` and selection state; keep tab panels in sync in your screen content.
diff --git a/apps/docs/docs/components/navigation/Tabs/_webExamples.mdx b/apps/docs/docs/components/navigation/Tabs/_webExamples.mdx
index 239731e356..3e288890bd 100644
--- a/apps/docs/docs/components/navigation/Tabs/_webExamples.mdx
+++ b/apps/docs/docs/components/navigation/Tabs/_webExamples.mdx
@@ -1,10 +1,8 @@
-Tabs is a low-level primitive for building custom tab interfaces. It requires a `TabComponent` and `TabsActiveIndicatorComponent` to render. For a ready-to-use tab experience, see [SegmentedTabs](/components/navigation/SegmentedTabs).
+Tabs manages which tab is active and positions the animated indicator. For the common **underline** pattern, pass **`TabsActiveIndicatorComponent={DefaultTabsActiveIndicator}`** and rely on the default **`TabComponent` (`DefaultTab`)**. Use a custom **`TabComponent`** when you need layout or content beyond what `DefaultTab` provides. For **pill / segmented** controls, use [SegmentedTabs](/components/navigation/SegmentedTabs) instead.
## Basics
-### Initial Value
-
-Use `useTabsContext` inside your `TabComponent` to access the active tab state. Pair with [TabLabel](/components/navigation/TabLabel) for consistent label styling and [TabsActiveIndicator](/components/navigation/TabIndicator) for the animated indicator.
+Out of the box, **`Tabs`** uses **`DefaultTab`** for each row (headline text, optional [DotCount](/components/other/DotCount/) via `count` / `max` on each tab) and **`DefaultTabsActiveIndicator`** for the animated underline. **`activeBackground`** sets the **underline color** (it is forwarded to the indicator as its `background` token).
```jsx live
function Example() {
@@ -13,44 +11,28 @@ function Example() {
{ id: 'tab2', label: 'Tab 2' },
{ id: 'tab3', label: 'Tab 3' },
];
-
- const TabComponent = useCallback(({ id, label, disabled, ...props }) => {
- const { activeTab, updateActiveTab } = useTabsContext();
- const isActive = activeTab?.id === id;
- return (
- updateActiveTab(id)}
- disabled={disabled}
- aria-pressed={isActive}
- {...props}
- >
-
- {label}
-
-
- );
- }, []);
-
- const ActiveIndicator = useCallback(
- (props) => ,
- [],
- );
-
const [activeTab, setActiveTab] = useState(tabs[0]);
return (
);
}
```
-Tabs can also start with no active selection by passing `null`.
+You can omit `TabComponent` explicitly: **`Tabs`** defaults it to **`DefaultTab`**.
+
+### No initial selection
```jsx live
function Example() {
@@ -59,38 +41,41 @@ function Example() {
{ id: 'tab2', label: 'Tab 2' },
{ id: 'tab3', label: 'Tab 3' },
];
-
- const TabComponent = useCallback(({ id, label, disabled, ...props }) => {
- const { activeTab, updateActiveTab } = useTabsContext();
- const isActive = activeTab?.id === id;
- return (
- updateActiveTab(id)}
- disabled={disabled}
- aria-pressed={isActive}
- {...props}
- >
-
- {label}
-
-
- );
- }, []);
-
- const ActiveIndicator = useCallback(
- (props) => ,
- [],
- );
-
const [activeTab, setActiveTab] = useState(null);
return (
+ );
+}
+```
+
+### Dot counts
+
+Optional **`count`** and **`max`** on each tab are forwarded to the badge next to the label (see [DotCount](/components/other/DotCount/)).
+
+```jsx live
+function Example() {
+ const tabs = [
+ { id: 'inbox', label: 'Inbox', count: 3, max: 99 },
+ { id: 'sent', label: 'Sent' },
+ ];
+ const [activeTab, setActiveTab] = useState(tabs[0]);
+ return (
+
);
}
@@ -98,60 +83,39 @@ function Example() {
### Disabled
-The entire component can be disabled with the `disabled` prop.
+Disable the whole row with **`disabled`**, or set **`disabled: true`** on individual tab items.
```jsx live
function Example() {
const tabs = [
{ id: 'tab1', label: 'Tab 1' },
- { id: 'tab2', label: 'Tab 2' },
+ { id: 'tab2', label: 'Tab 2', disabled: true },
{ id: 'tab3', label: 'Tab 3' },
];
-
- const TabComponent = useCallback(({ id, label, disabled, ...props }) => {
- const { activeTab, updateActiveTab } = useTabsContext();
- const isActive = activeTab?.id === id;
- return (
- updateActiveTab(id)}
- disabled={disabled}
- aria-pressed={isActive}
- {...props}
- >
-
- {label}
-
-
- );
- }, []);
-
- const ActiveIndicator = useCallback(
- (props) => ,
- [],
- );
-
const [activeTab, setActiveTab] = useState(tabs[0]);
return (
);
}
```
-Individual tabs can also be disabled while keeping others interactive.
+## Custom `TabComponent`
+
+Use **`useTabsContext`** inside your own tab button.
```jsx live
function Example() {
const tabs = [
{ id: 'tab1', label: 'Tab 1' },
- { id: 'tab2', label: 'Tab 2', disabled: true },
+ { id: 'tab2', label: 'Tab 2' },
{ id: 'tab3', label: 'Tab 3' },
];
@@ -165,9 +129,9 @@ function Example() {
aria-pressed={isActive}
{...props}
>
-
+
{label}
-
+
);
}, []);
@@ -191,11 +155,9 @@ function Example() {
}
```
-## Custom Components
+### Custom label content
-### Tab
-
-Pass additional data through the tab definitions and access it in your `TabComponent` to render custom content like icons.
+Pass extra fields on each tab and read them in your `TabComponent` (for example icons).
```jsx live
function Example() {
@@ -217,9 +179,9 @@ function Example() {
>
-
+
{label}
-
+
);
@@ -243,3 +205,7 @@ function Example() {
);
}
```
+
+## Accessibility
+
+Provide a descriptive **`accessibilityLabel`** on **`Tabs`** for the tab list. **`DefaultTab`** sets `aria-controls` / `aria-selected` for each tab; pair tabs with **`role="tabpanel"`** regions in your page content when you switch panels.
diff --git a/apps/docs/docs/components/navigation/Tabs/_webStyles.mdx b/apps/docs/docs/components/navigation/Tabs/_webStyles.mdx
index ee3c6a6678..45c03dac67 100644
--- a/apps/docs/docs/components/navigation/Tabs/_webStyles.mdx
+++ b/apps/docs/docs/components/navigation/Tabs/_webStyles.mdx
@@ -1,10 +1,7 @@
-import { useState, useCallback } from 'react';
-import { useTabsContext } from '@coinbase/cds-common/tabs/TabsContext';
+import { useState } from 'react';
import { ComponentStylesTable } from '@site/src/components/page/ComponentStylesTable';
import { StylesExplorer } from '@site/src/components/page/StylesExplorer';
-import { Tabs, TabsActiveIndicator } from '@coinbase/cds-web/tabs/Tabs';
-import { Pressable } from '@coinbase/cds-web/system/Pressable';
-import { TabLabel } from '@coinbase/cds-web/tabs/TabLabel';
+import { DefaultTabsActiveIndicator, Tabs } from '@coinbase/cds-web/tabs';
import webStylesData from ':docgen/web/tabs/Tabs/styles-data';
@@ -14,36 +11,17 @@ export const TabsExample = ({ classNames }) => {
{ id: 'tab2', label: 'Tab 2' },
{ id: 'tab3', label: 'Tab 3' },
];
- const TabComponent = useCallback(({ id, label, disabled, ...props }) => {
- const api = useTabsContext();
- const isActive = api.activeTab?.id === id;
- return (
- api.updateActiveTab(id)}
- disabled={disabled}
- aria-pressed={isActive}
- {...props}
- >
-
- {label}
-
-
- );
- }, []);
- const CustomIndicator = useCallback(
- (props) => ,
- [],
- );
const [activeTab, setActiveTab] = useState(tabs[0]);
return (
);
};
diff --git a/apps/docs/docs/components/navigation/Tabs/mobileMetadata.json b/apps/docs/docs/components/navigation/Tabs/mobileMetadata.json
index 07c03025ec..c8fc565aa3 100644
--- a/apps/docs/docs/components/navigation/Tabs/mobileMetadata.json
+++ b/apps/docs/docs/components/navigation/Tabs/mobileMetadata.json
@@ -1,16 +1,20 @@
{
- "import": "import { Tabs } from '@coinbase/cds-mobile/tabs/Tabs'",
+ "import": "import { Tabs, DefaultTabsActiveIndicator } from '@coinbase/cds-mobile/tabs'",
"source": "https://github.com/coinbase/cds/blob/master/packages/mobile/src/tabs/Tabs.tsx",
- "description": "Tabs is a flexible, accessible tab navigation component for React Native, supporting animated indicators, custom tab components, and full accessibility.",
+ "description": "Tabs is a flexible, accessible tab list for switching between related views. Use `DefaultTab` and `DefaultTabsActiveIndicator` for a standard underline tab row without custom tab wiring, or provide your own `TabComponent` and `TabsActiveIndicatorComponent`. For pill-style selection, see SegmentedTabs.",
"figma": "https://www.figma.com/design/k5CtyJccNQUGMI5bI4lJ2g/%E2%9C%A8-CDS-Components?node-id=25128-9889&t=7bpcjquwgXNk9lnN-4",
"relatedComponents": [
- {
- "label": "TabNavigation",
- "url": "/components/navigation/TabNavigation/"
- },
{
"label": "SegmentedTabs",
"url": "/components/navigation/SegmentedTabs/"
+ },
+ {
+ "label": "TabIndicator",
+ "url": "/components/navigation/TabIndicator/"
+ },
+ {
+ "label": "TabLabel",
+ "url": "/components/navigation/TabLabel/"
}
],
"dependencies": [
diff --git a/apps/docs/docs/components/navigation/Tabs/webMetadata.json b/apps/docs/docs/components/navigation/Tabs/webMetadata.json
index cefe3817ee..f5a729f2a7 100644
--- a/apps/docs/docs/components/navigation/Tabs/webMetadata.json
+++ b/apps/docs/docs/components/navigation/Tabs/webMetadata.json
@@ -1,15 +1,20 @@
{
- "import": "import { Tabs } from '@coinbase/cds-web/tabs/Tabs'",
+ "import": "import { Tabs, DefaultTabsActiveIndicator } from '@coinbase/cds-web/tabs'",
"source": "https://github.com/coinbase/cds/blob/master/packages/web/src/tabs/Tabs.tsx",
- "description": "Tabs is a flexible, accessible tab navigation component for switching between content sections. It supports custom tab components, animated active indicators, and full keyboard navigation.",
+ "storybook": "https://cds-storybook.coinbase.com/?path=/story/components-tabs-tabs--all",
+ "description": "Tabs is a flexible, accessible tab list for switching between related views. Use `DefaultTab` and `DefaultTabsActiveIndicator` for a standard underline tab row without wiring custom components, or supply your own `TabComponent` and `TabsActiveIndicatorComponent` for full control. For pill-style selection, see SegmentedTabs.",
"relatedComponents": [
- {
- "label": "TabNavigation",
- "url": "/components/navigation/TabNavigation/"
- },
{
"label": "SegmentedTabs",
"url": "/components/navigation/SegmentedTabs"
+ },
+ {
+ "label": "TabIndicator",
+ "url": "/components/navigation/TabIndicator/"
+ },
+ {
+ "label": "TabLabel",
+ "url": "/components/navigation/TabLabel/"
}
],
"dependencies": [
diff --git a/packages/common/CHANGELOG.md b/packages/common/CHANGELOG.md
index 65fc89fe03..fe69471111 100644
--- a/packages/common/CHANGELOG.md
+++ b/packages/common/CHANGELOG.md
@@ -8,6 +8,16 @@ All notable changes to this project will be documented in this file.
+## 8.64.0 (4/2/2026 PST)
+
+#### 🚀 Updates
+
+- UseTabs: Added an optional second generic TTab extends TabValue so tabs, activeTab, and onChange can be typed with custom tab row shapes (defaults preserve the old behavior). [[#558](https://github.com/coinbase/cds/pull/558)]
+
+## 8.63.0 ((4/1/2026, 03:43 PM PST))
+
+This is an artificial version bump with no new change.
+
## 8.62.1 (4/1/2026 PST)
#### 🐞 Fixes
diff --git a/packages/common/package.json b/packages/common/package.json
index 9c0b17fca4..49a44fc828 100644
--- a/packages/common/package.json
+++ b/packages/common/package.json
@@ -1,6 +1,6 @@
{
"name": "@coinbase/cds-common",
- "version": "8.62.1",
+ "version": "8.64.0",
"description": "Coinbase Design System - Common",
"repository": {
"type": "git",
diff --git a/packages/common/src/tabs/TabsContext.ts b/packages/common/src/tabs/TabsContext.ts
index 766c51b4a0..ae6e0e261a 100644
--- a/packages/common/src/tabs/TabsContext.ts
+++ b/packages/common/src/tabs/TabsContext.ts
@@ -1,13 +1,19 @@
import { createContext, useContext } from 'react';
-import { type TabsApi } from './useTabs';
+import { type TabsApi, type TabValue } from './useTabs';
-export type TabsContextValue = TabsApi;
+export type TabsContextValue<
+ TabId extends string = string,
+ TTab extends TabValue = TabValue,
+> = TabsApi;
export const TabsContext = createContext(undefined);
-export const useTabsContext = (): TabsContextValue => {
- const context = useContext(TabsContext) as TabsContextValue | undefined;
+export const useTabsContext = <
+ TabId extends string,
+ TTab extends TabValue = TabValue,
+>(): TabsContextValue => {
+ const context = useContext(TabsContext) as TabsContextValue | undefined;
if (!context) throw Error('useTabsContext must be used within a TabsContext.Provider');
return context;
};
diff --git a/packages/common/src/tabs/useTabs.ts b/packages/common/src/tabs/useTabs.ts
index 2b0543c61f..c5f4ce11b4 100644
--- a/packages/common/src/tabs/useTabs.ts
+++ b/packages/common/src/tabs/useTabs.ts
@@ -9,18 +9,24 @@ export type TabValue = {
disabled?: boolean;
};
-export type TabsOptions = {
+export type TabsOptions<
+ TabId extends string = string,
+ TTab extends TabValue = TabValue,
+> = {
/** The array of tabs data. */
- tabs: TabValue[];
+ tabs: TTab[];
/** React state for the currently active tab. Setting it to `null` results in no active tab. */
- activeTab: TabValue | null;
+ activeTab: TTab | null;
/** Callback that is fired when the active tab changes. Use this callback to update the `activeTab` state. */
- onChange: (activeTab: TabValue | null) => void;
+ onChange: (activeTab: TTab | null) => void;
/** Disable interactions on all the tabs. */
disabled?: boolean;
};
-export type TabsApi = Omit, 'onChange'> & {
+export type TabsApi<
+ TabId extends string = string,
+ TTab extends TabValue = TabValue,
+> = Omit, 'onChange'> & {
/** Update the currently active tab to the tab with `tabId`. */
updateActiveTab: (tabId: TabId | null) => void;
/** Update the currently active tab to the next enabled tab in the tabs array. Does nothing if the last tab is already active. */
@@ -30,15 +36,15 @@ export type TabsApi = Omit, 'o
};
/** A controlled hook for managing tabs state, such as the currently active tab. */
-export const useTabs = ({
+export const useTabs = = TabValue>({
tabs,
activeTab,
disabled,
onChange,
-}: TabsOptions): TabsApi => {
+}: TabsOptions): TabsApi => {
const updateActiveTab = useCallback(
(tabId: TabId | null) => {
- let newActiveTab: TabValue | null = null;
+ let newActiveTab: TTab | null = null;
if (typeof tabId === 'string' && tabId !== '') {
newActiveTab = tabs.find((tab) => tab.id === tabId) ?? tabs[0];
}
diff --git a/packages/mcp-server/CHANGELOG.md b/packages/mcp-server/CHANGELOG.md
index 946b32a060..26f599eb75 100644
--- a/packages/mcp-server/CHANGELOG.md
+++ b/packages/mcp-server/CHANGELOG.md
@@ -8,6 +8,14 @@ All notable changes to this project will be documented in this file.
+## 8.64.0 ((4/2/2026, 07:51 AM PST))
+
+This is an artificial version bump with no new change.
+
+## 8.63.0 ((4/1/2026, 03:43 PM PST))
+
+This is an artificial version bump with no new change.
+
## 8.62.1 ((4/1/2026, 12:25 PM PST))
This is an artificial version bump with no new change.
diff --git a/packages/mcp-server/package.json b/packages/mcp-server/package.json
index 53973741bf..d63fb6ac71 100644
--- a/packages/mcp-server/package.json
+++ b/packages/mcp-server/package.json
@@ -1,6 +1,6 @@
{
"name": "@coinbase/cds-mcp-server",
- "version": "8.62.1",
+ "version": "8.64.0",
"description": "Coinbase Design System - MCP Server",
"repository": {
"type": "git",
diff --git a/packages/mobile/CHANGELOG.md b/packages/mobile/CHANGELOG.md
index eb68ec44c6..a24da1c14e 100644
--- a/packages/mobile/CHANGELOG.md
+++ b/packages/mobile/CHANGELOG.md
@@ -8,6 +8,16 @@ All notable changes to this project will be documented in this file.
+## 8.64.0 (4/2/2026 PST)
+
+#### 🚀 Updates
+
+- Added DefaultTab and DefaultTabActiveIndicator and deprecate types used by TabNavigation. [[#558](https://github.com/coinbase/cds/pull/558)]
+
+## 8.63.0 ((4/1/2026, 03:43 PM PST))
+
+This is an artificial version bump with no new change.
+
## 8.62.1 ((4/1/2026, 12:25 PM PST))
This is an artificial version bump with no new change.
diff --git a/packages/mobile/package.json b/packages/mobile/package.json
index 3e17ed120a..23d44fc6b8 100644
--- a/packages/mobile/package.json
+++ b/packages/mobile/package.json
@@ -1,6 +1,6 @@
{
"name": "@coinbase/cds-mobile",
- "version": "8.62.1",
+ "version": "8.64.0",
"description": "Coinbase Design System - Mobile",
"repository": {
"type": "git",
diff --git a/packages/mobile/src/tabs/DefaultTab.tsx b/packages/mobile/src/tabs/DefaultTab.tsx
new file mode 100644
index 0000000000..82173dc3ca
--- /dev/null
+++ b/packages/mobile/src/tabs/DefaultTab.tsx
@@ -0,0 +1,111 @@
+import React, { forwardRef, memo, useCallback, useMemo } from 'react';
+import {
+ type GestureResponderEvent,
+ Pressable,
+ type PressableProps,
+ type StyleProp,
+ type View,
+ type ViewStyle,
+} from 'react-native';
+import type { SharedAccessibilityProps } from '@coinbase/cds-common';
+import { useTabsContext } from '@coinbase/cds-common/tabs/TabsContext';
+import type { TabValue } from '@coinbase/cds-common/tabs/useTabs';
+import { accessibleOpacityDisabled } from '@coinbase/cds-common/tokens/interactable';
+
+import { DotCount, type DotCountBaseProps } from '../dots/DotCount';
+import { useTheme } from '../hooks/useTheme';
+import { HStack } from '../layout';
+import { Text } from '../typography/Text';
+
+import type { TabComponentProps } from './Tabs';
+
+/** Optional dot count and a11y overrides for the default tab row. */
+export type DefaultTabLabelProps = Partial> &
+ Pick;
+
+export type DefaultTabProps = Omit<
+ PressableProps,
+ 'children' | 'onPress' | 'style'
+> &
+ TabComponentProps & DefaultTabLabelProps> & {
+ /** Callback that is fired when the tab is pressed, after the active tab updates. */
+ onPress?: (id: TabId, event: GestureResponderEvent) => void;
+ style?: StyleProp;
+ };
+
+type DefaultTabComponent = (
+ props: DefaultTabProps & { ref?: React.ForwardedRef },
+) => React.ReactElement;
+
+const DefaultTabComponent = memo(
+ forwardRef(
+ (
+ {
+ id,
+ label,
+ disabled: disabledProp,
+ onPress,
+ count,
+ max,
+ accessibilityLabel,
+ style,
+ testID,
+ ...props
+ }: DefaultTabProps,
+ ref: React.ForwardedRef,
+ ) => {
+ const theme = useTheme();
+ const {
+ activeTab,
+ updateActiveTab,
+ disabled: allTabsDisabled,
+ } = useTabsContext & DefaultTabLabelProps>();
+ const isActive = activeTab?.id === id;
+ const isDisabled = disabledProp || allTabsDisabled;
+
+ const handlePress = useCallback(
+ (event: GestureResponderEvent) => {
+ updateActiveTab(id);
+ onPress?.(id, event);
+ },
+ [id, onPress, updateActiveTab],
+ );
+
+ const labelPaddingStyle = useMemo(
+ () => ({
+ paddingTop: theme.space[2],
+ paddingBottom: theme.space[2] - 2,
+ }),
+ [theme.space],
+ );
+
+ return (
+
+
+
+ {label}
+
+ {!!count && }
+
+
+ );
+ },
+ ),
+);
+
+DefaultTabComponent.displayName = 'DefaultTab';
+
+export const DefaultTab = DefaultTabComponent as DefaultTabComponent;
diff --git a/packages/mobile/src/tabs/DefaultTabsActiveIndicator.tsx b/packages/mobile/src/tabs/DefaultTabsActiveIndicator.tsx
new file mode 100644
index 0000000000..50cbd3e266
--- /dev/null
+++ b/packages/mobile/src/tabs/DefaultTabsActiveIndicator.tsx
@@ -0,0 +1,58 @@
+import { memo, useEffect } from 'react';
+import Animated, { useAnimatedStyle, useSharedValue, withSpring } from 'react-native-reanimated';
+
+import { Box } from '../layout';
+
+import { type TabsActiveIndicatorProps, tabsSpringConfig } from './Tabs';
+
+/**
+ * Default underline-style indicator for mobile `Tabs`. Pass as
+ * `TabsActiveIndicatorComponent={DefaultTabsActiveIndicator}` with `TabComponent={DefaultTab}`.
+ */
+const AnimatedBox = Animated.createAnimatedComponent(Box);
+
+export const DefaultTabsActiveIndicator = memo(
+ ({
+ activeTabRect,
+ background = 'bgPrimary',
+ style,
+ testID,
+ ...props
+ }: TabsActiveIndicatorProps) => {
+ const { width, x } = activeTabRect;
+ const rect = useSharedValue({ width, x });
+
+ useEffect(() => {
+ if (!width) return;
+ rect.value = withSpring({ x, width }, tabsSpringConfig);
+ }, [rect, width, x]);
+
+ const animatedBoxStyle = useAnimatedStyle(
+ () => ({
+ transform: [{ translateX: rect.value.x }],
+ width: rect.value.width,
+ }),
+ [],
+ );
+
+ if (!width) return null;
+
+ return (
+
+ );
+ },
+);
+
+DefaultTabsActiveIndicator.displayName = 'DefaultTabsActiveIndicator';
diff --git a/packages/mobile/src/tabs/TabIndicator.tsx b/packages/mobile/src/tabs/TabIndicator.tsx
index c21aa1c0fa..8ea545eec7 100644
--- a/packages/mobile/src/tabs/TabIndicator.tsx
+++ b/packages/mobile/src/tabs/TabIndicator.tsx
@@ -19,6 +19,8 @@ export type TabIndicatorProps = SharedProps & {
background?: ThemeVars.Color;
};
+/** @deprecated Use DefaultTabsActiveIndicator instead. This will be removed in a future major release. */
+/** @deprecationExpectedRemoval v10 */
export const TabIndicator = memo(
forwardRef(
(
diff --git a/packages/mobile/src/tabs/TabLabel.tsx b/packages/mobile/src/tabs/TabLabel.tsx
index 7cd566e9bb..3a4f51eb02 100644
--- a/packages/mobile/src/tabs/TabLabel.tsx
+++ b/packages/mobile/src/tabs/TabLabel.tsx
@@ -34,6 +34,8 @@ export type TabLabelBaseProps = SharedProps &
export type TabLabelProps = TabLabelBaseProps & TextProps;
+/** @deprecated Use DefaultTab instead. This will be removed in a future major release. */
+/** @deprecationExpectedRemoval v10 */
export const TabLabel = memo(
({ active, variant = 'primary', count = 0, max, ...props }: TabLabelProps) => {
const theme = useTheme();
diff --git a/packages/mobile/src/tabs/TabNavigation.tsx b/packages/mobile/src/tabs/TabNavigation.tsx
index 0cb14c837a..5c680058cf 100644
--- a/packages/mobile/src/tabs/TabNavigation.tsx
+++ b/packages/mobile/src/tabs/TabNavigation.tsx
@@ -15,6 +15,10 @@ import { Pressable } from '../system/Pressable';
import { TabIndicator } from './TabIndicator';
import { TabLabel } from './TabLabel';
+/**
+ * @deprecated Use Tabs instead. This will be removed in a future major release.
+ * @deprecationExpectedRemoval v10
+ */
export type TabProps = SharedProps &
Partial> & {
/** The id should be a meaningful and useful identifier like "watchlist" or "forSale" */
@@ -35,6 +39,10 @@ export type TabProps = SharedProps &
Component?: (props: CustomTabProps) => React.ReactNode;
};
+/**
+ * @deprecated Use Tabs instead. This will be removed in a future major release.
+ * @deprecationExpectedRemoval v10
+ */
export type CustomTabProps = Pick & {
/**
* @default false
@@ -45,6 +53,10 @@ export type CustomTabProps = Pick & {
label?: React.ReactNode;
};
+/**
+ * @deprecated Use Tabs instead. This will be removed in a future major release.
+ * @deprecationExpectedRemoval v10
+ */
export type TabNavigationBaseProps = BoxBaseProps &
Pick &
Pick & {
@@ -88,6 +100,10 @@ export type TabNavigationBaseProps =
id?: string;
};
+/**
+ * @deprecated Use Tabs instead. This will be removed in a future major release.
+ * @deprecationExpectedRemoval v10
+ */
export type TabNavigationProps =
TabNavigationBaseProps;
diff --git a/packages/mobile/src/tabs/Tabs.tsx b/packages/mobile/src/tabs/Tabs.tsx
index 54ddedc6e9..9749b4ea72 100644
--- a/packages/mobile/src/tabs/Tabs.tsx
+++ b/packages/mobile/src/tabs/Tabs.tsx
@@ -22,6 +22,9 @@ import { useComponentConfig } from '../hooks/useComponentConfig';
import type { BoxBaseProps, BoxProps, HStackProps } from '../layout';
import { Box, HStack } from '../layout';
+import { DefaultTab } from './DefaultTab';
+import { DefaultTabsActiveIndicator } from './DefaultTabsActiveIndicator';
+
const AnimatedBox = Animated.createAnimatedComponent(Box);
type TabContainerProps = {
@@ -51,31 +54,44 @@ export type TabsActiveIndicatorProps = {
activeTabRect: Rect;
} & BoxProps;
-export type TabComponentProps = TabValue & {
+export type TabComponentProps<
+ TabId extends string = string,
+ TTab extends TabValue = TabValue,
+> = Omit & {
+ id: TabId;
style?: StyleProp;
};
-export type TabComponent = React.FC>;
+export type TabComponent<
+ TabId extends string = string,
+ TTab extends TabValue = TabValue,
+> = React.FC>;
export type TabsActiveIndicatorComponent = React.FC;
-export type TabsBaseProps = Omit &
- Omit, 'tabs'> & {
+export type TabsBaseProps<
+ TabId extends string = string,
+ TTab extends TabValue = TabValue,
+> = Omit &
+ Omit, 'tabs'> & {
/** The array of tabs data. Each tab may optionally define a custom Component to render. */
- tabs: (TabValue & { Component?: TabComponent })[];
+ tabs: (TTab & { Component?: TabComponent })[];
/** The default Component to render each tab. */
- TabComponent: TabComponent;
+ TabComponent?: TabComponent;
/** The default Component to render the tabs active indicator. */
- TabsActiveIndicatorComponent: TabsActiveIndicatorComponent;
+ TabsActiveIndicatorComponent?: TabsActiveIndicatorComponent;
/** Background color passed to the TabsActiveIndicatorComponent. */
activeBackground?: ThemeVars.Color;
/** Optional callback to receive the active tab element. */
onActiveTabElementChange?: (element: View | null) => void;
- /** Custom styles for individual elements of the Tabs component */
};
-export type TabsProps = TabsBaseProps &
+export type TabsProps<
+ TabId extends string = string,
+ TTab extends TabValue = TabValue,
+> = TabsBaseProps &
Omit & {
+ /** Custom styles for individual elements of the Tabs component */
styles?: {
/** Root container element */
root?: StyleProp;
@@ -86,106 +102,114 @@ export type TabsProps = TabsBaseProps &
};
};
-type TabsFC = (
- props: TabsProps & { ref?: React.ForwardedRef },
+type TabsFC = = TabValue>(
+ props: TabsProps & { ref?: React.ForwardedRef },
) => React.ReactElement;
const TabsComponent = memo(
- forwardRef((_props: TabsProps, ref: React.ForwardedRef) => {
- const mergedProps = useComponentConfig('Tabs', _props);
- const {
- tabs,
- TabComponent,
- TabsActiveIndicatorComponent,
- activeBackground,
- activeTab,
- disabled,
- onChange,
- styles,
- style,
- role = 'tablist',
- position = 'relative',
- alignSelf = 'flex-start',
- opacity,
- onActiveTabElementChange,
- borderRadius,
- borderTopLeftRadius,
- borderTopRightRadius,
- borderBottomLeftRadius,
- borderBottomRightRadius,
- ...props
- } = mergedProps;
- const tabsContainerRef = useRef(null);
- useImperativeHandle(ref, () => tabsContainerRef.current as View, []); // merge internal ref to forwarded ref
-
- const refMap = useRefMap();
- const api = useTabs({ tabs, activeTab, disabled, onChange });
-
- const [activeTabRect, setActiveTabRect] = useState(defaultRect);
- const previousActiveRef = useRef(activeTab);
-
- const updateActiveTabRect = useCallback(() => {
- const activeTabRef = activeTab ? refMap.getRef(activeTab.id) : null;
- if (!activeTabRef || !tabsContainerRef.current) return;
- activeTabRef.measureLayout(tabsContainerRef.current, (x, y, width, height) =>
- setActiveTabRect({ x, y, width, height }),
+ forwardRef(
+ = TabValue>(
+ _props: TabsProps,
+ ref: React.ForwardedRef,
+ ) => {
+ const mergedProps = useComponentConfig('Tabs', _props);
+ const {
+ tabs,
+ TabComponent = DefaultTab,
+ TabsActiveIndicatorComponent = DefaultTabsActiveIndicator,
+ activeBackground,
+ activeTab,
+ disabled,
+ onChange,
+ styles,
+ style,
+ role = 'tablist',
+ position = 'relative',
+ alignSelf = 'flex-start',
+ opacity,
+ onActiveTabElementChange,
+ borderRadius,
+ borderTopLeftRadius,
+ borderTopRightRadius,
+ borderBottomLeftRadius,
+ borderBottomRightRadius,
+ testID,
+ ...props
+ } = mergedProps;
+ const tabsContainerRef = useRef(null);
+ useImperativeHandle(ref, () => tabsContainerRef.current as View, []); // merge internal ref to forwarded ref
+
+ const refMap = useRefMap();
+ const api = useTabs({ tabs, activeTab, disabled, onChange });
+
+ const [activeTabRect, setActiveTabRect] = useState(defaultRect);
+ const previousActiveRef = useRef(activeTab);
+
+ const updateActiveTabRect = useCallback(() => {
+ const activeTabRef = activeTab ? refMap.getRef(activeTab.id) : null;
+ if (!activeTabRef || !tabsContainerRef.current) return;
+ activeTabRef.measureLayout(tabsContainerRef.current, (x, y, width, height) =>
+ setActiveTabRect({ x, y, width, height }),
+ );
+ }, [activeTab, refMap]);
+
+ const registerRef = useCallback(
+ (tabId: string, ref: View) => {
+ refMap.registerRef(tabId, ref);
+ if (activeTab?.id === tabId) {
+ onActiveTabElementChange?.(ref);
+ }
+ },
+ [activeTab, onActiveTabElementChange, refMap],
);
- }, [activeTab, refMap]);
-
- const registerRef = useCallback(
- (tabId: string, ref: View) => {
- refMap.registerRef(tabId, ref);
- if (activeTab?.id === tabId) {
- onActiveTabElementChange?.(ref);
- }
- },
- [activeTab, onActiveTabElementChange, refMap],
- );
-
- if (previousActiveRef.current !== activeTab) {
- previousActiveRef.current = activeTab;
- updateActiveTabRect();
- }
-
- return (
-
- }>
-
- {tabs.map(({ id, Component: CustomTabComponent, disabled: tabDisabled, ...props }) => {
- const RenderedTab = CustomTabComponent ?? TabComponent;
- return (
-
-
-
- );
- })}
-
-
- );
- }),
+
+ if (previousActiveRef.current !== activeTab) {
+ previousActiveRef.current = activeTab;
+ updateActiveTabRect();
+ }
+
+ return (
+
+ }>
+
+ {tabs.map(({ id, Component: CustomTabComponent, ...props }) => {
+ const RenderedTab = CustomTabComponent ?? TabComponent;
+ return (
+
+
+
+ );
+ })}
+
+
+ );
+ },
+ ),
);
TabsComponent.displayName = 'Tabs';
@@ -196,6 +220,7 @@ export const TabsActiveIndicator = ({
activeTabRect,
position = 'absolute',
style,
+ testID = 'tabs-active-indicator',
...props
}: TabsActiveIndicatorProps) => {
const previousActiveTabRect = useRef(activeTabRect);
@@ -227,7 +252,7 @@ export const TabsActiveIndicator = ({
position={position}
role="none"
style={[animatedBoxStyle, style]}
- testID="tabs-active-indicator"
+ testID={testID}
{...props}
/>
);
diff --git a/packages/mobile/src/tabs/__stories__/Tabs.stories.tsx b/packages/mobile/src/tabs/__stories__/Tabs.stories.tsx
index 59925fd5d7..bc3d0fa7a6 100644
--- a/packages/mobile/src/tabs/__stories__/Tabs.stories.tsx
+++ b/packages/mobile/src/tabs/__stories__/Tabs.stories.tsx
@@ -1,67 +1,187 @@
-import React, { useState } from 'react';
+import { useCallback, useState } from 'react';
+import { sampleTabs } from '@coinbase/cds-common/internal/data/tabs';
+import type { TabValue } from '@coinbase/cds-common/tabs/useTabs';
import { gutter } from '@coinbase/cds-common/tokens/sizing';
+import { zIndex } from '@coinbase/cds-common/tokens/zIndex';
import { Example, ExampleScreen } from '../../examples/ExampleScreen';
-import { VStack } from '../../layout/VStack';
+import { VStack } from '../../layout';
+import { ThemeProvider } from '../../system/ThemeProvider';
+import { defaultTheme } from '../../themes/defaultTheme';
import { Text } from '../../typography/Text';
-import { TabNavigation, type TabNavigationProps, type TabProps } from '../TabNavigation';
-
-const tabs: TabProps[] = [
- {
- id: 'first_item',
- label: 'First item',
- onPress: console.warn,
- },
- {
- id: 'second_item',
- label: 'Second item',
- },
- {
- id: 'third_item',
- label: 'Third item',
- onPress: console.warn,
- },
- {
- id: 'fourth_item',
- label: 'Fourth item',
- },
- {
- id: 'fifth_item',
- label: 'Fifth item',
- },
+import { DefaultTab, type DefaultTabLabelProps } from '../DefaultTab';
+import { DefaultTabsActiveIndicator } from '../DefaultTabsActiveIndicator';
+import {
+ type TabComponent,
+ Tabs,
+ TabsActiveIndicator,
+ type TabsActiveIndicatorComponent,
+ type TabsActiveIndicatorProps,
+ type TabsProps,
+} from '../Tabs';
+
+type TradingAction = 'buy' | 'sell' | 'convert';
+
+type TabRowWithTestId = TabValue & { testID?: string };
+
+const basicTabs: TabRowWithTestId[] = [
+ { id: 'buy', label: 'Buy', testID: 'buy-tab' },
+ { id: 'sell', label: 'Sell', testID: 'sell-tab' },
+ { id: 'convert', label: 'Convert', testID: 'convert-tab' },
];
-// TODO update once _Tabs_ component is complete
-const TabScreen = () => {
- const [activeTabOne, setActiveTabOne] = useState(tabs[0].id);
+const longTabs = sampleTabs.slice(0, 9);
+
+const tabsWithDisabled = [
+ { id: 'buy', label: 'Buy' },
+ { id: 'sell', label: 'Sell', disabled: true },
+ { id: 'convert', label: 'Convert' },
+];
+
+const typedTabs: TabValue[] = [
+ { id: 'buy', label: 'Buy' },
+ { id: 'sell', label: 'Sell' },
+ { id: 'convert', label: 'Convert' },
+];
+
+type TradingTab = TabValue & DefaultTabLabelProps;
+const tabsWithDotCounts: TradingTab[] = basicTabs.map((tab, index) =>
+ index === 0 ? { ...tab, count: 3, max: 99 } : tab,
+);
+
+const CustomSpringIndicator = (props: TabsActiveIndicatorProps) => (
+
+);
+
+type TabsExampleProps = TabValue> = {
+ title: string;
+ defaultActiveTab: TTab | null;
+ TabComponent?: TabComponent;
+ TabsActiveIndicatorComponent?: TabsActiveIndicatorComponent;
+} & Omit<
+ TabsProps,
+ 'activeTab' | 'onChange' | 'TabComponent' | 'TabsActiveIndicatorComponent'
+>;
+
+const TabsExample = = TabValue>({
+ title,
+ defaultActiveTab,
+ TabComponent = DefaultTab,
+ TabsActiveIndicatorComponent = DefaultTabsActiveIndicator,
+ ...props
+}: TabsExampleProps) => {
+ const [activeTab, setActiveTab] = useState(defaultActiveTab);
+ const handleChange = useCallback((next: TTab | null) => setActiveTab(next), []);
return (
-
-
-
-
- Static preview
-
- {activeTabOne}
-
-
-
-
-
+
+
+ );
+};
+
+const panelTabs = sampleTabs.slice(0, 3);
+
+const TabsWithPanelsExample = () => {
+ const [activeTab, setActiveTab] = useState | null>(panelTabs[0]);
+
+ return (
+
+
+
+ Pair tab buttons with content regions that follow the active tab (see panel below).
+
+
-
- Static preview
-
- {activeTabOne}
-
-
-
-
+ {panelTabs.map((tab) =>
+ activeTab?.id === tab.id ? (
+
+ Panel: {tab.label}
+ Content for this tab.
+
+ ) : null,
+ )}
+
+
);
};
-export default TabScreen;
+const DefaultTabsScreen = () => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+);
+
+export default DefaultTabsScreen;
diff --git a/packages/mobile/src/tabs/__tests__/SegmentedTabs.test.tsx b/packages/mobile/src/tabs/__tests__/SegmentedTabs.test.tsx
index fe02cafb01..95557c4658 100644
--- a/packages/mobile/src/tabs/__tests__/SegmentedTabs.test.tsx
+++ b/packages/mobile/src/tabs/__tests__/SegmentedTabs.test.tsx
@@ -93,7 +93,7 @@ describe('SegmentedTabs', () => {
});
jest.advanceTimersByTime(300);
- expect(screen.getByTestId('tabs-active-indicator')).toHaveAnimatedStyle({
+ expect(screen.getByTestId(`${TEST_ID}-active-indicator`)).toHaveAnimatedStyle({
width: 68,
transform: [{ translateX: 0 }, { translateY: 0 }],
});
@@ -132,7 +132,7 @@ describe('SegmentedTabs', () => {
jest.advanceTimersByTime(300);
- expect(screen.getByTestId('tabs-active-indicator')).toHaveAnimatedStyle({
+ expect(screen.getByTestId(`${TEST_ID}-active-indicator`)).toHaveAnimatedStyle({
width: 68,
transform: [{ translateX: 68 }, { translateY: 0 }],
});
@@ -210,7 +210,7 @@ describe('SegmentedTabs', () => {
jest.advanceTimersByTime(300);
- expect(screen.getByTestId('tabs-active-indicator')).toHaveAnimatedStyle({
+ expect(screen.getByTestId(`${TEST_ID}-active-indicator`)).toHaveAnimatedStyle({
width: 68,
transform: [{ translateX: 20 }, { translateY: 0 }],
});
@@ -243,7 +243,7 @@ describe('SegmentedTabs', () => {
jest.advanceTimersByTime(300);
- expect(screen.getByTestId('tabs-active-indicator')).toHaveAnimatedStyle({
+ expect(screen.getByTestId(`${TEST_ID}-active-indicator`)).toHaveAnimatedStyle({
width: 68,
transform: [{ translateX: 0 }, { translateY: 8 }],
});
@@ -276,7 +276,7 @@ describe('SegmentedTabs', () => {
jest.advanceTimersByTime(300);
- expect(screen.getByTestId('tabs-active-indicator')).toHaveAnimatedStyle({
+ expect(screen.getByTestId(`${TEST_ID}-active-indicator`)).toHaveAnimatedStyle({
width: 68,
transform: [{ translateX: 20 }, { translateY: 8 }],
});
diff --git a/packages/mobile/src/tabs/index.ts b/packages/mobile/src/tabs/index.ts
index 498c1d29a4..4d05fca873 100644
--- a/packages/mobile/src/tabs/index.ts
+++ b/packages/mobile/src/tabs/index.ts
@@ -1,3 +1,5 @@
+export * from './DefaultTab';
+export * from './DefaultTabsActiveIndicator';
export * from './SegmentedTabs';
export * from './TabIndicator';
export * from './TabLabel';
diff --git a/packages/web/CHANGELOG.md b/packages/web/CHANGELOG.md
index 05114d15c1..3c394c4bf0 100644
--- a/packages/web/CHANGELOG.md
+++ b/packages/web/CHANGELOG.md
@@ -8,6 +8,18 @@ All notable changes to this project will be documented in this file.
+## 8.64.0 (4/2/2026 PST)
+
+#### 🚀 Updates
+
+- Added DefaultTab and DefaultTabActiveIndicator and deprecate types used by TabNavigation. [[#558](https://github.com/coinbase/cds/pull/558)]
+
+## 8.63.0 (4/1/2026 PST)
+
+#### 🚀 Updates
+
+- Add type focus to Select. [[#571](https://github.com/coinbase/cds/pull/571)]
+
## 8.62.1 (4/1/2026 PST)
#### 🐞 Fixes
diff --git a/packages/web/jest/setup.js b/packages/web/jest/setup.js
index b99d21bd76..e36e89068d 100644
--- a/packages/web/jest/setup.js
+++ b/packages/web/jest/setup.js
@@ -1,3 +1,32 @@
+/* -------------------------------------------------------------------------- */
+/* @floating-ui/react-dom */
+/* -------------------------------------------------------------------------- */
+jest.mock('@floating-ui/react-dom', () => {
+ const floatingRef = { current: null };
+ return {
+ useFloating: () => ({
+ refs: {
+ setReference: jest.fn(),
+ setFloating: jest.fn((el) => {
+ floatingRef.current = el;
+ }),
+ reference: { current: null },
+ floating: floatingRef,
+ },
+ floatingStyles: {},
+ placement: 'bottom',
+ middlewareData: { arrow: {} },
+ }),
+ // Middleware stubs — these are called as functions and must return a value
+ arrow: () => ({}),
+ autoPlacement: () => ({}),
+ autoUpdate: jest.fn(),
+ flip: () => ({}),
+ offset: () => ({}),
+ shift: () => ({}),
+ };
+});
+
jest.mock('framer-motion', () => ({
...jest.requireActual('framer-motion'),
m: jest.requireActual('framer-motion')?.motion,
diff --git a/packages/web/package.json b/packages/web/package.json
index 1a1baa4478..8e8d469894 100644
--- a/packages/web/package.json
+++ b/packages/web/package.json
@@ -1,6 +1,6 @@
{
"name": "@coinbase/cds-web",
- "version": "8.62.1",
+ "version": "8.64.0",
"description": "Coinbase Design System - Web",
"repository": {
"type": "git",
diff --git a/packages/web/src/alpha/combobox/__tests__/Combobox.test.tsx b/packages/web/src/alpha/combobox/__tests__/Combobox.test.tsx
index a8f474733b..50fa341a13 100644
--- a/packages/web/src/alpha/combobox/__tests__/Combobox.test.tsx
+++ b/packages/web/src/alpha/combobox/__tests__/Combobox.test.tsx
@@ -23,20 +23,6 @@ const defaultProps: ComboboxProps<'single' | 'multi'> = {
label: 'Test Combobox',
};
-// Mock floating-ui to simplify testing
-jest.mock('@floating-ui/react-dom', () => ({
- useFloating: () => ({
- refs: {
- setReference: jest.fn(),
- setFloating: jest.fn(),
- reference: { current: null },
- floating: { current: null },
- },
- floatingStyles: {},
- }),
- flip: () => ({}),
-}));
-
jest.mock('../../../overlays/Portal', () => ({
Portal: ({ children }: { children: React.ReactNode }) => (
{children}
diff --git a/packages/web/src/alpha/select-chip/__tests__/SelectChip.test.tsx b/packages/web/src/alpha/select-chip/__tests__/SelectChip.test.tsx
index 335f24a7c1..cf90e43a67 100644
--- a/packages/web/src/alpha/select-chip/__tests__/SelectChip.test.tsx
+++ b/packages/web/src/alpha/select-chip/__tests__/SelectChip.test.tsx
@@ -8,19 +8,6 @@ import type { SelectOption } from '../../select/types';
import type { SelectChipProps } from '../SelectChip';
import { SelectChip } from '../SelectChip';
-jest.mock('@floating-ui/react-dom', () => ({
- useFloating: () => ({
- refs: {
- setReference: jest.fn(),
- setFloating: jest.fn(),
- reference: { current: null },
- floating: { current: null },
- },
- floatingStyles: {},
- }),
- flip: () => ({}),
-}));
-
jest.mock('../../../overlays/Portal', () => ({
Portal: ({ children, containerId }: { children: React.ReactNode; containerId?: string }) => (
{children}
diff --git a/packages/web/src/alpha/select/DefaultSelectControl.tsx b/packages/web/src/alpha/select/DefaultSelectControl.tsx
index 173b02a2b9..2171d36439 100644
--- a/packages/web/src/alpha/select/DefaultSelectControl.tsx
+++ b/packages/web/src/alpha/select/DefaultSelectControl.tsx
@@ -90,6 +90,7 @@ const DefaultSelectControlComponent = memo(
accessibilityLabel,
ariaHaspopup,
tabIndex = 0,
+ onKeyDown,
styles,
classNames,
...props
@@ -368,6 +369,8 @@ const DefaultSelectControlComponent = memo(
focusable={false}
minWidth={0}
onClick={() => setOpen((s) => !s)}
+ onKeyDown={onKeyDown}
+ paddingStart={1}
role={role}
style={styles?.controlInputNode}
tabIndex={tabIndex}
@@ -432,6 +435,7 @@ const DefaultSelectControlComponent = memo(
styles?.controlStartNode,
styles?.controlValueNode,
tabIndex,
+ onKeyDown,
startNode,
shouldShowCompactLabel,
labelNode,
diff --git a/packages/web/src/alpha/select/DefaultSelectDropdown.tsx b/packages/web/src/alpha/select/DefaultSelectDropdown.tsx
index 538c39f2bd..528b02e566 100644
--- a/packages/web/src/alpha/select/DefaultSelectDropdown.tsx
+++ b/packages/web/src/alpha/select/DefaultSelectDropdown.tsx
@@ -295,6 +295,7 @@ const DefaultSelectDropdownComponent = memo(
>
],
});
+ const pendingTypeAheadKeyRef = useRef(null);
+
+ const handleControlKeyDown = useCallback(
+ (event: React.KeyboardEvent) => {
+ if (disabled || open) return;
+ if (event.ctrlKey || event.metaKey || event.altKey) return;
+
+ const key = event.key;
+ if (/^[a-z]$/.test(key)) {
+ pendingTypeAheadKeyRef.current = key;
+ setOpen(true);
+ }
+ },
+ [disabled, open, setOpen],
+ );
+
+ useEffect(() => {
+ if (!open || !pendingTypeAheadKeyRef.current) return;
+
+ const key = pendingTypeAheadKeyRef.current;
+ pendingTypeAheadKeyRef.current = null;
+
+ const floatingEl = refs.floating.current;
+ if (!floatingEl) return;
+
+ const optionRole = accessibilityRoles?.option ?? 'option';
+ const options = floatingEl.querySelectorAll(`[role="${optionRole}"]`);
+ const matchingOption = Array.from(options).find((option) => {
+ const firstLetterMatch = option.textContent?.match(/[a-z]/i);
+ return firstLetterMatch?.[0]?.toLowerCase() === key;
+ });
+
+ if (matchingOption) {
+ (matchingOption as HTMLElement).focus();
+ }
+ }, [open, refs.floating, accessibilityRoles?.option]);
+
const rootStyles = useMemo(
() => ({
...style,
@@ -274,6 +320,7 @@ const SelectBase = memo(
labelVariant={labelVariant}
maxSelectedOptionsToShow={maxSelectedOptionsToShow}
onChange={onChange}
+ onKeyDown={handleControlKeyDown}
open={open}
options={options}
placeholder={placeholder}
diff --git a/packages/web/src/alpha/select/__stories__/MultiSelect.stories.tsx b/packages/web/src/alpha/select/__stories__/MultiSelect.stories.tsx
index b313f5761d..0d27b8396b 100644
--- a/packages/web/src/alpha/select/__stories__/MultiSelect.stories.tsx
+++ b/packages/web/src/alpha/select/__stories__/MultiSelect.stories.tsx
@@ -40,14 +40,14 @@ const hoveredBackgroundCss = css`
export const Default = () => {
const exampleOptions = [
{ value: null, label: 'Remove selection' },
- { value: '1', label: 'Option 1' },
- { value: '2', label: 'Option 2' },
- { value: '3', label: 'Option 3' },
- { value: '4', label: 'Option 4' },
- { value: '5', label: 'Option 5' },
- { value: '6', label: 'Option 6' },
- { value: '7', label: 'Option 7' },
- { value: '8', label: 'Option 8' },
+ { value: '1', label: 'Apple' },
+ { value: '2', label: 'Banana' },
+ { value: '3', label: 'Cherry' },
+ { value: '4', label: 'Date' },
+ { value: '5', label: 'Elderberry' },
+ { value: '6', label: 'Fig' },
+ { value: '7', label: 'Grape' },
+ { value: '8', label: 'Honeydew' },
];
const { value, onChange } = useMultiSelect({
initialValue: ['1', '2'],
@@ -69,14 +69,14 @@ export const Default = () => {
export const Compact = () => {
const exampleOptions = [
{ value: null, label: 'Remove selection' },
- { value: '1', label: 'Option 1' },
- { value: '2', label: 'Option 2' },
- { value: '3', label: 'Option 3' },
- { value: '4', label: 'Option 4' },
- { value: '5', label: 'Option 5' },
- { value: '6', label: 'Option 6' },
- { value: '7', label: 'Option 7' },
- { value: '8', label: 'Option 8' },
+ { value: '1', label: 'Apple' },
+ { value: '2', label: 'Banana' },
+ { value: '3', label: 'Cherry' },
+ { value: '4', label: 'Date' },
+ { value: '5', label: 'Elderberry' },
+ { value: '6', label: 'Fig' },
+ { value: '7', label: 'Grape' },
+ { value: '8', label: 'Honeydew' },
];
const { value, onChange } = useMultiSelect({
initialValue: ['1', '2'],
@@ -98,10 +98,10 @@ export const Compact = () => {
export const InsideLabelVariant = () => {
const exampleOptions = [
{ value: null, label: 'Remove selection' },
- { value: '1', label: 'Option 1' },
- { value: '2', label: 'Option 2' },
- { value: '3', label: 'Option 3' },
- { value: '4', label: 'Option 4' },
+ { value: '1', label: 'Apple' },
+ { value: '2', label: 'Banana' },
+ { value: '3', label: 'Cherry' },
+ { value: '4', label: 'Date' },
];
const { value, onChange } = useMultiSelect({
initialValue: ['1', '2', '3', '4'],
@@ -123,16 +123,16 @@ export const InsideLabelVariant = () => {
export const CompactManySelected = () => {
const exampleOptions = [
{ value: null, label: 'Remove selection' },
- { value: '1', label: 'Option 1' },
- { value: '2', label: 'Option 2' },
- { value: '3', label: 'Option 3' },
- { value: '4', label: 'Option 4' },
- { value: '5', label: 'Option 5' },
- { value: '6', label: 'Option 6' },
- { value: '7', label: 'Option 7' },
- { value: '8', label: 'Option 8' },
- { value: '9', label: 'Option 9' },
- { value: '10', label: 'Option 10' },
+ { value: '1', label: 'Apple' },
+ { value: '2', label: 'Banana' },
+ { value: '3', label: 'Cherry' },
+ { value: '4', label: 'Date' },
+ { value: '5', label: 'Elderberry' },
+ { value: '6', label: 'Fig' },
+ { value: '7', label: 'Grape' },
+ { value: '8', label: 'Honeydew' },
+ { value: '9', label: 'Jackfruit' },
+ { value: '10', label: 'Kiwi' },
];
const { value, onChange } = useMultiSelect({
initialValue: ['1', '3', '7', '8', '9', '10'],
@@ -154,14 +154,14 @@ export const CompactManySelected = () => {
export const HideSelectAll = () => {
const exampleOptions = [
{ value: null, label: 'Remove selection' },
- { value: '1', label: 'Option 1' },
- { value: '2', label: 'Option 2' },
- { value: '3', label: 'Option 3' },
- { value: '4', label: 'Option 4' },
- { value: '5', label: 'Option 5' },
- { value: '6', label: 'Option 6' },
- { value: '7', label: 'Option 7' },
- { value: '8', label: 'Option 8' },
+ { value: '1', label: 'Apple' },
+ { value: '2', label: 'Banana' },
+ { value: '3', label: 'Cherry' },
+ { value: '4', label: 'Date' },
+ { value: '5', label: 'Elderberry' },
+ { value: '6', label: 'Fig' },
+ { value: '7', label: 'Grape' },
+ { value: '8', label: 'Honeydew' },
];
const { value, onChange } = useMultiSelect({
initialValue: ['1'],
@@ -183,14 +183,14 @@ export const HideSelectAll = () => {
export const Alignments = () => {
const exampleOptions = [
{ value: null, label: 'Remove selection' },
- { value: '1', label: 'Option 1' },
- { value: '2', label: 'Option 2' },
- { value: '3', label: 'Option 3' },
- { value: '4', label: 'Option 4' },
- { value: '5', label: 'Option 5' },
- { value: '6', label: 'Option 6' },
- { value: '7', label: 'Option 7' },
- { value: '8', label: 'Option 8' },
+ { value: '1', label: 'Apple' },
+ { value: '2', label: 'Banana' },
+ { value: '3', label: 'Cherry' },
+ { value: '4', label: 'Date' },
+ { value: '5', label: 'Elderberry' },
+ { value: '6', label: 'Fig' },
+ { value: '7', label: 'Grape' },
+ { value: '8', label: 'Honeydew' },
];
const { value, onChange } = useMultiSelect({
initialValue: ['1'],
@@ -260,14 +260,14 @@ export const Alignments = () => {
export const CustomSelectAllLabel = () => {
const exampleOptions = [
{ value: null, label: 'Remove selection' },
- { value: '1', label: 'Option 1' },
- { value: '2', label: 'Option 2' },
- { value: '3', label: 'Option 3' },
- { value: '4', label: 'Option 4' },
- { value: '5', label: 'Option 5' },
- { value: '6', label: 'Option 6' },
- { value: '7', label: 'Option 7' },
- { value: '8', label: 'Option 8' },
+ { value: '1', label: 'Apple' },
+ { value: '2', label: 'Banana' },
+ { value: '3', label: 'Cherry' },
+ { value: '4', label: 'Date' },
+ { value: '5', label: 'Elderberry' },
+ { value: '6', label: 'Fig' },
+ { value: '7', label: 'Grape' },
+ { value: '8', label: 'Honeydew' },
];
const { value, onChange } = useMultiSelect({
initialValue: ['1'],
@@ -292,14 +292,14 @@ export const CustomClearAllLabel = () => {
});
const exampleOptions = [
{ value: null, label: 'Remove selection' },
- { value: '1', label: 'Option 1' },
- { value: '2', label: 'Option 2' },
- { value: '3', label: 'Option 3' },
- { value: '4', label: 'Option 4' },
- { value: '5', label: 'Option 5' },
- { value: '6', label: 'Option 6' },
- { value: '7', label: 'Option 7' },
- { value: '8', label: 'Option 8' },
+ { value: '1', label: 'Apple' },
+ { value: '2', label: 'Banana' },
+ { value: '3', label: 'Cherry' },
+ { value: '4', label: 'Date' },
+ { value: '5', label: 'Elderberry' },
+ { value: '6', label: 'Fig' },
+ { value: '7', label: 'Grape' },
+ { value: '8', label: 'Honeydew' },
];
return (
@@ -318,14 +318,14 @@ export const CustomClearAllLabel = () => {
export const CustomSelectAllOption = () => {
const exampleOptions = [
{ value: null, label: 'Remove selection' },
- { value: '1', label: 'Option 1' },
- { value: '2', label: 'Option 2' },
- { value: '3', label: 'Option 3' },
- { value: '4', label: 'Option 4' },
- { value: '5', label: 'Option 5' },
- { value: '6', label: 'Option 6' },
- { value: '7', label: 'Option 7' },
- { value: '8', label: 'Option 8' },
+ { value: '1', label: 'Apple' },
+ { value: '2', label: 'Banana' },
+ { value: '3', label: 'Cherry' },
+ { value: '4', label: 'Date' },
+ { value: '5', label: 'Elderberry' },
+ { value: '6', label: 'Fig' },
+ { value: '7', label: 'Grape' },
+ { value: '8', label: 'Honeydew' },
];
const { value, onChange } = useMultiSelect({
initialValue: ['1'],
@@ -399,14 +399,14 @@ export const LongOptionLabels = () => {
export const Disabled = () => {
const exampleOptions = [
{ value: null, label: 'Remove selection' },
- { value: '1', label: 'Option 1' },
- { value: '2', label: 'Option 2' },
- { value: '3', label: 'Option 3' },
- { value: '4', label: 'Option 4' },
- { value: '5', label: 'Option 5' },
- { value: '6', label: 'Option 6' },
- { value: '7', label: 'Option 7' },
- { value: '8', label: 'Option 8' },
+ { value: '1', label: 'Apple' },
+ { value: '2', label: 'Banana' },
+ { value: '3', label: 'Cherry' },
+ { value: '4', label: 'Date' },
+ { value: '5', label: 'Elderberry' },
+ { value: '6', label: 'Fig' },
+ { value: '7', label: 'Grape' },
+ { value: '8', label: 'Honeydew' },
];
const { value, onChange } = useMultiSelect({
initialValue: ['1'],
@@ -437,14 +437,14 @@ Disabled.parameters = {
export const DisabledOptions = () => {
const exampleOptions = [
{ value: null, label: 'Remove selection' },
- { value: '1', label: 'Option 1', disabled: true },
- { value: '2', label: 'Option 2' },
- { value: '3', label: 'Option 3' },
- { value: '4', label: 'Option 4', disabled: true },
- { value: '5', label: 'Option 5' },
- { value: '6', label: 'Option 6', disabled: true },
- { value: '7', label: 'Option 7', disabled: true },
- { value: '8', label: 'Option 8' },
+ { value: '1', label: 'Apple', disabled: true },
+ { value: '2', label: 'Banana' },
+ { value: '3', label: 'Cherry' },
+ { value: '4', label: 'Date', disabled: true },
+ { value: '5', label: 'Elderberry' },
+ { value: '6', label: 'Fig', disabled: true },
+ { value: '7', label: 'Grape', disabled: true },
+ { value: '8', label: 'Honeydew' },
];
const { value, onChange } = useMultiSelect({
initialValue: ['1', '2', '3', '4', '5', '6', '7', '8'],
@@ -465,14 +465,14 @@ export const DisabledOptions = () => {
export const CustomAccessory = () => {
const exampleOptions = [
{ value: null, label: 'Remove selection' },
- { value: '1', label: 'Option 1' },
- { value: '2', label: 'Option 2' },
- { value: '3', label: 'Option 3' },
- { value: '4', label: 'Option 4' },
- { value: '5', label: 'Option 5' },
- { value: '6', label: 'Option 6' },
- { value: '7', label: 'Option 7' },
- { value: '8', label: 'Option 8' },
+ { value: '1', label: 'Apple' },
+ { value: '2', label: 'Banana' },
+ { value: '3', label: 'Cherry' },
+ { value: '4', label: 'Date' },
+ { value: '5', label: 'Elderberry' },
+ { value: '6', label: 'Fig' },
+ { value: '7', label: 'Grape' },
+ { value: '8', label: 'Honeydew' },
];
const { value, onChange } = useMultiSelect({
initialValue: ['1'],
@@ -494,14 +494,14 @@ export const CustomAccessory = () => {
export const CustomMedia = () => {
const exampleOptions = [
{ value: null, label: 'Remove selection' },
- { value: '1', label: 'Option 1' },
- { value: '2', label: 'Option 2' },
- { value: '3', label: 'Option 3' },
- { value: '4', label: 'Option 4' },
- { value: '5', label: 'Option 5' },
- { value: '6', label: 'Option 6' },
- { value: '7', label: 'Option 7' },
- { value: '8', label: 'Option 8' },
+ { value: '1', label: 'Apple' },
+ { value: '2', label: 'Banana' },
+ { value: '3', label: 'Cherry' },
+ { value: '4', label: 'Date' },
+ { value: '5', label: 'Elderberry' },
+ { value: '6', label: 'Fig' },
+ { value: '7', label: 'Grape' },
+ { value: '8', label: 'Honeydew' },
];
const { value, onChange } = useMultiSelect({
initialValue: ['1'],
@@ -589,14 +589,14 @@ export const CustomHiddenSelectedOptionsLabel = () => {
export const Descriptions = () => {
const exampleOptionsWithDescription = [
{ value: null, label: 'Remove selection' },
- { value: '1', label: 'Option 1', description: 'Description 1' },
- { value: '2', label: 'Option 2', description: 'Description 2' },
- { value: '3', label: 'Option 3', description: 'Description 3' },
- { value: '4', label: 'Option 4', description: 'Description 4' },
- { value: '5', label: 'Option 5', description: 'Description 5' },
- { value: '6', label: 'Option 6', description: 'Description 6' },
- { value: '7', label: 'Option 7', description: 'Description 7' },
- { value: '8', label: 'Option 8', description: 'Description 8' },
+ { value: '1', label: 'Apple', description: 'Crisp and sweet' },
+ { value: '2', label: 'Banana', description: 'Bright and yellow' },
+ { value: '3', label: 'Cherry', description: 'Dark and tart' },
+ { value: '4', label: 'Date', description: 'Dense and sweet' },
+ { value: '5', label: 'Elderberry', description: 'Earthy and rich' },
+ { value: '6', label: 'Fig', description: 'Fresh and jammy' },
+ { value: '7', label: 'Grape', description: 'Juicy clusters' },
+ { value: '8', label: 'Honeydew', description: 'Honeyed melon' },
];
const { value, onChange } = useMultiSelect({
initialValue: ['1'],
@@ -617,10 +617,10 @@ export const Descriptions = () => {
export const DescriptionsOnly = () => {
const exampleOptionsWithOnlyDescription = [
{ value: null, label: 'Remove selection' },
- { value: '1', description: 'Description 1' },
- { value: '2', description: 'Description 2' },
- { value: '3', description: 'Description 3' },
- { value: '4', description: 'Description 4' },
+ { value: '1', description: 'A crisp red apple' },
+ { value: '2', description: 'Bright yellow banana' },
+ { value: '3', description: 'Cherry red and tart' },
+ { value: '4', description: 'Date palm fruit' },
];
const { value, onChange } = useMultiSelect({
initialValue: ['1'],
@@ -643,31 +643,31 @@ export const MixedAccessoriesMedia = () => {
{ value: null, label: 'Remove selection' },
{
value: '1',
- label: 'Option 1',
+ label: 'Apple',
accessory: ,
media: ,
},
{
value: '2',
- label: 'Option 2',
+ label: 'Banana',
accessory: ,
media: ,
},
{
value: '3',
- label: 'Option 3',
+ label: 'Cherry',
accessory: ,
media: ,
},
{
value: '4',
- label: 'Option 4',
+ label: 'Date',
accessory: ,
media: ,
},
{
value: '5',
- label: 'Option 5',
+ label: 'Elderberry',
accessory: ,
media: ,
},
@@ -693,31 +693,31 @@ export const AllCombinedFeatures = () => {
{ value: null, label: 'Remove selection' },
{
value: '1',
- label: 'Option 1',
+ label: 'Apple',
accessory: ,
media: ,
},
{
value: '2',
- label: 'Option 2',
+ label: 'Banana',
accessory: ,
media: ,
},
{
value: '3',
- label: 'Option 3',
+ label: 'Cherry',
accessory: ,
media: ,
},
{
value: '4',
- label: 'Option 4',
+ label: 'Date',
accessory: ,
media: ,
},
{
value: '5',
- label: 'Option 5',
+ label: 'Elderberry',
accessory: ,
media: ,
},
@@ -769,14 +769,14 @@ export const EdgeCaseEmptyLabels = () => {
export const ControlledOpen = () => {
const exampleOptions = [
{ value: null, label: 'Remove selection' },
- { value: '1', label: 'Option 1' },
- { value: '2', label: 'Option 2' },
- { value: '3', label: 'Option 3' },
- { value: '4', label: 'Option 4' },
- { value: '5', label: 'Option 5' },
- { value: '6', label: 'Option 6' },
- { value: '7', label: 'Option 7' },
- { value: '8', label: 'Option 8' },
+ { value: '1', label: 'Apple' },
+ { value: '2', label: 'Banana' },
+ { value: '3', label: 'Cherry' },
+ { value: '4', label: 'Date' },
+ { value: '5', label: 'Elderberry' },
+ { value: '6', label: 'Fig' },
+ { value: '7', label: 'Grape' },
+ { value: '8', label: 'Honeydew' },
];
const { value, onChange } = useMultiSelect({
initialValue: ['1'],
@@ -806,14 +806,14 @@ export const ControlledOpen = () => {
export const PositiveVariant = () => {
const exampleOptions = [
{ value: null, label: 'Remove selection' },
- { value: '1', label: 'Option 1' },
- { value: '2', label: 'Option 2' },
- { value: '3', label: 'Option 3' },
- { value: '4', label: 'Option 4' },
- { value: '5', label: 'Option 5' },
- { value: '6', label: 'Option 6' },
- { value: '7', label: 'Option 7' },
- { value: '8', label: 'Option 8' },
+ { value: '1', label: 'Apple' },
+ { value: '2', label: 'Banana' },
+ { value: '3', label: 'Cherry' },
+ { value: '4', label: 'Date' },
+ { value: '5', label: 'Elderberry' },
+ { value: '6', label: 'Fig' },
+ { value: '7', label: 'Grape' },
+ { value: '8', label: 'Honeydew' },
];
const { value, onChange } = useMultiSelect({
initialValue: ['1'],
@@ -836,14 +836,14 @@ export const PositiveVariant = () => {
export const NegativeVariant = () => {
const exampleOptions = [
{ value: null, label: 'Remove selection' },
- { value: '1', label: 'Option 1' },
- { value: '2', label: 'Option 2' },
- { value: '3', label: 'Option 3' },
- { value: '4', label: 'Option 4' },
- { value: '5', label: 'Option 5' },
- { value: '6', label: 'Option 6' },
- { value: '7', label: 'Option 7' },
- { value: '8', label: 'Option 8' },
+ { value: '1', label: 'Apple' },
+ { value: '2', label: 'Banana' },
+ { value: '3', label: 'Cherry' },
+ { value: '4', label: 'Date' },
+ { value: '5', label: 'Elderberry' },
+ { value: '6', label: 'Fig' },
+ { value: '7', label: 'Grape' },
+ { value: '8', label: 'Honeydew' },
];
const { value, onChange } = useMultiSelect({
initialValue: ['1'],
@@ -866,14 +866,14 @@ export const NegativeVariant = () => {
export const StartNode = () => {
const exampleOptions = [
{ value: null, label: 'Remove selection' },
- { value: '1', label: 'Option 1' },
- { value: '2', label: 'Option 2' },
- { value: '3', label: 'Option 3' },
- { value: '4', label: 'Option 4' },
- { value: '5', label: 'Option 5' },
- { value: '6', label: 'Option 6' },
- { value: '7', label: 'Option 7' },
- { value: '8', label: 'Option 8' },
+ { value: '1', label: 'Apple' },
+ { value: '2', label: 'Banana' },
+ { value: '3', label: 'Cherry' },
+ { value: '4', label: 'Date' },
+ { value: '5', label: 'Elderberry' },
+ { value: '6', label: 'Fig' },
+ { value: '7', label: 'Grape' },
+ { value: '8', label: 'Honeydew' },
];
const { value, onChange } = useMultiSelect({
initialValue: ['1'],
@@ -910,14 +910,14 @@ export const EmptyOptions = () => {
export const ComplexStyles = () => {
const exampleOptions = [
{ value: null, label: 'Remove selection' },
- { value: '1', label: 'Option 1' },
- { value: '2', label: 'Option 2' },
- { value: '3', label: 'Option 3' },
- { value: '4', label: 'Option 4' },
- { value: '5', label: 'Option 5' },
- { value: '6', label: 'Option 6' },
- { value: '7', label: 'Option 7' },
- { value: '8', label: 'Option 8' },
+ { value: '1', label: 'Apple' },
+ { value: '2', label: 'Banana' },
+ { value: '3', label: 'Cherry' },
+ { value: '4', label: 'Date' },
+ { value: '5', label: 'Elderberry' },
+ { value: '6', label: 'Fig' },
+ { value: '7', label: 'Grape' },
+ { value: '8', label: 'Honeydew' },
];
const { value, onChange } = useMultiSelect({
initialValue: ['1', '2'],
diff --git a/packages/web/src/alpha/select/__stories__/Select.stories.tsx b/packages/web/src/alpha/select/__stories__/Select.stories.tsx
index f11d62b569..9f32e38a3f 100644
--- a/packages/web/src/alpha/select/__stories__/Select.stories.tsx
+++ b/packages/web/src/alpha/select/__stories__/Select.stories.tsx
@@ -51,15 +51,15 @@ const hoveredBackgroundCss = css`
export const Default = () => {
const exampleOptions = [
{ value: null, label: 'Remove selection' },
- { value: '1', label: 'Option 1' },
- { value: '2', label: 'Option 2' },
- { value: '3', label: 'Option 3' },
- { value: '4', label: 'Option 4' },
- { value: '5', label: 'Option 5' },
- { value: '6', label: 'Option 6' },
- { value: '7', label: 'Option 7' },
- { value: '8', label: 'Option 8' },
- { value: '9', label: 'Option 9' },
+ { value: '1', label: 'Apple' },
+ { value: '2', label: 'Banana' },
+ { value: '3', label: 'Cherry' },
+ { value: '4', label: 'Date' },
+ { value: '5', label: 'Elderberry' },
+ { value: '6', label: 'Fig' },
+ { value: '7', label: 'Grape' },
+ { value: '8', label: 'Honeydew' },
+ { value: '9', label: 'Jackfruit' },
];
const [value, setValue] = useState('1');
@@ -77,15 +77,15 @@ export const Default = () => {
export const Compact = () => {
const exampleOptions = [
{ value: null, label: 'Remove selection' },
- { value: '1', label: 'Option 1' },
- { value: '2', label: 'Option 2' },
- { value: '3', label: 'Option 3' },
- { value: '4', label: 'Option 4' },
- { value: '5', label: 'Option 5' },
- { value: '6', label: 'Option 6' },
- { value: '7', label: 'Option 7' },
- { value: '8', label: 'Option 8' },
- { value: '9', label: 'Option 9' },
+ { value: '1', label: 'Apple' },
+ { value: '2', label: 'Banana' },
+ { value: '3', label: 'Cherry' },
+ { value: '4', label: 'Date' },
+ { value: '5', label: 'Elderberry' },
+ { value: '6', label: 'Fig' },
+ { value: '7', label: 'Grape' },
+ { value: '8', label: 'Honeydew' },
+ { value: '9', label: 'Jackfruit' },
];
const [value, setValue] = useState('1');
@@ -104,15 +104,15 @@ export const Compact = () => {
export const LabelVariant = () => {
const exampleOptions = [
{ value: null, label: 'Remove selection' },
- { value: '1', label: 'Option 1' },
- { value: '2', label: 'Option 2' },
- { value: '3', label: 'Option 3' },
- { value: '4', label: 'Option 4' },
- { value: '5', label: 'Option 5' },
- { value: '6', label: 'Option 6' },
- { value: '7', label: 'Option 7' },
- { value: '8', label: 'Option 8' },
- { value: '9', label: 'Option 9' },
+ { value: '1', label: 'Apple' },
+ { value: '2', label: 'Banana' },
+ { value: '3', label: 'Cherry' },
+ { value: '4', label: 'Date' },
+ { value: '5', label: 'Elderberry' },
+ { value: '6', label: 'Fig' },
+ { value: '7', label: 'Grape' },
+ { value: '8', label: 'Honeydew' },
+ { value: '9', label: 'Jackfruit' },
];
const [value, setValue] = useState('1');
return (
@@ -130,15 +130,15 @@ export const LabelVariant = () => {
export const ExampleForm = () => {
const exampleOptions = [
{ value: null, label: 'Remove selection' },
- { value: '1', label: 'Option 1' },
- { value: '2', label: 'Option 2' },
- { value: '3', label: 'Option 3' },
- { value: '4', label: 'Option 4' },
- { value: '5', label: 'Option 5' },
- { value: '6', label: 'Option 6' },
- { value: '7', label: 'Option 7' },
- { value: '8', label: 'Option 8' },
- { value: '9', label: 'Option 9' },
+ { value: '1', label: 'Apple' },
+ { value: '2', label: 'Banana' },
+ { value: '3', label: 'Cherry' },
+ { value: '4', label: 'Date' },
+ { value: '5', label: 'Elderberry' },
+ { value: '6', label: 'Fig' },
+ { value: '7', label: 'Grape' },
+ { value: '8', label: 'Honeydew' },
+ { value: '9', label: 'Jackfruit' },
];
const [value, setValue] = useState('1');
const { value: multiSelectValue, onChange: multiSelectOnChange } = useMultiSelect({
@@ -204,15 +204,15 @@ export const ExampleForm = () => {
export const HelperText = () => {
const exampleOptions = [
{ value: null, label: 'Remove selection' },
- { value: '1', label: 'Option 1' },
- { value: '2', label: 'Option 2' },
- { value: '3', label: 'Option 3' },
- { value: '4', label: 'Option 4' },
- { value: '5', label: 'Option 5' },
- { value: '6', label: 'Option 6' },
- { value: '7', label: 'Option 7' },
- { value: '8', label: 'Option 8' },
- { value: '9', label: 'Option 9' },
+ { value: '1', label: 'Apple' },
+ { value: '2', label: 'Banana' },
+ { value: '3', label: 'Cherry' },
+ { value: '4', label: 'Date' },
+ { value: '5', label: 'Elderberry' },
+ { value: '6', label: 'Fig' },
+ { value: '7', label: 'Grape' },
+ { value: '8', label: 'Honeydew' },
+ { value: '9', label: 'Jackfruit' },
];
const [value, setValue] = useState('1');
@@ -231,14 +231,14 @@ export const HelperText = () => {
export const Description = () => {
const exampleOptionsWithDescription = [
{ value: null, label: 'Remove selection' },
- { value: '1', label: 'Option 1', description: 'Description 1' },
- { value: '2', label: 'Option 2', description: 'Description 2' },
- { value: '3', label: 'Option 3', description: 'Description 3' },
- { value: '4', label: 'Option 4', description: 'Description 4' },
- { value: '5', label: 'Option 5', description: 'Description 5' },
- { value: '6', label: 'Option 6', description: 'Description 6' },
- { value: '7', label: 'Option 7', description: 'Description 7' },
- { value: '8', label: 'Option 8', description: 'Description 8' },
+ { value: '1', label: 'Apple', description: 'Crisp and sweet' },
+ { value: '2', label: 'Banana', description: 'Bright and yellow' },
+ { value: '3', label: 'Cherry', description: 'Dark and tart' },
+ { value: '4', label: 'Date', description: 'Dense and sweet' },
+ { value: '5', label: 'Elderberry', description: 'Earthy and rich' },
+ { value: '6', label: 'Fig', description: 'Fresh and jammy' },
+ { value: '7', label: 'Grape', description: 'Juicy clusters' },
+ { value: '8', label: 'Honeydew', description: 'Honeyed melon' },
];
const [value, setValue] = useState('1');
@@ -256,10 +256,10 @@ export const Description = () => {
export const OnlyDescription = () => {
const exampleOptionsWithOnlyDescription = [
{ value: null, label: 'Remove selection' },
- { value: '1', description: 'Description 1' },
- { value: '2', description: 'Description 2' },
- { value: '3', description: 'Description 3' },
- { value: '4', description: 'Description 4' },
+ { value: '1', description: 'A crisp red apple' },
+ { value: '2', description: 'Bright yellow banana' },
+ { value: '3', description: 'Cherry red and tart' },
+ { value: '4', description: 'Date palm fruit' },
];
const [value, setValue] = useState('1');
@@ -277,15 +277,15 @@ export const OnlyDescription = () => {
export const AccessibilityLabel = () => {
const exampleOptions = [
{ value: null, label: 'Remove selection' },
- { value: '1', label: 'Option 1' },
- { value: '2', label: 'Option 2' },
- { value: '3', label: 'Option 3' },
- { value: '4', label: 'Option 4' },
- { value: '5', label: 'Option 5' },
- { value: '6', label: 'Option 6' },
- { value: '7', label: 'Option 7' },
- { value: '8', label: 'Option 8' },
- { value: '9', label: 'Option 9' },
+ { value: '1', label: 'Apple' },
+ { value: '2', label: 'Banana' },
+ { value: '3', label: 'Cherry' },
+ { value: '4', label: 'Date' },
+ { value: '5', label: 'Elderberry' },
+ { value: '6', label: 'Fig' },
+ { value: '7', label: 'Grape' },
+ { value: '8', label: 'Honeydew' },
+ { value: '9', label: 'Jackfruit' },
];
const [value, setValue] = useState('1');
@@ -305,15 +305,15 @@ export const AccessibilityLabel = () => {
export const AccessibilityRoles = () => {
const exampleOptions = [
{ value: null, label: 'Remove selection' },
- { value: '1', label: 'Option 1' },
- { value: '2', label: 'Option 2' },
- { value: '3', label: 'Option 3' },
- { value: '4', label: 'Option 4' },
- { value: '5', label: 'Option 5' },
- { value: '6', label: 'Option 6' },
- { value: '7', label: 'Option 7' },
- { value: '8', label: 'Option 8' },
- { value: '9', label: 'Option 9' },
+ { value: '1', label: 'Apple' },
+ { value: '2', label: 'Banana' },
+ { value: '3', label: 'Cherry' },
+ { value: '4', label: 'Date' },
+ { value: '5', label: 'Elderberry' },
+ { value: '6', label: 'Fig' },
+ { value: '7', label: 'Grape' },
+ { value: '8', label: 'Honeydew' },
+ { value: '9', label: 'Jackfruit' },
];
const [value, setValue] = useState('1');
@@ -332,9 +332,9 @@ export const AccessibilityRoles = () => {
export const Alignments = () => {
const exampleOptions = [
{ value: null, label: 'Remove selection' },
- { value: '1', label: 'Option 1' },
- { value: '2', label: 'Option 2' },
- { value: '3', label: 'Option 3' },
+ { value: '1', label: 'Apple' },
+ { value: '2', label: 'Banana' },
+ { value: '3', label: 'Cherry' },
];
const [value, setValue] = useState('1');
@@ -396,15 +396,15 @@ export const Alignments = () => {
export const NoLabel = () => {
const exampleOptions = [
{ value: null, label: 'Remove selection' },
- { value: '1', label: 'Option 1' },
- { value: '2', label: 'Option 2' },
- { value: '3', label: 'Option 3' },
- { value: '4', label: 'Option 4' },
- { value: '5', label: 'Option 5' },
- { value: '6', label: 'Option 6' },
- { value: '7', label: 'Option 7' },
- { value: '8', label: 'Option 8' },
- { value: '9', label: 'Option 9' },
+ { value: '1', label: 'Apple' },
+ { value: '2', label: 'Banana' },
+ { value: '3', label: 'Cherry' },
+ { value: '4', label: 'Date' },
+ { value: '5', label: 'Elderberry' },
+ { value: '6', label: 'Fig' },
+ { value: '7', label: 'Grape' },
+ { value: '8', label: 'Honeydew' },
+ { value: '9', label: 'Jackfruit' },
];
const [value, setValue] = useState('1');
@@ -422,14 +422,14 @@ export const NoLabel = () => {
export const Disabled = () => {
const exampleOptionsWithDescription = [
{ value: null, label: 'Remove selection' },
- { value: '1', label: 'Option 1', description: 'Description 1' },
- { value: '2', label: 'Option 2', description: 'Description 2' },
- { value: '3', label: 'Option 3', description: 'Description 3' },
- { value: '4', label: 'Option 4', description: 'Description 4' },
- { value: '5', label: 'Option 5', description: 'Description 5' },
- { value: '6', label: 'Option 6', description: 'Description 6' },
- { value: '7', label: 'Option 7', description: 'Description 7' },
- { value: '8', label: 'Option 8', description: 'Description 8' },
+ { value: '1', label: 'Apple', description: 'Crisp and sweet' },
+ { value: '2', label: 'Banana', description: 'Bright and yellow' },
+ { value: '3', label: 'Cherry', description: 'Dark and tart' },
+ { value: '4', label: 'Date', description: 'Dense and sweet' },
+ { value: '5', label: 'Elderberry', description: 'Earthy and rich' },
+ { value: '6', label: 'Fig', description: 'Fresh and jammy' },
+ { value: '7', label: 'Grape', description: 'Juicy clusters' },
+ { value: '8', label: 'Honeydew', description: 'Honeyed melon' },
];
const [value, setValue] = useState('1');
@@ -463,10 +463,10 @@ Disabled.parameters = {
export const DisabledOptions = () => {
const exampleOptionsWithSomeDisabled = [
{ value: null, label: 'Remove selection' },
- { value: '1', label: 'Option 1', disabled: true },
- { value: '2', label: 'Option 2' },
- { value: '3', label: 'Option 3' },
- { value: '4', label: 'Option 4', disabled: true },
+ { value: '1', label: 'Apple', disabled: true },
+ { value: '2', label: 'Banana' },
+ { value: '3', label: 'Cherry' },
+ { value: '4', label: 'Date', disabled: true },
];
const [value, setValue] = useState('1');
@@ -483,14 +483,14 @@ export const DisabledOptions = () => {
export const WithoutNull = () => {
const exampleOptionsWithoutNull = [
- { value: '1', label: 'Option 1' },
- { value: '2', label: 'Option 2' },
- { value: '3', label: 'Option 3' },
- { value: '4', label: 'Option 4' },
- { value: '5', label: 'Option 5' },
- { value: '6', label: 'Option 6' },
- { value: '7', label: 'Option 7' },
- { value: '8', label: 'Option 8' },
+ { value: '1', label: 'Apple' },
+ { value: '2', label: 'Banana' },
+ { value: '3', label: 'Cherry' },
+ { value: '4', label: 'Date' },
+ { value: '5', label: 'Elderberry' },
+ { value: '6', label: 'Fig' },
+ { value: '7', label: 'Grape' },
+ { value: '8', label: 'Honeydew' },
];
const [value, setValue] = useState(null);
@@ -509,28 +509,28 @@ export const OptionsAsReactNodes = () => {
const exampleOptionsWithReactNodes = [
{
value: '1',
- label: Option 1,
- description: Description 1,
+ label: Apple,
+ description: Crisp and sweet,
},
{
value: '2',
- label: 'Option 2',
+ label: 'Banana',
description: 'Not a react node',
},
{
value: '3',
- label: Option 3,
- description: Description 3,
+ label: Cherry,
+ description: Dark and tart,
},
{
value: '4',
- label: 'Option 4',
+ label: 'Date',
description: 'Not a react node',
},
{
value: '5',
- label: Option 5,
- description: Description 5,
+ label: Elderberry,
+ description: Earthy and rich,
},
];
const [value, setValue] = useState('1');
@@ -561,10 +561,10 @@ export const MixedDefaultAndCustomComponentOptions = () => {
};
const exampleOptions = [
{ value: null, label: 'Remove selection' },
- { value: '1', label: 'Option 1', Component: CustomOptionComponent },
- { value: '2', label: 'Option 2' },
- { value: '3', label: 'Option 3', Component: CustomOptionComponent },
- { value: '4', label: 'Option 4' },
+ { value: '1', label: 'Apple', Component: CustomOptionComponent },
+ { value: '2', label: 'Banana' },
+ { value: '3', label: 'Cherry', Component: CustomOptionComponent },
+ { value: '4', label: 'Date' },
];
const [value, setValue] = useState('1');
@@ -583,15 +583,15 @@ export const MixedDefaultAndCustomComponentOptions = () => {
export const StartNode = () => {
const exampleOptions = [
{ value: null, label: 'Remove selection' },
- { value: '1', label: 'Option 1' },
- { value: '2', label: 'Option 2' },
- { value: '3', label: 'Option 3' },
- { value: '4', label: 'Option 4' },
- { value: '5', label: 'Option 5' },
- { value: '6', label: 'Option 6' },
- { value: '7', label: 'Option 7' },
- { value: '8', label: 'Option 8' },
- { value: '9', label: 'Option 9' },
+ { value: '1', label: 'Apple' },
+ { value: '2', label: 'Banana' },
+ { value: '3', label: 'Cherry' },
+ { value: '4', label: 'Date' },
+ { value: '5', label: 'Elderberry' },
+ { value: '6', label: 'Fig' },
+ { value: '7', label: 'Grape' },
+ { value: '8', label: 'Honeydew' },
+ { value: '9', label: 'Jackfruit' },
];
const [value, setValue] = useState('1');
@@ -610,15 +610,15 @@ export const StartNode = () => {
export const CustomEndNode = () => {
const exampleOptions = [
{ value: null, label: 'Remove selection' },
- { value: '1', label: 'Option 1' },
- { value: '2', label: 'Option 2' },
- { value: '3', label: 'Option 3' },
- { value: '4', label: 'Option 4' },
- { value: '5', label: 'Option 5' },
- { value: '6', label: 'Option 6' },
- { value: '7', label: 'Option 7' },
- { value: '8', label: 'Option 8' },
- { value: '9', label: 'Option 9' },
+ { value: '1', label: 'Apple' },
+ { value: '2', label: 'Banana' },
+ { value: '3', label: 'Cherry' },
+ { value: '4', label: 'Date' },
+ { value: '5', label: 'Elderberry' },
+ { value: '6', label: 'Fig' },
+ { value: '7', label: 'Grape' },
+ { value: '8', label: 'Honeydew' },
+ { value: '9', label: 'Jackfruit' },
];
const [value, setValue] = useState('1');
@@ -637,15 +637,15 @@ export const CustomEndNode = () => {
export const CustomAccessory = () => {
const exampleOptions = [
{ value: null, label: 'Remove selection' },
- { value: '1', label: 'Option 1' },
- { value: '2', label: 'Option 2' },
- { value: '3', label: 'Option 3' },
- { value: '4', label: 'Option 4' },
- { value: '5', label: 'Option 5' },
- { value: '6', label: 'Option 6' },
- { value: '7', label: 'Option 7' },
- { value: '8', label: 'Option 8' },
- { value: '9', label: 'Option 9' },
+ { value: '1', label: 'Apple' },
+ { value: '2', label: 'Banana' },
+ { value: '3', label: 'Cherry' },
+ { value: '4', label: 'Date' },
+ { value: '5', label: 'Elderberry' },
+ { value: '6', label: 'Fig' },
+ { value: '7', label: 'Grape' },
+ { value: '8', label: 'Honeydew' },
+ { value: '9', label: 'Jackfruit' },
];
const [value, setValue] = useState('1');
@@ -664,15 +664,15 @@ export const CustomAccessory = () => {
export const CustomMedia = () => {
const exampleOptions = [
{ value: null, label: 'Remove selection' },
- { value: '1', label: 'Option 1' },
- { value: '2', label: 'Option 2' },
- { value: '3', label: 'Option 3' },
- { value: '4', label: 'Option 4' },
- { value: '5', label: 'Option 5' },
- { value: '6', label: 'Option 6' },
- { value: '7', label: 'Option 7' },
- { value: '8', label: 'Option 8' },
- { value: '9', label: 'Option 9' },
+ { value: '1', label: 'Apple' },
+ { value: '2', label: 'Banana' },
+ { value: '3', label: 'Cherry' },
+ { value: '4', label: 'Date' },
+ { value: '5', label: 'Elderberry' },
+ { value: '6', label: 'Fig' },
+ { value: '7', label: 'Grape' },
+ { value: '8', label: 'Honeydew' },
+ { value: '9', label: 'Jackfruit' },
];
const [value, setValue] = useState('1');
@@ -692,31 +692,31 @@ export const UniqueAccessoryAndMedia = () => {
const exampleOptionsWithCustomAccessoriesAndMedia = [
{
value: '1',
- label: 'Option 1',
+ label: 'Apple',
accessory: ,
media: ,
},
{
value: '2',
- label: 'Option 2',
+ label: 'Banana',
accessory: ,
media: ,
},
{
value: '3',
- label: 'Option 3',
+ label: 'Cherry',
accessory: ,
media: ,
},
{
value: '4',
- label: 'Option 4',
+ label: 'Date',
accessory: ,
media: ,
},
{
value: '5',
- label: 'Option 5',
+ label: 'Elderberry',
accessory: ,
media: ,
},
@@ -737,9 +737,9 @@ export const UniqueAccessoryAndMedia = () => {
export const UniqueEndNodeForEachOption = () => {
const exampleOptions = [
{ value: null, label: 'Remove selection' },
- { value: '1', label: 'Option 1', end: },
- { value: '2', label: 'Option 2', end: },
- { value: '3', label: 'Option 3', end: },
+ { value: '1', label: 'Apple', end: },
+ { value: '2', label: 'Banana', end: },
+ { value: '3', label: 'Cherry', end: },
];
const [value, setValue] = useState('1');
@@ -757,15 +757,15 @@ export const UniqueEndNodeForEachOption = () => {
export const PositiveVariant = () => {
const exampleOptions = [
{ value: null, label: 'Remove selection' },
- { value: '1', label: 'Option 1' },
- { value: '2', label: 'Option 2' },
- { value: '3', label: 'Option 3' },
- { value: '4', label: 'Option 4' },
- { value: '5', label: 'Option 5' },
- { value: '6', label: 'Option 6' },
- { value: '7', label: 'Option 7' },
- { value: '8', label: 'Option 8' },
- { value: '9', label: 'Option 9' },
+ { value: '1', label: 'Apple' },
+ { value: '2', label: 'Banana' },
+ { value: '3', label: 'Cherry' },
+ { value: '4', label: 'Date' },
+ { value: '5', label: 'Elderberry' },
+ { value: '6', label: 'Fig' },
+ { value: '7', label: 'Grape' },
+ { value: '8', label: 'Honeydew' },
+ { value: '9', label: 'Jackfruit' },
];
const [value, setValue] = useState('1');
@@ -785,15 +785,15 @@ export const PositiveVariant = () => {
export const NegativeVariant = () => {
const exampleOptions = [
{ value: null, label: 'Remove selection' },
- { value: '1', label: 'Option 1' },
- { value: '2', label: 'Option 2' },
- { value: '3', label: 'Option 3' },
- { value: '4', label: 'Option 4' },
- { value: '5', label: 'Option 5' },
- { value: '6', label: 'Option 6' },
- { value: '7', label: 'Option 7' },
- { value: '8', label: 'Option 8' },
- { value: '9', label: 'Option 9' },
+ { value: '1', label: 'Apple' },
+ { value: '2', label: 'Banana' },
+ { value: '3', label: 'Cherry' },
+ { value: '4', label: 'Date' },
+ { value: '5', label: 'Elderberry' },
+ { value: '6', label: 'Fig' },
+ { value: '7', label: 'Grape' },
+ { value: '8', label: 'Honeydew' },
+ { value: '9', label: 'Jackfruit' },
];
const [value, setValue] = useState('1');
@@ -813,15 +813,15 @@ export const NegativeVariant = () => {
export const CustomStyles = () => {
const exampleOptions = [
{ value: null, label: 'Remove selection' },
- { value: '1', label: 'Option 1' },
- { value: '2', label: 'Option 2' },
- { value: '3', label: 'Option 3' },
- { value: '4', label: 'Option 4' },
- { value: '5', label: 'Option 5' },
- { value: '6', label: 'Option 6' },
- { value: '7', label: 'Option 7' },
- { value: '8', label: 'Option 8' },
- { value: '9', label: 'Option 9' },
+ { value: '1', label: 'Apple' },
+ { value: '2', label: 'Banana' },
+ { value: '3', label: 'Cherry' },
+ { value: '4', label: 'Date' },
+ { value: '5', label: 'Elderberry' },
+ { value: '6', label: 'Fig' },
+ { value: '7', label: 'Grape' },
+ { value: '8', label: 'Honeydew' },
+ { value: '9', label: 'Jackfruit' },
];
const [value, setValue] = useState('1');
@@ -858,15 +858,15 @@ export const CustomStyles = () => {
export const CustomClassNames = () => {
const exampleOptions = [
{ value: null, label: 'Remove selection' },
- { value: '1', label: 'Option 1' },
- { value: '2', label: 'Option 2' },
- { value: '3', label: 'Option 3' },
- { value: '4', label: 'Option 4' },
- { value: '5', label: 'Option 5' },
- { value: '6', label: 'Option 6' },
- { value: '7', label: 'Option 7' },
- { value: '8', label: 'Option 8' },
- { value: '9', label: 'Option 9' },
+ { value: '1', label: 'Apple' },
+ { value: '2', label: 'Banana' },
+ { value: '3', label: 'Cherry' },
+ { value: '4', label: 'Date' },
+ { value: '5', label: 'Elderberry' },
+ { value: '6', label: 'Fig' },
+ { value: '7', label: 'Grape' },
+ { value: '8', label: 'Honeydew' },
+ { value: '9', label: 'Jackfruit' },
];
const [value, setValue] = useState('1');
@@ -890,15 +890,15 @@ export const Typed = () => {
const typedOptions: SelectOption[] = [
{ value: null, label: 'Remove selection' },
- { value: '1', label: 'Option 1' },
- { value: '2', label: 'Option 2' },
- { value: '3', label: 'Option 3' },
- { value: '4', label: 'Option 4' },
- { value: '5', label: 'Option 5' },
- { value: '6', label: 'Option 6' },
- { value: '7', label: 'Option 7' },
- { value: '8', label: 'Option 8' },
- { value: '9', label: 'Option 9' },
+ { value: '1', label: 'Apple' },
+ { value: '2', label: 'Banana' },
+ { value: '3', label: 'Cherry' },
+ { value: '4', label: 'Date' },
+ { value: '5', label: 'Elderberry' },
+ { value: '6', label: 'Fig' },
+ { value: '7', label: 'Grape' },
+ { value: '8', label: 'Honeydew' },
+ { value: '9', label: 'Jackfruit' },
];
const [value, setValue] = useState('1');
@@ -916,15 +916,15 @@ export const Typed = () => {
export const DefaultOpen = () => {
const exampleOptions = [
{ value: null, label: 'Remove selection' },
- { value: '1', label: 'Option 1' },
- { value: '2', label: 'Option 2' },
- { value: '3', label: 'Option 3' },
- { value: '4', label: 'Option 4' },
- { value: '5', label: 'Option 5' },
- { value: '6', label: 'Option 6' },
- { value: '7', label: 'Option 7' },
- { value: '8', label: 'Option 8' },
- { value: '9', label: 'Option 9' },
+ { value: '1', label: 'Apple' },
+ { value: '2', label: 'Banana' },
+ { value: '3', label: 'Cherry' },
+ { value: '4', label: 'Date' },
+ { value: '5', label: 'Elderberry' },
+ { value: '6', label: 'Fig' },
+ { value: '7', label: 'Grape' },
+ { value: '8', label: 'Honeydew' },
+ { value: '9', label: 'Jackfruit' },
];
const [value, setValue] = useState('1');
@@ -952,15 +952,15 @@ DefaultOpen.parameters = {
export const DisabledClickOutsideClose = () => {
const exampleOptions = [
{ value: null, label: 'Remove selection' },
- { value: '1', label: 'Option 1' },
- { value: '2', label: 'Option 2' },
- { value: '3', label: 'Option 3' },
- { value: '4', label: 'Option 4' },
- { value: '5', label: 'Option 5' },
- { value: '6', label: 'Option 6' },
- { value: '7', label: 'Option 7' },
- { value: '8', label: 'Option 8' },
- { value: '9', label: 'Option 9' },
+ { value: '1', label: 'Apple' },
+ { value: '2', label: 'Banana' },
+ { value: '3', label: 'Cherry' },
+ { value: '4', label: 'Date' },
+ { value: '5', label: 'Elderberry' },
+ { value: '6', label: 'Fig' },
+ { value: '7', label: 'Grape' },
+ { value: '8', label: 'Honeydew' },
+ { value: '9', label: 'Jackfruit' },
];
const [value, setValue] = useState('1');
@@ -979,15 +979,15 @@ export const DisabledClickOutsideClose = () => {
export const ControlledOpen = () => {
const exampleOptions = [
{ value: null, label: 'Remove selection' },
- { value: '1', label: 'Option 1' },
- { value: '2', label: 'Option 2' },
- { value: '3', label: 'Option 3' },
- { value: '4', label: 'Option 4' },
- { value: '5', label: 'Option 5' },
- { value: '6', label: 'Option 6' },
- { value: '7', label: 'Option 7' },
- { value: '8', label: 'Option 8' },
- { value: '9', label: 'Option 9' },
+ { value: '1', label: 'Apple' },
+ { value: '2', label: 'Banana' },
+ { value: '3', label: 'Cherry' },
+ { value: '4', label: 'Date' },
+ { value: '5', label: 'Elderberry' },
+ { value: '6', label: 'Fig' },
+ { value: '7', label: 'Grape' },
+ { value: '8', label: 'Honeydew' },
+ { value: '9', label: 'Jackfruit' },
];
const [value, setValue] = useState('1');
const [open, setOpen] = useState(false);
@@ -1084,14 +1084,12 @@ export const VeryLongLabels = () => {
},
{
value: '4',
- label: 'A moderately long label that is somewhere between short and extremely long',
- description:
- 'A moderately long description that is somewhere between short and extremely long',
+ label: 'Moderately long label that is somewhere between short and extremely long',
+ description: 'Moderately long description that is somewhere between short and extremely long',
},
{
value: '5',
- description:
- 'This is a very long description that is somewhere between short and extremely long',
+ description: 'Distinctly long description that is somewhere between short and extremely long',
},
];
const [value, setValue] = useState('1');
@@ -1136,11 +1134,11 @@ export const VeryLongLabels = () => {
export const MixedOptionsWithAndWithoutDescriptions = () => {
const mixedOptions = [
{ value: null, label: 'Remove selection' },
- { value: '1', label: 'Option 1', description: 'Has description' },
- { value: '2', label: 'Option 2' },
- { value: '3', label: 'Option 3', description: 'Also has description' },
- { value: '4', label: 'Option 4' },
- { value: '5', label: 'Option 5', description: 'Another description' },
+ { value: '1', label: 'Apple', description: 'Has description' },
+ { value: '2', label: 'Banana' },
+ { value: '3', label: 'Cherry', description: 'Also has description' },
+ { value: '4', label: 'Date' },
+ { value: '5', label: 'Elderberry', description: 'Another description' },
];
const [value, setValue] = useState('1');
@@ -1159,17 +1157,17 @@ export const OptionsWithOnlyAccessory = () => {
const accessoryOnlyOptions = [
{
value: '1',
- label: 'Option 1',
+ label: 'Apple',
accessory: ,
},
{
value: '2',
- label: 'Option 2',
+ label: 'Banana',
accessory: ,
},
{
value: '3',
- label: 'Option 3',
+ label: 'Cherry',
accessory: ,
},
];
@@ -1190,17 +1188,17 @@ export const OptionsWithOnlyMedia = () => {
const mediaOnlyOptions = [
{
value: '1',
- label: 'Option 1',
+ label: 'Apple',
media: ,
},
{
value: '2',
- label: 'Option 2',
+ label: 'Banana',
media: ,
},
{
value: '3',
- label: 'Option 3',
+ label: 'Cherry',
media: ,
},
];
@@ -1220,15 +1218,15 @@ export const OptionsWithOnlyMedia = () => {
export const CompactWithVariants = () => {
const exampleOptions = [
{ value: null, label: 'Remove selection' },
- { value: '1', label: 'Option 1' },
- { value: '2', label: 'Option 2' },
- { value: '3', label: 'Option 3' },
- { value: '4', label: 'Option 4' },
- { value: '5', label: 'Option 5' },
- { value: '6', label: 'Option 6' },
- { value: '7', label: 'Option 7' },
- { value: '8', label: 'Option 8' },
- { value: '9', label: 'Option 9' },
+ { value: '1', label: 'Apple' },
+ { value: '2', label: 'Banana' },
+ { value: '3', label: 'Cherry' },
+ { value: '4', label: 'Date' },
+ { value: '5', label: 'Elderberry' },
+ { value: '6', label: 'Fig' },
+ { value: '7', label: 'Grape' },
+ { value: '8', label: 'Honeydew' },
+ { value: '9', label: 'Jackfruit' },
];
const [positiveValue, setPositiveValue] = useState('1');
const [negativeValue, setNegativeValue] = useState('2');
@@ -1262,15 +1260,15 @@ export const CompactWithVariants = () => {
export const DisabledWithVariants = () => {
const exampleOptions = [
{ value: null, label: 'Remove selection' },
- { value: '1', label: 'Option 1' },
- { value: '2', label: 'Option 2' },
- { value: '3', label: 'Option 3' },
- { value: '4', label: 'Option 4' },
- { value: '5', label: 'Option 5' },
- { value: '6', label: 'Option 6' },
- { value: '7', label: 'Option 7' },
- { value: '8', label: 'Option 8' },
- { value: '9', label: 'Option 9' },
+ { value: '1', label: 'Apple' },
+ { value: '2', label: 'Banana' },
+ { value: '3', label: 'Cherry' },
+ { value: '4', label: 'Date' },
+ { value: '5', label: 'Elderberry' },
+ { value: '6', label: 'Fig' },
+ { value: '7', label: 'Grape' },
+ { value: '8', label: 'Honeydew' },
+ { value: '9', label: 'Jackfruit' },
];
const [positiveValue, setPositiveValue] = useState('1');
const [negativeValue, setNegativeValue] = useState('2');
@@ -1314,15 +1312,15 @@ DisabledWithVariants.parameters = {
export const StartNodeWithVariants = () => {
const exampleOptions = [
{ value: null, label: 'Remove selection' },
- { value: '1', label: 'Option 1' },
- { value: '2', label: 'Option 2' },
- { value: '3', label: 'Option 3' },
- { value: '4', label: 'Option 4' },
- { value: '5', label: 'Option 5' },
- { value: '6', label: 'Option 6' },
- { value: '7', label: 'Option 7' },
- { value: '8', label: 'Option 8' },
- { value: '9', label: 'Option 9' },
+ { value: '1', label: 'Apple' },
+ { value: '2', label: 'Banana' },
+ { value: '3', label: 'Cherry' },
+ { value: '4', label: 'Date' },
+ { value: '5', label: 'Elderberry' },
+ { value: '6', label: 'Fig' },
+ { value: '7', label: 'Grape' },
+ { value: '8', label: 'Honeydew' },
+ { value: '9', label: 'Jackfruit' },
];
const [positiveValue, setPositiveValue] = useState('1');
const [negativeValue, setNegativeValue] = useState('2');
@@ -1356,15 +1354,15 @@ export const StartNodeWithVariants = () => {
export const LongHelperText = () => {
const exampleOptions = [
{ value: null, label: 'Remove selection' },
- { value: '1', label: 'Option 1' },
- { value: '2', label: 'Option 2' },
- { value: '3', label: 'Option 3' },
- { value: '4', label: 'Option 4' },
- { value: '5', label: 'Option 5' },
- { value: '6', label: 'Option 6' },
- { value: '7', label: 'Option 7' },
- { value: '8', label: 'Option 8' },
- { value: '9', label: 'Option 9' },
+ { value: '1', label: 'Apple' },
+ { value: '2', label: 'Banana' },
+ { value: '3', label: 'Cherry' },
+ { value: '4', label: 'Date' },
+ { value: '5', label: 'Elderberry' },
+ { value: '6', label: 'Fig' },
+ { value: '7', label: 'Grape' },
+ { value: '8', label: 'Honeydew' },
+ { value: '9', label: 'Jackfruit' },
];
const [value, setValue] = useState('1');
@@ -1383,15 +1381,15 @@ export const LongHelperText = () => {
export const CustomLongPlaceholder = () => {
const exampleOptions = [
{ value: null, label: 'Remove selection' },
- { value: '1', label: 'Option 1' },
- { value: '2', label: 'Option 2' },
- { value: '3', label: 'Option 3' },
- { value: '4', label: 'Option 4' },
- { value: '5', label: 'Option 5' },
- { value: '6', label: 'Option 6' },
- { value: '7', label: 'Option 7' },
- { value: '8', label: 'Option 8' },
- { value: '9', label: 'Option 9' },
+ { value: '1', label: 'Apple' },
+ { value: '2', label: 'Banana' },
+ { value: '3', label: 'Cherry' },
+ { value: '4', label: 'Date' },
+ { value: '5', label: 'Elderberry' },
+ { value: '6', label: 'Fig' },
+ { value: '7', label: 'Grape' },
+ { value: '8', label: 'Honeydew' },
+ { value: '9', label: 'Jackfruit' },
];
const [value, setValue] = useState(null);
@@ -1410,31 +1408,31 @@ export const AllCombinedFeatures = () => {
const exampleOptionsWithCustomAccessoriesAndMedia = [
{
value: '1',
- label: 'Option 1',
+ label: 'Apple',
accessory: ,
media: ,
},
{
value: '2',
- label: 'Option 2',
+ label: 'Banana',
accessory: ,
media: ,
},
{
value: '3',
- label: 'Option 3',
+ label: 'Cherry',
accessory: ,
media: ,
},
{
value: '4',
- label: 'Option 4',
+ label: 'Date',
accessory: ,
media: ,
},
{
value: '5',
- label: 'Option 5',
+ label: 'Elderberry',
accessory: ,
media: ,
},
@@ -1458,15 +1456,15 @@ export const AllCombinedFeatures = () => {
export const ComplexStyleCombinations = () => {
const exampleOptions = [
{ value: null, label: 'Remove selection' },
- { value: '1', label: 'Option 1' },
- { value: '2', label: 'Option 2' },
- { value: '3', label: 'Option 3' },
- { value: '4', label: 'Option 4' },
- { value: '5', label: 'Option 5' },
- { value: '6', label: 'Option 6' },
- { value: '7', label: 'Option 7' },
- { value: '8', label: 'Option 8' },
- { value: '9', label: 'Option 9' },
+ { value: '1', label: 'Apple' },
+ { value: '2', label: 'Banana' },
+ { value: '3', label: 'Cherry' },
+ { value: '4', label: 'Date' },
+ { value: '5', label: 'Elderberry' },
+ { value: '6', label: 'Fig' },
+ { value: '7', label: 'Grape' },
+ { value: '8', label: 'Honeydew' },
+ { value: '9', label: 'Jackfruit' },
];
const [value, setValue] = useState('1');
@@ -1601,15 +1599,15 @@ export const StressTestManyOptionsWithDescriptions = () => {
export const CustomControlComponent = () => {
const exampleOptions = [
{ value: null, label: 'Remove selection' },
- { value: '1', label: 'Option 1' },
- { value: '2', label: 'Option 2' },
- { value: '3', label: 'Option 3' },
- { value: '4', label: 'Option 4' },
- { value: '5', label: 'Option 5' },
- { value: '6', label: 'Option 6' },
- { value: '7', label: 'Option 7' },
- { value: '8', label: 'Option 8' },
- { value: '9', label: 'Option 9' },
+ { value: '1', label: 'Apple' },
+ { value: '2', label: 'Banana' },
+ { value: '3', label: 'Cherry' },
+ { value: '4', label: 'Date' },
+ { value: '5', label: 'Elderberry' },
+ { value: '6', label: 'Fig' },
+ { value: '7', label: 'Grape' },
+ { value: '8', label: 'Honeydew' },
+ { value: '9', label: 'Jackfruit' },
];
const [value, setValue] = useState('1');
@@ -1632,15 +1630,15 @@ export const CustomControlComponent = () => {
export const CustomOptionComponent = () => {
const exampleOptions = [
{ value: null, label: 'Remove selection' },
- { value: '1', label: 'Option 1' },
- { value: '2', label: 'Option 2' },
- { value: '3', label: 'Option 3' },
- { value: '4', label: 'Option 4' },
- { value: '5', label: 'Option 5' },
- { value: '6', label: 'Option 6' },
- { value: '7', label: 'Option 7' },
- { value: '8', label: 'Option 8' },
- { value: '9', label: 'Option 9' },
+ { value: '1', label: 'Apple' },
+ { value: '2', label: 'Banana' },
+ { value: '3', label: 'Cherry' },
+ { value: '4', label: 'Date' },
+ { value: '5', label: 'Elderberry' },
+ { value: '6', label: 'Fig' },
+ { value: '7', label: 'Grape' },
+ { value: '8', label: 'Honeydew' },
+ { value: '9', label: 'Jackfruit' },
];
const [value, setValue] = useState('1');
@@ -1671,15 +1669,15 @@ export const CustomOptionComponent = () => {
export const ValueDisplayed = () => {
const exampleOptions = [
{ value: null, label: 'Remove selection' },
- { value: '1', label: 'Option 1' },
- { value: '2', label: 'Option 2' },
- { value: '3', label: 'Option 3' },
- { value: '4', label: 'Option 4' },
- { value: '5', label: 'Option 5' },
- { value: '6', label: 'Option 6' },
- { value: '7', label: 'Option 7' },
- { value: '8', label: 'Option 8' },
- { value: '9', label: 'Option 9' },
+ { value: '1', label: 'Apple' },
+ { value: '2', label: 'Banana' },
+ { value: '3', label: 'Cherry' },
+ { value: '4', label: 'Date' },
+ { value: '5', label: 'Elderberry' },
+ { value: '6', label: 'Fig' },
+ { value: '7', label: 'Grape' },
+ { value: '8', label: 'Honeydew' },
+ { value: '9', label: 'Jackfruit' },
];
const [value, setValue] = useState('1');
@@ -1700,15 +1698,15 @@ export const ValueDisplayed = () => {
export const RefImperativeHandle = () => {
const exampleOptions = [
{ value: null, label: 'Remove selection' },
- { value: '1', label: 'Option 1' },
- { value: '2', label: 'Option 2' },
- { value: '3', label: 'Option 3' },
- { value: '4', label: 'Option 4' },
- { value: '5', label: 'Option 5' },
- { value: '6', label: 'Option 6' },
- { value: '7', label: 'Option 7' },
- { value: '8', label: 'Option 8' },
- { value: '9', label: 'Option 9' },
+ { value: '1', label: 'Apple' },
+ { value: '2', label: 'Banana' },
+ { value: '3', label: 'Cherry' },
+ { value: '4', label: 'Date' },
+ { value: '5', label: 'Elderberry' },
+ { value: '6', label: 'Fig' },
+ { value: '7', label: 'Grape' },
+ { value: '8', label: 'Honeydew' },
+ { value: '9', label: 'Jackfruit' },
];
const [value, setValue] = useState('1');
const selectRef = useRef(null);
@@ -1742,10 +1740,10 @@ export const RefImperativeHandle = () => {
export const Borderless = () => {
const exampleOptions = [
{ value: null, label: 'Remove selection' },
- { value: '1', label: 'Option 1' },
- { value: '2', label: 'Option 2' },
- { value: '3', label: 'Option 3' },
- { value: '4', label: 'Option 4' },
+ { value: '1', label: 'Apple' },
+ { value: '2', label: 'Banana' },
+ { value: '3', label: 'Cherry' },
+ { value: '4', label: 'Date' },
];
const [singleValue, setSingleValue] = useState('1');
diff --git a/packages/web/src/alpha/select/__tests__/Select.test.tsx b/packages/web/src/alpha/select/__tests__/Select.test.tsx
index 1cd8402035..29b82019b6 100644
--- a/packages/web/src/alpha/select/__tests__/Select.test.tsx
+++ b/packages/web/src/alpha/select/__tests__/Select.test.tsx
@@ -22,19 +22,6 @@ const defaultProps: SelectProps<'single' | 'multi'> = {
label: 'Test Select',
};
-jest.mock('@floating-ui/react-dom', () => ({
- useFloating: () => ({
- refs: {
- setReference: jest.fn(),
- setFloating: jest.fn(),
- reference: { current: null },
- floating: { current: null },
- },
- floatingStyles: {},
- }),
- flip: () => ({}),
-}));
-
jest.mock('../../../overlays/Portal', () => ({
Portal: ({ children, containerId }: { children: React.ReactNode; containerId?: string }) => (
{children}
@@ -592,6 +579,111 @@ describe('Select', () => {
expect(screen.queryByRole('listbox')).not.toBeInTheDocument();
});
});
+
+ it('opens dropdown when a letter key is pressed while closed', async () => {
+ const user = userEvent.setup();
+ render(
+
+
+ ,
+ );
+
+ const button = screen.getByRole('button');
+ button.focus();
+ await user.keyboard('o');
+
+ await waitFor(() => {
+ expect(screen.getByRole('listbox')).toBeInTheDocument();
+ });
+ });
+
+ it('focuses the first matching option when a letter key opens the dropdown', async () => {
+ const user = userEvent.setup();
+ const typeAheadOptions = [
+ { value: 'apple', label: 'Apple' },
+ { value: 'banana', label: 'Banana' },
+ { value: 'cherry', label: 'Cherry' },
+ { value: 'date', label: 'Date' },
+ ];
+
+ render(
+
+
+ ,
+ );
+
+ const button = screen.getByRole('button');
+ button.focus();
+ await user.keyboard('b');
+
+ await waitFor(() => {
+ expect(screen.getByRole('listbox')).toBeInTheDocument();
+ });
+
+ await waitFor(() => {
+ const options = screen.getAllByRole('option');
+ const bananaOption = options.find((opt) => opt.textContent?.includes('Banana'));
+ expect(bananaOption).toHaveFocus();
+ });
+ });
+
+ it('focuses the matching option even when textContent has non-letter prefix characters', async () => {
+ const user = userEvent.setup();
+ const typeAheadOptions = [
+ { value: 'apple', label: 'Apple' },
+ { value: 'banana', label: 'Banana' },
+ ];
+
+ render(
+
+
+ ,
+ );
+
+ const button = screen.getByRole('button');
+ button.focus();
+ await user.keyboard('b');
+
+ await waitFor(() => {
+ expect(screen.getByRole('listbox')).toBeInTheDocument();
+ });
+
+ await waitFor(() => {
+ const options = screen.getAllByRole('option');
+ const bananaOption = options.find((opt) => opt.textContent?.includes('Banana'));
+ expect(bananaOption).toHaveFocus();
+ });
+ });
+
+ it('does not open dropdown when letter key is pressed while disabled', async () => {
+ const user = userEvent.setup();
+ render(
+
+
+ ,
+ );
+
+ const button = screen.getByRole('button');
+ button.focus();
+ await user.keyboard('o');
+
+ expect(screen.queryByRole('listbox')).not.toBeInTheDocument();
+ });
+
+ it('does not open dropdown when modifier key + letter is pressed', async () => {
+ const user = userEvent.setup();
+ render(
+
+
+ ,
+ );
+
+ const button = screen.getByRole('button');
+ button.focus();
+ await user.keyboard('{Control>}a{/Control}');
+
+ expect(screen.queryByRole('listbox')).not.toBeInTheDocument();
+ });
});
describe('Ref Forwarding', () => {
diff --git a/packages/web/src/tabs/DefaultTab.tsx b/packages/web/src/tabs/DefaultTab.tsx
new file mode 100644
index 0000000000..0fe0a6c30e
--- /dev/null
+++ b/packages/web/src/tabs/DefaultTab.tsx
@@ -0,0 +1,125 @@
+import React, { forwardRef, memo, useCallback } from 'react';
+import type { SharedAccessibilityProps } from '@coinbase/cds-common';
+import { useTabsContext } from '@coinbase/cds-common/tabs/TabsContext';
+import type { TabValue } from '@coinbase/cds-common/tabs/useTabs';
+import { css } from '@linaria/core';
+
+import { cx } from '../cx';
+import { DotCount, type DotCountBaseProps } from '../dots/DotCount';
+import { HStack } from '../layout';
+import { Pressable, type PressableBaseProps } from '../system/Pressable';
+import { Text } from '../typography/Text';
+
+import type { TabComponentProps } from './Tabs';
+
+/** Optional dot count and a11y overrides for the default tab row. */
+export type DefaultTabLabelProps = Partial> &
+ Pick;
+
+const pressableCss = css`
+ margin: 0;
+ padding: 0;
+ white-space: nowrap;
+`;
+
+const insetFocusRingCss = css`
+ position: relative;
+ &:focus {
+ outline: none;
+ }
+ &:focus-visible {
+ outline-style: solid;
+ outline-width: 2px;
+ outline-color: var(--color-bgPrimary);
+ outline-offset: -3px;
+ border-radius: 4px;
+ }
+`;
+
+const labelPaddingCss = css`
+ padding-top: var(--space-2);
+ padding-bottom: calc(var(--space-2) - 2px); /* Account for the 2px tab indicator */
+`;
+
+export type DefaultTabProps = Omit &
+ TabComponentProps & DefaultTabLabelProps> & {
+ /** Callback that is fired when the tab is pressed, after the active tab updates. */
+ onClick?: (id: TabId) => void;
+ };
+
+type DefaultTabComponent = (
+ props: DefaultTabProps & { ref?: React.ForwardedRef },
+) => React.ReactElement;
+
+const DefaultTabComponent = memo(
+ forwardRef(
+ (
+ {
+ id,
+ label,
+ disabled: disabledProp,
+ onClick,
+ count,
+ max,
+ accessibilityLabel,
+ className,
+ ...props
+ }: DefaultTabProps,
+ ref: React.ForwardedRef,
+ ) => {
+ const {
+ activeTab,
+ updateActiveTab,
+ disabled: allTabsDisabled,
+ } = useTabsContext & DefaultTabLabelProps>();
+ const isActive = activeTab?.id === id;
+ const isDisabled = disabledProp || allTabsDisabled;
+
+ const handlePress = useCallback(() => {
+ updateActiveTab(id);
+ onClick?.(id);
+ }, [id, onClick, updateActiveTab]);
+
+ return (
+
+
+
+ {label}
+
+ {!!count && (
+
+ )}
+
+
+ );
+ },
+ ),
+);
+
+DefaultTabComponent.displayName = 'DefaultTab';
+
+export const DefaultTab = DefaultTabComponent as DefaultTabComponent;
diff --git a/packages/web/src/tabs/DefaultTabsActiveIndicator.tsx b/packages/web/src/tabs/DefaultTabsActiveIndicator.tsx
new file mode 100644
index 0000000000..df74a326a9
--- /dev/null
+++ b/packages/web/src/tabs/DefaultTabsActiveIndicator.tsx
@@ -0,0 +1,47 @@
+import { memo } from 'react';
+import { m as motion } from 'framer-motion';
+
+import { Box } from '../layout/Box';
+
+import { type TabsActiveIndicatorProps, tabsTransitionConfig } from './Tabs';
+
+const MotionBox = motion(Box);
+
+/**
+ * Default underline-style indicator for `Tabs`. Pass as
+ * `TabsActiveIndicatorComponent={DefaultTabIndicator}` with `TabComponent={DefaultTab}`.
+ */
+export const DefaultTabsActiveIndicator = memo(
+ ({
+ activeTabRect,
+ background = 'bgPrimary',
+ className,
+ style,
+ testID,
+ ...props
+ }: TabsActiveIndicatorProps) => {
+ const { width, x } = activeTabRect;
+
+ if (!width) return null;
+
+ return (
+
+ );
+ },
+);
+
+DefaultTabsActiveIndicator.displayName = 'DefaultTabsActiveIndicator';
diff --git a/packages/web/src/tabs/TabIndicator.tsx b/packages/web/src/tabs/TabIndicator.tsx
index a02ac66b36..350dd6c7be 100644
--- a/packages/web/src/tabs/TabIndicator.tsx
+++ b/packages/web/src/tabs/TabIndicator.tsx
@@ -20,6 +20,8 @@ export type TabIndicatorProps = SharedProps & {
background?: ThemeVars.Color;
};
+/** @deprecated Use DefaultTabsActiveIndicator instead. This will be removed in a future major release. */
+/** @deprecationExpectedRemoval v10 */
export const TabIndicator = memo(
forwardRef(
(
diff --git a/packages/web/src/tabs/TabLabel.tsx b/packages/web/src/tabs/TabLabel.tsx
index 7495c9c0c3..0b729641c7 100644
--- a/packages/web/src/tabs/TabLabel.tsx
+++ b/packages/web/src/tabs/TabLabel.tsx
@@ -58,6 +58,9 @@ export type TabLabelBaseProps = SharedProps &
export type TabLabelProps = TabLabelBaseProps &
TextProps<'h2'> & { onLayout?: (key: string, props: TabIndicatorProps) => void };
+/** @deprecated Use DefaultTab instead. This will be removed in a future major release. */
+/** @deprecationExpectedRemoval v10 */
+
export const TabLabel = memo(
({
id = '',
diff --git a/packages/web/src/tabs/TabNavigation.tsx b/packages/web/src/tabs/TabNavigation.tsx
index 625232c229..ba8e57cb0a 100644
--- a/packages/web/src/tabs/TabNavigation.tsx
+++ b/packages/web/src/tabs/TabNavigation.tsx
@@ -75,6 +75,10 @@ const insetFocusRingCss = css`
}
`;
+/**
+ * @deprecated Use Tabs instead. This will be removed in a future major release.
+ * @deprecationExpectedRemoval v10
+ */
export type TabProps = SharedProps &
Partial> & {
/** The id should be a meaningful and useful identifier like "watchlist" or "forSale" */
@@ -95,6 +99,10 @@ export type TabProps = SharedProps &
Component?: (props: CustomTabProps) => React.ReactNode;
};
+/**
+ * @deprecated Use Tabs instead. This will be removed in a future major release.
+ * @deprecationExpectedRemoval v10
+ */
export type CustomTabProps = Pick & {
/**
* @default false
@@ -105,6 +113,10 @@ export type CustomTabProps = Pick & {
label?: React.ReactNode;
};
+/**
+ * @deprecated Use Tabs instead. This will be removed in a future major release.
+ * @deprecationExpectedRemoval v10
+ */
export type TabNavigationBaseProps = SharedProps &
BoxBaseProps &
Pick, 'variant' | 'Component'> & {
@@ -146,6 +158,10 @@ export type TabNavigationBaseProps =
paddleStyle?: React.CSSProperties;
};
+/**
+ * @deprecated Use Tabs instead. This will be removed in a future major release.
+ * @deprecationExpectedRemoval v10
+ */
export type TabNavigationProps =
TabNavigationBaseProps;
diff --git a/packages/web/src/tabs/Tabs.tsx b/packages/web/src/tabs/Tabs.tsx
index 014f665a62..1f3280afc4 100644
--- a/packages/web/src/tabs/Tabs.tsx
+++ b/packages/web/src/tabs/Tabs.tsx
@@ -19,6 +19,9 @@ import { useComponentConfig } from '../hooks/useComponentConfig';
import { Box, type BoxBaseProps, type BoxDefaultElement, type BoxProps } from '../layout/Box';
import { HStack, type HStackDefaultElement, type HStackProps } from '../layout/HStack';
+import { DefaultTab } from './DefaultTab';
+import { DefaultTabsActiveIndicator } from './DefaultTabsActiveIndicator';
+
const MotionBox = motion>(Box);
type TabContainerProps = {
@@ -50,7 +53,10 @@ export type TabsActiveIndicatorProps = {
} & BoxProps &
MotionProps;
-export type TabComponentProps = TabValue & {
+export type TabComponentProps<
+ TabId extends string = string,
+ TTab extends TabValue = TabValue,
+> = Omit & {
/** The tab index for the tab. Automatically set to manage focus behavior. */
tabIndex?: number;
/**
@@ -60,27 +66,37 @@ export type TabComponentProps = TabValue &
role?: string;
className?: string;
style?: React.CSSProperties;
+ 'data-rendered-tab'?: boolean;
};
-export type TabComponent = React.FC>;
+export type TabComponent<
+ TabId extends string = string,
+ TTab extends TabValue = TabValue,
+> = React.FC>;
export type TabsActiveIndicatorComponent = React.FC;
-export type TabsBaseProps = Omit &
- Omit, 'tabs'> & {
+export type TabsBaseProps<
+ TabId extends string = string,
+ TTab extends TabValue = TabValue,
+> = Omit &
+ Omit, 'tabs'> & {
/** The array of tabs data. Each tab may optionally define a custom Component to render. */
- tabs: (TabValue & { Component?: TabComponent })[];
+ tabs: (TTab & { Component?: TabComponent })[];
/** The default Component to render each tab. */
- TabComponent: TabComponent;
+ TabComponent?: TabComponent;
/** The default Component to render the tabs active indicator. */
- TabsActiveIndicatorComponent: TabsActiveIndicatorComponent;
+ TabsActiveIndicatorComponent?: TabsActiveIndicatorComponent;
/** Background color passed to the TabsActiveIndicatorComponent. */
activeBackground?: ThemeVars.Color;
/** Optional callback to receive the active tab element. */
onActiveTabElementChange?: (element: HTMLElement | null) => void;
};
-export type TabsProps = TabsBaseProps &
+export type TabsProps<
+ TabId extends string = string,
+ TTab extends TabValue = TabValue,
+> = TabsBaseProps &
Omit, 'onChange' | 'ref'> & {
/** Custom styles for individual elements of the Tabs component */
styles?: {
@@ -102,18 +118,21 @@ export type TabsProps = TabsBaseProps &
};
};
-type TabsFC = (
- props: TabsProps & { ref?: React.ForwardedRef },
+type TabsFC = = TabValue>(
+ props: TabsProps & { ref?: React.ForwardedRef },
) => React.ReactElement;
const TabsComponent = memo(
forwardRef(
- (_props: TabsProps, ref: React.ForwardedRef) => {
+ = TabValue>(
+ _props: TabsProps,
+ ref: React.ForwardedRef,
+ ) => {
const mergedProps = useComponentConfig('Tabs', _props);
const {
tabs,
- TabComponent,
- TabsActiveIndicatorComponent,
+ TabComponent = DefaultTab,
+ TabsActiveIndicatorComponent = DefaultTabsActiveIndicator,
activeBackground,
activeTab,
onActiveTabElementChange,
@@ -131,9 +150,10 @@ const TabsComponent = memo(
borderBottomLeftRadius,
borderBottomRightRadius,
style,
+ testID,
...props
} = mergedProps;
- const api = useTabs({ tabs, activeTab, disabled, onChange });
+ const api = useTabs({ tabs, activeTab, disabled, onChange });
const [tabsContainerRef, tabsContainerRect] = useMeasure({
debounce: 20,
@@ -208,10 +228,7 @@ const TabsComponent = memo(
[tabs, refMap],
);
- const containerStyle = useMemo(
- () => ({ opacity: disabled ? accessibleOpacityDisabled : 1, ...style, ...styles?.root }),
- [disabled, style, styles?.root],
- );
+ const containerStyle = useMemo(() => ({ ...style, ...styles?.root }), [style, styles?.root]);
const registerRef = useCallback(
(tabId: string, ref: HTMLElement) => {
@@ -233,13 +250,15 @@ const TabsComponent = memo(
borderTopRightRadius={borderTopRightRadius}
className={cx(className, classNames?.root)}
onKeyDown={handleTabsContainerKeyDown}
+ opacity={disabled ? accessibleOpacityDisabled : 1}
position={position}
role={role}
style={containerStyle}
+ testID={testID}
width={width}
{...props}
>
- }>
+ }>
- {tabs.map(({ id, Component: CustomTabComponent, disabled: tabDisabled, ...props }) => {
- const RenderedTab = CustomTabComponent ?? TabComponent;
+ {tabs.map((props) => {
+ const RenderedTab = props.Component ?? TabComponent;
+ const renderedTabProps = {
+ ...props,
+ 'data-rendered-tab': true,
+ className: classNames?.tab,
+ role: 'tab',
+ style: styles?.tab,
+ tabIndex: activeTab?.id === props.id || !activeTab ? 0 : -1,
+ };
return (
-
-
+
+
);
})}
@@ -282,6 +301,7 @@ export const Tabs = TabsComponent as TabsFC;
export const TabsActiveIndicator = ({
activeTabRect,
position = 'absolute',
+ testID = 'tabs-active-indicator',
...props
}: TabsActiveIndicatorProps) => {
const { width, height, x } = activeTabRect;
@@ -290,12 +310,12 @@ export const TabsActiveIndicator = ({
return (
diff --git a/packages/web/src/tabs/__stories__/Tabs.stories.tsx b/packages/web/src/tabs/__stories__/Tabs.stories.tsx
new file mode 100644
index 0000000000..3bf5af8288
--- /dev/null
+++ b/packages/web/src/tabs/__stories__/Tabs.stories.tsx
@@ -0,0 +1,228 @@
+import { useCallback, useState } from 'react';
+import { sampleTabs } from '@coinbase/cds-common/internal/data/tabs';
+import type { TabValue } from '@coinbase/cds-common/tabs/useTabs';
+import { zIndex } from '@coinbase/cds-common/tokens/zIndex';
+
+import { Box, VStack } from '../../layout';
+import { ThemeProvider } from '../../system/ThemeProvider';
+import { defaultTheme } from '../../themes/defaultTheme';
+import { Text } from '../../typography/Text';
+import { DefaultTab, type DefaultTabLabelProps } from '../DefaultTab';
+import { DefaultTabsActiveIndicator } from '../DefaultTabsActiveIndicator';
+import {
+ type TabComponent,
+ Tabs,
+ TabsActiveIndicator,
+ type TabsActiveIndicatorComponent,
+ type TabsActiveIndicatorProps,
+ type TabsProps,
+} from '../Tabs';
+
+import { MockTabPanel } from './MockTabPanel';
+
+export default {
+ title: 'Components/Tabs/Tabs',
+ parameters: {
+ a11y: {
+ context: {
+ include: ['body'],
+ exclude: ['.no-a11y-checks'],
+ },
+ },
+ },
+};
+
+type TradingAction = 'buy' | 'sell' | 'convert';
+
+type TabRowWithTestId = TabValue & { testID?: string };
+
+const basicTabs: TabRowWithTestId[] = [
+ { id: 'buy', label: 'Buy', testID: 'buy-tab' },
+ { id: 'sell', label: 'Sell', testID: 'sell-tab' },
+ { id: 'convert', label: 'Convert', testID: 'convert-tab' },
+];
+
+const longTabs = sampleTabs.slice(0, 9);
+
+const tabsWithDisabled = [
+ { id: 'buy', label: 'Buy' },
+ { id: 'sell', label: 'Sell', disabled: true },
+ { id: 'convert', label: 'Convert' },
+];
+
+const typedTabs: TabValue[] = [
+ { id: 'buy', label: 'Buy' },
+ { id: 'sell', label: 'Sell' },
+ { id: 'convert', label: 'Convert' },
+];
+
+type TradingTab = TabValue & DefaultTabLabelProps;
+const tabsWithDotCounts: TradingTab[] = basicTabs.map((tab, index) =>
+ index === 0 ? { ...tab, count: 3, max: 99 } : tab,
+);
+
+const CustomSpringIndicator = (props: TabsActiveIndicatorProps) => (
+