@@ -66,3 +66,4 @@ | |||
# Others | |||
- [X] Add `select-none` to input labels, etc. | |||
- [ ] Test all components! |
@@ -3,7 +3,10 @@ | |||
"rules": { | |||
"quote-props": "off", | |||
"react/jsx-props-no-spreading": "off", | |||
"react/button-has-type": "off" | |||
"react/button-has-type": "off", | |||
"@typescript-eslint/no-unsafe-call": "off", | |||
"@typescript-eslint/no-unsafe-member-access": "off", | |||
"import/no-extraneous-dependencies": "off" | |||
}, | |||
"extends": [ | |||
"lxsmnsyc/typescript/react" | |||
@@ -18,6 +18,7 @@ | |||
"@testing-library/user-event": "^14.4.3", | |||
"@types/node": "^18.14.1", | |||
"@types/react": "^18.0.27", | |||
"@types/testing-library__jest-dom": "^5.14.7", | |||
"eslint": "^8.35.0", | |||
"eslint-config-lxsmnsyc": "^0.5.0", | |||
"jsdom": "^21.1.0", | |||
@@ -1,4 +0,0 @@ | |||
import matchers from '@testing-library/jest-dom/matchers'; | |||
import { expect } from 'vitest'; | |||
expect.extend(matchers); |
@@ -12,13 +12,12 @@ import { | |||
it, | |||
expect, afterEach, | |||
} from 'vitest'; | |||
import matchers from '@testing-library/jest-dom/matchers'; | |||
import { | |||
ActionButton, | |||
} from '.'; | |||
import matchers from '@testing-library/jest-dom/matchers'; | |||
expect.extend(matchers); | |||
vi.mock('@tesseract-design/web-base-button'); | |||
expect.extend(matchers); | |||
describe('ActionButton', () => { | |||
afterEach(() => { | |||
@@ -65,7 +64,7 @@ describe('ActionButton', () => { | |||
}); | |||
it('handles click events', async () => { | |||
const onClick = vi.fn().mockImplementationOnce((e) => { e.preventDefault() }); | |||
const onClick = vi.fn().mockImplementationOnce((e) => { e.preventDefault(); }); | |||
render( | |||
<ActionButton | |||
onClick={onClick} | |||
@@ -94,7 +93,7 @@ describe('ActionButton', () => { | |||
${'small'} | ${'h-10'} | |||
${'medium'} | ${'h-12'} | |||
${'large'} | ${'h-16'} | |||
`('on %s size', ({ | |||
`('on $size size', ({ | |||
size, | |||
className, | |||
}: { size: Button.Size, className: string }) => { | |||
@@ -127,7 +126,7 @@ describe('ActionButton', () => { | |||
${'bare'} | ${'bg-negative'} | |||
${'outline'} | ${'border-2'} | |||
${'filled'} | ${'bg-primary'} | |||
`('renders a button with variant %s', ({ | |||
`('renders a button with $variant variant', ({ | |||
variant, | |||
className, | |||
}: { variant: Button.Variant, className: string }) => { | |||
@@ -157,7 +156,7 @@ describe('ActionButton', () => { | |||
render( | |||
<ActionButton> | |||
Foo | |||
</ActionButton> | |||
</ActionButton>, | |||
); | |||
const children: HTMLElement = screen.getByTestId('children'); | |||
@@ -168,7 +167,7 @@ describe('ActionButton', () => { | |||
render( | |||
<ActionButton | |||
type={buttonType} | |||
/> | |||
/>, | |||
); | |||
const button: HTMLButtonElement = screen.getByRole('button'); | |||
expect(button).toHaveProperty('type', buttonType); | |||
@@ -178,7 +177,7 @@ describe('ActionButton', () => { | |||
render( | |||
<ActionButton | |||
disabled | |||
/> | |||
/>, | |||
); | |||
const button: HTMLButtonElement = screen.getByTestId('button'); | |||
expect(button).toBeDisabled(); | |||
@@ -15,8 +15,10 @@ | |||
"devDependencies": { | |||
"@testing-library/jest-dom": "^5.16.5", | |||
"@testing-library/react": "^13.4.0", | |||
"@testing-library/user-event": "^14.4.3", | |||
"@types/node": "^18.14.1", | |||
"@types/react": "^18.0.27", | |||
"@types/testing-library__jest-dom": "^5.14.7", | |||
"eslint": "^8.35.0", | |||
"eslint-config-lxsmnsyc": "^0.5.0", | |||
"jsdom": "^21.1.0", | |||
@@ -83,4 +85,4 @@ | |||
"typesVersions": { | |||
"*": {} | |||
} | |||
} | |||
} |
@@ -1,9 +1,3 @@ | |||
{ | |||
"target": "es2018", | |||
"entryPoints": { | |||
".": "src/index.ts", | |||
"./dist/DropdownSelect.css": "src/components/DropdownSelect/DropdownSelect.css", | |||
"./dist/RadioButton.css": "src/components/RadioButton/RadioButton.css", | |||
"./dist/RadioTickBox.css": "src/components/RadioTickBox/RadioTickBox.css" | |||
} | |||
"target": "es2018" | |||
} |
@@ -10,7 +10,7 @@ export type ComboBoxDerivedElement = HTMLInputElement; | |||
/** | |||
* Props of the {@link ComboBox} component. | |||
*/ | |||
export interface ComboBoxProps extends Omit<React.HTMLProps<ComboBoxDerivedElement>, 'size' | 'type' | 'style' | 'label' | 'list' | 'inputMode'> { | |||
export interface ComboBoxProps extends Omit<React.HTMLProps<ComboBoxDerivedElement>, 'size' | 'type' | 'label' | 'list' | 'inputMode'> { | |||
/** | |||
* Short textual description indicating the nature of the component's value. | |||
*/ | |||
@@ -73,6 +73,7 @@ export const ComboBox = React.forwardRef<ComboBoxDerivedElement, ComboBoxProps>( | |||
children, | |||
inputMode = 'text' as const, | |||
id: idProp, | |||
style, | |||
...etcProps | |||
}: ComboBoxProps, | |||
forwardedRef, | |||
@@ -106,6 +107,7 @@ export const ComboBox = React.forwardRef<ComboBoxDerivedElement, ComboBoxProps>( | |||
}, | |||
className, | |||
)} | |||
style={style} | |||
> | |||
<input | |||
{...etcProps} | |||
@@ -0,0 +1,235 @@ | |||
import * as React from 'react'; | |||
import { | |||
cleanup, | |||
render, | |||
screen, | |||
} from '@testing-library/react'; | |||
import { TextControl } from '@tesseract-design/web-base'; | |||
import userEvent from '@testing-library/user-event'; | |||
import { | |||
afterEach, | |||
expect, | |||
vi, | |||
describe, | |||
it, | |||
} from 'vitest'; | |||
import matchers from '@testing-library/jest-dom/matchers'; | |||
import { | |||
DropdownSelect, | |||
} from '.'; | |||
expect.extend(matchers); | |||
describe('DropdownSelect', () => { | |||
afterEach(() => { | |||
cleanup(); | |||
}); | |||
it('renders a combobox', () => { | |||
render(<DropdownSelect />); | |||
const combobox = screen.getByRole('combobox'); | |||
expect(combobox).toBeInTheDocument(); | |||
}); | |||
it('renders a border', () => { | |||
render( | |||
<DropdownSelect | |||
border | |||
/>, | |||
); | |||
const border = screen.getByTestId('border'); | |||
expect(border).toBeInTheDocument(); | |||
}); | |||
it('renders a label', () => { | |||
render( | |||
<DropdownSelect | |||
label="foo" | |||
/>, | |||
); | |||
const combobox = screen.getByLabelText('foo'); | |||
expect(combobox).toBeInTheDocument(); | |||
const label = screen.getByTestId('label'); | |||
expect(label).toHaveTextContent('foo'); | |||
}); | |||
it('renders a hidden label', () => { | |||
render( | |||
<DropdownSelect | |||
label="foo" | |||
hiddenLabel | |||
/>, | |||
); | |||
const combobox = screen.getByLabelText('foo'); | |||
expect(combobox).toBeInTheDocument(); | |||
const label = screen.getByTestId('label'); | |||
expect(label).toHaveClass('sr-only'); | |||
}); | |||
it('renders a hint', () => { | |||
render( | |||
<DropdownSelect | |||
hint="foo" | |||
/>, | |||
); | |||
const hint = screen.getByTestId('hint'); | |||
expect(hint).toBeInTheDocument(); | |||
}); | |||
it('render options with implicit values', () => { | |||
render( | |||
<DropdownSelect> | |||
<option>foo</option> | |||
<option>bar</option> | |||
</DropdownSelect>, | |||
); | |||
const combobox = screen.getByRole('combobox'); | |||
expect(combobox.children).toHaveLength(2); | |||
}); | |||
it('renders valid options', () => { | |||
render( | |||
<DropdownSelect> | |||
<option value="foo">foo</option> | |||
<option value="bar">bar</option> | |||
</DropdownSelect>, | |||
); | |||
const combobox = screen.getByRole('combobox'); | |||
expect(combobox.children).toHaveLength(2); | |||
}); | |||
it('renders option groups', () => { | |||
render( | |||
<DropdownSelect> | |||
<optgroup label="foo"> | |||
<option value="baz">baz</option> | |||
</optgroup> | |||
<optgroup label="bar"> | |||
<option value="quux">quux</option> | |||
<option value="quuux">quuux</option> | |||
</optgroup> | |||
</DropdownSelect>, | |||
); | |||
const combobox = screen.getByRole('combobox'); | |||
expect(combobox.children).toHaveLength(2); | |||
expect(combobox.children[0].children).toHaveLength(1); | |||
expect(combobox.children[1].children).toHaveLength(2); | |||
}); | |||
describe.each` | |||
size | inputClassNames | hintClassNames | indicatorClassNames | |||
${'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, | |||
inputClassNames, | |||
hintClassNames, | |||
indicatorClassNames, | |||
}: { | |||
size: TextControl.Size, | |||
inputClassNames: string[], | |||
hintClassNames: string[], | |||
indicatorClassNames: string[], | |||
}) => { | |||
it('renders input styles', () => { | |||
render( | |||
<DropdownSelect | |||
size={size} | |||
/>, | |||
); | |||
const combobox = screen.getByRole('combobox'); | |||
expect(combobox).toHaveClass(...inputClassNames); | |||
}); | |||
it('renders hint styles', () => { | |||
render( | |||
<DropdownSelect | |||
size={size} | |||
hint="hint" | |||
indicator="a" | |||
/>, | |||
); | |||
const hint = screen.getByTestId('hint'); | |||
expect(hint).toHaveClass(...hintClassNames); | |||
}); | |||
it('renders indicator styles', () => { | |||
render( | |||
<DropdownSelect | |||
size={size} | |||
indicator="a'" | |||
/>, | |||
); | |||
const indicator = screen.getByTestId('indicator'); | |||
expect(indicator).toHaveClass(...indicatorClassNames); | |||
}); | |||
}); | |||
it('renders a block input', () => { | |||
render( | |||
<DropdownSelect | |||
block | |||
/>, | |||
); | |||
const base = screen.getByTestId('base'); | |||
expect(base).toHaveClass('block'); | |||
}); | |||
describe.each` | |||
variant | inputClassNames | hintClassNames | |||
${'default'} | ${['pl-4']} | ${['bottom-0', 'pl-4', 'pb-1']} | |||
${'alternate'} | ${['pl-1.5', 'pt-4']} | ${['top-0.5']} | |||
`('on $variant variant', ({ | |||
variant, | |||
inputClassNames, | |||
hintClassNames, | |||
}: { | |||
variant: TextControl.Variant, | |||
inputClassNames: string[], | |||
hintClassNames: string[], | |||
}) => { | |||
it('renders input styles', () => { | |||
render( | |||
<DropdownSelect | |||
variant={variant} | |||
/>, | |||
); | |||
const combobox = screen.getByRole('combobox'); | |||
expect(combobox).toHaveClass(...inputClassNames); | |||
}); | |||
it('renders hint styles', () => { | |||
render( | |||
<DropdownSelect | |||
variant={variant} | |||
hint="hint" | |||
/>, | |||
); | |||
const hint = screen.getByTestId('hint'); | |||
expect(hint).toHaveClass(...hintClassNames); | |||
}); | |||
}); | |||
it('handles change events', async () => { | |||
const onChange = vi.fn().mockImplementationOnce((e) => { e.preventDefault(); }) | |||
render( | |||
<DropdownSelect | |||
onChange={onChange} | |||
> | |||
<option value="foo">foo</option> | |||
<option value="bar">bar</option> | |||
</DropdownSelect>, | |||
); | |||
const combobox: HTMLSelectElement = screen.getByRole('combobox'); | |||
const [, secondOption] = screen.getAllByRole('option'); | |||
await userEvent.selectOptions(combobox, secondOption); | |||
expect(onChange).toBeCalled(); | |||
}); | |||
}); |
@@ -10,7 +10,7 @@ export type DropdownSelectDerivedElement = HTMLSelectElement; | |||
/** | |||
* Props of the {@link DropdownSelect} component. | |||
*/ | |||
export interface DropdownSelectProps extends Omit<React.HTMLProps<DropdownSelectDerivedElement>, 'size' | 'type' | 'style' | 'label' | 'list' | 'multiple'> { | |||
export interface DropdownSelectProps extends Omit<React.HTMLProps<DropdownSelectDerivedElement>, 'size' | 'type' | 'label' | 'list' | 'multiple'> { | |||
/** | |||
* Short textual description indicating the nature of the component's value. | |||
*/ | |||
@@ -61,6 +61,7 @@ export const DropdownSelect = React.forwardRef<DropdownSelectDerivedElement, Dro | |||
className, | |||
children, | |||
id: idProp, | |||
style, | |||
...etcProps | |||
}: DropdownSelectProps, | |||
forwardedRef, | |||
@@ -80,6 +81,8 @@ export const DropdownSelect = React.forwardRef<DropdownSelectDerivedElement, Dro | |||
}, | |||
className, | |||
)} | |||
data-testid="base" | |||
style={style} | |||
> | |||
<select | |||
{...etcProps} | |||
@@ -87,6 +90,7 @@ export const DropdownSelect = React.forwardRef<DropdownSelectDerivedElement, Dro | |||
id={id} | |||
aria-labelledby={labelId} | |||
data-testid="input" | |||
role="combobox" | |||
className={clsx( | |||
'tesseract-design-dropdown-select bg-negative rounded-inherit w-full peer block appearance-none cursor-pointer select-none font-inherit', | |||
'focus:outline-0', | |||
@@ -180,6 +184,7 @@ export const DropdownSelect = React.forwardRef<DropdownSelectDerivedElement, Dro | |||
)} | |||
{indicator && ( | |||
<div | |||
data-testid="indicator" | |||
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', | |||
{ | |||
@@ -54,6 +54,9 @@ importers: | |||
'@types/react': | |||
specifier: ^18.0.27 | |||
version: 18.2.14 | |||
'@types/testing-library__jest-dom': | |||
specifier: ^5.14.7 | |||
version: 5.14.7 | |||
eslint: | |||
specifier: ^8.35.0 | |||
version: 8.43.0 | |||
@@ -152,12 +155,18 @@ importers: | |||
'@testing-library/react': | |||
specifier: ^13.4.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': | |||
specifier: ^18.14.1 | |||
version: 18.14.1 | |||
'@types/react': | |||
specifier: ^18.0.27 | |||
version: 18.2.14 | |||
'@types/testing-library__jest-dom': | |||
specifier: ^5.14.7 | |||
version: 5.14.7 | |||
eslint: | |||
specifier: ^8.35.0 | |||
version: 8.43.0 | |||
@@ -1906,7 +1915,7 @@ packages: | |||
dependencies: | |||
'@adobe/css-tools': 4.2.0 | |||
'@babel/runtime': 7.22.5 | |||
'@types/testing-library__jest-dom': 5.14.6 | |||
'@types/testing-library__jest-dom': 5.14.7 | |||
aria-query: 5.3.0 | |||
chalk: 3.0.0 | |||
css.escape: 1.5.1 | |||
@@ -2054,8 +2063,8 @@ packages: | |||
resolution: {integrity: sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==} | |||
dev: true | |||
/@types/testing-library__jest-dom@5.14.6: | |||
resolution: {integrity: sha512-FkHXCb+ikSoUP4Y4rOslzTdX5sqYwMxfefKh1GmZ8ce1GOkEHntSp6b5cGadmNfp5e4BMEWOMx+WSKd5/MqlDA==} | |||
/@types/testing-library__jest-dom@5.14.7: | |||
resolution: {integrity: sha512-PFDoAbR9y8pD9+41oM1Yy0nVCkaRPlklmDZoPCXhNpR0ZO13HAYWqdNEjLtvIiveBmfB/+jdvmuOVeOXehKOaA==} | |||
dependencies: | |||
'@types/jest': 29.5.2 | |||
dev: true | |||