From 08703cfe00e1fe4f8ae6851f35e62f0daf59a3bc Mon Sep 17 00:00:00 2001 From: Paul Cramer Date: Sun, 25 Aug 2024 22:05:39 -0700 Subject: [PATCH] Add button type prop --- .changeset/clean-mirrors-sparkle.md | 5 - site/docs/pages/swap/types.mdx | 10 -- src/swap/components/Swap.tsx | 29 ++-- src/swap/components/SwapSettings.test.tsx | 47 ------- src/swap/components/SwapSettings.tsx | 128 ------------------ .../components/SwapSettingsBottomSheet.tsx | 0 .../SwapSettingsSlippageLayout.test.tsx | 100 ++++++++++++++ .../components/SwapSettingsSlippageLayout.tsx | 40 ++++++ ...SettingsSlippageLayoutBottomSheet.test.tsx | 69 ++++++++++ .../SwapSettingsSlippageLayoutBottomSheet.tsx | 48 +++++++ src/swap/index.ts | 4 +- src/swap/types.ts | 16 +++ 12 files changed, 286 insertions(+), 210 deletions(-) delete mode 100644 .changeset/clean-mirrors-sparkle.md delete mode 100644 src/swap/components/SwapSettings.test.tsx delete mode 100644 src/swap/components/SwapSettings.tsx delete mode 100644 src/swap/components/SwapSettingsBottomSheet.tsx create mode 100644 src/swap/components/SwapSettingsSlippageLayout.test.tsx create mode 100644 src/swap/components/SwapSettingsSlippageLayout.tsx create mode 100644 src/swap/components/SwapSettingsSlippageLayoutBottomSheet.test.tsx create mode 100644 src/swap/components/SwapSettingsSlippageLayoutBottomSheet.tsx diff --git a/.changeset/clean-mirrors-sparkle.md b/.changeset/clean-mirrors-sparkle.md deleted file mode 100644 index 81c21d35b..000000000 --- a/.changeset/clean-mirrors-sparkle.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@coinbase/onchainkit": patch ---- - -**feat**: Add SwapSettings sub-component. This allows the user to customize their swaps max slippage percentage. If max slippage is not specified then the component will default to a max slippage of 10%. by @0xAlec @cpcramer #1051 diff --git a/site/docs/pages/swap/types.mdx b/site/docs/pages/swap/types.mdx index d5cb7333f..73beff276 100644 --- a/site/docs/pages/swap/types.mdx +++ b/site/docs/pages/swap/types.mdx @@ -180,16 +180,6 @@ type SwapReact = { }; ``` -## `SwapSettingsReact` - -```ts -type SwapSettingsReact = { - className?: string; // Optional className override for top div element. - icon?: ReactNode; // Optional icon override - text?: string; // Optional text override -}; -``` - ## `SwapToggleButtonReact` ```ts diff --git a/src/swap/components/Swap.tsx b/src/swap/components/Swap.tsx index 9fe8ca074..f0914690e 100644 --- a/src/swap/components/Swap.tsx +++ b/src/swap/components/Swap.tsx @@ -7,7 +7,6 @@ import { SwapAmountInput } from './SwapAmountInput'; import { SwapButton } from './SwapButton'; import { SwapMessage } from './SwapMessage'; import { SwapProvider } from './SwapProvider'; -import { SwapSettings } from './SwapSettings'; import { SwapToggleButton } from './SwapToggleButton'; export function Swap({ @@ -19,17 +18,15 @@ export function Swap({ onSuccess, title = 'Swap', }: SwapReact) { - const { inputs, toggleButton, swapButton, swapMessage, swapSettings } = - useMemo(() => { - const childrenArray = Children.toArray(children); - return { - inputs: childrenArray.filter(findComponent(SwapAmountInput)), - toggleButton: childrenArray.find(findComponent(SwapToggleButton)), - swapButton: childrenArray.find(findComponent(SwapButton)), - swapMessage: childrenArray.find(findComponent(SwapMessage)), - swapSettings: childrenArray.find(findComponent(SwapSettings)), - }; - }, [children]); + const { inputs, toggleButton, swapButton, swapMessage } = useMemo(() => { + const childrenArray = Children.toArray(children); + return { + inputs: childrenArray.filter(findComponent(SwapAmountInput)), + toggleButton: childrenArray.find(findComponent(SwapToggleButton)), + swapButton: childrenArray.find(findComponent(SwapButton)), + swapMessage: childrenArray.find(findComponent(SwapMessage)), + }; + }, [children]); const isMounted = useIsMounted(); @@ -53,14 +50,10 @@ export function Swap({ )} data-testid="ockSwap_Container" > -
-

+
+

{title}

-
{swapSettings}
{inputs[0]}
{toggleButton}
diff --git a/src/swap/components/SwapSettings.test.tsx b/src/swap/components/SwapSettings.test.tsx deleted file mode 100644 index 010c245d7..000000000 --- a/src/swap/components/SwapSettings.test.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { render, screen } from '@testing-library/react'; -import { describe, expect, it } from 'vitest'; -import { useBreakpoints } from '../../useBreakpoints'; -import { SwapSettings } from './SwapSettings'; - -vi.mock('../../useBreakpoints', () => ({ - useBreakpoints: vi.fn(), -})); - -const useBreakpointsMock = useBreakpoints as vi.Mock; - -describe('SwapSettings', () => { - it('renders with default title', () => { - useBreakpointsMock.mockReturnValue('md'); - render(); - const settingsContainer = screen.getByTestId('ockSwapSettings_Settings'); - expect(settingsContainer.textContent).toBe(''); - }); - - it('renders with custom title', () => { - useBreakpointsMock.mockReturnValue('md'); - render(); - expect(screen.getByText('Custom')).toBeInTheDocument(); - }); - - it('renders custom icon when provided', () => { - useBreakpointsMock.mockReturnValue('md'); - const CustomIcon = () => ( - - ); - render(} />); - expect(screen.getByTestId('custom-icon')).toBeInTheDocument(); - }); - - it('applies correct classes to the button', () => { - useBreakpointsMock.mockReturnValue('md'); - render(); - const button = screen.getByRole('button', { - name: /toggle swap settings/i, - }); - expect(button).toHaveClass( - 'rounded-full p-2 opacity-50 transition-opacity hover:opacity-100', - ); - }); -}); diff --git a/src/swap/components/SwapSettings.tsx b/src/swap/components/SwapSettings.tsx deleted file mode 100644 index bdff23790..000000000 --- a/src/swap/components/SwapSettings.tsx +++ /dev/null @@ -1,128 +0,0 @@ -import { useCallback, useState } from 'react'; -import { - background, - cn, - color, - pressable, - text as themeText, -} from '../../styles/theme'; -import { useBreakpoints } from '../../useBreakpoints'; -import { useIcon } from '../../wallet/hooks/useIcon'; -import type { SwapSettingsReact } from '../types'; - -export function SwapSettings({ - className, - icon = 'swapSettings', - text = '', -}: SwapSettingsReact) { - const [isOpen, setIsOpen] = useState(false); - const [slippageMode, setSlippageMode] = useState<'Auto' | 'Custom'>('Auto'); - const [customSlippage, setCustomSlippage] = useState('0.5'); - const breakpoint = useBreakpoints(); - - const handleToggle = useCallback(() => { - setIsOpen(!isOpen); - }, [isOpen]); - - const iconSvg = useIcon({ icon }); - - if (!breakpoint) { - return null; - } - - // Placeholder for SwapSettingsBottomSheet - // Implement mobile version here, similar to WalletBottomSheet - if (breakpoint === 'sm') { - return
Mobile version not implemented
; - } - - return ( -
- {text} -
- - {isOpen && ( -
-
-

- Max. slippage -

-

- Your swap will revert if the prices change by more than the - selected percentage. -

-
-
- - -
-
- setCustomSlippage(e.target.value)} - className={cn( - background.default, - 'w-16 rounded-l-md border-t border-b border-l px-2 py-1 text-left', - )} - disabled={slippageMode === 'Auto'} - /> - - % - -
-
-
-
- )} -
-
- ); -} diff --git a/src/swap/components/SwapSettingsBottomSheet.tsx b/src/swap/components/SwapSettingsBottomSheet.tsx deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/swap/components/SwapSettingsSlippageLayout.test.tsx b/src/swap/components/SwapSettingsSlippageLayout.test.tsx new file mode 100644 index 000000000..b1ab9b10f --- /dev/null +++ b/src/swap/components/SwapSettingsSlippageLayout.test.tsx @@ -0,0 +1,100 @@ +import { render, screen } from '@testing-library/react'; +import type React from 'react'; +import { describe, expect, it, vi } from 'vitest'; +import { SwapSettingsSlippageDescription } from './SwapSettingsSlippageDescription'; +import { SwapSettingsSlippageInput } from './SwapSettingsSlippageInput'; +import { SwapSettingsSlippageLayout } from './SwapSettingsSlippageLayout'; +import { SwapSettingsSlippageTitle } from './SwapSettingsSlippageTitle'; + +vi.mock('./SwapSettingsSlippageTitle', () => ({ + SwapSettingsSlippageTitle: ({ children }: { children: React.ReactNode }) => ( +
{children}
+ ), +})); + +vi.mock('./SwapSettingsSlippageDescription', () => ({ + SwapSettingsSlippageDescription: ({ + children, + }: { children: React.ReactNode }) => ( +
{children}
+ ), +})); + +vi.mock('./SwapSettingsSlippageInput', () => ({ + SwapSettingsSlippageInput: () =>
Input
, +})); + +vi.mock('../../styles/theme', () => ({ + cn: (...args: string[]) => args.join(' '), +})); + +describe('SwapSettingsSlippageLayout', () => { + it('renders with all child components', () => { + render( + + Title + + Description + + + , + ); + expect( + screen.getByTestId('ockSwapSettingsLayout_container'), + ).toBeInTheDocument(); + expect(screen.getByTestId('mock-title')).toBeInTheDocument(); + expect(screen.getByTestId('mock-description')).toBeInTheDocument(); + expect(screen.getByTestId('mock-input')).toBeInTheDocument(); + }); + + it('renders with only some child components', () => { + render( + + Title + , + ); + expect( + screen.getByTestId('ockSwapSettingsLayout_container'), + ).toBeInTheDocument(); + expect(screen.getByTestId('mock-title')).toBeInTheDocument(); + expect(screen.queryByTestId('mock-description')).not.toBeInTheDocument(); + expect(screen.queryByTestId('mock-input')).not.toBeInTheDocument(); + }); + + it('applies custom className', () => { + render( + + Title + , + ); + const container = screen.getByTestId('ockSwapSettingsLayout_container'); + expect(container.className).toContain('custom-class'); + }); + + it('renders without any child components', () => { + render(); + expect( + screen.getByTestId('ockSwapSettingsLayout_container'), + ).toBeInTheDocument(); + expect(screen.queryByTestId('mock-title')).not.toBeInTheDocument(); + expect(screen.queryByTestId('mock-description')).not.toBeInTheDocument(); + expect(screen.queryByTestId('mock-toggle')).not.toBeInTheDocument(); + expect(screen.queryByTestId('mock-input')).not.toBeInTheDocument(); + }); + + it('renders with correct layout structure', () => { + render( + + Title + + Description + + + , + ); + const container = screen.getByTestId('ockSwapSettingsLayout_container'); + expect(container.children[0]).toHaveTextContent('Title'); + expect(container.children[1]).toHaveTextContent('Description'); + expect(container.children[2].children[0]).toHaveTextContent('Input'); + }); +}); diff --git a/src/swap/components/SwapSettingsSlippageLayout.tsx b/src/swap/components/SwapSettingsSlippageLayout.tsx new file mode 100644 index 000000000..fd0f4d31c --- /dev/null +++ b/src/swap/components/SwapSettingsSlippageLayout.tsx @@ -0,0 +1,40 @@ +import { Children, useMemo } from 'react'; +import { findComponent } from '../../internal/utils/findComponent'; +import { cn } from '../../styles/theme'; +import type { SwapSettingsSlippageLayoutReact } from '../types'; +import { SwapSettingsSlippageDescription } from './SwapSettingsSlippageDescription'; +import { SwapSettingsSlippageInput } from './SwapSettingsSlippageInput'; +import { SwapSettingsSlippageTitle } from './SwapSettingsSlippageTitle'; + +export function SwapSettingsSlippageLayout({ + children, + className, +}: SwapSettingsSlippageLayoutReact) { + const { title, description, input } = useMemo(() => { + const childrenArray = Children.toArray(children); + return { + title: childrenArray.find(findComponent(SwapSettingsSlippageTitle)), + description: childrenArray.find( + findComponent(SwapSettingsSlippageDescription), + ), + input: childrenArray.find(findComponent(SwapSettingsSlippageInput)), + }; + }, [children]); + + return ( +
+ {title} + {description} +
+ {input &&
{input}
} +
+
+ ); +} diff --git a/src/swap/components/SwapSettingsSlippageLayoutBottomSheet.test.tsx b/src/swap/components/SwapSettingsSlippageLayoutBottomSheet.test.tsx new file mode 100644 index 000000000..b785774d9 --- /dev/null +++ b/src/swap/components/SwapSettingsSlippageLayoutBottomSheet.test.tsx @@ -0,0 +1,69 @@ +import { render, screen } from '@testing-library/react'; +import type React from 'react'; +import { describe, expect, it, vi } from 'vitest'; +import { SwapSettingsSlippageDescription } from './SwapSettingsSlippageDescription'; +import { SwapSettingsSlippageInput } from './SwapSettingsSlippageInput'; +import { SwapSettingsSlippageLayoutBottomSheet } from './SwapSettingsSlippageLayoutBottomSheet'; +import { SwapSettingsSlippageTitle } from './SwapSettingsSlippageTitle'; + +vi.mock('./SwapSettingsSlippageTitle', () => ({ + SwapSettingsSlippageTitle: ({ children }: { children: React.ReactNode }) => ( +
{children}
+ ), +})); + +vi.mock('./SwapSettingsSlippageDescription', () => ({ + SwapSettingsSlippageDescription: ({ + children, + }: { + children: React.ReactNode; + }) =>
{children}
, +})); + +vi.mock('./SwapSettingsSlippageInput', () => ({ + SwapSettingsSlippageInput: () =>
Input
, +})); + +vi.mock('../../styles/theme', () => ({ + cn: (...args: string[]) => args.join(' '), +})); + +describe('SwapSettingsSlippageLayoutBottomSheet', () => { + it('renders with all child components', () => { + render( + + Title + + Description + + + , + ); + expect( + screen.getByTestId('ockSwapSettingsLayout_container'), + ).toBeInTheDocument(); + expect(screen.getByTestId('mock-title')).toBeInTheDocument(); + expect(screen.getByTestId('mock-description')).toBeInTheDocument(); + expect(screen.getByTestId('mock-input')).toBeInTheDocument(); + }); + + it('applies custom className', () => { + render( + + Title + , + ); + const container = screen.getByTestId('ockSwapSettingsLayout_container'); + expect(container.className).toContain('custom-class'); + }); + + it('renders without any child components', () => { + render(); + expect( + screen.getByTestId('ockSwapSettingsLayout_container'), + ).toBeInTheDocument(); + expect(screen.queryByTestId('mock-title')).not.toBeInTheDocument(); + expect(screen.queryByTestId('mock-description')).not.toBeInTheDocument(); + expect(screen.queryByTestId('mock-input')).not.toBeInTheDocument(); + }); +}); diff --git a/src/swap/components/SwapSettingsSlippageLayoutBottomSheet.tsx b/src/swap/components/SwapSettingsSlippageLayoutBottomSheet.tsx new file mode 100644 index 000000000..03e19b3c0 --- /dev/null +++ b/src/swap/components/SwapSettingsSlippageLayoutBottomSheet.tsx @@ -0,0 +1,48 @@ +import { Children, useMemo } from 'react'; +import { findComponent } from '../../internal/utils/findComponent'; +import { cn } from '../../styles/theme'; +import type { SwapSettingsSlippageLayoutReact } from '../types'; +import { SwapSettingsSlippageDescription } from './SwapSettingsSlippageDescription'; +import { SwapSettingsSlippageInput } from './SwapSettingsSlippageInput'; +import { SwapSettingsSlippageTitle } from './SwapSettingsSlippageTitle'; + +export function SwapSettingsSlippageLayoutBottomSheet({ + children, + className, +}: SwapSettingsSlippageLayoutReact) { + const { title, description, input } = useMemo(() => { + const childrenArray = Children.toArray(children); + return { + title: childrenArray.find(findComponent(SwapSettingsSlippageTitle)), + description: childrenArray.find( + findComponent(SwapSettingsSlippageDescription), + ), + input: childrenArray.find(findComponent(SwapSettingsSlippageInput)), + }; + }, [children]); + + return ( +
+
+
+

Settings

+
+ +
+ {title} +
{description}
+ {input &&
{input}
} +
+
+
+
+
+ ); +} diff --git a/src/swap/index.ts b/src/swap/index.ts index 3586996a8..33682b151 100644 --- a/src/swap/index.ts +++ b/src/swap/index.ts @@ -4,9 +4,9 @@ export { SwapAmountInput } from './components/SwapAmountInput'; export { SwapButton } from './components/SwapButton'; export { SwapMessage } from './components/SwapMessage'; export { SwapSettings } from './components/SwapSettings'; +export { SwapSettingsSlippageDescription } from './components/SwapSettingsSlippageDescription'; export { SwapSettingsSlippageInput } from './components/SwapSettingsSlippageInput'; export { SwapSettingsSlippageTitle } from './components/SwapSettingsSlippageTitle'; -export { SwapSettingsSlippageDescription } from './components/SwapSettingsSlippageDescription'; export { SwapToggleButton } from './components/SwapToggleButton'; export type { BuildSwapTransaction, @@ -21,9 +21,9 @@ export type { SwapQuote, SwapReact, SwapSettingsReact, + SwapSettingsSlippageDescriptionReact, SwapSettingsSlippageInputReact, SwapSettingsSlippageTitleReact, - SwapSettingsSlippageDescriptionReact, SwapToggleButtonReact, Transaction, } from './types'; diff --git a/src/swap/types.ts b/src/swap/types.ts index 4227d9334..13d21e82c 100644 --- a/src/swap/types.ts +++ b/src/swap/types.ts @@ -160,7 +160,9 @@ export type SwapContextType = { ) => void; handleSubmit: () => void; handleToggle: () => void; + maxSlippage: number; setLifeCycleStatus: (state: LifeCycleStatus) => void; // A function to set the lifecycle status of the component + setMaxSlippage: (maxSlippage: number) => void; // A function to set the maximum slippage to: SwapUnit; }; @@ -237,6 +239,7 @@ export type SwapReact = { * Note: exported as public Type */ export type SwapSettingsReact = { + children: React.ReactNode; className?: string; // Optional className override for top div element. icon?: ReactNode; // Optional icon override text?: string; // Optional text override @@ -266,6 +269,19 @@ export type SwapSettingsSlippageInputReact = { defaultSlippage?: number; // Optional default slippage value in pecentage. }; +export type SwapSettingsSlippageLayoutReact = { + children: ReactNode; + className?: string; // Optional className override for top div element. +}; + +/** + * Note: exported as public Type + */ +export type SwapSettingsSlippageInputReact = { + className?: string; // Optional className override for top div element. + defaultSlippage?: number; // Optional default slippage value in pecentage. +}; + /** * Note: exported as public Type */