@@ -1,5 +1,11 @@ | |||||
export type Type = 'submit' | 'reset' | 'button'; | |||||
export const AVAILABLE_TYPES = ['submit', 'reset', 'button'] as const; | |||||
export type Variant = 'bare' | 'filled' | 'outline'; | |||||
export type Type = typeof AVAILABLE_TYPES[number]; | |||||
export type Size = 'small' | 'medium' | 'large'; | |||||
export const AVAILABLE_VARIANTS = ['bare', 'filled', 'outline'] as const; | |||||
export type Variant = typeof AVAILABLE_VARIANTS[number]; | |||||
export const AVAILABLE_SIZES = ['small', 'medium', 'large'] as const; | |||||
export type Size = typeof AVAILABLE_SIZES[number]; |
@@ -1,7 +1,15 @@ | |||||
export type Size = 'small' | 'medium' | 'large'; | |||||
export const AVAILABLE_SIZES = ['small', 'medium', 'large'] as const; | |||||
export type Variant = 'default' | 'alternate'; | |||||
export type Size = typeof AVAILABLE_SIZES[number]; | |||||
export type InputType = 'text' | 'search'; | |||||
export const AVAILABLE_VARIANTS = ['default', 'alternate'] as const; | |||||
export type InputMode = 'none' | 'numeric' | 'decimal' | InputType; | |||||
export type Variant = typeof AVAILABLE_VARIANTS[number]; | |||||
export const AVAILABLE_INPUT_TYPES = ['text', 'search'] as const; | |||||
export type InputType = typeof AVAILABLE_INPUT_TYPES[number]; | |||||
export const AVAILABLE_INPUT_MODES = ['none', 'numeric', 'decimal', ...AVAILABLE_INPUT_TYPES] as const; | |||||
export type InputMode = typeof AVAILABLE_INPUT_MODES[number]; |
@@ -16,6 +16,7 @@ import { | |||||
import matchers from '@testing-library/jest-dom/matchers'; | import matchers from '@testing-library/jest-dom/matchers'; | ||||
import { | import { | ||||
ActionButton, | ActionButton, | ||||
ActionButtonDerivedElement, | |||||
} from '.'; | } from '.'; | ||||
expect.extend(matchers); | expect.extend(matchers); | ||||
@@ -29,7 +30,7 @@ describe('ActionButton', () => { | |||||
render( | render( | ||||
<ActionButton />, | <ActionButton />, | ||||
); | ); | ||||
const button: HTMLButtonElement = screen.getByRole('button'); | |||||
const button = screen.getByRole('button'); | |||||
expect(button).toBeInTheDocument(); | expect(button).toBeInTheDocument(); | ||||
expect(button).toHaveProperty('type', 'button'); | expect(button).toHaveProperty('type', 'button'); | ||||
}); | }); | ||||
@@ -40,7 +41,7 @@ describe('ActionButton', () => { | |||||
subtext="subtext" | subtext="subtext" | ||||
/>, | />, | ||||
); | ); | ||||
const subtext: HTMLElement = screen.getByTestId('subtext'); | |||||
const subtext = screen.getByTestId('subtext'); | |||||
expect(subtext).toBeInTheDocument(); | expect(subtext).toBeInTheDocument(); | ||||
}); | }); | ||||
@@ -50,7 +51,7 @@ describe('ActionButton', () => { | |||||
badge="badge" | badge="badge" | ||||
/>, | />, | ||||
); | ); | ||||
const badge: HTMLElement = screen.getByTestId('badge'); | |||||
const badge = screen.getByTestId('badge'); | |||||
expect(badge).toBeInTheDocument(); | expect(badge).toBeInTheDocument(); | ||||
}); | }); | ||||
@@ -60,21 +61,23 @@ describe('ActionButton', () => { | |||||
menuItem | menuItem | ||||
/>, | />, | ||||
); | ); | ||||
const menuItemIndicator: HTMLElement = screen.getByTestId('menuItemIndicator'); | |||||
const menuItemIndicator = screen.getByTestId('menuItemIndicator'); | |||||
expect(menuItemIndicator).toBeInTheDocument(); | expect(menuItemIndicator).toBeInTheDocument(); | ||||
}); | }); | ||||
it('handles click events', async () => { | it('handles click events', async () => { | ||||
const onClick = vi.fn().mockImplementationOnce((e: React.MouseEvent) => { | |||||
e.preventDefault(); | |||||
}); | |||||
const onClick = vi.fn().mockImplementationOnce( | |||||
(e: React.MouseEvent<ActionButtonDerivedElement>) => { | |||||
e.preventDefault(); | |||||
}, | |||||
); | |||||
render( | render( | ||||
<ActionButton | <ActionButton | ||||
onClick={onClick} | onClick={onClick} | ||||
/>, | />, | ||||
); | ); | ||||
const button: HTMLButtonElement = screen.getByRole('button'); | |||||
const button = screen.getByRole('button'); | |||||
await userEvent.click(button); | await userEvent.click(button); | ||||
expect(onClick).toBeCalled(); | expect(onClick).toBeCalled(); | ||||
}); | }); | ||||
@@ -86,7 +89,7 @@ describe('ActionButton', () => { | |||||
/>, | />, | ||||
); | ); | ||||
const button: HTMLButtonElement = screen.getByRole('button'); | |||||
const button = screen.getByRole('button'); | |||||
expect(button).toHaveClass('pl-2 gap-2 pr-2'); | expect(button).toHaveClass('pl-2 gap-2 pr-2'); | ||||
}); | }); | ||||
@@ -106,7 +109,7 @@ describe('ActionButton', () => { | |||||
/>, | />, | ||||
); | ); | ||||
const button: HTMLButtonElement = screen.getByRole('button'); | |||||
const button = screen.getByRole('button'); | |||||
expect(button).toHaveClass(className); | expect(button).toHaveClass(className); | ||||
}); | }); | ||||
@@ -138,7 +141,7 @@ describe('ActionButton', () => { | |||||
/>, | />, | ||||
); | ); | ||||
const button: HTMLButtonElement = screen.getByRole('button'); | |||||
const button = screen.getByRole('button'); | |||||
expect(button).toHaveClass(className); | expect(button).toHaveClass(className); | ||||
}); | }); | ||||
@@ -149,7 +152,7 @@ describe('ActionButton', () => { | |||||
/>, | />, | ||||
); | ); | ||||
const button: HTMLButtonElement = screen.getByRole('button'); | |||||
const button = screen.getByRole('button'); | |||||
expect(button).toHaveClass('w-full flex'); | expect(button).toHaveClass('w-full flex'); | ||||
}); | }); | ||||
@@ -160,17 +163,17 @@ describe('ActionButton', () => { | |||||
</ActionButton>, | </ActionButton>, | ||||
); | ); | ||||
const children: HTMLElement = screen.getByTestId('children'); | |||||
const children = screen.getByTestId('children'); | |||||
expect(children).toHaveTextContent('Foo'); | expect(children).toHaveTextContent('Foo'); | ||||
}); | }); | ||||
it.each(['button', 'submit'] as const)('renders a button with type %s', (buttonType) => { | |||||
it.each(Button.AVAILABLE_TYPES)('renders a button with type %s', (buttonType) => { | |||||
render( | render( | ||||
<ActionButton | <ActionButton | ||||
type={buttonType} | type={buttonType} | ||||
/>, | />, | ||||
); | ); | ||||
const button: HTMLButtonElement = screen.getByRole('button'); | |||||
const button = screen.getByRole('button'); | |||||
expect(button).toHaveProperty('type', buttonType); | expect(button).toHaveProperty('type', buttonType); | ||||
}); | }); | ||||
@@ -180,7 +183,7 @@ describe('ActionButton', () => { | |||||
disabled | disabled | ||||
/>, | />, | ||||
); | ); | ||||
const button: HTMLButtonElement = screen.getByTestId('button'); | |||||
const button = screen.getByTestId('button'); | |||||
expect(button).toBeDisabled(); | expect(button).toBeDisabled(); | ||||
}); | }); | ||||
}); | }); |
@@ -86,4 +86,4 @@ | |||||
"typesVersions": { | "typesVersions": { | ||||
"*": {} | "*": {} | ||||
} | } | ||||
} | |||||
} |
@@ -15,7 +15,7 @@ import { | |||||
} from 'vitest'; | } from 'vitest'; | ||||
import matchers from '@testing-library/jest-dom/matchers'; | import matchers from '@testing-library/jest-dom/matchers'; | ||||
import { | import { | ||||
DropdownSelect, | |||||
DropdownSelect, DropdownSelectDerivedElement, | |||||
} from '.'; | } from '.'; | ||||
expect.extend(matchers); | expect.extend(matchers); | ||||
@@ -231,9 +231,11 @@ describe('DropdownSelect', () => { | |||||
}); | }); | ||||
it('handles change events', async () => { | it('handles change events', async () => { | ||||
const onChange = vi.fn().mockImplementationOnce((e: React.ChangeEvent) => { | |||||
e.preventDefault(); | |||||
}); | |||||
const onChange = vi.fn().mockImplementationOnce( | |||||
(e: React.ChangeEvent<DropdownSelectDerivedElement>) => { | |||||
e.preventDefault(); | |||||
}, | |||||
); | |||||
render( | render( | ||||
<DropdownSelect | <DropdownSelect | ||||
@@ -10,10 +10,14 @@ import { | |||||
vi, | vi, | ||||
expect, | expect, | ||||
describe, | describe, | ||||
it, afterEach, | |||||
it, | |||||
afterEach, | |||||
} from 'vitest'; | } from 'vitest'; | ||||
import matchers from '@testing-library/jest-dom/matchers'; | import matchers from '@testing-library/jest-dom/matchers'; | ||||
import { RadioButton } from '.'; | |||||
import { | |||||
RadioButton, | |||||
RadioButtonDerivedElement, | |||||
} from '.'; | |||||
expect.extend(matchers); | expect.extend(matchers); | ||||
@@ -36,7 +40,7 @@ describe('RadioButton', () => { | |||||
subtext="subtext" | subtext="subtext" | ||||
/>, | />, | ||||
); | ); | ||||
const subtext: HTMLElement = screen.getByTestId('subtext'); | |||||
const subtext = screen.getByTestId('subtext'); | |||||
expect(subtext).toBeInTheDocument(); | expect(subtext).toBeInTheDocument(); | ||||
}); | }); | ||||
@@ -46,24 +50,10 @@ describe('RadioButton', () => { | |||||
badge="badge" | badge="badge" | ||||
/>, | />, | ||||
); | ); | ||||
const badge: HTMLElement = screen.getByTestId('badge'); | |||||
const badge = screen.getByTestId('badge'); | |||||
expect(badge).toBeInTheDocument(); | expect(badge).toBeInTheDocument(); | ||||
}); | }); | ||||
it('handles click events', async () => { | |||||
const onClick = vi.fn().mockImplementationOnce((e: React.MouseEvent) => { | |||||
e.preventDefault(); | |||||
}); | |||||
render( | |||||
<RadioButton | |||||
onClick={onClick} | |||||
/>, | |||||
); | |||||
const button: HTMLInputElement = screen.getByRole('radio'); | |||||
await userEvent.click(button); | |||||
expect(onClick).toBeCalled(); | |||||
}); | |||||
it('renders a compact button', () => { | it('renders a compact button', () => { | ||||
render( | render( | ||||
<RadioButton | <RadioButton | ||||
@@ -91,7 +81,7 @@ describe('RadioButton', () => { | |||||
/>, | />, | ||||
); | ); | ||||
const button: HTMLButtonElement = screen.getByTestId('button'); | |||||
const button = screen.getByTestId('button'); | |||||
expect(button).toHaveClass(className); | expect(button).toHaveClass(className); | ||||
}); | }); | ||||
@@ -123,7 +113,7 @@ describe('RadioButton', () => { | |||||
/>, | />, | ||||
); | ); | ||||
const button: HTMLButtonElement = screen.getByTestId('button'); | |||||
const button = screen.getByTestId('button'); | |||||
expect(button).toHaveClass(className); | expect(button).toHaveClass(className); | ||||
}); | }); | ||||
@@ -134,7 +124,7 @@ describe('RadioButton', () => { | |||||
/>, | />, | ||||
); | ); | ||||
const button: HTMLButtonElement = screen.getByTestId('button'); | |||||
const button = screen.getByTestId('button'); | |||||
expect(button).toHaveClass('w-full flex'); | expect(button).toHaveClass('w-full flex'); | ||||
}); | }); | ||||
@@ -145,7 +135,7 @@ describe('RadioButton', () => { | |||||
</RadioButton>, | </RadioButton>, | ||||
); | ); | ||||
const children: HTMLElement = screen.getByTestId('children'); | |||||
const children = screen.getByTestId('children'); | |||||
expect(children).toHaveTextContent('Foo'); | expect(children).toHaveTextContent('Foo'); | ||||
}); | }); | ||||
@@ -155,14 +145,32 @@ describe('RadioButton', () => { | |||||
disabled | disabled | ||||
/>, | />, | ||||
); | ); | ||||
const button: HTMLButtonElement = screen.getByRole('radio'); | |||||
const button = screen.getByRole('radio'); | |||||
expect(button).toBeDisabled(); | expect(button).toBeDisabled(); | ||||
}); | }); | ||||
it('handles click events', async () => { | |||||
const onClick = vi.fn().mockImplementationOnce( | |||||
(e: React.MouseEvent<RadioButtonDerivedElement>) => { | |||||
e.preventDefault(); | |||||
}, | |||||
); | |||||
render( | |||||
<RadioButton | |||||
onClick={onClick} | |||||
/>, | |||||
); | |||||
const button = screen.getByRole('radio'); | |||||
await userEvent.click(button); | |||||
expect(onClick).toBeCalled(); | |||||
}); | |||||
it('handles change events', async () => { | it('handles change events', async () => { | ||||
const onChange = vi.fn().mockImplementationOnce((e: React.ChangeEvent) => { | |||||
e.preventDefault(); | |||||
}); | |||||
const onChange = vi.fn().mockImplementationOnce( | |||||
(e: React.ChangeEvent<RadioButtonDerivedElement>) => { | |||||
e.preventDefault(); | |||||
}, | |||||
); | |||||
render( | render( | ||||
<RadioButton | <RadioButton | ||||
@@ -78,7 +78,6 @@ export const RadioButton = React.forwardRef<RadioButtonDerivedElement, RadioButt | |||||
'peer-focus:outline-0 peer-focus:ring-4 peer-focus:ring-secondary/50', | 'peer-focus:outline-0 peer-focus:ring-4 peer-focus:ring-secondary/50', | ||||
'active:ring-tertiary/50 active:ring-4', | 'active:ring-tertiary/50 active:ring-4', | ||||
'peer-disabled:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:ring-0', | 'peer-disabled:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:ring-0', | ||||
'text-primary peer-disabled:text-primary peer-focus:text-secondary peer-checked:text-tertiary active:text-tertiary', | |||||
{ | { | ||||
'flex w-full': block, | 'flex w-full': block, | ||||
'inline-flex max-w-full align-middle': !block, | 'inline-flex max-w-full align-middle': !block, | ||||
@@ -89,7 +88,7 @@ export const RadioButton = React.forwardRef<RadioButtonDerivedElement, RadioButt | |||||
}, | }, | ||||
{ | { | ||||
'border-2 border-primary peer-disabled:border-primary peer-focus:border-secondary peer-checked:border-tertiary active:border-tertiary': variant !== 'bare', | 'border-2 border-primary peer-disabled:border-primary peer-focus:border-secondary peer-checked:border-tertiary active:border-tertiary': variant !== 'bare', | ||||
'bg-negative': variant === 'bare', | |||||
'bg-negative text-primary peer-disabled:text-primary peer-focus:text-secondary peer-checked:text-tertiary active:text-tertiary': variant !== 'filled', | |||||
'bg-primary text-negative peer-disabled:bg-primary peer-focus:bg-secondary peer-checked:bg-tertiary active:bg-tertiary': variant === 'filled', | 'bg-primary text-negative peer-disabled:bg-primary peer-focus:bg-secondary peer-checked:bg-tertiary active:bg-tertiary': variant === 'filled', | ||||
}, | }, | ||||
{ | { | ||||
@@ -104,7 +103,6 @@ export const RadioButton = React.forwardRef<RadioButtonDerivedElement, RadioButt | |||||
className={clsx( | className={clsx( | ||||
'w-6 h-6 block rounded-full border-2 p-0.5 box-border', | 'w-6 h-6 block rounded-full border-2 p-0.5 box-border', | ||||
{ | { | ||||
'-mr-2': compact, | |||||
'border-current': variant !== 'filled', | 'border-current': variant !== 'filled', | ||||
'border-negative': variant === 'filled', | 'border-negative': variant === 'filled', | ||||
}, | }, | ||||
@@ -0,0 +1,90 @@ | |||||
import * as React from 'react'; | |||||
import { | |||||
cleanup, | |||||
render, | |||||
screen, | |||||
} from '@testing-library/react'; | |||||
import userEvent from '@testing-library/user-event'; | |||||
import { | |||||
vi, | |||||
expect, | |||||
describe, | |||||
it, | |||||
afterEach, | |||||
} from 'vitest'; | |||||
import matchers from '@testing-library/jest-dom/matchers'; | |||||
import { | |||||
RadioTickBox, | |||||
RadioTickBoxDerivedElement, | |||||
} from '.'; | |||||
expect.extend(matchers); | |||||
describe('RadioTickBox', () => { | |||||
afterEach(() => { | |||||
cleanup(); | |||||
}); | |||||
it('renders a radio button', () => { | |||||
render( | |||||
<RadioTickBox />, | |||||
); | |||||
const checkbox = screen.getByRole('radio'); | |||||
expect(checkbox).toBeInTheDocument(); | |||||
}); | |||||
it('renders a block tick box', () => { | |||||
render( | |||||
<RadioTickBox | |||||
block | |||||
/>, | |||||
); | |||||
const base = screen.getByTestId('base'); | |||||
expect(base).toHaveClass('flex'); | |||||
}); | |||||
it('renders a subtext', () => { | |||||
render( | |||||
<RadioTickBox | |||||
subtext="subtext" | |||||
/>, | |||||
); | |||||
const subtext = screen.getByTestId('subtext'); | |||||
expect(subtext).toBeInTheDocument(); | |||||
}); | |||||
it('handles click events', async () => { | |||||
const onClick = vi.fn().mockImplementationOnce( | |||||
(e: React.MouseEvent<RadioTickBoxDerivedElement>) => { | |||||
e.preventDefault(); | |||||
}, | |||||
); | |||||
render( | |||||
<RadioTickBox | |||||
onClick={onClick} | |||||
/>, | |||||
); | |||||
const radio = screen.getByRole('radio'); | |||||
await userEvent.click(radio); | |||||
expect(onClick).toBeCalled(); | |||||
}); | |||||
it('handles change events', async () => { | |||||
const onChange = vi.fn().mockImplementationOnce( | |||||
(e: React.ChangeEvent<RadioTickBoxDerivedElement>) => { | |||||
e.preventDefault(); | |||||
}, | |||||
); | |||||
render( | |||||
<RadioTickBox | |||||
onChange={onChange} | |||||
/>, | |||||
); | |||||
const radio = screen.getByRole('radio'); | |||||
await userEvent.click(radio); | |||||
expect(onChange).toBeCalled(); | |||||
}); | |||||
}); |
@@ -48,6 +48,7 @@ export const RadioTickBox = React.forwardRef<RadioTickBoxDerivedElement, RadioTi | |||||
className, | className, | ||||
)} | )} | ||||
style={style} | style={style} | ||||
data-testid="base" | |||||
> | > | ||||
<input | <input | ||||
{...etcProps} | {...etcProps} | ||||
@@ -2,7 +2,8 @@ | |||||
"root": true, | "root": true, | ||||
"rules": { | "rules": { | ||||
"quote-props": "off", | "quote-props": "off", | ||||
"react/jsx-props-no-spreading": "off" | |||||
"react/jsx-props-no-spreading": "off", | |||||
"import/no-extraneous-dependencies": "off" | |||||
}, | }, | ||||
"extends": [ | "extends": [ | ||||
"lxsmnsyc/typescript/react" | "lxsmnsyc/typescript/react" | ||||
@@ -15,8 +15,11 @@ | |||||
"devDependencies": { | "devDependencies": { | ||||
"@testing-library/jest-dom": "^5.16.5", | "@testing-library/jest-dom": "^5.16.5", | ||||
"@testing-library/react": "^13.4.0", | "@testing-library/react": "^13.4.0", | ||||
"@testing-library/user-event": "^14.4.3", | |||||
"@types/node": "^18.14.1", | "@types/node": "^18.14.1", | ||||
"@types/react": "^18.0.27", | "@types/react": "^18.0.27", | ||||
"@types/testing-library__jest-dom": "^5.14.7", | |||||
"@vitest/coverage-v8": "^0.33.0", | |||||
"eslint": "^8.35.0", | "eslint": "^8.35.0", | ||||
"eslint-config-lxsmnsyc": "^0.5.0", | "eslint-config-lxsmnsyc": "^0.5.0", | ||||
"jsdom": "^21.1.0", | "jsdom": "^21.1.0", | ||||
@@ -26,7 +29,7 @@ | |||||
"react-test-renderer": "^18.2.0", | "react-test-renderer": "^18.2.0", | ||||
"tslib": "^2.5.0", | "tslib": "^2.5.0", | ||||
"typescript": "^4.9.5", | "typescript": "^4.9.5", | ||||
"vitest": "^0.28.1" | |||||
"vitest": "^0.33.0" | |||||
}, | }, | ||||
"peerDependencies": { | "peerDependencies": { | ||||
"react": "^16.8 || ^17.0 || ^18.0", | "react": "^16.8 || ^17.0 || ^18.0", | ||||
@@ -0,0 +1,242 @@ | |||||
import * as React from 'react'; | |||||
import { | |||||
cleanup, | |||||
render, | |||||
screen, | |||||
} from '@testing-library/react'; | |||||
import userEvent from '@testing-library/user-event'; | |||||
import { TextControl } from '@tesseract-design/web-base'; | |||||
import { | |||||
vi, | |||||
expect, | |||||
describe, | |||||
it, | |||||
afterEach, | |||||
} from 'vitest'; | |||||
import matchers from '@testing-library/jest-dom/matchers'; | |||||
import { | |||||
MaskedTextInput, | |||||
MaskedTextInputDerivedElement, | |||||
} from '.'; | |||||
expect.extend(matchers); | |||||
describe('MaskedTextInput', () => { | |||||
afterEach(() => { | |||||
cleanup(); | |||||
}); | |||||
it('renders a password input', () => { | |||||
render( | |||||
<MaskedTextInput />, | |||||
); | |||||
const textbox = screen.getByTestId('input'); | |||||
expect(textbox).toBeInTheDocument(); | |||||
expect(textbox).toHaveProperty('type', 'password'); | |||||
}); | |||||
it('renders a border', () => { | |||||
render( | |||||
<MaskedTextInput | |||||
border | |||||
/>, | |||||
); | |||||
const border = screen.getByTestId('border'); | |||||
expect(border).toBeInTheDocument(); | |||||
}); | |||||
it('renders a label', () => { | |||||
render( | |||||
<MaskedTextInput | |||||
label="foo" | |||||
/>, | |||||
); | |||||
const textbox = screen.getByLabelText('foo'); | |||||
expect(textbox).toBeInTheDocument(); | |||||
const label = screen.getByTestId('label'); | |||||
expect(label).toHaveTextContent('foo'); | |||||
}); | |||||
it('renders a hidden label', () => { | |||||
render( | |||||
<MaskedTextInput | |||||
label="foo" | |||||
hiddenLabel | |||||
/>, | |||||
); | |||||
const textbox = screen.getByLabelText('foo'); | |||||
expect(textbox).toBeInTheDocument(); | |||||
const label = screen.queryByTestId('label'); | |||||
expect(label).toBeInTheDocument(); | |||||
expect(label).toHaveClass('sr-only'); | |||||
}); | |||||
it('renders a hint', () => { | |||||
render( | |||||
<MaskedTextInput | |||||
hint="foo" | |||||
/>, | |||||
); | |||||
const hint = screen.getByTestId('hint'); | |||||
expect(hint).toBeInTheDocument(); | |||||
}); | |||||
it('renders an indicator', () => { | |||||
render( | |||||
<MaskedTextInput | |||||
indicator={ | |||||
<div /> | |||||
} | |||||
/>, | |||||
); | |||||
const indicator = screen.getByTestId('indicator'); | |||||
expect(indicator).toBeInTheDocument(); | |||||
}); | |||||
describe.each` | |||||
size | inputClassName | hintClassName | indicatorClassName | |||||
${'small'} | ${'h-10'} | ${'pr-10'} | ${'w-10'} | |||||
${'medium'} | ${'h-12'} | ${'pr-12'} | ${'w-12'} | |||||
${'large'} | ${'h-16'} | ${'pr-16'} | ${'w-16'} | |||||
`('on $size size', ({ | |||||
size, | |||||
inputClassName, | |||||
hintClassName, | |||||
indicatorClassName, | |||||
}: { | |||||
size: TextControl.Size, | |||||
inputClassName: string, | |||||
hintClassName: string, | |||||
indicatorClassName: string, | |||||
}) => { | |||||
it('renders input styles', () => { | |||||
render( | |||||
<MaskedTextInput | |||||
size={size} | |||||
/>, | |||||
); | |||||
const input = screen.getByTestId('input'); | |||||
expect(input).toHaveClass(inputClassName); | |||||
}); | |||||
it('renders label styles with indicator', () => { | |||||
render( | |||||
<MaskedTextInput | |||||
size={size} | |||||
label="foo" | |||||
indicator={<div />} | |||||
/>, | |||||
); | |||||
const label = screen.getByTestId('label'); | |||||
expect(label).toHaveClass(hintClassName); | |||||
}); | |||||
it('renders hint styles when indicator is present', () => { | |||||
render( | |||||
<MaskedTextInput | |||||
size={size} | |||||
hint="hint" | |||||
indicator={<div />} | |||||
/>, | |||||
); | |||||
const hint = screen.getByTestId('hint'); | |||||
expect(hint).toHaveClass(hintClassName); | |||||
}); | |||||
it('renders indicator styles', () => { | |||||
render( | |||||
<MaskedTextInput | |||||
size={size} | |||||
indicator={ | |||||
<div /> | |||||
} | |||||
/>, | |||||
); | |||||
const indicator = screen.getByTestId('indicator'); | |||||
expect(indicator).toHaveClass(indicatorClassName); | |||||
}); | |||||
}); | |||||
it('renders a block textbox', () => { | |||||
render( | |||||
<MaskedTextInput | |||||
block | |||||
/>, | |||||
); | |||||
const base = screen.getByTestId('base'); | |||||
expect(base).toHaveClass('block'); | |||||
}); | |||||
describe.each` | |||||
variant | inputClassName | hintClassName | |||||
${'default'} | ${'pl-4'} | ${'bottom-0 pl-4 pb-1'} | |||||
${'alternate'} | ${'pl-1.5 pt-4'} | ${'top-0.5'} | |||||
`('on $variant style', ({ | |||||
variant, | |||||
inputClassName, | |||||
hintClassName, | |||||
}: { | |||||
variant: TextControl.Variant, | |||||
inputClassName: string, | |||||
hintClassName: string, | |||||
}) => { | |||||
it('renders input styles', () => { | |||||
render( | |||||
<MaskedTextInput | |||||
variant={variant} | |||||
/>, | |||||
); | |||||
const input = screen.getByTestId('input'); | |||||
expect(input).toHaveClass(inputClassName); | |||||
}); | |||||
it('renders hint styles', () => { | |||||
render( | |||||
<MaskedTextInput | |||||
variant={variant} | |||||
hint="hint" | |||||
/>, | |||||
); | |||||
const hint = screen.getByTestId('hint'); | |||||
expect(hint).toHaveClass(hintClassName); | |||||
}); | |||||
}); | |||||
it('handles change events', async () => { | |||||
const onChange = vi.fn().mockImplementationOnce( | |||||
(e: React.ChangeEvent<MaskedTextInputDerivedElement>) => { | |||||
e.preventDefault(); | |||||
}, | |||||
); | |||||
render( | |||||
<MaskedTextInput | |||||
onChange={onChange} | |||||
/>, | |||||
); | |||||
const textbox = screen.getByTestId('input'); | |||||
await userEvent.type(textbox, 'foobar'); | |||||
expect(onChange).toBeCalled(); | |||||
}); | |||||
it('handles input events', async () => { | |||||
const onInput = vi.fn().mockImplementationOnce( | |||||
(e: React.SyntheticEvent<MaskedTextInputDerivedElement>) => { | |||||
e.preventDefault(); | |||||
}, | |||||
); | |||||
render( | |||||
<MaskedTextInput | |||||
onInput={onInput} | |||||
/>, | |||||
); | |||||
const textbox = screen.getByTestId('input'); | |||||
await userEvent.type(textbox, 'foobar'); | |||||
expect(onInput).toBeCalled(); | |||||
}); | |||||
}); |
@@ -78,6 +78,7 @@ export const MaskedTextInput = React.forwardRef< | |||||
className, | className, | ||||
)} | )} | ||||
style={style} | style={style} | ||||
data-testid="base" | |||||
> | > | ||||
<input | <input | ||||
{...etcProps} | {...etcProps} | ||||
@@ -177,6 +178,7 @@ export const MaskedTextInput = React.forwardRef< | |||||
)} | )} | ||||
{indicator && ( | {indicator && ( | ||||
<div | <div | ||||
data-testid="indicator" | |||||
className={clsx( | className={clsx( | ||||
'text-center flex items-center justify-center peer-disabled:opacity-50 aspect-square absolute bottom-0 right-0 pointer-events-none select-none', | 'text-center flex items-center justify-center peer-disabled:opacity-50 aspect-square absolute bottom-0 right-0 pointer-events-none select-none', | ||||
{ | { | ||||
@@ -0,0 +1,241 @@ | |||||
import * as React from 'react'; | |||||
import { | |||||
cleanup, | |||||
render, | |||||
screen, | |||||
} from '@testing-library/react'; | |||||
import userEvent from '@testing-library/user-event'; | |||||
import { TextControl } from '@tesseract-design/web-base'; | |||||
import { | |||||
vi, | |||||
expect, | |||||
describe, | |||||
it, | |||||
afterEach, | |||||
} from 'vitest'; | |||||
import matchers from '@testing-library/jest-dom/matchers'; | |||||
import { | |||||
MultilineTextInput, | |||||
MultilineTextInputDerivedElement, | |||||
} from '.'; | |||||
expect.extend(matchers); | |||||
describe('TextInput', () => { | |||||
afterEach(() => { | |||||
cleanup(); | |||||
}); | |||||
it('renders a textbox', () => { | |||||
render( | |||||
<MultilineTextInput />, | |||||
); | |||||
const textbox = screen.getByRole('textbox'); | |||||
expect(textbox).toBeInTheDocument(); | |||||
}); | |||||
it('renders a border', () => { | |||||
render( | |||||
<MultilineTextInput | |||||
border | |||||
/>, | |||||
); | |||||
const border = screen.getByTestId('border'); | |||||
expect(border).toBeInTheDocument(); | |||||
}); | |||||
it('renders a label', () => { | |||||
render( | |||||
<MultilineTextInput | |||||
label="foo" | |||||
/>, | |||||
); | |||||
const textbox = screen.getByLabelText('foo'); | |||||
expect(textbox).toBeInTheDocument(); | |||||
const label = screen.getByTestId('label'); | |||||
expect(label).toHaveTextContent('foo'); | |||||
}); | |||||
it('renders a hidden label', () => { | |||||
render( | |||||
<MultilineTextInput | |||||
label="foo" | |||||
hiddenLabel | |||||
/>, | |||||
); | |||||
const textbox = screen.getByLabelText('foo'); | |||||
expect(textbox).toBeInTheDocument(); | |||||
const label = screen.queryByTestId('label'); | |||||
expect(label).toBeInTheDocument(); | |||||
expect(label).toHaveClass('sr-only'); | |||||
}); | |||||
it('renders a hint', () => { | |||||
render( | |||||
<MultilineTextInput | |||||
hint="foo" | |||||
/>, | |||||
); | |||||
const hint = screen.getByTestId('hint'); | |||||
expect(hint).toBeInTheDocument(); | |||||
}); | |||||
it('renders an indicator', () => { | |||||
render( | |||||
<MultilineTextInput | |||||
indicator={ | |||||
<div /> | |||||
} | |||||
/>, | |||||
); | |||||
const indicator = screen.getByTestId('indicator'); | |||||
expect(indicator).toBeInTheDocument(); | |||||
}); | |||||
describe.each` | |||||
size | inputClassName | hintClassName | indicatorClassName | |||||
${'small'} | ${'min-h-10'} | ${'pr-10'} | ${'w-10'} | |||||
${'medium'} | ${'min-h-12'} | ${'pr-12'} | ${'w-12'} | |||||
${'large'} | ${'min-h-16'} | ${'pr-16'} | ${'w-16'} | |||||
`('on $size size', ({ | |||||
size, | |||||
inputClassName, | |||||
hintClassName, | |||||
indicatorClassName, | |||||
}: { | |||||
size: TextControl.Size, | |||||
inputClassName: string, | |||||
hintClassName: string, | |||||
indicatorClassName: string, | |||||
}) => { | |||||
it('renders input styles', () => { | |||||
render( | |||||
<MultilineTextInput | |||||
size={size} | |||||
/>, | |||||
); | |||||
const input = screen.getByTestId('input'); | |||||
expect(input).toHaveClass(inputClassName); | |||||
}); | |||||
it('renders label styles with indicator', () => { | |||||
render( | |||||
<MultilineTextInput | |||||
size={size} | |||||
label="foo" | |||||
indicator={<div />} | |||||
/>, | |||||
); | |||||
const label = screen.getByTestId('label'); | |||||
expect(label).toHaveClass(hintClassName); | |||||
}); | |||||
it('renders hint styles when indicator is present', () => { | |||||
render( | |||||
<MultilineTextInput | |||||
size={size} | |||||
hint="hint" | |||||
indicator={<div />} | |||||
/>, | |||||
); | |||||
const hint = screen.getByTestId('hint'); | |||||
expect(hint).toHaveClass(hintClassName); | |||||
}); | |||||
it('renders indicator styles', () => { | |||||
render( | |||||
<MultilineTextInput | |||||
size={size} | |||||
indicator={ | |||||
<div /> | |||||
} | |||||
/>, | |||||
); | |||||
const indicator = screen.getByTestId('indicator'); | |||||
expect(indicator).toHaveClass(indicatorClassName); | |||||
}); | |||||
}); | |||||
it('renders a block textbox', () => { | |||||
render( | |||||
<MultilineTextInput | |||||
block | |||||
/>, | |||||
); | |||||
const base = screen.getByTestId('base'); | |||||
expect(base).toHaveClass('block'); | |||||
}); | |||||
describe.each` | |||||
variant | inputClassName | hintClassName | |||||
${'default'} | ${'pl-4'} | ${'bottom-0 pl-4 pb-1'} | |||||
${'alternate'} | ${'pl-1.5'} | ${'top-0.5'} | |||||
`('on $variant style', ({ | |||||
variant, | |||||
inputClassName, | |||||
hintClassName, | |||||
}: { | |||||
variant: TextControl.Variant, | |||||
inputClassName: string, | |||||
hintClassName: string, | |||||
}) => { | |||||
it('renders input styles', () => { | |||||
render( | |||||
<MultilineTextInput | |||||
variant={variant} | |||||
/>, | |||||
); | |||||
const input = screen.getByTestId('input'); | |||||
expect(input).toHaveClass(inputClassName); | |||||
}); | |||||
it('renders hint styles', () => { | |||||
render( | |||||
<MultilineTextInput | |||||
variant={variant} | |||||
hint="hint" | |||||
/>, | |||||
); | |||||
const hint = screen.getByTestId('hint'); | |||||
expect(hint).toHaveClass(hintClassName); | |||||
}); | |||||
}); | |||||
it('handles change events', async () => { | |||||
const onChange = vi.fn().mockImplementationOnce( | |||||
(e: React.ChangeEvent<MultilineTextInputDerivedElement>) => { | |||||
e.preventDefault(); | |||||
}, | |||||
); | |||||
render( | |||||
<MultilineTextInput | |||||
onChange={onChange} | |||||
/>, | |||||
); | |||||
const textbox: HTMLInputElement = screen.getByRole('textbox'); | |||||
await userEvent.type(textbox, 'foobar'); | |||||
expect(onChange).toBeCalled(); | |||||
}); | |||||
it('handles input events', async () => { | |||||
const onInput = vi.fn().mockImplementationOnce( | |||||
(e: React.SyntheticEvent<MultilineTextInputDerivedElement>) => { | |||||
e.preventDefault(); | |||||
}, | |||||
); | |||||
render( | |||||
<MultilineTextInput | |||||
onInput={onInput} | |||||
/>, | |||||
); | |||||
const textbox: HTMLInputElement = screen.getByTestId('input'); | |||||
await userEvent.type(textbox, 'foobar'); | |||||
expect(onInput).toBeCalled(); | |||||
}); | |||||
}); |
@@ -80,6 +80,7 @@ export const MultilineTextInput = React.forwardRef< | |||||
className, | className, | ||||
)} | )} | ||||
style={style} | style={style} | ||||
data-testid="base" | |||||
> | > | ||||
<textarea | <textarea | ||||
{...etcProps} | {...etcProps} | ||||
@@ -192,6 +193,7 @@ export const MultilineTextInput = React.forwardRef< | |||||
)} | )} | ||||
{indicator && ( | {indicator && ( | ||||
<div | <div | ||||
data-testid="indicator" | |||||
className={clsx( | className={clsx( | ||||
'text-center flex items-center justify-center peer-disabled:opacity-50 aspect-square absolute bottom-0 right-0 pointer-events-none select-none', | 'text-center flex items-center justify-center peer-disabled:opacity-50 aspect-square absolute bottom-0 right-0 pointer-events-none select-none', | ||||
{ | { | ||||
@@ -0,0 +1,263 @@ | |||||
import * as React from 'react'; | |||||
import { | |||||
cleanup, | |||||
render, | |||||
screen, | |||||
} from '@testing-library/react'; | |||||
import userEvent from '@testing-library/user-event'; | |||||
import { TextControl } from '@tesseract-design/web-base'; | |||||
import { | |||||
vi, | |||||
expect, | |||||
describe, | |||||
it, | |||||
afterEach, | |||||
} from 'vitest'; | |||||
import matchers from '@testing-library/jest-dom/matchers'; | |||||
import { | |||||
TextInput, | |||||
TextInputDerivedElement, | |||||
} from '.'; | |||||
expect.extend(matchers); | |||||
describe('TextInput', () => { | |||||
afterEach(() => { | |||||
cleanup(); | |||||
}); | |||||
it('renders a textbox', () => { | |||||
render( | |||||
<TextInput />, | |||||
); | |||||
const textbox = screen.getByRole('textbox'); | |||||
expect(textbox).toBeInTheDocument(); | |||||
expect(textbox).toHaveProperty('type', 'text'); | |||||
}); | |||||
it('renders a border', () => { | |||||
render( | |||||
<TextInput | |||||
border | |||||
/>, | |||||
); | |||||
const border = screen.getByTestId('border'); | |||||
expect(border).toBeInTheDocument(); | |||||
}); | |||||
it('renders a label', () => { | |||||
render( | |||||
<TextInput | |||||
label="foo" | |||||
/>, | |||||
); | |||||
const textbox = screen.getByLabelText('foo'); | |||||
expect(textbox).toBeInTheDocument(); | |||||
const label = screen.getByTestId('label'); | |||||
expect(label).toHaveTextContent('foo'); | |||||
}); | |||||
it('renders a hidden label', () => { | |||||
render( | |||||
<TextInput | |||||
label="foo" | |||||
hiddenLabel | |||||
/>, | |||||
); | |||||
const textbox = screen.getByLabelText('foo'); | |||||
expect(textbox).toBeInTheDocument(); | |||||
const label = screen.queryByTestId('label'); | |||||
expect(label).toBeInTheDocument(); | |||||
expect(label).toHaveClass('sr-only'); | |||||
}); | |||||
it('renders a hint', () => { | |||||
render( | |||||
<TextInput | |||||
hint="foo" | |||||
/>, | |||||
); | |||||
const hint = screen.getByTestId('hint'); | |||||
expect(hint).toBeInTheDocument(); | |||||
}); | |||||
it('renders an indicator', () => { | |||||
render( | |||||
<TextInput | |||||
indicator={ | |||||
<div /> | |||||
} | |||||
/>, | |||||
); | |||||
const indicator = screen.getByTestId('indicator'); | |||||
expect(indicator).toBeInTheDocument(); | |||||
}); | |||||
describe.each` | |||||
size | inputClassName | hintClassName | indicatorClassName | |||||
${'small'} | ${'h-10'} | ${'pr-10'} | ${'w-10'} | |||||
${'medium'} | ${'h-12'} | ${'pr-12'} | ${'w-12'} | |||||
${'large'} | ${'h-16'} | ${'pr-16'} | ${'w-16'} | |||||
`('on $size size', ({ | |||||
size, | |||||
inputClassName, | |||||
hintClassName, | |||||
indicatorClassName, | |||||
}: { | |||||
size: TextControl.Size, | |||||
inputClassName: string, | |||||
hintClassName: string, | |||||
indicatorClassName: string, | |||||
}) => { | |||||
it('renders input styles', () => { | |||||
render( | |||||
<TextInput | |||||
size={size} | |||||
/>, | |||||
); | |||||
const input = screen.getByTestId('input'); | |||||
expect(input).toHaveClass(inputClassName); | |||||
}); | |||||
it('renders label styles with indicator', () => { | |||||
render( | |||||
<TextInput | |||||
size={size} | |||||
label="foo" | |||||
indicator={<div />} | |||||
/>, | |||||
); | |||||
const label = screen.getByTestId('label'); | |||||
expect(label).toHaveClass(hintClassName); | |||||
}); | |||||
it('renders hint styles when indicator is present', () => { | |||||
render( | |||||
<TextInput | |||||
size={size} | |||||
hint="hint" | |||||
indicator={<div />} | |||||
/>, | |||||
); | |||||
const hint = screen.getByTestId('hint'); | |||||
expect(hint).toHaveClass(hintClassName); | |||||
}); | |||||
it('renders indicator styles', () => { | |||||
render( | |||||
<TextInput | |||||
size={size} | |||||
indicator={ | |||||
<div /> | |||||
} | |||||
/>, | |||||
); | |||||
const indicator = screen.getByTestId('indicator'); | |||||
expect(indicator).toHaveClass(indicatorClassName); | |||||
}); | |||||
}); | |||||
it('renders a block textbox', () => { | |||||
render( | |||||
<TextInput | |||||
block | |||||
/>, | |||||
); | |||||
const base = screen.getByTestId('base'); | |||||
expect(base).toHaveClass('block'); | |||||
}); | |||||
it.each(TextControl.AVAILABLE_INPUT_TYPES)('renders a textbox with type %s', (inputType) => { | |||||
render( | |||||
<TextInput | |||||
type={inputType} | |||||
/>, | |||||
); | |||||
const textbox = screen.getByTestId('input'); | |||||
expect(textbox).toHaveProperty('type', inputType); | |||||
}); | |||||
it('falls back to text input mode when it clashes with the input type', () => { | |||||
render( | |||||
<TextInput | |||||
type="text" | |||||
inputMode="search" | |||||
/>, | |||||
); | |||||
const textbox = screen.getByTestId('input'); | |||||
expect(textbox).toHaveProperty('inputMode', 'text'); | |||||
}); | |||||
describe.each` | |||||
variant | inputClassName | hintClassName | |||||
${'default'} | ${'pl-4'} | ${'bottom-0 pl-4 pb-1'} | |||||
${'alternate'} | ${'pl-1.5 pt-4'} | ${'top-0.5'} | |||||
`('on $variant style', ({ | |||||
variant, | |||||
inputClassName, | |||||
hintClassName, | |||||
}: { | |||||
variant: TextControl.Variant, | |||||
inputClassName: string, | |||||
hintClassName: string, | |||||
}) => { | |||||
it('renders input styles', () => { | |||||
render( | |||||
<TextInput | |||||
variant={variant} | |||||
/>, | |||||
); | |||||
const input = screen.getByTestId('input'); | |||||
expect(input).toHaveClass(inputClassName); | |||||
}); | |||||
it('renders hint styles', () => { | |||||
render( | |||||
<TextInput | |||||
variant={variant} | |||||
hint="hint" | |||||
/>, | |||||
); | |||||
const hint = screen.getByTestId('hint'); | |||||
expect(hint).toHaveClass(hintClassName); | |||||
}); | |||||
}); | |||||
it('handles change events', async () => { | |||||
const onChange = vi.fn().mockImplementationOnce( | |||||
(e: React.ChangeEvent<TextInputDerivedElement>) => { | |||||
e.preventDefault(); | |||||
}, | |||||
); | |||||
render( | |||||
<TextInput | |||||
onChange={onChange} | |||||
/>, | |||||
); | |||||
const textbox: HTMLInputElement = screen.getByRole('textbox'); | |||||
await userEvent.type(textbox, 'foobar'); | |||||
expect(onChange).toBeCalled(); | |||||
}); | |||||
it('handles input events', async () => { | |||||
const onInput = vi.fn().mockImplementationOnce( | |||||
(e: React.SyntheticEvent<TextInputDerivedElement>) => { | |||||
e.preventDefault(); | |||||
}, | |||||
); | |||||
render( | |||||
<TextInput | |||||
onInput={onInput} | |||||
/>, | |||||
); | |||||
const textbox: HTMLInputElement = screen.getByTestId('input'); | |||||
await userEvent.type(textbox, 'foobar'); | |||||
expect(onInput).toBeCalled(); | |||||
}); | |||||
}); |
@@ -94,6 +94,7 @@ export const TextInput = React.forwardRef<TextInputDerivedElement, TextInputProp | |||||
className, | className, | ||||
)} | )} | ||||
style={style} | style={style} | ||||
data-testid="base" | |||||
> | > | ||||
<input | <input | ||||
{...etcProps} | {...etcProps} | ||||
@@ -194,6 +195,7 @@ export const TextInput = React.forwardRef<TextInputDerivedElement, TextInputProp | |||||
)} | )} | ||||
{indicator && ( | {indicator && ( | ||||
<div | <div | ||||
data-testid="indicator" | |||||
className={clsx( | className={clsx( | ||||
'text-center flex items-center justify-center peer-disabled:opacity-50 aspect-square absolute bottom-0 right-0 pointer-events-none select-none', | 'text-center flex items-center justify-center peer-disabled:opacity-50 aspect-square absolute bottom-0 right-0 pointer-events-none select-none', | ||||
{ | { | ||||
@@ -0,0 +1,12 @@ | |||||
import { describe, it, expect } from 'vitest'; | |||||
import * as WebFreeformReact from '.'; | |||||
describe('web-freeform-react', () => { | |||||
it.each([ | |||||
'MaskedTextInput', | |||||
'MultilineTextInput', | |||||
'TextInput', | |||||
])('exports %s', (namedExport) => { | |||||
expect(WebFreeformReact).toHaveProperty(namedExport); | |||||
}); | |||||
}); |
@@ -15,8 +15,11 @@ | |||||
"devDependencies": { | "devDependencies": { | ||||
"@testing-library/jest-dom": "^5.16.5", | "@testing-library/jest-dom": "^5.16.5", | ||||
"@testing-library/react": "^13.4.0", | "@testing-library/react": "^13.4.0", | ||||
"@testing-library/user-event": "^14.4.3", | |||||
"@types/node": "^18.14.1", | "@types/node": "^18.14.1", | ||||
"@types/react": "^18.0.27", | "@types/react": "^18.0.27", | ||||
"@types/testing-library__jest-dom": "^5.14.7", | |||||
"@vitest/coverage-v8": "^0.33.0", | |||||
"eslint": "^8.35.0", | "eslint": "^8.35.0", | ||||
"eslint-config-lxsmnsyc": "^0.5.0", | "eslint-config-lxsmnsyc": "^0.5.0", | ||||
"jsdom": "^21.1.0", | "jsdom": "^21.1.0", | ||||
@@ -26,7 +29,7 @@ | |||||
"react-test-renderer": "^18.2.0", | "react-test-renderer": "^18.2.0", | ||||
"tslib": "^2.5.0", | "tslib": "^2.5.0", | ||||
"typescript": "^4.9.5", | "typescript": "^4.9.5", | ||||
"vitest": "^0.28.1" | |||||
"vitest": "^0.33.0" | |||||
}, | }, | ||||
"peerDependencies": { | "peerDependencies": { | ||||
"react": "^16.8 || ^17.0 || ^18.0", | "react": "^16.8 || ^17.0 || ^18.0", | ||||
@@ -0,0 +1,44 @@ | |||||
import * as React from 'react'; | |||||
import { | |||||
cleanup, | |||||
render, | |||||
screen, | |||||
} from '@testing-library/react'; | |||||
import { | |||||
expect, | |||||
describe, | |||||
it, | |||||
afterEach, | |||||
} from 'vitest'; | |||||
import matchers from '@testing-library/jest-dom/matchers'; | |||||
import { | |||||
Badge, | |||||
} from '.'; | |||||
expect.extend(matchers); | |||||
describe('Badge', () => { | |||||
afterEach(() => { | |||||
cleanup(); | |||||
}); | |||||
it('renders a badge', () => { | |||||
render( | |||||
<Badge />, | |||||
); | |||||
const badge: HTMLButtonElement = screen.getByTestId('badge'); | |||||
expect(badge).toBeInTheDocument(); | |||||
expect(badge).toHaveClass('rounded px-1'); | |||||
}); | |||||
it('renders a rounded badge', () => { | |||||
render( | |||||
<Badge | |||||
rounded | |||||
/>, | |||||
); | |||||
const badge: HTMLButtonElement = screen.getByTestId('badge'); | |||||
expect(badge).toBeInTheDocument(); | |||||
expect(badge).toHaveClass('rounded-full px-2'); | |||||
}); | |||||
}); |
@@ -28,6 +28,7 @@ export const Badge = React.forwardRef<BadgeDerivedElement, BadgeProps>(( | |||||
}, | }, | ||||
className, | className, | ||||
)} | )} | ||||
data-testid="badge" | |||||
> | > | ||||
<span className="relative w-full"> | <span className="relative w-full"> | ||||
{children} | {children} | ||||
@@ -0,0 +1,11 @@ | |||||
import { describe, it, expect } from 'vitest'; | |||||
import * as WebInformationReact from '.'; | |||||
describe('web-information-react', () => { | |||||
it.each([ | |||||
'Badge', | |||||
'KeyValueTable', | |||||
])('exports %s', (namedExport) => { | |||||
expect(WebInformationReact).toHaveProperty(namedExport); | |||||
}); | |||||
}); |
@@ -2,7 +2,8 @@ | |||||
"root": true, | "root": true, | ||||
"rules": { | "rules": { | ||||
"quote-props": "off", | "quote-props": "off", | ||||
"react/jsx-props-no-spreading": "off" | |||||
"react/jsx-props-no-spreading": "off", | |||||
"import/no-extraneous-dependencies": "off" | |||||
}, | }, | ||||
"extends": [ | "extends": [ | ||||
"lxsmnsyc/typescript/react" | "lxsmnsyc/typescript/react" | ||||
@@ -15,8 +15,11 @@ | |||||
"devDependencies": { | "devDependencies": { | ||||
"@testing-library/jest-dom": "^5.16.5", | "@testing-library/jest-dom": "^5.16.5", | ||||
"@testing-library/react": "^13.4.0", | "@testing-library/react": "^13.4.0", | ||||
"@testing-library/user-event": "^14.4.3", | |||||
"@types/node": "^18.14.1", | "@types/node": "^18.14.1", | ||||
"@types/react": "^18.0.27", | "@types/react": "^18.0.27", | ||||
"@types/testing-library__jest-dom": "^5.14.7", | |||||
"@vitest/coverage-v8": "^0.33.0", | |||||
"eslint": "^8.35.0", | "eslint": "^8.35.0", | ||||
"eslint-config-lxsmnsyc": "^0.5.0", | "eslint-config-lxsmnsyc": "^0.5.0", | ||||
"jsdom": "^21.1.0", | "jsdom": "^21.1.0", | ||||
@@ -27,7 +30,7 @@ | |||||
"tslib": "^2.5.0", | "tslib": "^2.5.0", | ||||
"tsx": "^3.12.7", | "tsx": "^3.12.7", | ||||
"typescript": "^4.9.5", | "typescript": "^4.9.5", | ||||
"vitest": "^0.28.1" | |||||
"vitest": "^0.33.0" | |||||
}, | }, | ||||
"peerDependencies": { | "peerDependencies": { | ||||
"react": "^16.8 || ^17.0 || ^18.0", | "react": "^16.8 || ^17.0 || ^18.0", | ||||
@@ -0,0 +1,224 @@ | |||||
import * as React from 'react'; | |||||
import { | |||||
cleanup, | |||||
render, | |||||
screen, | |||||
} from '@testing-library/react'; | |||||
import userEvent from '@testing-library/user-event'; | |||||
import { Button } from '@tesseract-design/web-base'; | |||||
import { | |||||
vi, | |||||
expect, | |||||
describe, | |||||
it, | |||||
afterEach, | |||||
} from 'vitest'; | |||||
import matchers from '@testing-library/jest-dom/matchers'; | |||||
import { | |||||
ToggleButton, | |||||
ToggleButtonDerivedElement, | |||||
} from '.'; | |||||
expect.extend(matchers); | |||||
describe('ToggleButton', () => { | |||||
afterEach(() => { | |||||
cleanup(); | |||||
}); | |||||
it('renders a checkbox', () => { | |||||
render( | |||||
<ToggleButton />, | |||||
); | |||||
const checkbox = screen.getByRole('checkbox'); | |||||
expect(checkbox).toBeInTheDocument(); | |||||
}); | |||||
it('renders a subtext', () => { | |||||
render( | |||||
<ToggleButton | |||||
subtext="subtext" | |||||
/>, | |||||
); | |||||
const subtext = screen.getByTestId('subtext'); | |||||
expect(subtext).toBeInTheDocument(); | |||||
}); | |||||
it('renders a badge', () => { | |||||
render( | |||||
<ToggleButton | |||||
badge="badge" | |||||
/>, | |||||
); | |||||
const badge = screen.getByTestId('badge'); | |||||
expect(badge).toBeInTheDocument(); | |||||
}); | |||||
describe('on indeterminate', () => { | |||||
it('renders an indeterminate checkbox', () => { | |||||
render( | |||||
<ToggleButton | |||||
indeterminate | |||||
/>, | |||||
); | |||||
const checkbox = screen.getByRole('checkbox'); | |||||
expect(checkbox).toHaveProperty('indeterminate', true); | |||||
}); | |||||
it('acknowledges passed ref object', () => { | |||||
const ref = React.createRef<ToggleButtonDerivedElement>(); | |||||
render( | |||||
<ToggleButton | |||||
indeterminate | |||||
ref={ref} | |||||
/>, | |||||
); | |||||
expect(ref.current).toHaveProperty('indeterminate', true); | |||||
}); | |||||
it('acknowledges passed legacy ref', () => { | |||||
let refElement = null as null | ToggleButtonDerivedElement; | |||||
const ref = (element: ToggleButtonDerivedElement) => { | |||||
refElement = element; | |||||
}; | |||||
render( | |||||
<ToggleButton | |||||
indeterminate | |||||
ref={ref} | |||||
/>, | |||||
); | |||||
const checkbox = screen.getByRole('checkbox'); | |||||
expect(checkbox).toHaveProperty('indeterminate', true); | |||||
expect(refElement).toBe(checkbox); | |||||
}); | |||||
}); | |||||
it('handles click events', async () => { | |||||
const onClick = vi.fn().mockImplementationOnce( | |||||
(e: React.MouseEvent<ToggleButtonDerivedElement>) => { | |||||
e.preventDefault(); | |||||
}, | |||||
); | |||||
render( | |||||
<ToggleButton | |||||
onClick={onClick} | |||||
/>, | |||||
); | |||||
const checkbox = screen.getByRole('checkbox'); | |||||
await userEvent.click(checkbox); | |||||
expect(onClick).toBeCalled(); | |||||
}); | |||||
it('renders a compact button', () => { | |||||
render( | |||||
<ToggleButton | |||||
compact | |||||
/>, | |||||
); | |||||
const button = screen.getByTestId('button'); | |||||
expect(button).toHaveClass('pl-2 gap-2 pr-2'); | |||||
}); | |||||
describe.each` | |||||
size | className | |||||
${'small'} | ${'h-10'} | |||||
${'medium'} | ${'h-12'} | |||||
${'large'} | ${'h-16'} | |||||
`('on $size size', ({ | |||||
size, | |||||
className, | |||||
}: { size: Button.Size, className: string }) => { | |||||
it('renders button styles', () => { | |||||
render( | |||||
<ToggleButton | |||||
size={size} | |||||
/>, | |||||
); | |||||
const button = screen.getByTestId('button'); | |||||
expect(button).toHaveClass(className); | |||||
}); | |||||
it('renders badge styles', () => { | |||||
render( | |||||
<ToggleButton | |||||
size={size} | |||||
badge="badge" | |||||
/>, | |||||
); | |||||
const badge = screen.getByTestId('badge'); | |||||
expect(badge).toBeInTheDocument(); | |||||
}); | |||||
}); | |||||
it.each` | |||||
variant | className | |||||
${'bare'} | ${'bg-negative'} | |||||
${'outline'} | ${'border-2'} | |||||
${'filled'} | ${'bg-primary'} | |||||
`('renders a button with $variant variant', ({ | |||||
variant, | |||||
className, | |||||
}: { variant: Button.Variant, className: string }) => { | |||||
render( | |||||
<ToggleButton | |||||
variant={variant} | |||||
/>, | |||||
); | |||||
const button = screen.getByTestId('button'); | |||||
expect(button).toHaveClass(className); | |||||
}); | |||||
it('renders a block button', () => { | |||||
render( | |||||
<ToggleButton | |||||
block | |||||
/>, | |||||
); | |||||
const button = screen.getByTestId('button'); | |||||
expect(button).toHaveClass('w-full flex'); | |||||
}); | |||||
it('renders children', () => { | |||||
render( | |||||
<ToggleButton> | |||||
Foo | |||||
</ToggleButton>, | |||||
); | |||||
const children = screen.getByTestId('children'); | |||||
expect(children).toHaveTextContent('Foo'); | |||||
}); | |||||
it('renders a disabled button', () => { | |||||
render( | |||||
<ToggleButton | |||||
disabled | |||||
/>, | |||||
); | |||||
const checkbox = screen.getByRole('checkbox'); | |||||
expect(checkbox).toBeDisabled(); | |||||
}); | |||||
it('handles change events', async () => { | |||||
const onChange = vi.fn().mockImplementationOnce( | |||||
(e: React.ChangeEvent<ToggleButtonDerivedElement>) => { | |||||
e.preventDefault(); | |||||
}, | |||||
); | |||||
render( | |||||
<ToggleButton | |||||
onChange={onChange} | |||||
/>, | |||||
); | |||||
const checkbox = screen.getByRole('checkbox'); | |||||
await userEvent.click(checkbox); | |||||
expect(onChange).toBeCalled(); | |||||
}); | |||||
}); |
@@ -36,29 +36,30 @@ export const ToggleButton = React.forwardRef<ToggleButtonDerivedElement, ToggleB | |||||
const id = idProp ?? defaultId; | const id = idProp ?? defaultId; | ||||
React.useEffect(() => { | React.useEffect(() => { | ||||
if (!(typeof ref === 'object' && ref)) { | |||||
return; | |||||
} | |||||
const { current: element } = ref; | |||||
if (!element) { | |||||
if (typeof ref === 'function') { | |||||
const defaultElement = defaultRef.current as ToggleButtonDerivedElement; | |||||
defaultElement.indeterminate = indeterminate; | |||||
ref(defaultElement); | |||||
return; | return; | ||||
} | } | ||||
const element = ref.current as ToggleButtonDerivedElement; | |||||
element.indeterminate = indeterminate; | element.indeterminate = indeterminate; | ||||
}, [indeterminate, ref]); | |||||
}, [indeterminate, defaultRef, ref]); | |||||
return ( | return ( | ||||
<> | <> | ||||
<input | <input | ||||
{...etcProps} | {...etcProps} | ||||
ref={ref} | |||||
ref={typeof ref === 'function' ? defaultRef : ref} | |||||
type="checkbox" | type="checkbox" | ||||
id={id} | id={id} | ||||
className="sr-only peer tesseract-design-toggle-button" | className="sr-only peer tesseract-design-toggle-button" | ||||
/> | /> | ||||
<label | <label | ||||
data-testid="button" | |||||
htmlFor={id} | htmlFor={id} | ||||
className={clsx( | className={clsx( | ||||
'items-center justify-start rounded overflow-hidden ring-secondary/50 gap-4 leading-none select-none cursor-pointer', | |||||
'items-center justify-start rounded overflow-hidden ring-secondary/50 leading-none select-none cursor-pointer', | |||||
'peer-focus:outline-0 peer-focus:ring-4 peer-focus:ring-secondary/50', | 'peer-focus:outline-0 peer-focus:ring-4 peer-focus:ring-secondary/50', | ||||
'active:ring-tertiary/50 active:ring-4', | 'active:ring-tertiary/50 active:ring-4', | ||||
'peer-disabled:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:ring-0', | 'peer-disabled:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:ring-0', | ||||
@@ -68,11 +69,12 @@ export const ToggleButton = React.forwardRef<ToggleButtonDerivedElement, ToggleB | |||||
'inline-flex max-w-full align-middle': !block, | 'inline-flex max-w-full align-middle': !block, | ||||
}, | }, | ||||
{ | { | ||||
'pl-2 pr-2': compact, | |||||
'pl-4 pr-4': !compact, | |||||
'pl-2 gap-2 pr-2': compact, | |||||
'pl-4 gap-4 pr-4': !compact, | |||||
}, | }, | ||||
{ | { | ||||
'border-2 border-primary peer-disabled:border-primary peer-focus:border-secondary peer-checked:border-tertiary active:border-tertiary': variant !== 'bare', | 'border-2 border-primary peer-disabled:border-primary peer-focus:border-secondary peer-checked:border-tertiary active:border-tertiary': variant !== 'bare', | ||||
'bg-negative text-primary peer-disabled:text-primary peer-focus:text-secondary peer-checked:text-tertiary active:text-tertiary': variant !== 'filled', | |||||
'bg-primary text-negative peer-disabled:bg-primary peer-focus:bg-secondary peer-checked:bg-tertiary active:bg-tertiary': variant === 'filled', | 'bg-primary text-negative peer-disabled:bg-primary peer-focus:bg-secondary peer-checked:bg-tertiary active:bg-tertiary': variant === 'filled', | ||||
}, | }, | ||||
{ | { | ||||
@@ -89,7 +91,6 @@ export const ToggleButton = React.forwardRef<ToggleButtonDerivedElement, ToggleB | |||||
{ | { | ||||
'border-current': variant !== 'filled', | 'border-current': variant !== 'filled', | ||||
'border-negative': variant === 'filled', | 'border-negative': variant === 'filled', | ||||
'-mr-2': compact, | |||||
}, | }, | ||||
)} | )} | ||||
> | > | ||||
@@ -0,0 +1,158 @@ | |||||
import * as React from 'react'; | |||||
import { | |||||
cleanup, | |||||
render, | |||||
screen, | |||||
} from '@testing-library/react'; | |||||
import userEvent from '@testing-library/user-event'; | |||||
import { | |||||
vi, | |||||
expect, | |||||
describe, | |||||
it, | |||||
afterEach, | |||||
} from 'vitest'; | |||||
import matchers from '@testing-library/jest-dom/matchers'; | |||||
import { | |||||
ToggleSwitch, | |||||
ToggleSwitchDerivedElement, | |||||
} from '.'; | |||||
expect.extend(matchers); | |||||
describe('ToggleSwitch', () => { | |||||
afterEach(() => { | |||||
cleanup(); | |||||
}); | |||||
it('renders a checkbox', () => { | |||||
render( | |||||
<ToggleSwitch />, | |||||
); | |||||
const checkbox = screen.getByRole('checkbox'); | |||||
expect(checkbox).toBeInTheDocument(); | |||||
}); | |||||
it('renders a block switch', () => { | |||||
render( | |||||
<ToggleSwitch | |||||
block | |||||
/>, | |||||
); | |||||
const base = screen.getByTestId('base'); | |||||
expect(base).toHaveClass('flex'); | |||||
}); | |||||
it('renders a label when the component is unchecked', () => { | |||||
render( | |||||
<ToggleSwitch | |||||
uncheckedLabel="label" | |||||
/>, | |||||
); | |||||
const uncheckedLabel = screen.getByTestId('uncheckedLabel'); | |||||
expect(uncheckedLabel).toBeInTheDocument(); | |||||
}); | |||||
describe('on subtext', () => { | |||||
it('renders without an unchecked label', () => { | |||||
render( | |||||
<ToggleSwitch | |||||
subtext="subtext" | |||||
/>, | |||||
); | |||||
const subtext = screen.getByTestId('subtext'); | |||||
expect(subtext).toBeInTheDocument(); | |||||
expect(subtext).toHaveClass('pl-16'); | |||||
}); | |||||
it('renders with an unchecked label', () => { | |||||
render( | |||||
<ToggleSwitch | |||||
subtext="subtext" | |||||
uncheckedLabel="label" | |||||
/>, | |||||
); | |||||
const subtext = screen.getByTestId('subtext'); | |||||
expect(subtext).toBeInTheDocument(); | |||||
expect(subtext).toHaveClass('pt-2'); | |||||
}); | |||||
}); | |||||
describe('on indeterminate', () => { | |||||
it('renders an indeterminate checkbox', () => { | |||||
render( | |||||
<ToggleSwitch | |||||
indeterminate | |||||
/>, | |||||
); | |||||
const checkbox = screen.getByRole('checkbox'); | |||||
expect(checkbox).toHaveProperty('indeterminate', true); | |||||
}); | |||||
it('acknowledges passed ref object', () => { | |||||
const ref = React.createRef<ToggleSwitchDerivedElement>(); | |||||
render( | |||||
<ToggleSwitch | |||||
indeterminate | |||||
ref={ref} | |||||
/>, | |||||
); | |||||
expect(ref.current).toHaveProperty('indeterminate', true); | |||||
}); | |||||
it('acknowledges passed legacy ref', () => { | |||||
let refElement = null as null | ToggleSwitchDerivedElement; | |||||
const ref = (element: ToggleSwitchDerivedElement) => { | |||||
refElement = element; | |||||
}; | |||||
render( | |||||
<ToggleSwitch | |||||
indeterminate | |||||
ref={ref} | |||||
/>, | |||||
); | |||||
const checkbox = screen.getByRole('checkbox'); | |||||
expect(checkbox).toHaveProperty('indeterminate', true); | |||||
expect(refElement).toBe(checkbox); | |||||
}); | |||||
}); | |||||
it('handles click events', async () => { | |||||
const onClick = vi.fn().mockImplementationOnce( | |||||
(e: React.MouseEvent<ToggleSwitchDerivedElement>) => { | |||||
e.preventDefault(); | |||||
}, | |||||
); | |||||
render( | |||||
<ToggleSwitch | |||||
onClick={onClick} | |||||
/>, | |||||
); | |||||
const checkbox: HTMLInputElement = screen.getByRole('checkbox'); | |||||
await userEvent.click(checkbox); | |||||
expect(onClick).toBeCalled(); | |||||
}); | |||||
it('handles change events', async () => { | |||||
const onChange = vi.fn().mockImplementationOnce( | |||||
(e: React.ChangeEvent<ToggleSwitchDerivedElement>) => { | |||||
e.preventDefault(); | |||||
}, | |||||
); | |||||
render( | |||||
<ToggleSwitch | |||||
onChange={onChange} | |||||
/>, | |||||
); | |||||
const checkbox: HTMLInputElement = screen.getByRole('checkbox'); | |||||
await userEvent.click(checkbox); | |||||
expect(onChange).toBeCalled(); | |||||
}); | |||||
}); |
@@ -31,15 +31,15 @@ export const ToggleSwitch = React.forwardRef<ToggleSwitchDerivedElement, ToggleS | |||||
const id = idProp ?? defaultId; | const id = idProp ?? defaultId; | ||||
React.useEffect(() => { | React.useEffect(() => { | ||||
if (!(typeof ref === 'object' && ref)) { | |||||
return; | |||||
} | |||||
const { current: element } = ref; | |||||
if (!element) { | |||||
if (typeof ref === 'function') { | |||||
const defaultElement = defaultRef.current as ToggleSwitchDerivedElement; | |||||
defaultElement.indeterminate = indeterminate; | |||||
ref(defaultElement); | |||||
return; | return; | ||||
} | } | ||||
const element = ref.current as ToggleSwitchDerivedElement; | |||||
element.indeterminate = indeterminate; | element.indeterminate = indeterminate; | ||||
}, [indeterminate, ref]); | |||||
}, [indeterminate, defaultRef, ref]); | |||||
return ( | return ( | ||||
<div | <div | ||||
@@ -50,10 +50,11 @@ export const ToggleSwitch = React.forwardRef<ToggleSwitchDerivedElement, ToggleS | |||||
className, | className, | ||||
)} | )} | ||||
style={style} | style={style} | ||||
data-testid="base" | |||||
> | > | ||||
<input | <input | ||||
{...etcProps} | {...etcProps} | ||||
ref={ref} | |||||
ref={typeof ref === 'function' ? defaultRef : ref} | |||||
type="checkbox" | type="checkbox" | ||||
id={id} | id={id} | ||||
className="sr-only peer/radio tesseract-design-toggle-switch" | className="sr-only peer/radio tesseract-design-toggle-switch" | ||||
@@ -0,0 +1,130 @@ | |||||
import * as React from 'react'; | |||||
import { | |||||
cleanup, | |||||
render, | |||||
screen, | |||||
} from '@testing-library/react'; | |||||
import userEvent from '@testing-library/user-event'; | |||||
import { | |||||
vi, | |||||
expect, | |||||
describe, | |||||
it, | |||||
afterEach, | |||||
} from 'vitest'; | |||||
import matchers from '@testing-library/jest-dom/matchers'; | |||||
import { | |||||
ToggleTickBox, | |||||
ToggleTickBoxDerivedElement, | |||||
} from '.'; | |||||
expect.extend(matchers); | |||||
describe('ToggleTickBox', () => { | |||||
afterEach(() => { | |||||
cleanup(); | |||||
}); | |||||
it('renders a checkbox', () => { | |||||
render( | |||||
<ToggleTickBox />, | |||||
); | |||||
const checkbox = screen.getByRole('checkbox'); | |||||
expect(checkbox).toBeInTheDocument(); | |||||
}); | |||||
describe('on indeterminate', () => { | |||||
it('renders an indeterminate checkbox', () => { | |||||
render( | |||||
<ToggleTickBox | |||||
indeterminate | |||||
/>, | |||||
); | |||||
const checkbox = screen.getByRole('checkbox'); | |||||
expect(checkbox).toHaveProperty('indeterminate', true); | |||||
}); | |||||
it('acknowledges passed ref object', () => { | |||||
const ref = React.createRef<ToggleTickBoxDerivedElement>(); | |||||
render( | |||||
<ToggleTickBox | |||||
indeterminate | |||||
ref={ref} | |||||
/>, | |||||
); | |||||
expect(ref.current).toHaveProperty('indeterminate', true); | |||||
}); | |||||
it('acknowledges passed legacy ref', () => { | |||||
let refElement = null as null | ToggleTickBoxDerivedElement; | |||||
const ref = (element: ToggleTickBoxDerivedElement) => { | |||||
refElement = element; | |||||
}; | |||||
render( | |||||
<ToggleTickBox | |||||
indeterminate | |||||
ref={ref} | |||||
/>, | |||||
); | |||||
const checkbox = screen.getByRole('checkbox'); | |||||
expect(checkbox).toHaveProperty('indeterminate', true); | |||||
expect(refElement).toBe(checkbox); | |||||
}); | |||||
}); | |||||
it('renders a block tick box', () => { | |||||
render( | |||||
<ToggleTickBox | |||||
block | |||||
/>, | |||||
); | |||||
const base = screen.getByTestId('base'); | |||||
expect(base).toHaveClass('flex'); | |||||
}); | |||||
it('renders a subtext', () => { | |||||
render( | |||||
<ToggleTickBox | |||||
subtext="subtext" | |||||
/>, | |||||
); | |||||
const subtext: HTMLElement = screen.getByTestId('subtext'); | |||||
expect(subtext).toBeInTheDocument(); | |||||
}); | |||||
it('handles click events', async () => { | |||||
const onClick = vi.fn().mockImplementationOnce( | |||||
(e: React.MouseEvent<ToggleTickBoxDerivedElement>) => { | |||||
e.preventDefault(); | |||||
}, | |||||
); | |||||
render( | |||||
<ToggleTickBox | |||||
onClick={onClick} | |||||
/>, | |||||
); | |||||
const checkbox: HTMLInputElement = screen.getByRole('checkbox'); | |||||
await userEvent.click(checkbox); | |||||
expect(onClick).toBeCalled(); | |||||
}); | |||||
it('handles change events', async () => { | |||||
const onChange = vi.fn().mockImplementationOnce( | |||||
(e: React.MouseEvent<ToggleTickBoxDerivedElement>) => { | |||||
e.preventDefault(); | |||||
}, | |||||
); | |||||
render( | |||||
<ToggleTickBox | |||||
onChange={onChange} | |||||
/>, | |||||
); | |||||
const checkbox: HTMLInputElement = screen.getByRole('checkbox'); | |||||
await userEvent.click(checkbox); | |||||
expect(onChange).toBeCalled(); | |||||
}); | |||||
}); |
@@ -28,15 +28,15 @@ export const ToggleTickBox = React.forwardRef<ToggleTickBoxDerivedElement, Toggl | |||||
const id = idProp ?? defaultId; | const id = idProp ?? defaultId; | ||||
React.useEffect(() => { | React.useEffect(() => { | ||||
if (!(typeof ref === 'object' && ref)) { | |||||
return; | |||||
} | |||||
const { current: element } = ref; | |||||
if (!element) { | |||||
if (typeof ref === 'function') { | |||||
const defaultElement = defaultRef.current as ToggleTickBoxDerivedElement; | |||||
defaultElement.indeterminate = indeterminate; | |||||
ref(defaultElement); | |||||
return; | return; | ||||
} | } | ||||
const element = ref.current as ToggleTickBoxDerivedElement; | |||||
element.indeterminate = indeterminate; | element.indeterminate = indeterminate; | ||||
}, [indeterminate, ref]); | |||||
}, [indeterminate, defaultRef, ref]); | |||||
return ( | return ( | ||||
<div | <div | ||||
@@ -47,10 +47,11 @@ export const ToggleTickBox = React.forwardRef<ToggleTickBoxDerivedElement, Toggl | |||||
className, | className, | ||||
)} | )} | ||||
style={style} | style={style} | ||||
data-testid="base" | |||||
> | > | ||||
<input | <input | ||||
{...etcProps} | {...etcProps} | ||||
ref={ref} | |||||
ref={typeof ref === 'function' ? defaultRef : ref} | |||||
type="checkbox" | type="checkbox" | ||||
id={id} | id={id} | ||||
className="sr-only peer/radio tesseract-design-toggle-tick-box" | className="sr-only peer/radio tesseract-design-toggle-tick-box" | ||||
@@ -0,0 +1,14 @@ | |||||
import { describe, it, expect } from 'vitest'; | |||||
import * as WebMultiChoiceReact from '.'; | |||||
describe('web-multichoice-react', () => { | |||||
it.each([ | |||||
'MenuMultiSelect', | |||||
'TagInput', | |||||
'ToggleButton', | |||||
'ToggleSwitch', | |||||
'ToggleTickBox', | |||||
])('exports %s', (namedExport) => { | |||||
expect(WebMultiChoiceReact).toHaveProperty(namedExport); | |||||
}); | |||||
}); |
@@ -15,8 +15,11 @@ | |||||
"devDependencies": { | "devDependencies": { | ||||
"@testing-library/jest-dom": "^5.16.5", | "@testing-library/jest-dom": "^5.16.5", | ||||
"@testing-library/react": "^13.4.0", | "@testing-library/react": "^13.4.0", | ||||
"@testing-library/user-event": "^14.4.3", | |||||
"@types/node": "^18.14.1", | "@types/node": "^18.14.1", | ||||
"@types/react": "^18.0.27", | "@types/react": "^18.0.27", | ||||
"@types/testing-library__jest-dom": "^5.14.7", | |||||
"@vitest/coverage-v8": "^0.33.0", | |||||
"eslint": "^8.35.0", | "eslint": "^8.35.0", | ||||
"eslint-config-lxsmnsyc": "^0.5.0", | "eslint-config-lxsmnsyc": "^0.5.0", | ||||
"jsdom": "^21.1.0", | "jsdom": "^21.1.0", | ||||
@@ -26,7 +29,7 @@ | |||||
"react-test-renderer": "^18.2.0", | "react-test-renderer": "^18.2.0", | ||||
"tslib": "^2.5.0", | "tslib": "^2.5.0", | ||||
"typescript": "^4.9.5", | "typescript": "^4.9.5", | ||||
"vitest": "^0.28.1" | |||||
"vitest": "^0.33.0" | |||||
}, | }, | ||||
"peerDependencies": { | "peerDependencies": { | ||||
"react": "^16.8 || ^17.0 || ^18.0", | "react": "^16.8 || ^17.0 || ^18.0", | ||||
@@ -0,0 +1,203 @@ | |||||
import * as React from 'react'; | |||||
import { | |||||
cleanup, | |||||
render, | |||||
screen, | |||||
} from '@testing-library/react'; | |||||
import userEvent from '@testing-library/user-event'; | |||||
import { Button } from '@tesseract-design/web-base'; | |||||
import { | |||||
vi, | |||||
expect, | |||||
describe, | |||||
it, | |||||
afterEach, | |||||
} from 'vitest'; | |||||
import matchers from '@testing-library/jest-dom/matchers'; | |||||
import { | |||||
LinkButton, | |||||
LinkButtonDerivedElement, | |||||
} from '.'; | |||||
expect.extend(matchers); | |||||
describe('LinkButton', () => { | |||||
afterEach(() => { | |||||
cleanup(); | |||||
}); | |||||
it('renders a button', () => { | |||||
render( | |||||
<LinkButton | |||||
href="https://www.example.com" | |||||
/>, | |||||
); | |||||
const button = screen.getByRole('link'); | |||||
expect(button).toBeInTheDocument(); | |||||
}); | |||||
it('renders a subtext', () => { | |||||
render( | |||||
<LinkButton | |||||
href="#" | |||||
subtext="subtext" | |||||
/>, | |||||
); | |||||
const subtext = screen.getByTestId('subtext'); | |||||
expect(subtext).toBeInTheDocument(); | |||||
}); | |||||
it('renders a badge', () => { | |||||
render( | |||||
<LinkButton | |||||
href="#" | |||||
badge="badge" | |||||
/>, | |||||
); | |||||
const badge = screen.getByTestId('badge'); | |||||
expect(badge).toBeInTheDocument(); | |||||
}); | |||||
it('renders as a menu item', () => { | |||||
render( | |||||
<LinkButton | |||||
href="#" | |||||
menuItem | |||||
/>, | |||||
); | |||||
const menuItemIndicator = screen.getByTestId('menuItemIndicator'); | |||||
expect(menuItemIndicator).toBeInTheDocument(); | |||||
}); | |||||
it('handles click events', async () => { | |||||
const onClick = vi.fn().mockImplementationOnce( | |||||
(e: React.MouseEvent<LinkButtonDerivedElement>) => { | |||||
e.preventDefault(); | |||||
}, | |||||
); | |||||
render( | |||||
<LinkButton | |||||
href="#" | |||||
onClick={onClick} | |||||
/>, | |||||
); | |||||
const button = screen.getByRole('link'); | |||||
await userEvent.click(button); | |||||
expect(onClick).toBeCalled(); | |||||
}); | |||||
it('renders a compact link', () => { | |||||
render( | |||||
<LinkButton | |||||
href="#" | |||||
compact | |||||
/>, | |||||
); | |||||
const button = screen.getByRole('link'); | |||||
expect(button).toHaveClass('pl-2 gap-2 pr-2'); | |||||
}); | |||||
describe.each` | |||||
size | className | |||||
${'small'} | ${'h-10'} | |||||
${'medium'} | ${'h-12'} | |||||
${'large'} | ${'h-16'} | |||||
`('on $size size', ({ | |||||
size, | |||||
className, | |||||
}: { size: Button.Size, className: string }) => { | |||||
it('renders link styles', () => { | |||||
render( | |||||
<LinkButton | |||||
href="#" | |||||
size={size} | |||||
/>, | |||||
); | |||||
const button = screen.getByRole('link'); | |||||
expect(button).toHaveClass(className); | |||||
}); | |||||
it('renders badge styles', () => { | |||||
render( | |||||
<LinkButton | |||||
href="#" | |||||
size={size} | |||||
badge="badge" | |||||
/>, | |||||
); | |||||
const badge = screen.getByTestId('badge'); | |||||
expect(badge).toBeInTheDocument(); | |||||
}); | |||||
}); | |||||
it.each` | |||||
variant | className | |||||
${'bare'} | ${'bg-negative'} | |||||
${'outline'} | ${'border-2'} | |||||
${'filled'} | ${'bg-primary'} | |||||
`('renders a link with $variant variant', ({ | |||||
variant, | |||||
className, | |||||
}: { variant: Button.Variant, className: string }) => { | |||||
render( | |||||
<LinkButton | |||||
href="#" | |||||
variant={variant} | |||||
/>, | |||||
); | |||||
const button = screen.getByRole('link'); | |||||
expect(button).toHaveClass(className); | |||||
}); | |||||
it('renders a block link', () => { | |||||
render( | |||||
<LinkButton | |||||
href="#" | |||||
block | |||||
/>, | |||||
); | |||||
const button = screen.getByRole('link'); | |||||
expect(button).toHaveClass('w-full flex'); | |||||
}); | |||||
it('renders children', () => { | |||||
render( | |||||
<LinkButton | |||||
href="#" | |||||
> | |||||
Foo | |||||
</LinkButton>, | |||||
); | |||||
const children = screen.getByTestId('children'); | |||||
expect(children).toHaveTextContent('Foo'); | |||||
}); | |||||
it.each(Button.AVAILABLE_TYPES)('renders a link with type %s', (buttonType) => { | |||||
render( | |||||
<LinkButton | |||||
href="#" | |||||
type={buttonType} | |||||
/>, | |||||
); | |||||
const button = screen.getByRole('link'); | |||||
expect(button).toHaveProperty('type', buttonType); | |||||
}); | |||||
it('renders a disabled link', () => { | |||||
render( | |||||
<LinkButton | |||||
href="#" | |||||
disabled | |||||
/>, | |||||
); | |||||
const button = screen.queryByRole('link'); | |||||
expect(button).toBeNull(); | |||||
}); | |||||
}); |
@@ -13,6 +13,7 @@ export interface LinkButtonProps<T = any> extends Omit<React.HTMLProps<LinkButto | |||||
size?: Button.Size; | size?: Button.Size; | ||||
compact?: boolean; | compact?: boolean; | ||||
component?: React.ElementType<T>; | component?: React.ElementType<T>; | ||||
disabled?: boolean; | |||||
} | } | ||||
export const LinkButton = React.forwardRef<LinkButtonDerivedElement, LinkButtonProps>(( | export const LinkButton = React.forwardRef<LinkButtonDerivedElement, LinkButtonProps>(( | ||||
@@ -26,97 +27,103 @@ export const LinkButton = React.forwardRef<LinkButtonDerivedElement, LinkButtonP | |||||
compact = false, | compact = false, | ||||
className, | className, | ||||
block = false, | block = false, | ||||
component: Component = 'a', | |||||
component: EnabledComponent = 'a', | |||||
disabled = false, | |||||
...etcProps | ...etcProps | ||||
}, | }, | ||||
forwardedRef, | forwardedRef, | ||||
) => ( | |||||
<Component | |||||
{...etcProps} | |||||
ref={forwardedRef} | |||||
className={clsx( | |||||
'items-center justify-center rounded overflow-hidden ring-secondary/50 leading-none select-none no-underline', | |||||
'focus:outline-0 focus:ring-4', | |||||
'active:ring-tertiary/50', | |||||
{ | |||||
'flex w-full': block, | |||||
'inline-flex max-w-full align-middle': !block, | |||||
}, | |||||
{ | |||||
'pl-2 gap-2': compact, | |||||
'pl-4 gap-4': !compact, | |||||
'pr-4': !(compact || menuItem), | |||||
'pr-2': compact || menuItem, | |||||
}, | |||||
{ | |||||
'border-2 border-primary focus:border-secondary active:border-tertiary': variant !== 'bare', | |||||
'bg-negative text-primary focus:text-secondary active:text-tertiary': variant !== 'filled', | |||||
'bg-primary text-negative focus:bg-secondary active:bg-tertiary focus:text-negative active:text-negative': variant === 'filled', | |||||
}, | |||||
{ | |||||
'h-10': size === 'small', | |||||
'h-12': size === 'medium', | |||||
'h-16': size === 'large', | |||||
}, | |||||
className, | |||||
)} | |||||
> | |||||
<span | |||||
) => { | |||||
const Component = disabled ? 'span' : EnabledComponent; | |||||
return ( | |||||
<Component | |||||
{...etcProps} | |||||
ref={forwardedRef} | |||||
className={clsx( | className={clsx( | ||||
'flex-auto min-w-0', | |||||
'items-center justify-center rounded overflow-hidden ring-secondary/50 leading-none select-none no-underline m-0', | |||||
'focus:outline-0 focus:ring-4', | |||||
'active:ring-tertiary/50', | |||||
disabled && 'opacity-50 cursor-not-allowed', | |||||
{ | { | ||||
'text-left': compact || menuItem, | |||||
'text-center': !(compact || menuItem), | |||||
'flex w-full': block, | |||||
'inline-flex max-w-full align-middle': !block, | |||||
}, | }, | ||||
{ | |||||
'pl-2 gap-2': compact, | |||||
'pl-4 gap-4': !compact, | |||||
'pr-4': !(compact || menuItem), | |||||
'pr-2': compact || menuItem, | |||||
}, | |||||
{ | |||||
'border-2 border-primary focus:border-secondary active:border-tertiary': variant !== 'bare', | |||||
'bg-negative text-primary focus:text-secondary active:text-tertiary': variant !== 'filled', | |||||
'bg-primary text-negative focus:bg-secondary active:bg-tertiary focus:text-negative active:text-negative': variant === 'filled', | |||||
}, | |||||
{ | |||||
'h-10': size === 'small', | |||||
'h-12': size === 'medium', | |||||
'h-16': size === 'large', | |||||
}, | |||||
className, | |||||
)} | )} | ||||
data-testid="link" | |||||
> | > | ||||
<span | <span | ||||
className="block uppercase font-bold h-[1.1em] w-full whitespace-nowrap overflow-hidden text-ellipsis font-semi-expanded" | |||||
data-testid="children" | |||||
className={clsx( | |||||
'flex-auto min-w-0', | |||||
{ | |||||
'text-left': compact || menuItem, | |||||
'text-center': !(compact || menuItem), | |||||
}, | |||||
)} | |||||
> | > | ||||
{children} | |||||
<span | |||||
className="block uppercase font-bold h-[1.1em] w-full whitespace-nowrap overflow-hidden text-ellipsis font-semi-expanded" | |||||
data-testid="children" | |||||
> | |||||
{children} | |||||
</span> | |||||
{subtext && ( | |||||
<> | |||||
<span className="sr-only"> | |||||
{' - '} | |||||
</span> | |||||
<span | |||||
className="block h-[1.3em] w-full whitespace-nowrap overflow-hidden text-ellipsis font-semi-expanded font-bold text-xs" | |||||
data-testid="subtext" | |||||
> | |||||
{subtext} | |||||
</span> | |||||
</> | |||||
)} | |||||
</span> | </span> | ||||
{subtext && ( | |||||
{badge && ( | |||||
<> | <> | ||||
<span className="sr-only"> | <span className="sr-only"> | ||||
{' - '} | {' - '} | ||||
</span> | </span> | ||||
<span | <span | ||||
className="block h-[1.3em] w-full whitespace-nowrap overflow-hidden text-ellipsis font-semi-expanded font-bold text-xs" | |||||
data-testid="subtext" | |||||
data-testid="badge" | |||||
> | > | ||||
{subtext} | |||||
{badge} | |||||
</span> | </span> | ||||
</> | </> | ||||
)} | )} | ||||
</span> | |||||
{badge && ( | |||||
<> | |||||
<span className="sr-only"> | |||||
{' - '} | |||||
</span> | |||||
{menuItem && ( | |||||
<span | <span | ||||
data-testid="badge" | |||||
data-testid="menuItemIndicator" | |||||
> | > | ||||
{badge} | |||||
<svg | |||||
className="w-6 h-6 fill-none stroke-current stroke-2 linejoin-round linecap-round" | |||||
viewBox="0 0 24 24" | |||||
role="presentation" | |||||
> | |||||
<polyline points="9 18 15 12 9 6" /> | |||||
</svg> | |||||
</span> | </span> | ||||
</> | |||||
)} | |||||
{menuItem && ( | |||||
<span | |||||
data-testid="menuItemIndicator" | |||||
> | |||||
<svg | |||||
className="w-6 h-6 fill-none stroke-current stroke-2 linejoin-round linecap-round" | |||||
viewBox="0 0 24 24" | |||||
role="presentation" | |||||
> | |||||
<polyline points="9 18 15 12 9 6" /> | |||||
</svg> | |||||
</span> | |||||
)} | |||||
</Component> | |||||
)); | |||||
)} | |||||
</Component> | |||||
); | |||||
}); | |||||
LinkButton.displayName = 'LinkButton'; | LinkButton.displayName = 'LinkButton'; | ||||
@@ -129,4 +136,5 @@ LinkButton.defaultProps = { | |||||
badge: undefined, | badge: undefined, | ||||
subtext: undefined, | subtext: undefined, | ||||
block: false, | block: false, | ||||
disabled: false, | |||||
}; | }; |
@@ -0,0 +1,10 @@ | |||||
import { describe, it, expect } from 'vitest'; | |||||
import * as WebNavigationReact from '.'; | |||||
describe('web-navigation-react', () => { | |||||
it.each([ | |||||
'LinkButton', | |||||
])('exports %s', (namedExport) => { | |||||
expect(WebNavigationReact).toHaveProperty(namedExport); | |||||
}); | |||||
}); |
@@ -2,7 +2,8 @@ | |||||
"root": true, | "root": true, | ||||
"rules": { | "rules": { | ||||
"quote-props": "off", | "quote-props": "off", | ||||
"react/jsx-props-no-spreading": "off" | |||||
"react/jsx-props-no-spreading": "off", | |||||
"import/no-extraneous-dependencies": "off" | |||||
}, | }, | ||||
"extends": [ | "extends": [ | ||||
"lxsmnsyc/typescript/react" | "lxsmnsyc/typescript/react" | ||||
@@ -329,12 +329,21 @@ importers: | |||||
'@testing-library/react': | '@testing-library/react': | ||||
specifier: ^13.4.0 | specifier: ^13.4.0 | ||||
version: 13.4.0(react-dom@18.2.0)(react@18.2.0) | version: 13.4.0(react-dom@18.2.0)(react@18.2.0) | ||||
'@testing-library/user-event': | |||||
specifier: ^14.4.3 | |||||
version: 14.4.3(@testing-library/dom@8.20.1) | |||||
'@types/node': | '@types/node': | ||||
specifier: ^18.14.1 | specifier: ^18.14.1 | ||||
version: 18.14.1 | version: 18.14.1 | ||||
'@types/react': | '@types/react': | ||||
specifier: ^18.0.27 | specifier: ^18.0.27 | ||||
version: 18.2.14 | version: 18.2.14 | ||||
'@types/testing-library__jest-dom': | |||||
specifier: ^5.14.7 | |||||
version: 5.14.7 | |||||
'@vitest/coverage-v8': | |||||
specifier: ^0.33.0 | |||||
version: 0.33.0(vitest@0.33.0) | |||||
eslint: | eslint: | ||||
specifier: ^8.35.0 | specifier: ^8.35.0 | ||||
version: 8.43.0 | version: 8.43.0 | ||||
@@ -363,8 +372,8 @@ importers: | |||||
specifier: ^4.9.5 | specifier: ^4.9.5 | ||||
version: 4.9.5 | version: 4.9.5 | ||||
vitest: | vitest: | ||||
specifier: ^0.28.1 | |||||
version: 0.28.1(jsdom@21.1.0) | |||||
specifier: ^0.33.0 | |||||
version: 0.33.0(jsdom@21.1.0) | |||||
categories/information/react: | categories/information/react: | ||||
dependencies: | dependencies: | ||||
@@ -378,12 +387,21 @@ importers: | |||||
'@testing-library/react': | '@testing-library/react': | ||||
specifier: ^13.4.0 | specifier: ^13.4.0 | ||||
version: 13.4.0(react-dom@18.2.0)(react@18.2.0) | version: 13.4.0(react-dom@18.2.0)(react@18.2.0) | ||||
'@testing-library/user-event': | |||||
specifier: ^14.4.3 | |||||
version: 14.4.3(@testing-library/dom@8.20.1) | |||||
'@types/node': | '@types/node': | ||||
specifier: ^18.14.1 | specifier: ^18.14.1 | ||||
version: 18.14.1 | version: 18.14.1 | ||||
'@types/react': | '@types/react': | ||||
specifier: ^18.0.27 | specifier: ^18.0.27 | ||||
version: 18.2.14 | version: 18.2.14 | ||||
'@types/testing-library__jest-dom': | |||||
specifier: ^5.14.7 | |||||
version: 5.14.7 | |||||
'@vitest/coverage-v8': | |||||
specifier: ^0.33.0 | |||||
version: 0.33.0(vitest@0.33.0) | |||||
eslint: | eslint: | ||||
specifier: ^8.35.0 | specifier: ^8.35.0 | ||||
version: 8.43.0 | version: 8.43.0 | ||||
@@ -412,8 +430,8 @@ importers: | |||||
specifier: ^4.9.5 | specifier: ^4.9.5 | ||||
version: 4.9.5 | version: 4.9.5 | ||||
vitest: | vitest: | ||||
specifier: ^0.28.1 | |||||
version: 0.28.1(jsdom@21.1.0) | |||||
specifier: ^0.33.0 | |||||
version: 0.33.0(jsdom@21.1.0) | |||||
categories/multichoice/react: | categories/multichoice/react: | ||||
dependencies: | dependencies: | ||||
@@ -436,12 +454,21 @@ importers: | |||||
'@testing-library/react': | '@testing-library/react': | ||||
specifier: ^13.4.0 | specifier: ^13.4.0 | ||||
version: 13.4.0(react-dom@18.2.0)(react@18.2.0) | version: 13.4.0(react-dom@18.2.0)(react@18.2.0) | ||||
'@testing-library/user-event': | |||||
specifier: ^14.4.3 | |||||
version: 14.4.3(@testing-library/dom@8.20.1) | |||||
'@types/node': | '@types/node': | ||||
specifier: ^18.14.1 | specifier: ^18.14.1 | ||||
version: 18.14.1 | version: 18.14.1 | ||||
'@types/react': | '@types/react': | ||||
specifier: ^18.0.27 | specifier: ^18.0.27 | ||||
version: 18.2.14 | version: 18.2.14 | ||||
'@types/testing-library__jest-dom': | |||||
specifier: ^5.14.7 | |||||
version: 5.14.7 | |||||
'@vitest/coverage-v8': | |||||
specifier: ^0.33.0 | |||||
version: 0.33.0(vitest@0.33.0) | |||||
eslint: | eslint: | ||||
specifier: ^8.35.0 | specifier: ^8.35.0 | ||||
version: 8.43.0 | version: 8.43.0 | ||||
@@ -473,8 +500,8 @@ importers: | |||||
specifier: ^4.9.5 | specifier: ^4.9.5 | ||||
version: 4.9.5 | version: 4.9.5 | ||||
vitest: | vitest: | ||||
specifier: ^0.28.1 | |||||
version: 0.28.1(jsdom@21.1.0) | |||||
specifier: ^0.33.0 | |||||
version: 0.33.0(jsdom@21.1.0) | |||||
categories/navigation/react: | categories/navigation/react: | ||||
dependencies: | dependencies: | ||||
@@ -491,12 +518,21 @@ importers: | |||||
'@testing-library/react': | '@testing-library/react': | ||||
specifier: ^13.4.0 | specifier: ^13.4.0 | ||||
version: 13.4.0(react-dom@18.2.0)(react@18.2.0) | version: 13.4.0(react-dom@18.2.0)(react@18.2.0) | ||||
'@testing-library/user-event': | |||||
specifier: ^14.4.3 | |||||
version: 14.4.3(@testing-library/dom@8.20.1) | |||||
'@types/node': | '@types/node': | ||||
specifier: ^18.14.1 | specifier: ^18.14.1 | ||||
version: 18.14.1 | version: 18.14.1 | ||||
'@types/react': | '@types/react': | ||||
specifier: ^18.0.27 | specifier: ^18.0.27 | ||||
version: 18.2.14 | version: 18.2.14 | ||||
'@types/testing-library__jest-dom': | |||||
specifier: ^5.14.7 | |||||
version: 5.14.7 | |||||
'@vitest/coverage-v8': | |||||
specifier: ^0.33.0 | |||||
version: 0.33.0(vitest@0.33.0) | |||||
eslint: | eslint: | ||||
specifier: ^8.35.0 | specifier: ^8.35.0 | ||||
version: 8.43.0 | version: 8.43.0 | ||||
@@ -525,8 +561,8 @@ importers: | |||||
specifier: ^4.9.5 | specifier: ^4.9.5 | ||||
version: 4.9.5 | version: 4.9.5 | ||||
vitest: | vitest: | ||||
specifier: ^0.28.1 | |||||
version: 0.28.1(jsdom@21.1.0) | |||||
specifier: ^0.33.0 | |||||
version: 0.33.0(jsdom@21.1.0) | |||||
categories/number/react: | categories/number/react: | ||||
dependencies: | dependencies: | ||||
@@ -905,6 +905,8 @@ const OptionPage: NextPage<Props> = ({ | |||||
<div> | <div> | ||||
<MultiChoice.ToggleButton | <MultiChoice.ToggleButton | ||||
variant="bare" | variant="bare" | ||||
indeterminate | |||||
ref={(el) => { console.log(el); }} | |||||
block | block | ||||
> | > | ||||
Button | Button | ||||