diff --git a/front/src/modules/ui/components/buttons/PrimaryButton.tsx b/front/src/modules/ui/components/buttons/PrimaryButton.tsx index fb26783e0..58922fb9b 100644 --- a/front/src/modules/ui/components/buttons/PrimaryButton.tsx +++ b/front/src/modules/ui/components/buttons/PrimaryButton.tsx @@ -2,11 +2,9 @@ import React from 'react'; import styled from '@emotion/styled'; type OwnProps = { - label: string; - icon?: React.ReactNode; + children: React.ReactNode; fullWidth?: boolean; - onClick?: () => void; -}; +} & React.ButtonHTMLAttributes; const StyledButton = styled.button<{ fullWidth: boolean }>` align-items: center; @@ -24,29 +22,20 @@ const StyledButton = styled.button<{ fullWidth: boolean }>` display: flex; flex-direction: row; font-weight: ${({ theme }) => theme.fontWeightBold}; - gap: 8px; + gap: ${({ theme }) => theme.spacing(2)}; justify-content: center; padding: ${({ theme }) => theme.spacing(2)} ${({ theme }) => theme.spacing(3)}; width: ${({ fullWidth }) => (fullWidth ? '100%' : 'auto')}; `; export function PrimaryButton({ - label, - icon, + children, fullWidth, - onClick, + ...props }: OwnProps): JSX.Element { return ( - { - if (onClick) { - onClick(); - } - }} - > - {icon} - {label} + + {children} ); } diff --git a/front/src/modules/ui/components/buttons/SecondaryButton.tsx b/front/src/modules/ui/components/buttons/SecondaryButton.tsx index af8664b7d..9c7caf000 100644 --- a/front/src/modules/ui/components/buttons/SecondaryButton.tsx +++ b/front/src/modules/ui/components/buttons/SecondaryButton.tsx @@ -2,10 +2,9 @@ import React from 'react'; import styled from '@emotion/styled'; type OwnProps = { - label: string; - icon?: React.ReactNode; + children: React.ReactNode; fullWidth?: boolean; -}; +} & React.ButtonHTMLAttributes; const StyledButton = styled.button<{ fullWidth: boolean }>` align-items: center; @@ -31,14 +30,13 @@ const StyledButton = styled.button<{ fullWidth: boolean }>` `; export function SecondaryButton({ - label, - icon, + children, fullWidth, + ...props }: OwnProps): JSX.Element { return ( - - {icon} - {label} + + {children} ); } diff --git a/front/src/modules/ui/components/buttons/TertiaryButton.tsx b/front/src/modules/ui/components/buttons/TertiaryButton.tsx deleted file mode 100644 index dfaccc9e6..000000000 --- a/front/src/modules/ui/components/buttons/TertiaryButton.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import styled from '@emotion/styled'; - -type OwnProps = { - text: string; -}; - -const StyledButton = styled.button``; - -export function TertiaryButton({ text }: OwnProps): JSX.Element { - return {text}; -} diff --git a/front/src/modules/ui/components/buttons/__stories__/IconButton.stories.tsx b/front/src/modules/ui/components/buttons/__stories__/IconButton.stories.tsx new file mode 100644 index 000000000..b875f901e --- /dev/null +++ b/front/src/modules/ui/components/buttons/__stories__/IconButton.stories.tsx @@ -0,0 +1,33 @@ +import { expect, jest } from '@storybook/jest'; +import type { Meta, StoryObj } from '@storybook/react'; +import { userEvent, within } from '@storybook/testing-library'; + +import { IconArrowRight } from '@/ui/icons'; +import { getRenderWrapperForComponent } from '~/testing/renderWrappers'; + +import { IconButton } from '../IconButton'; + +const meta: Meta = { + title: 'UI/Buttons/IconButton', + component: IconButton, +}; + +export default meta; +type Story = StoryObj; + +const clickJestFn = jest.fn(); + +export const Default: Story = { + render: getRenderWrapperForComponent( + } />, + ), + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + expect(clickJestFn).toHaveBeenCalledTimes(0); + const button = canvas.getByRole('button'); + await userEvent.click(button); + + expect(clickJestFn).toHaveBeenCalledTimes(1); + }, +}; diff --git a/front/src/modules/ui/components/buttons/__stories__/PrimaryButton.stories.tsx b/front/src/modules/ui/components/buttons/__stories__/PrimaryButton.stories.tsx new file mode 100644 index 000000000..81d3c02b8 --- /dev/null +++ b/front/src/modules/ui/components/buttons/__stories__/PrimaryButton.stories.tsx @@ -0,0 +1,47 @@ +import { expect, jest } from '@storybook/jest'; +import type { Meta, StoryObj } from '@storybook/react'; +import { userEvent, within } from '@storybook/testing-library'; + +import { IconBrandGoogle } from '@/ui/icons'; +import { getRenderWrapperForComponent } from '~/testing/renderWrappers'; + +import { PrimaryButton } from '../PrimaryButton'; + +const meta: Meta = { + title: 'UI/Buttons/PrimaryButton', + component: PrimaryButton, +}; + +export default meta; +type Story = StoryObj; + +const clickJestFn = jest.fn(); + +export const Default: Story = { + render: getRenderWrapperForComponent( + A Primary Button, + ), + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + expect(clickJestFn).toHaveBeenCalledTimes(0); + const button = canvas.getByRole('button'); + await userEvent.click(button); + + expect(clickJestFn).toHaveBeenCalledTimes(1); + }, +}; + +export const WithIcon: Story = { + render: getRenderWrapperForComponent( + + A Primary Button + , + ), +}; + +export const FullWidth: Story = { + render: getRenderWrapperForComponent( + A Primary Button, + ), +}; diff --git a/front/src/modules/ui/components/buttons/__stories__/SecondaryButton.stories.tsx b/front/src/modules/ui/components/buttons/__stories__/SecondaryButton.stories.tsx new file mode 100644 index 000000000..64b35eeaf --- /dev/null +++ b/front/src/modules/ui/components/buttons/__stories__/SecondaryButton.stories.tsx @@ -0,0 +1,47 @@ +import { expect, jest } from '@storybook/jest'; +import type { Meta, StoryObj } from '@storybook/react'; +import { userEvent, within } from '@storybook/testing-library'; + +import { IconBrandGoogle } from '@/ui/icons'; +import { getRenderWrapperForComponent } from '~/testing/renderWrappers'; + +import { SecondaryButton } from '../SecondaryButton'; + +const meta: Meta = { + title: 'UI/Buttons/SecondaryButton', + component: SecondaryButton, +}; + +export default meta; +type Story = StoryObj; + +const clickJestFn = jest.fn(); + +export const Default: Story = { + render: getRenderWrapperForComponent( + A Primary Button, + ), + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + expect(clickJestFn).toHaveBeenCalledTimes(0); + const button = canvas.getByRole('button'); + await userEvent.click(button); + + expect(clickJestFn).toHaveBeenCalledTimes(1); + }, +}; + +export const WithIcon: Story = { + render: getRenderWrapperForComponent( + + A Primary Button + , + ), +}; + +export const FullWidth: Story = { + render: getRenderWrapperForComponent( + A Primary Button, + ), +}; diff --git a/front/src/modules/ui/components/editable-cell/types/EditablePhone.tsx b/front/src/modules/ui/components/editable-cell/types/EditablePhone.tsx index 97448418f..e9da7ad30 100644 --- a/front/src/modules/ui/components/editable-cell/types/EditablePhone.tsx +++ b/front/src/modules/ui/components/editable-cell/types/EditablePhone.tsx @@ -4,7 +4,7 @@ import { isValidPhoneNumber, parsePhoneNumber } from 'libphonenumber-js'; import { textInputStyle } from '@/ui/layout/styles/themes'; -import Link from '../../link/Link'; +import { RawLink } from '../../links/RawLink'; import { EditableCell } from '../EditableCell'; type OwnProps = { @@ -50,7 +50,7 @@ export function EditablePhone({ value, placeholder, changeHandler }: OwnProps) { nonEditModeContent={
{isValidPhoneNumber(inputValue) ? ( - ) => { event.stopPropagation(); @@ -58,9 +58,9 @@ export function EditablePhone({ value, placeholder, changeHandler }: OwnProps) { > {parsePhoneNumber(inputValue, 'FR')?.formatInternational() || inputValue} - + ) : ( - {inputValue} + {inputValue} )}
} diff --git a/front/src/modules/ui/components/inputs/TextInput.tsx b/front/src/modules/ui/components/inputs/TextInput.tsx index 064382cb9..308a1990c 100644 --- a/front/src/modules/ui/components/inputs/TextInput.tsx +++ b/front/src/modules/ui/components/inputs/TextInput.tsx @@ -2,8 +2,9 @@ import { ChangeEvent, useState } from 'react'; import styled from '@emotion/styled'; type OwnProps = { - initialValue: string; - onChange: (text: string) => void; + value: string; + onChange?: (text: string) => void; + placeholder?: string; fullWidth?: boolean; }; @@ -29,20 +30,23 @@ const StyledInput = styled.input<{ fullWidth: boolean }>` `; export function TextInput({ - initialValue, + value, onChange, + placeholder, fullWidth, }: OwnProps): JSX.Element { - const [value, setValue] = useState(initialValue); + const [internalValue, setInternalValue] = useState(value); return ( ) => { - setValue(event.target.value); - onChange(event.target.value); + setInternalValue(event.target.value); + if (onChange) { + onChange(event.target.value); + } }} /> ); diff --git a/front/src/modules/ui/components/inputs/__stories__/TextInput.stories.tsx b/front/src/modules/ui/components/inputs/__stories__/TextInput.stories.tsx new file mode 100644 index 000000000..4e0476f52 --- /dev/null +++ b/front/src/modules/ui/components/inputs/__stories__/TextInput.stories.tsx @@ -0,0 +1,52 @@ +import { expect } from '@storybook/jest'; +import { jest } from '@storybook/jest'; +import type { Meta, StoryObj } from '@storybook/react'; +import { userEvent, within } from '@storybook/testing-library'; + +import { getRenderWrapperForComponent } from '~/testing/renderWrappers'; + +import { TextInput } from '../TextInput'; + +const meta: Meta = { + title: 'UI/Inputs/TextInput', + component: TextInput, +}; + +export default meta; +type Story = StoryObj; + +const changeJestFn = jest.fn(); + +export const Default: Story = { + render: getRenderWrapperForComponent( + , + ), + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + expect(changeJestFn).toHaveBeenCalledTimes(0); + const input = canvas.getByRole('textbox'); + await userEvent.type(input, 'cou'); + + expect(changeJestFn).toHaveBeenNthCalledWith(1, 'A good value c'); + expect(changeJestFn).toHaveBeenNthCalledWith(2, 'A good value co'); + expect(changeJestFn).toHaveBeenNthCalledWith(3, 'A good value cou'); + }, +}; + +export const Placeholder: Story = { + render: getRenderWrapperForComponent( + , + ), +}; + +export const FullWidth: Story = { + render: getRenderWrapperForComponent( + , + ), +}; diff --git a/front/src/modules/ui/components/links/PrimaryLink.tsx b/front/src/modules/ui/components/links/PrimaryLink.tsx new file mode 100644 index 000000000..da65cdea6 --- /dev/null +++ b/front/src/modules/ui/components/links/PrimaryLink.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import { Link as ReactLink } from 'react-router-dom'; +import styled from '@emotion/styled'; + +type OwnProps = { + children: React.ReactNode; + href: string; + onClick?: () => void; + fullWidth?: boolean; +}; + +const StyledClickable = styled.div` + display: flex; + a { + color: ${({ theme }) => theme.text40}; + font-size: ${({ theme }) => theme.fontSizeSmall}; + text-decoration: none; + } +`; + +export function PrimaryLink({ href, children, onClick }: OwnProps) { + return ( + + + {children} + + + ); +} diff --git a/front/src/modules/ui/components/link/Link.tsx b/front/src/modules/ui/components/links/RawLink.tsx similarity index 87% rename from front/src/modules/ui/components/link/Link.tsx rename to front/src/modules/ui/components/links/RawLink.tsx index 1f5b936e8..70266f024 100644 --- a/front/src/modules/ui/components/link/Link.tsx +++ b/front/src/modules/ui/components/links/RawLink.tsx @@ -17,7 +17,7 @@ const StyledClickable = styled.div` } `; -function Link({ href, children, onClick }: OwnProps) { +export function RawLink({ href, children, onClick }: OwnProps) { return ( @@ -26,5 +26,3 @@ function Link({ href, children, onClick }: OwnProps) { ); } - -export default Link; diff --git a/front/src/modules/ui/components/links/__stories__/PrimaryLink.stories.tsx b/front/src/modules/ui/components/links/__stories__/PrimaryLink.stories.tsx new file mode 100644 index 000000000..f6c5962de --- /dev/null +++ b/front/src/modules/ui/components/links/__stories__/PrimaryLink.stories.tsx @@ -0,0 +1,37 @@ +import { MemoryRouter } from 'react-router-dom'; +import { expect, jest } from '@storybook/jest'; +import type { Meta, StoryObj } from '@storybook/react'; +import { userEvent, within } from '@storybook/testing-library'; + +import { getRenderWrapperForComponent } from '~/testing/renderWrappers'; + +import { PrimaryLink } from '../PrimaryLink'; + +const meta: Meta = { + title: 'UI/Links/PrimaryLink', + component: PrimaryLink, +}; + +export default meta; +type Story = StoryObj; + +const clickJestFn = jest.fn(); + +export const Default: Story = { + render: getRenderWrapperForComponent( + + + A primary link + + , + ), + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + expect(clickJestFn).toHaveBeenCalledTimes(0); + const link = canvas.getByRole('link'); + await userEvent.click(link); + + expect(clickJestFn).toHaveBeenCalledTimes(1); + }, +}; diff --git a/front/src/pages/auth/Index.tsx b/front/src/pages/auth/Index.tsx index 51c965216..ec2fd467a 100644 --- a/front/src/pages/auth/Index.tsx +++ b/front/src/pages/auth/Index.tsx @@ -45,19 +45,17 @@ export function Index() { <StyledContentContainer> - <PrimaryButton - fullWidth={true} - label="Continue With Google" - icon={<IconBrandGoogle size={theme.iconSizeSmall} stroke={4} />} - onClick={onGoogleLoginClick} - /> + <PrimaryButton fullWidth={true} onClick={onGoogleLoginClick}> + <IconBrandGoogle size={theme.iconSizeSmall} stroke={4} /> + Continue With Google + </PrimaryButton> <HorizontalSeparator /> <TextInput initialValue="" onChange={(value) => console.log(value)} fullWidth={true} /> - <SecondaryButton label="Continue" fullWidth={true} /> + <SecondaryButton fullWidth={true}>Continue</SecondaryButton> </StyledContentContainer> <FooterNote> By using Twenty, you agree to the Terms of Service and Data Processing diff --git a/front/src/testing/ComponentStorybookLayout.tsx b/front/src/testing/ComponentStorybookLayout.tsx index 18f0e1e35..0ac897dc7 100644 --- a/front/src/testing/ComponentStorybookLayout.tsx +++ b/front/src/testing/ComponentStorybookLayout.tsx @@ -2,12 +2,14 @@ import styled from '@emotion/styled'; const StyledLayout = styled.div` background: ${(props) => props.theme.primaryBackground}; + border: 1px solid ${(props) => props.theme.lightBorder}; border-radius: 5px; - box-shadow: 0px 0px 2px; - display: flex; + display: flex; flex-direction: row; + height: fit-content; + min-width: 300px; padding: 20px; width: fit-content; `;