diff --git a/apps/docs/docs/components/other/Calendar/_mobileExamples.mdx b/apps/docs/docs/components/other/Calendar/_mobileExamples.mdx
new file mode 100644
index 000000000..b36dd16cd
--- /dev/null
+++ b/apps/docs/docs/components/other/Calendar/_mobileExamples.mdx
@@ -0,0 +1,232 @@
+### Basic usage
+
+A basic Calendar with date selection functionality. The Calendar component is used within the DatePicker and can also be used independently.
+
+```jsx
+function Example() {
+ const [selectedDate, setSelectedDate] = useState(new Date());
+
+ return ;
+}
+```
+
+### No selection
+
+A Calendar without an initially selected date.
+
+```jsx
+function Example() {
+ const [selectedDate, setSelectedDate] = useState(null);
+
+ return ;
+}
+```
+
+### Seeding the calendar
+
+The `seedDate` prop controls which month the Calendar opens to when there is no selected date value. Defaults to today when undefined.
+
+```jsx
+function Example() {
+ const [selectedDate, setSelectedDate] = useState(null);
+
+ const today = new Date(new Date().setHours(0, 0, 0, 0));
+ const seedDate = new Date(today.getFullYear(), today.getMonth() + 1, 15);
+
+ return ;
+}
+```
+
+### Minimum and maximum dates
+
+Use `minDate` and `maxDate` to restrict the selectable date range. Navigation to dates before the `minDate` and after the `maxDate` is disabled. Make sure to provide the `disabledDateError` prop.
+
+```jsx
+function Example() {
+ const [selectedDate, setSelectedDate] = useState(new Date());
+
+ const today = new Date(new Date().setHours(0, 0, 0, 0));
+ const lastMonth15th = new Date(today.getFullYear(), today.getMonth() - 1, 15);
+ const nextMonth15th = new Date(today.getFullYear(), today.getMonth() + 1, 15);
+
+ return (
+
+ );
+}
+```
+
+### Future dates only
+
+Restrict selection to future dates by setting `minDate` to today.
+
+```jsx
+function Example() {
+ const [selectedDate, setSelectedDate] = useState(null);
+
+ const today = new Date(new Date().setHours(0, 0, 0, 0));
+
+ return (
+
+ );
+}
+```
+
+### Highlighted dates
+
+Use `highlightedDates` to visually emphasize specific dates or date ranges. A number is created for every individual date within a tuple range, so do not abuse this with massive ranges.
+
+```jsx
+function Example() {
+ const [selectedDate, setSelectedDate] = useState(new Date());
+
+ const today = new Date(new Date().setHours(0, 0, 0, 0));
+ const yesterday = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 1);
+ const nextWeek = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 7);
+
+ return (
+
+ );
+}
+```
+
+### Disabled dates
+
+Use `disabledDates` to prevent selection of specific dates or date ranges. Make sure to provide the `disabledDateError` prop.
+
+```jsx
+function Example() {
+ const [selectedDate, setSelectedDate] = useState(null);
+
+ const today = new Date(new Date().setHours(0, 0, 0, 0));
+
+ // Disable weekends for demonstration
+ const getNextWeekendDates = (centerDate) => {
+ const weekends = [];
+ const currentDate = new Date(centerDate);
+
+ // Find next 4 weekends
+ for (let i = 0; i < 4; i++) {
+ // Find next Saturday
+ const daysUntilSaturday = (6 - currentDate.getDay() + 7) % 7 || 7;
+ currentDate.setDate(currentDate.getDate() + daysUntilSaturday);
+
+ const saturday = new Date(currentDate);
+ const sunday = new Date(currentDate);
+ sunday.setDate(sunday.getDate() + 1);
+
+ weekends.push([saturday, sunday]);
+
+ // Move to next week
+ currentDate.setDate(currentDate.getDate() + 7);
+ }
+
+ return weekends;
+ };
+
+ return (
+
+ );
+}
+```
+
+### Date ranges
+
+Highlight a date range using a tuple `[startDate, endDate]`.
+
+```jsx
+function Example() {
+ const [selectedDate, setSelectedDate] = useState(new Date());
+
+ const today = new Date(new Date().setHours(0, 0, 0, 0));
+ const yesterday = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 1);
+ const nextWeek = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 7);
+
+ return (
+
+ );
+}
+```
+
+### Hidden controls
+
+Hide the navigation arrows with `hideControls`. This is typically used when `minDate` and `maxDate` are set to the first and last days of the same month.
+
+```jsx
+function Example() {
+ const [selectedDate, setSelectedDate] = useState(new Date());
+
+ const today = new Date(new Date().setHours(0, 0, 0, 0));
+ const firstDayOfMonth = new Date(today.getFullYear(), today.getMonth(), 1);
+ const lastDayOfMonth = new Date(today.getFullYear(), today.getMonth() + 1, 0);
+
+ return (
+
+ );
+}
+```
+
+### Disabled
+
+Disable the entire Calendar with the `disabled` prop.
+
+```jsx
+function Example() {
+ const selectedDate = new Date();
+
+ return ;
+}
+```
+
+### Accessibility
+
+Always provide accessibility labels for the navigation controls and error messages for disabled dates.
+
+```jsx
+function Example() {
+ const [selectedDate, setSelectedDate] = useState(new Date());
+
+ const today = new Date(new Date().setHours(0, 0, 0, 0));
+ const nextMonth = new Date(today.getFullYear(), today.getMonth() + 1, today.getDate());
+
+ return (
+
+ );
+}
+```
diff --git a/apps/docs/docs/components/other/Calendar/_mobilePropsTable.mdx b/apps/docs/docs/components/other/Calendar/_mobilePropsTable.mdx
new file mode 100644
index 000000000..5d811d0d8
--- /dev/null
+++ b/apps/docs/docs/components/other/Calendar/_mobilePropsTable.mdx
@@ -0,0 +1,11 @@
+import ComponentPropsTable from '@site/src/components/page/ComponentPropsTable';
+
+import mobilePropsData from ':docgen/mobile/dates/Calendar/data';
+import { sharedParentTypes } from ':docgen/_types/sharedParentTypes';
+import { sharedTypeAliases } from ':docgen/_types/sharedTypeAliases';
+
+
diff --git a/apps/docs/docs/components/other/Calendar/index.mdx b/apps/docs/docs/components/other/Calendar/index.mdx
index 5d8c7137e..1aed2a0d4 100644
--- a/apps/docs/docs/components/other/Calendar/index.mdx
+++ b/apps/docs/docs/components/other/Calendar/index.mdx
@@ -1,7 +1,7 @@
---
id: calendar
title: Calendar
-platform_switcher_options: { web: true, mobile: false }
+platform_switcher_options: { web: true, mobile: true }
hide_title: true
---
@@ -10,16 +10,26 @@ import { ComponentHeader } from '@site/src/components/page/ComponentHeader';
import { ComponentTabsContainer } from '@site/src/components/page/ComponentTabsContainer';
import webPropsToc from ':docgen/web/dates/Calendar/toc-props';
+import mobilePropsToc from ':docgen/mobile/dates/Calendar/toc-props';
+
import WebPropsTable from './_webPropsTable.mdx';
+import MobilePropsTable from './_mobilePropsTable.mdx';
+import MobileExamples, { toc as mobileExamplesToc } from './_mobileExamples.mdx';
import WebExamples, { toc as webExamplesToc } from './_webExamples.mdx';
+
import webMetadata from './webMetadata.json';
+import mobileMetadata from './mobileMetadata.json';
-
+ }
webExamples={}
+ mobilePropsTable={}
+ mobileExamples={}
webExamplesToc={webExamplesToc}
+ mobileExamplesToc={mobileExamplesToc}
webPropsToc={webPropsToc}
+ mobilePropsToc={mobilePropsToc}
/>
diff --git a/apps/docs/docs/components/other/Calendar/mobileMetadata.json b/apps/docs/docs/components/other/Calendar/mobileMetadata.json
new file mode 100644
index 000000000..297b563f5
--- /dev/null
+++ b/apps/docs/docs/components/other/Calendar/mobileMetadata.json
@@ -0,0 +1,11 @@
+{
+ "import": "import { Calendar } from '@coinbase/cds-mobile/dates/Calendar'",
+ "source": "https://github.com/coinbase/cds/blob/master/packages/mobile/src/dates/Calendar.tsx",
+ "description": "Calendar is a flexible, accessible date grid component for selecting dates, supporting keyboard navigation, disabled/highlighted dates, and custom rendering.",
+ "relatedComponents": [
+ {
+ "label": "DatePicker",
+ "url": "/components/other/DatePicker/"
+ }
+ ]
+}
diff --git a/apps/docs/docs/components/other/DatePicker/_mobileExamples.mdx b/apps/docs/docs/components/other/DatePicker/_mobileExamples.mdx
index 7f5abec8e..644e36db9 100644
--- a/apps/docs/docs/components/other/DatePicker/_mobileExamples.mdx
+++ b/apps/docs/docs/components/other/DatePicker/_mobileExamples.mdx
@@ -90,12 +90,9 @@ function Example() {
error={error}
onChangeDate={setDate}
onErrorDate={setError}
- disabledDates={[new Date()]}
label="Birthdate"
- calendarIconButtonAccessibilityLabel="Birthdate calendar"
- nextArrowAccessibilityLabel="Next month"
- previousArrowAccessibilityLabel="Previous month"
- helperTextErrorIconAccessibilityLabel="Error"
+ calendarIconButtonAccessibilityLabel="Open calendar to select birthdate"
+ confirmButtonAccessibilityLabel="Confirm birthdate selection"
invalidDateError="Please enter a valid date"
disabledDateError="Date unavailable"
requiredError="This field is required"
@@ -132,7 +129,7 @@ function Example() {
Defaults to today when undefined.
-On mobile the `seedDate` prop is the default date that the react-native-date-picker keyboard control will open to when there is no selected date value.
+The `seedDate` prop is used to generate the Calendar month when there is no selected date value.
```jsx
function Example() {
@@ -179,6 +176,36 @@ function Example() {
}
```
+### Highlighted dates
+
+The `highlightedDates` prop is an array of Dates and Date tuples for date ranges. A number is created for every individual date within a tuple range, so do not abuse this with massive ranges.
+
+```jsx
+function Example() {
+ const [date, setDate] = useState(null);
+ const [error, setError] = useState(null);
+
+ const today = new Date(new Date().setHours(0, 0, 0, 0));
+ const oneWeekAgo = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 7);
+ const twoDaysAgo = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 2);
+ const oneWeekLater = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 7);
+
+ const highlightedDates = [[oneWeekAgo, twoDaysAgo], oneWeekLater];
+
+ return (
+
+ );
+}
+```
+
### Minimum and maximum dates
Make sure to provide the `disabledDateError` prop when providing `minDate`, `maxDate`, or `disabledDates` props. Navigation to dates before the `minDate` and after the `maxDate` is disabled.
@@ -208,6 +235,41 @@ function Example() {
}
```
+### Disabled dates
+
+The `disabledDates` prop is an array of Dates and Date tuples for date ranges. A number is created for every individual date within a tuple range, so do not abuse this with massive ranges.
+
+Make sure to provide the `disabledDateError` prop when providing `minDate`, `maxDate`, or `disabledDates` props.
+
+```jsx
+function Example() {
+ const [date, setDate] = useState(null);
+ const [error, setError] = useState(null);
+
+ const today = new Date(new Date().setHours(0, 0, 0, 0));
+ const tomorrow = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 1);
+ const startOfNextWeek = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 7);
+ const endOfNextWeek = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 13);
+
+ return (
+
+ );
+}
+```
+
### Multiple pickers
This is a complex example using many different props. We use multiple DatePickers together to allow a user to select a date range.
@@ -223,11 +285,8 @@ function Example() {
const today = new Date(new Date().setHours(0, 0, 0, 0));
const firstDayThisMonth = new Date(today.getFullYear(), today.getMonth(), 1);
- const seventhDayThisMonth = new Date(today.getFullYear(), today.getMonth(), 7);
const lastDayThisMonth = new Date(today.getFullYear(), today.getMonth() + 1, 0);
- const disabledDates = [[firstDayThisMonth, seventhDayThisMonth]];
-
const updateEndDate = (endDate, startDate) => {
setEndDate(endDate);
setEndError(null);
@@ -266,12 +325,10 @@ function Example() {
};
return (
-
+
-
+
);
}
```
### Event lifecycle
-- Selecting a date with the native picker (mobile) or Calendar (web):
+- Selecting a date with the Calendar:
`onOpen -> onConfirm -> onChangeDate -> onErrorDate -> onClose`
-- Closing the native picker (mobile) or Calendar (web) without selecting a date:
+- Closing the Calendar without selecting a date:
`onOpen -> onCancel -> onClose`
@@ -328,6 +384,10 @@ function Example() {
`onChange -> onChangeDate -> onChange -> onChange -> ... -> onChangeDate -> onErrorDate`
+:::note
+The Calendar picker requires pressing the confirm button to select a date.
+:::
+
```jsx
function Example() {
const [date, setDate] = useState(null);
diff --git a/apps/docs/docs/components/other/DatePicker/_webExamples.mdx b/apps/docs/docs/components/other/DatePicker/_webExamples.mdx
index b88c1e199..641fdd0dd 100644
--- a/apps/docs/docs/components/other/DatePicker/_webExamples.mdx
+++ b/apps/docs/docs/components/other/DatePicker/_webExamples.mdx
@@ -132,9 +132,7 @@ function Example() {
Defaults to today when undefined.
-On web the `seedDate` prop is used to generate the Calendar month when there is no selected date value.
-
-On mobile the `seedDate` prop is the default date that the react-native-date-picker keyboard control will open to when there is no selected date value.
+The `seedDate` prop is used to generate the Calendar month when there is no selected date value.
```jsx live
function Example() {
@@ -185,8 +183,6 @@ function Example() {
The `highlightedDates` prop is an array of Dates and Date tuples for date ranges. A number is created for every individual date within a tuple range, so do not abuse this with massive ranges.
-The `highlightedDates` prop is only available on web because the mobile DatePicker uses react-native-date-picker instead of a Calendar component.
-
```jsx live
function Example() {
const [date, setDate] = useState(null);
@@ -246,8 +242,6 @@ function Example() {
The `disabledDates` prop is an array of Dates and Date tuples for date ranges. A number is created for every individual date within a tuple range, so do not abuse this with massive ranges.
-The `disabledDates` prop is only available on web because the mobile DatePicker uses react-native-date-picker instead of a Calendar component.
-
Make sure to provide the `disabledDateError` prop when providing `minDate`, `maxDate`, or `disabledDates` props.
```jsx live
@@ -383,11 +377,11 @@ function Example() {
### Event lifecycle
-- Selecting a date with the native picker (mobile) or Calendar (web):
+- Selecting a date with the Calendar:
`onOpen -> onConfirm -> onChangeDate -> onErrorDate -> onClose`
-- Closing the native picker (mobile) or Calendar (web) without selecting a date:
+- Closing the Calendar without selecting a date:
`onOpen -> onCancel -> onClose`
diff --git a/apps/docs/docs/components/other/DatePicker/mobileMetadata.json b/apps/docs/docs/components/other/DatePicker/mobileMetadata.json
index 7fda90133..f6dc4495f 100644
--- a/apps/docs/docs/components/other/DatePicker/mobileMetadata.json
+++ b/apps/docs/docs/components/other/DatePicker/mobileMetadata.json
@@ -2,21 +2,19 @@
"import": "import { DatePicker } from '@coinbase/cds-mobile/dates/DatePicker'",
"source": "https://github.com/coinbase/cds/blob/master/packages/mobile/src/dates/DatePicker.tsx",
"figma": "https://www.figma.com/design/k5CtyJccNQUGMI5bI4lJ2g/%E2%9C%A8-CDS-Components?node-id=14743-52589",
- "description": "Date Picker allows our global users to input past, present, future and important dates into our interface in a simple and intuitive manner. Date Picker offers both manual and calendar entry options - accommodating both internationalization and accessibility needs while being adaptable across screen platforms.",
+ "description": "Date Picker allows our global users to input past, present, future and important dates into our interface in a simple and intuitive manner. Date Picker offers both manual and calendar entry options - accommodating both internationalization and accessibility needs.",
"relatedComponents": [
+ {
+ "label": "Calendar",
+ "url": "/components/other/Calendar/"
+ },
{
"label": "TextInput",
"url": "/components/inputs/TextInput/"
},
{
- "label": "Modal",
- "url": "/components/overlay/Modal/"
- }
- ],
- "dependencies": [
- {
- "name": "react-native-date-picker",
- "version": "^4.4.2"
+ "label": "Tray",
+ "url": "/components/overlay/Tray/"
}
]
}
diff --git a/apps/docs/docs/components/other/DatePicker/webMetadata.json b/apps/docs/docs/components/other/DatePicker/webMetadata.json
index 2ef73aa40..d8d791525 100644
--- a/apps/docs/docs/components/other/DatePicker/webMetadata.json
+++ b/apps/docs/docs/components/other/DatePicker/webMetadata.json
@@ -5,6 +5,10 @@
"figma": "https://www.figma.com/design/k5CtyJccNQUGMI5bI4lJ2g/%E2%9C%A8-CDS-Components?node-id=14743-52589",
"description": "Date Picker allows our global users to input past, present, future and important dates into our interface in a simple and intuitive manner. Date Picker offers both manual and calendar entry options - accommodating both internationalization and accessibility needs while being adaptable across screen platforms.",
"relatedComponents": [
+ {
+ "label": "Calendar",
+ "url": "/components/other/Calendar/"
+ },
{
"label": "TextInput",
"url": "/components/inputs/TextInput/"
diff --git a/apps/docs/docs/components/overlay/Tooltip/_mobileExamples.mdx b/apps/docs/docs/components/overlay/Tooltip/_mobileExamples.mdx
index 57d836899..7c7a18b8a 100644
--- a/apps/docs/docs/components/overlay/Tooltip/_mobileExamples.mdx
+++ b/apps/docs/docs/components/overlay/Tooltip/_mobileExamples.mdx
@@ -1,22 +1,29 @@
+### Basic usage
+
+A basic Tooltip that displays additional information when the trigger element is pressed.
+
+```jsx
+function Example() {
+ return (
+
+
+
+ );
+}
+```
+
### Placement
+Control the tooltip position using the `placement` prop. Available options are `top` and `bottom`.
+
```jsx
-function DefaultSelect() {
- const content = 'This is the tooltip Content';
+function Example() {
+ const content = 'This is the tooltip content';
return (
-
-
-
-
+
-
-
-
-
-
-
@@ -24,3 +31,56 @@ function DefaultSelect() {
);
}
```
+
+### Disabled trigger
+
+Use `triggerDisabled` to indicate that the trigger element represents disabled content. When true, screen readers will perceive the element as disabled, but the tooltip remains tappable for sighted users. This is useful for displaying tooltips on disabled interactive elements.
+
+```jsx
+function Example() {
+ return (
+
+
+ Disabled feature
+
+
+ );
+}
+```
+
+### Accessibility
+
+Always provide appropriate accessibility labels when the tooltip trigger is not a simple text string.
+
+```jsx
+function Example() {
+ return (
+
+
+
+ );
+}
+```
+
+### Color scheme
+
+By default, tooltips use an inverted color scheme. You can disable this with `invertColorScheme={false}`.
+
+```jsx
+function Example() {
+ return (
+
+
+
+ );
+}
+```
diff --git a/apps/mobile-app/package.json b/apps/mobile-app/package.json
index 12a21f64b..146419a70 100644
--- a/apps/mobile-app/package.json
+++ b/apps/mobile-app/package.json
@@ -48,7 +48,6 @@
"lottie-react-native": "6.7.0",
"react": "^18.3.1",
"react-native": "0.74.5",
- "react-native-date-picker": "4.4.2",
"react-native-gesture-handler": "2.16.2",
"react-native-inappbrowser-reborn": "3.7.0",
"react-native-navigation-bar-color": "2.0.2",
diff --git a/apps/mobile-app/src/routes.ts b/apps/mobile-app/src/routes.ts
index 6b48a77ff..21d57d5f6 100644
--- a/apps/mobile-app/src/routes.ts
+++ b/apps/mobile-app/src/routes.ts
@@ -86,6 +86,10 @@ export const routes = [
getComponent: () =>
require('@coinbase/cds-mobile/buttons/__stories__/ButtonGroup.stories').default,
},
+ {
+ key: 'Calendar',
+ getComponent: () => require('@coinbase/cds-mobile/dates/__stories__/Calendar.stories').default,
+ },
{
key: 'Card',
getComponent: () => require('@coinbase/cds-mobile/cards/__stories__/Card.stories').default,
diff --git a/packages/common/CHANGELOG.md b/packages/common/CHANGELOG.md
index 4cd337441..e7d07bc88 100644
--- a/packages/common/CHANGELOG.md
+++ b/packages/common/CHANGELOG.md
@@ -8,6 +8,10 @@ All notable changes to this project will be documented in this file.
+## 8.20.0 ((10/30/2025, 08:33 PM PST))
+
+This is an artificial version bump with no new change.
+
## 8.19.0 (10/29/2025 PST)
#### 🚀 Updates
diff --git a/packages/common/package.json b/packages/common/package.json
index c47282673..0ef3b71bd 100644
--- a/packages/common/package.json
+++ b/packages/common/package.json
@@ -1,6 +1,6 @@
{
"name": "@coinbase/cds-common",
- "version": "8.19.0",
+ "version": "8.20.0",
"description": "Coinbase Design System - Common",
"repository": {
"type": "git",
diff --git a/packages/mcp-server/CHANGELOG.md b/packages/mcp-server/CHANGELOG.md
index 6d9db3dba..2db0b0fa8 100644
--- a/packages/mcp-server/CHANGELOG.md
+++ b/packages/mcp-server/CHANGELOG.md
@@ -8,6 +8,10 @@ All notable changes to this project will be documented in this file.
+## 8.20.0 ((10/30/2025, 08:33 PM PST))
+
+This is an artificial version bump with no new change.
+
## 8.19.0 ((10/29/2025, 02:11 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 42c15d705..4f41aaf68 100644
--- a/packages/mcp-server/package.json
+++ b/packages/mcp-server/package.json
@@ -1,6 +1,6 @@
{
"name": "@coinbase/cds-mcp-server",
- "version": "8.19.0",
+ "version": "8.20.0",
"description": "Coinbase Design System - MCP Server",
"repository": {
"type": "git",
diff --git a/packages/mobile/CHANGELOG.md b/packages/mobile/CHANGELOG.md
index 6e687591c..8ad277cd7 100644
--- a/packages/mobile/CHANGELOG.md
+++ b/packages/mobile/CHANGELOG.md
@@ -8,6 +8,23 @@ All notable changes to this project will be documented in this file.
+## 8.20.0 (10/30/2025 PST)
+
+#### 🚀 Updates
+
+- Added Calendar component to Mobile. [[#139](https://github.com/coinbase/cds/pull/139)]
+- Integrated Calendar into DatePicker. [[#139](https://github.com/coinbase/cds/pull/139)]
+
+#### 🐞 Fixes
+
+- Added "triggerDisabled" prop for Tooltip for accessibility. [[#139](https://github.com/coinbase/cds/pull/139)]
+- Removed react-native-date-picker dependencies. [[#139](https://github.com/coinbase/cds/pull/139)]
+
+#### 📘 Misc
+
+- Added unit and a11y tests for Calendar and DatePicker. [[#139](https://github.com/coinbase/cds/pull/139)]
+- Added Mobile docs for Calendar, updated mobile docs for DatePicker and Tooltip. [[#139](https://github.com/coinbase/cds/pull/139)]
+
## 8.19.0 (10/29/2025 PST)
#### 🚀 Updates
diff --git a/packages/mobile/package.json b/packages/mobile/package.json
index 8082dd234..aef3d7f00 100644
--- a/packages/mobile/package.json
+++ b/packages/mobile/package.json
@@ -1,6 +1,6 @@
{
"name": "@coinbase/cds-mobile",
- "version": "8.19.0",
+ "version": "8.20.0",
"description": "Coinbase Design System - Mobile",
"repository": {
"type": "git",
@@ -139,7 +139,6 @@
"lottie-react-native": "^6.7.0",
"react": "^18.3.1",
"react-native": "^0.74.5",
- "react-native-date-picker": "^4.4.2",
"react-native-gesture-handler": "^2.16.2",
"react-native-inappbrowser-reborn": "^3.7.0",
"react-native-linear-gradient": "^2.8.3",
@@ -175,7 +174,6 @@
"eslint-plugin-reanimated": "^2.0.1",
"lottie-react-native": "6.7.0",
"react-native-accessibility-engine": "^3.2.0",
- "react-native-date-picker": "4.4.2",
"react-native-gesture-handler": "2.16.2",
"react-native-inappbrowser-reborn": "3.7.0",
"react-native-linear-gradient": "2.8.3",
diff --git a/packages/mobile/src/dates/Calendar.tsx b/packages/mobile/src/dates/Calendar.tsx
new file mode 100644
index 000000000..ca03a7a2e
--- /dev/null
+++ b/packages/mobile/src/dates/Calendar.tsx
@@ -0,0 +1,488 @@
+import { forwardRef, memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
+import { type StyleProp, StyleSheet, type View, type ViewStyle } from 'react-native';
+import { generateCalendarMonth } from '@coinbase/cds-common/dates/generateCalendarMonth';
+import { getMidnightDate } from '@coinbase/cds-common/dates/getMidnightDate';
+import { getTimesFromDatesAndRanges } from '@coinbase/cds-common/dates/getTimesFromDatesAndRanges';
+import { useLocale } from '@coinbase/cds-common/system/LocaleProvider';
+import { accessibleOpacityDisabled } from '@coinbase/cds-common/tokens/interactable';
+
+import { useA11y } from '../hooks/useA11y';
+import { Icon } from '../icons/Icon';
+import { Box } from '../layout/Box';
+import { HStack } from '../layout/HStack';
+import { VStack, type VStackProps } from '../layout/VStack';
+import { Tooltip } from '../overlays/tooltip/Tooltip';
+import { Pressable, type PressableBaseProps } from '../system/Pressable';
+import { Text } from '../typography/Text';
+
+const CALENDAR_DAY_SIZE = 40;
+// Delay for initial focus - waiting for Calendar date refs to populate after mount
+const A11Y_INITIAL_FOCUS_DELAY_MS = 300;
+
+const styles = StyleSheet.create({
+ pressable: {
+ alignItems: 'center',
+ justifyContent: 'center',
+ width: '100%',
+ height: '100%',
+ },
+});
+
+export type CalendarPressableBaseProps = PressableBaseProps & {
+ borderRadius?: number;
+ width?: number;
+ height?: number;
+ background?: 'transparent' | 'bg' | 'bgPrimary';
+};
+
+const CalendarPressable = memo(
+ forwardRef(
+ (
+ {
+ background = 'transparent',
+ borderRadius = 1000,
+ width = CALENDAR_DAY_SIZE,
+ height = CALENDAR_DAY_SIZE,
+ children,
+ ...props
+ },
+ ref,
+ ) => {
+ return (
+
+ {children}
+
+ );
+ },
+ ),
+);
+
+CalendarPressable.displayName = 'CalendarPressable';
+
+export type CalendarDayProps = {
+ /** Date of this CalendarDay. */
+ date: Date;
+ /** Callback function fired when pressing this CalendarDay. */
+ onPress?: (date: Date) => void;
+ /** Toggle active styles. */
+ active?: boolean;
+ /** Disables user interaction. */
+ disabled?: boolean;
+ /** Toggle highlighted styles. */
+ highlighted?: boolean;
+ /** Toggle today's date styles. */
+ isToday?: boolean;
+ /** Toggle current month styles. */
+ isCurrentMonth?: boolean;
+ /** Tooltip content shown when hovering or focusing a disabled Calendar Day. */
+ disabledError?: string;
+};
+
+const getDayAccessibilityLabel = (date: Date, locale = 'en-US') =>
+ `${date.toLocaleDateString(locale, {
+ weekday: 'long',
+ day: 'numeric',
+ })} ${date.toLocaleDateString(locale, {
+ month: 'long',
+ year: 'numeric',
+ })}`;
+
+const CalendarDay = memo(
+ forwardRef(
+ (
+ {
+ date,
+ active,
+ disabled,
+ highlighted,
+ isToday,
+ isCurrentMonth,
+ onPress,
+ disabledError = 'Date unavailable',
+ },
+ ref,
+ ) => {
+ const { locale } = useLocale();
+ const handlePress = useCallback(() => onPress?.(date), [date, onPress]);
+ const accessibilityLabel = getDayAccessibilityLabel(date, locale);
+
+ if (!isCurrentMonth) {
+ return ;
+ }
+
+ // Render disabled dates as non-interactive elements
+ if (disabled) {
+ const disabledDayView = (
+
+
+ {date.getDate()}
+
+
+ );
+
+ return (
+
+ {disabledDayView}
+
+ );
+ }
+
+ // Render interactive dates as Pressable buttons
+ return (
+
+
+ {date.getDate()}
+
+
+ );
+ },
+ ),
+);
+
+CalendarDay.displayName = 'CalendarDay';
+
+export type CalendarBaseProps = {
+ /** Currently selected Calendar date. Date used to generate the Calendar month. Will be rendered with active styles. */
+ selectedDate?: Date | null;
+ /** Date used to generate the Calendar month when there is no value for the `selectedDate` prop, defaults to today. */
+ seedDate?: Date;
+ /** Callback function fired when pressing a Calendar date. */
+ onPressDate?: (date: Date) => void;
+ /** Disables user interaction. */
+ disabled?: boolean;
+ /** Hides the Calendar next and previous month arrows, but does not prevent navigating to the next or previous months via keyboard. This probably only makes sense to be used when `minDate` and `maxDate` are set to the first and last days of the same month. */
+ hideControls?: boolean;
+ /** Array of disabled dates, and date tuples for date ranges. Make sure to set `disabledDateError` as well. A number is created for every individual date within a tuple range, so do not abuse this with massive ranges. */
+ disabledDates?: (Date | [Date, Date])[];
+ /** Array of highlighted dates, and date tuples for date ranges. A number is created for every individual date within a tuple range, so do not abuse this with massive ranges. */
+ highlightedDates?: (Date | [Date, Date])[];
+ /** Minimum date allowed to be selected, inclusive. Dates before the `minDate` are disabled. All navigation to months before the `minDate` is disabled. */
+ minDate?: Date;
+ /** Maximum date allowed to be selected, inclusive. Dates after the `maxDate` are disabled. All navigation to months after the `maxDate` is disabled. */
+ maxDate?: Date;
+ /**
+ * Tooltip content shown when hovering or focusing a disabled date, including dates before the `minDate` or after the `maxDate`.
+ * @default 'Date unavailable'
+ */
+ disabledDateError?: string;
+ /**
+ * Accessibility label describing the Calendar next month arrow.
+ * @default 'Go to next month'
+ */
+ nextArrowAccessibilityLabel?: string;
+ /**
+ * Accessibility label describing the Calendar previous month arrow.
+ * @default 'Go to previous month'
+ */
+ previousArrowAccessibilityLabel?: string;
+ /** Used to locate this element in unit and end-to-end tests. */
+ testID?: string;
+ /** Custom style to apply to the Calendar container. */
+ style?: StyleProp;
+};
+
+export type CalendarProps = CalendarBaseProps & Omit;
+
+// These could be dynamically generated, but our Calendar and DatePicker aren't localized so there's no point
+const daysOfWeek = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
+
+export const Calendar = memo(
+ forwardRef(
+ (
+ {
+ selectedDate,
+ seedDate,
+ onPressDate,
+ disabled,
+ hideControls,
+ disabledDates,
+ highlightedDates,
+ minDate,
+ maxDate,
+ disabledDateError = 'Date unavailable',
+ nextArrowAccessibilityLabel = 'Go to next month',
+ previousArrowAccessibilityLabel = 'Go to previous month',
+ testID,
+ style,
+ ...props
+ },
+ ref,
+ ) => {
+ const { setA11yFocus } = useA11y();
+ const today = useMemo(() => getMidnightDate(new Date()), []);
+
+ // Determine default calendar seed date: use whichever comes first between maxDate and today
+ const defaultSeedDate = useMemo(() => {
+ if (selectedDate) {
+ return selectedDate;
+ }
+ if (seedDate) {
+ return seedDate;
+ }
+ if (maxDate) {
+ const maxDateTime = getMidnightDate(maxDate).getTime();
+ const todayTime = today.getTime();
+ return maxDateTime < todayTime ? maxDate : today;
+ }
+ return today;
+ }, [selectedDate, seedDate, maxDate, today]);
+
+ const [calendarSeedDate, setCalendarSeedDate] = useState(defaultSeedDate);
+
+ // Refs to track date buttons for focus management
+ const dateRefs = useRef