diff --git a/site/docs/pages/token/token-select-dropdown.mdx b/site/docs/pages/token/token-select-dropdown.mdx
new file mode 100644
index 0000000000..83bdf78fcf
--- /dev/null
+++ b/site/docs/pages/token/token-select-dropdown.mdx
@@ -0,0 +1,156 @@
+import { TokenSelectDropdown } from '../../../../src/token';
+import TokenSelectorContainer from '../../components/TokenSelectorContainer.tsx';
+import App from '../App';
+
+# ``
+
+The `TokenSelectDropdown` component is a dropdown component that selects a token in a given list of tokens.
+
+## Usage
+
+:::code-group
+
+```tsx [code]
+ // [!code focus]
+```
+
+```html [return html]
+
+```
+
+:::
+
+## Props
+
+[`TokenSelectDropdownReact`](/token/types#tokenselectdropdownreact)
+
+## CSS
+
+```css
+.ock-scrollbar {
+ scrollbar-width: thin;
+ scrollbar-color: #d1d5db #ffffff;
+}
+```
diff --git a/site/docs/pages/token/types.mdx b/site/docs/pages/token/types.mdx
index 47eac5abad..51988d18c5 100644
--- a/site/docs/pages/token/types.mdx
+++ b/site/docs/pages/token/types.mdx
@@ -89,12 +89,12 @@ export type TokenSearchReact = {
};
```
-## `TokenSelectorDropdownReact`
+## `TokenSearchReact`
```ts
-export type TokenSelectorDropdownReact = {
- onToggle: () => void; // Injected by TokenSelector. To be removed.
+export type TokenSelectDropdownReact = {
options: Token[]; // List of tokens
setToken: (token: Token) => void; // Token setter
+ token?: Token; // Selected token
};
```
diff --git a/site/sidebar.ts b/site/sidebar.ts
index e63fee2e98..caae3a3642 100644
--- a/site/sidebar.ts
+++ b/site/sidebar.ts
@@ -172,7 +172,7 @@ export const sidebar = [
{
text: 'TokenSelectDropdown',
link: '/token/token-select-dropdown',
- }
+ },
],
},
{
diff --git a/src/token/components/TokenSelectDropdown.css b/src/token/components/TokenSelectDropdown.css
new file mode 100644
index 0000000000..f1f8a88275
--- /dev/null
+++ b/src/token/components/TokenSelectDropdown.css
@@ -0,0 +1,4 @@
+.ock-scrollbar {
+ scrollbar-width: thin;
+ scrollbar-color: #d1d5db #ffffff;
+}
diff --git a/src/token/components/TokenSelectDropdown.test.tsx b/src/token/components/TokenSelectDropdown.test.tsx
new file mode 100644
index 0000000000..252d6f3e38
--- /dev/null
+++ b/src/token/components/TokenSelectDropdown.test.tsx
@@ -0,0 +1,71 @@
+/**
+ * @jest-environment jsdom
+ */
+import React from 'react';
+import '@testing-library/jest-dom';
+import { render, screen, fireEvent, waitFor } from '@testing-library/react';
+import { Address } from 'viem';
+import { TokenSelectDropdown } from './TokenSelectDropdown';
+import { Token } from '../types';
+
+describe('TokenSelectDropdown', () => {
+ const setToken = jest.fn();
+ const options: Token[] = [
+ {
+ name: 'Ethereum',
+ address: '' as Address,
+ symbol: 'ETH',
+ decimals: 18,
+ image: 'https://wallet-api-production.s3.amazonaws.com/uploads/tokens/eth_288.png',
+ chainId: 8453,
+ },
+ {
+ name: 'USDC',
+ address: '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913' as Address,
+ symbol: 'USDC',
+ decimals: 6,
+ image:
+ 'https://d3r81g40ycuhqg.cloudfront.net/wallet/wais/44/2b/442b80bd16af0c0d9b22e03a16753823fe826e5bfd457292b55fa0ba8c1ba213-ZWUzYjJmZGUtMDYxNy00NDcyLTg0NjQtMWI4OGEwYjBiODE2',
+ chainId: 8453,
+ },
+ ];
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('renders the TokenSelectDropdown component', async () => {
+ render();
+
+ await waitFor(() => {
+ const button = screen.getByTestId('ockTokenSelectButton_Button');
+ const list = screen.queryByTestId('ockTokenSelectDropdown_List');
+ expect(button).toBeInTheDocument();
+ expect(list).toBeNull();
+ });
+ });
+
+ it('calls setToken when clicking on a token', async () => {
+ render();
+
+ const button = screen.getByTestId('ockTokenSelectButton_Button');
+ fireEvent.click(button);
+
+ await waitFor(() => {
+ fireEvent.click(screen.getByText(options[0].name));
+
+ expect(setToken).toHaveBeenCalledWith(options[0]);
+ });
+ });
+
+ it('calls onToggle when clicking outside the component', async () => {
+ render();
+
+ const button = screen.getByTestId('ockTokenSelectButton_Button');
+ fireEvent.click(button);
+
+ const result = screen.getByTestId('ockTokenSelectDropdown_List');
+ console.log(result);
+ // expect(screen.getByTestId('ockTokenSelectDropdown_List')).toBeInTheDocument();
+ });
+});
diff --git a/src/token/components/TokenSelectDropdown.tsx b/src/token/components/TokenSelectDropdown.tsx
new file mode 100644
index 0000000000..bcde1f5b2f
--- /dev/null
+++ b/src/token/components/TokenSelectDropdown.tsx
@@ -0,0 +1,68 @@
+import { useCallback, useEffect, useRef, useState } from 'react';
+import { TokenSelectDropdownReact } from '../types';
+import { TokenRow } from './TokenRow';
+import { TokenSelectButton } from './TokenSelectButton';
+import { cn } from '../../lib/utils';
+
+export function TokenSelectDropdown({ options, setToken, token }: TokenSelectDropdownReact) {
+ const [isOpen, setIsOpen] = useState(false);
+
+ const handleToggle = useCallback(() => {
+ setIsOpen(!isOpen);
+ }, [isOpen]);
+
+ const dropdownRef = useRef(null);
+ const buttonRef = useRef(null);
+
+ /* istanbul ignore next */
+ const handleBlur = useCallback((event: MouseEvent) => {
+ const isOutsideDropdown =
+ dropdownRef.current && !dropdownRef.current.contains(event.target as Node);
+ const isOutsideButton = buttonRef.current && !buttonRef.current.contains(event.target as Node);
+
+ if (isOutsideDropdown && isOutsideButton) {
+ setIsOpen(false);
+ }
+ }, []);
+
+ useEffect(() => {
+ // NOTE: this ensures that handleBlur doesn't get called on initial mount
+ // We need to use non-div elements to properly handle onblur events
+ setTimeout(() => {
+ document.addEventListener('click', handleBlur);
+ }, 0);
+
+ return () => {
+ document.removeEventListener('click', handleBlur);
+ };
+ }, []);
+
+ return (
+
+
+ {isOpen && (
+
+
+ {options.map((token) => (
+ {
+ setToken(token);
+ handleToggle();
+ }}
+ />
+ ))}
+
+
+ )}
+
+ );
+}
diff --git a/src/token/index.ts b/src/token/index.ts
index 692ed0ba84..29a460084a 100644
--- a/src/token/index.ts
+++ b/src/token/index.ts
@@ -3,6 +3,7 @@ export { TokenChip } from './components/TokenChip';
export { TokenImage } from './components/TokenImage';
export { TokenRow } from './components/TokenRow';
export { TokenSearch } from './components/TokenSearch';
+export { TokenSelectDropdown } from './components/TokenSelectDropdown';
export { TokenSelectorDropdown } from './components/TokenSelectorDropdown';
export { formatAmount } from './core/formatAmount';
export { getTokens } from './core/getTokens';
@@ -13,4 +14,5 @@ export type {
Token,
TokenChipReact,
TokenRowReact,
+ TokenSelectDropdownReact,
} from './types';
diff --git a/src/token/types.ts b/src/token/types.ts
index d0449b4fca..7827d63dbf 100644
--- a/src/token/types.ts
+++ b/src/token/types.ts
@@ -94,6 +94,15 @@ export type TokenSelectButtonReact = {
token?: Token; // Selected token
};
+/**
+ * Note: exported as public Type
+ */
+export type TokenSelectDropdownReact = {
+ options: Token[]; // List of tokens
+ setToken: (token: Token) => void; // Token setter
+ token?: Token; // Selected token
+};
+
/**
* Note: exported as public Type
*/