Use consistent formatting for conditional markup for components.master
@@ -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 { | |||
ComboBox, | |||
ComboBoxDerivedElement, | |||
} from '.'; | |||
expect.extend(matchers); | |||
describe('ComboBox', () => { | |||
afterEach(() => { | |||
cleanup(); | |||
}); | |||
it('renders a combobox', () => { | |||
render( | |||
<ComboBox />, | |||
); | |||
const combobox = screen.getByRole('combobox'); | |||
expect(combobox).toBeInTheDocument(); | |||
expect(combobox).toHaveProperty('type', 'text'); | |||
}); | |||
it('renders a border', () => { | |||
render( | |||
<ComboBox | |||
border | |||
/>, | |||
); | |||
const border = screen.getByTestId('border'); | |||
expect(border).toBeInTheDocument(); | |||
}); | |||
it('renders a label', () => { | |||
render( | |||
<ComboBox | |||
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( | |||
<ComboBox | |||
label="foo" | |||
hiddenLabel | |||
/>, | |||
); | |||
const combobox = screen.getByLabelText('foo'); | |||
expect(combobox).toBeInTheDocument(); | |||
const label = screen.queryByTestId('label'); | |||
expect(label).toBeInTheDocument(); | |||
expect(label).toHaveClass('sr-only'); | |||
}); | |||
it('renders a hint', () => { | |||
render( | |||
<ComboBox | |||
hint="foo" | |||
/>, | |||
); | |||
const hint = screen.getByTestId('hint'); | |||
expect(hint).toBeInTheDocument(); | |||
}); | |||
it('renders an indicator', () => { | |||
render( | |||
<ComboBox | |||
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( | |||
<ComboBox | |||
size={size} | |||
/>, | |||
); | |||
const input = screen.getByTestId('input'); | |||
expect(input).toHaveClass(inputClassName); | |||
}); | |||
it('renders label styles with indicator', () => { | |||
render( | |||
<ComboBox | |||
size={size} | |||
label="foo" | |||
indicator={<div />} | |||
/>, | |||
); | |||
const label = screen.getByTestId('label'); | |||
expect(label).toHaveClass(hintClassName); | |||
}); | |||
it('renders hint styles when indicator is present', () => { | |||
render( | |||
<ComboBox | |||
size={size} | |||
hint="hint" | |||
indicator={<div />} | |||
/>, | |||
); | |||
const hint = screen.getByTestId('hint'); | |||
expect(hint).toHaveClass(hintClassName); | |||
}); | |||
it('renders indicator styles', () => { | |||
render( | |||
<ComboBox | |||
size={size} | |||
indicator={ | |||
<div /> | |||
} | |||
/>, | |||
); | |||
const indicator = screen.getByTestId('indicator'); | |||
expect(indicator).toHaveClass(indicatorClassName); | |||
}); | |||
}); | |||
it('renders a block combobox', () => { | |||
render( | |||
<ComboBox | |||
block | |||
/>, | |||
); | |||
const base = screen.getByTestId('base'); | |||
expect(base).toHaveClass('block'); | |||
}); | |||
it.each(TextControl.AVAILABLE_INPUT_TYPES)('renders a combobox with type %s', (inputType) => { | |||
render( | |||
<ComboBox | |||
type={inputType} | |||
/>, | |||
); | |||
const combobox = screen.getByTestId('input'); | |||
expect(combobox).toHaveProperty('type', inputType); | |||
}); | |||
it('falls back to text input mode when it clashes with the input type', () => { | |||
render( | |||
<ComboBox | |||
type="text" | |||
inputMode="search" | |||
/>, | |||
); | |||
const combobox = screen.getByTestId('input'); | |||
expect(combobox).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( | |||
<ComboBox | |||
variant={variant} | |||
/>, | |||
); | |||
const input = screen.getByTestId('input'); | |||
expect(input).toHaveClass(inputClassName); | |||
}); | |||
it('renders hint styles', () => { | |||
render( | |||
<ComboBox | |||
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<ComboBoxDerivedElement>) => { | |||
e.preventDefault(); | |||
}, | |||
); | |||
render( | |||
<ComboBox | |||
onChange={onChange} | |||
/>, | |||
); | |||
const combobox: HTMLInputElement = screen.getByRole('combobox'); | |||
await userEvent.type(combobox, 'foobar'); | |||
expect(onChange).toBeCalled(); | |||
}); | |||
it('handles input events', async () => { | |||
const onInput = vi.fn().mockImplementationOnce( | |||
(e: React.SyntheticEvent<ComboBoxDerivedElement>) => { | |||
e.preventDefault(); | |||
}, | |||
); | |||
render( | |||
<ComboBox | |||
onInput={onInput} | |||
/>, | |||
); | |||
const combobox: HTMLInputElement = screen.getByTestId('input'); | |||
await userEvent.type(combobox, 'foobar'); | |||
expect(onInput).toBeCalled(); | |||
}); | |||
}); |
@@ -108,6 +108,7 @@ export const ComboBox = React.forwardRef<ComboBoxDerivedElement, ComboBoxProps>( | |||
className, | |||
)} | |||
style={style} | |||
data-testid="base" | |||
> | |||
<input | |||
{...etcProps} | |||
@@ -149,33 +150,31 @@ export const ComboBox = React.forwardRef<ComboBoxDerivedElement, ComboBoxProps>( | |||
}, | |||
)} | |||
/> | |||
{ | |||
label && ( | |||
<label | |||
data-testid="label" | |||
id={labelId} | |||
htmlFor={id} | |||
className={clsx( | |||
'absolute z-[1] w-full top-0.5 left-0 pointer-events-none pl-1 text-xxs font-bold peer-disabled:opacity-50 peer-focus:text-secondary text-primary leading-none bg-negative select-none', | |||
{ | |||
'sr-only': hiddenLabel, | |||
}, | |||
{ | |||
'pr-1': !indicator, | |||
}, | |||
{ | |||
'pr-10': indicator && size === 'small', | |||
'pr-12': indicator && size === 'medium', | |||
'pr-16': indicator && size === 'large', | |||
}, | |||
)} | |||
> | |||
<span className="w-full whitespace-nowrap h-[1.1em] overflow-hidden text-ellipsis"> | |||
{label} | |||
</span> | |||
</label> | |||
) | |||
} | |||
{label && ( | |||
<label | |||
data-testid="label" | |||
id={labelId} | |||
htmlFor={id} | |||
className={clsx( | |||
'absolute z-[1] w-full top-0.5 left-0 pointer-events-none pl-1 text-xxs font-bold peer-disabled:opacity-50 peer-focus:text-secondary text-primary leading-none bg-negative select-none', | |||
{ | |||
'sr-only': hiddenLabel, | |||
}, | |||
{ | |||
'pr-1': !indicator, | |||
}, | |||
{ | |||
'pr-10': indicator && size === 'small', | |||
'pr-12': indicator && size === 'medium', | |||
'pr-16': indicator && size === 'large', | |||
}, | |||
)} | |||
> | |||
<span className="w-full whitespace-nowrap h-[1.1em] overflow-hidden text-ellipsis"> | |||
{label} | |||
</span> | |||
</label> | |||
)} | |||
{hint && ( | |||
<div | |||
data-testid="hint" | |||
@@ -209,6 +208,7 @@ export const ComboBox = React.forwardRef<ComboBoxDerivedElement, ComboBoxProps>( | |||
)} | |||
{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', | |||
{ | |||
@@ -221,14 +221,12 @@ export const ComboBox = React.forwardRef<ComboBoxDerivedElement, ComboBoxProps>( | |||
{indicator} | |||
</div> | |||
)} | |||
{ | |||
border && ( | |||
<span | |||
data-testid="border" | |||
className="absolute z-[1] peer-disabled:opacity-50 inset-0 rounded-inherit border-2 border-primary pointer-events-none peer-focus:border-secondary" | |||
/> | |||
) | |||
} | |||
{border && ( | |||
<span | |||
data-testid="border" | |||
className="absolute z-[1] peer-disabled:opacity-50 inset-0 rounded-inherit border-2 border-primary pointer-events-none peer-focus:border-secondary" | |||
/> | |||
)} | |||
</div> | |||
</> | |||
); | |||
@@ -15,7 +15,8 @@ import { | |||
} from 'vitest'; | |||
import matchers from '@testing-library/jest-dom/matchers'; | |||
import { | |||
DropdownSelect, DropdownSelectDerivedElement, | |||
DropdownSelect, | |||
DropdownSelectDerivedElement, | |||
} from '.'; | |||
expect.extend(matchers); | |||
@@ -124,33 +124,31 @@ export const DropdownSelect = React.forwardRef<DropdownSelectDerivedElement, Dro | |||
> | |||
{children} | |||
</select> | |||
{ | |||
label && ( | |||
<label | |||
htmlFor={id} | |||
data-testid="label" | |||
id={labelId} | |||
className={clsx( | |||
'absolute z-[1] w-full top-0.5 left-0 pointer-events-none pl-1 text-xxs font-bold peer-disabled:opacity-50 peer-focus:text-secondary text-primary leading-none bg-negative select-none', | |||
{ | |||
'sr-only': hiddenLabel, | |||
}, | |||
{ | |||
'pr-1': !indicator, | |||
}, | |||
{ | |||
'pr-10': indicator && size === 'small', | |||
'pr-12': indicator && size === 'medium', | |||
'pr-16': indicator && size === 'large', | |||
}, | |||
)} | |||
> | |||
<span className="block w-full whitespace-nowrap h-[1.1em] overflow-hidden text-ellipsis"> | |||
{label} | |||
</span> | |||
</label> | |||
) | |||
} | |||
{label && ( | |||
<label | |||
htmlFor={id} | |||
data-testid="label" | |||
id={labelId} | |||
className={clsx( | |||
'absolute z-[1] w-full top-0.5 left-0 pointer-events-none pl-1 text-xxs font-bold peer-disabled:opacity-50 peer-focus:text-secondary text-primary leading-none bg-negative select-none', | |||
{ | |||
'sr-only': hiddenLabel, | |||
}, | |||
{ | |||
'pr-1': !indicator, | |||
}, | |||
{ | |||
'pr-10': indicator && size === 'small', | |||
'pr-12': indicator && size === 'medium', | |||
'pr-16': indicator && size === 'large', | |||
}, | |||
)} | |||
> | |||
<span className="block w-full whitespace-nowrap h-[1.1em] overflow-hidden text-ellipsis"> | |||
{label} | |||
</span> | |||
</label> | |||
)} | |||
{hint && ( | |||
<div | |||
data-testid="hint" | |||
@@ -197,14 +195,12 @@ export const DropdownSelect = React.forwardRef<DropdownSelectDerivedElement, Dro | |||
{indicator} | |||
</div> | |||
)} | |||
{ | |||
border && ( | |||
<span | |||
data-testid="border" | |||
className="absolute z-[1] peer-disabled:opacity-50 inset-0 rounded-inherit border-2 border-primary pointer-events-none peer-focus:border-secondary" | |||
/> | |||
) | |||
} | |||
{border && ( | |||
<span | |||
data-testid="border" | |||
className="absolute z-[1] peer-disabled:opacity-50 inset-0 rounded-inherit border-2 border-primary pointer-events-none peer-focus:border-secondary" | |||
/> | |||
)} | |||
</div> | |||
); | |||
}); | |||
@@ -0,0 +1,254 @@ | |||
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 { | |||
MenuSelect, | |||
MenuSelectDerivedElement, | |||
} from '.'; | |||
expect.extend(matchers); | |||
describe('MenuSelect', () => { | |||
afterEach(() => { | |||
cleanup(); | |||
}); | |||
it('renders a listbox', () => { | |||
render(<MenuSelect />); | |||
const listbox = screen.getByRole('listbox'); | |||
expect(listbox).toBeInTheDocument(); | |||
}); | |||
it('renders a border', () => { | |||
render( | |||
<MenuSelect | |||
border | |||
/>, | |||
); | |||
const border = screen.getByTestId('border'); | |||
expect(border).toBeInTheDocument(); | |||
}); | |||
it('renders a label', () => { | |||
render( | |||
<MenuSelect | |||
label="foo" | |||
/>, | |||
); | |||
const listbox = screen.getByLabelText('foo'); | |||
expect(listbox).toBeInTheDocument(); | |||
const label = screen.getByTestId('label'); | |||
expect(label).toHaveTextContent('foo'); | |||
}); | |||
it('renders a hidden label', () => { | |||
render( | |||
<MenuSelect | |||
label="foo" | |||
hiddenLabel | |||
/>, | |||
); | |||
const listbox = screen.getByLabelText('foo'); | |||
expect(listbox).toBeInTheDocument(); | |||
const label = screen.getByTestId('label'); | |||
expect(label).toHaveClass('sr-only'); | |||
}); | |||
it('renders a hint', () => { | |||
render( | |||
<MenuSelect | |||
hint="foo" | |||
/>, | |||
); | |||
const hint = screen.getByTestId('hint'); | |||
expect(hint).toBeInTheDocument(); | |||
}); | |||
it('render options with implicit values', () => { | |||
render( | |||
<MenuSelect> | |||
<option>foo</option> | |||
<option>bar</option> | |||
</MenuSelect>, | |||
); | |||
const listbox = screen.getByRole('listbox'); | |||
expect(listbox.children).toHaveLength(2); | |||
}); | |||
it('renders valid options', () => { | |||
render( | |||
<MenuSelect> | |||
<option value="foo">foo</option> | |||
<option value="bar">bar</option> | |||
</MenuSelect>, | |||
); | |||
const listbox = screen.getByRole('listbox'); | |||
expect(listbox.children).toHaveLength(2); | |||
}); | |||
it('renders option groups', () => { | |||
render( | |||
<MenuSelect> | |||
<optgroup label="foo"> | |||
<option value="baz">baz</option> | |||
</optgroup> | |||
<optgroup label="bar"> | |||
<option value="quux">quux</option> | |||
<option value="quuux">quuux</option> | |||
</optgroup> | |||
</MenuSelect>, | |||
); | |||
const listbox = screen.getByRole('listbox'); | |||
expect(listbox.children).toHaveLength(2); | |||
expect(listbox.children[0].children).toHaveLength(1); | |||
expect(listbox.children[1].children).toHaveLength(2); | |||
}); | |||
describe.each` | |||
size | inputClassNames | hintClassNames | indicatorClassNames | |||
${'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, | |||
inputClassNames, | |||
hintClassNames, | |||
indicatorClassNames, | |||
}: { | |||
size: TextControl.Size, | |||
inputClassNames: string, | |||
hintClassNames: string, | |||
indicatorClassNames: string, | |||
}) => { | |||
it('renders input styles', () => { | |||
render( | |||
<MenuSelect | |||
size={size} | |||
/>, | |||
); | |||
const listbox = screen.getByRole('listbox'); | |||
expect(listbox).toHaveClass(inputClassNames); | |||
}); | |||
it('renders hint styles with indicator', () => { | |||
render( | |||
<MenuSelect | |||
size={size} | |||
hint="hint" | |||
indicator="a" | |||
/>, | |||
); | |||
const hint = screen.getByTestId('hint'); | |||
expect(hint).toHaveClass(hintClassNames); | |||
}); | |||
it('renders indicator styles', () => { | |||
render( | |||
<MenuSelect | |||
size={size} | |||
indicator="a" | |||
/>, | |||
); | |||
const indicator = screen.getByTestId('indicator'); | |||
expect(indicator).toHaveClass(indicatorClassNames); | |||
}); | |||
it('renders indicator styles for label', () => { | |||
render( | |||
<MenuSelect | |||
size={size} | |||
label="label" | |||
indicator="a" | |||
/>, | |||
); | |||
const label = screen.getByTestId('label'); | |||
expect(label).toHaveClass(hintClassNames); | |||
}); | |||
}); | |||
it('renders a block input', () => { | |||
render( | |||
<MenuSelect | |||
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-5'} | ${'top-0.5'} | |||
`('on $variant variant', ({ | |||
variant, | |||
inputClassNames, | |||
hintClassNames, | |||
}: { | |||
variant: TextControl.Variant, | |||
inputClassNames: string, | |||
hintClassNames: string, | |||
}) => { | |||
it('renders input styles', () => { | |||
render( | |||
<MenuSelect | |||
variant={variant} | |||
/>, | |||
); | |||
const listbox = screen.getByRole('listbox'); | |||
expect(listbox).toHaveClass(inputClassNames); | |||
}); | |||
it('renders hint styles', () => { | |||
render( | |||
<MenuSelect | |||
variant={variant} | |||
hint="hint" | |||
/>, | |||
); | |||
const hint = screen.getByTestId('hint'); | |||
expect(hint).toHaveClass(hintClassNames); | |||
}); | |||
}); | |||
it('handles change events', async () => { | |||
const onChange = vi.fn().mockImplementationOnce( | |||
(e: React.ChangeEvent<MenuSelectDerivedElement>) => { | |||
e.preventDefault(); | |||
}, | |||
); | |||
render( | |||
<MenuSelect | |||
onChange={onChange} | |||
> | |||
<option value="foo">foo</option> | |||
<option value="bar">bar</option> | |||
</MenuSelect>, | |||
); | |||
const listbox: HTMLSelectElement = screen.getByRole('listbox'); | |||
const [, secondOption] = screen.getAllByRole('option'); | |||
await userEvent.selectOptions(listbox, secondOption); | |||
expect(onChange).toBeCalled(); | |||
}); | |||
}); |
@@ -84,6 +84,7 @@ export const MenuSelect = React.forwardRef<MenuSelectDerivedElement, MenuSelectP | |||
}, | |||
className, | |||
)} | |||
data-testid="base" | |||
> | |||
<select | |||
{...etcProps} | |||
@@ -137,33 +138,31 @@ export const MenuSelect = React.forwardRef<MenuSelectDerivedElement, MenuSelectP | |||
}, | |||
)} | |||
/> | |||
{ | |||
label && ( | |||
<label | |||
data-testid="label" | |||
htmlFor={id} | |||
id={labelId} | |||
className={clsx( | |||
'absolute z-[1] w-full top-0.5 left-0 pointer-events-none pl-1 text-xxs font-bold peer-disabled:opacity-50 peer-focus:text-secondary text-primary leading-none bg-negative select-none', | |||
{ | |||
'sr-only': hiddenLabel, | |||
}, | |||
{ | |||
'pr-1': !indicator, | |||
}, | |||
{ | |||
'pr-10': indicator && size === 'small', | |||
'pr-12': indicator && size === 'medium', | |||
'pr-16': indicator && size === 'large', | |||
}, | |||
)} | |||
> | |||
<span className="block w-full whitespace-nowrap h-[1.1em] overflow-hidden text-ellipsis"> | |||
{label} | |||
</span> | |||
</label> | |||
) | |||
} | |||
{label && ( | |||
<label | |||
data-testid="label" | |||
htmlFor={id} | |||
id={labelId} | |||
className={clsx( | |||
'absolute z-[1] w-full top-0.5 left-0 pointer-events-none pl-1 text-xxs font-bold peer-disabled:opacity-50 peer-focus:text-secondary text-primary leading-none bg-negative select-none', | |||
{ | |||
'sr-only': hiddenLabel, | |||
}, | |||
{ | |||
'pr-1': !indicator, | |||
}, | |||
{ | |||
'pr-10': indicator && size === 'small', | |||
'pr-12': indicator && size === 'medium', | |||
'pr-16': indicator && size === 'large', | |||
}, | |||
)} | |||
> | |||
<span className="block w-full whitespace-nowrap h-[1.1em] overflow-hidden text-ellipsis"> | |||
{label} | |||
</span> | |||
</label> | |||
)} | |||
{hint && ( | |||
<div | |||
data-testid="hint" | |||
@@ -197,6 +196,7 @@ export const MenuSelect = React.forwardRef<MenuSelectDerivedElement, MenuSelectP | |||
)} | |||
{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', | |||
{ | |||
@@ -209,14 +209,12 @@ export const MenuSelect = React.forwardRef<MenuSelectDerivedElement, MenuSelectP | |||
{indicator} | |||
</div> | |||
)} | |||
{ | |||
border && ( | |||
<span | |||
data-testid="border" | |||
className="absolute z-[1] peer-disabled:opacity-50 inset-0 rounded-inherit border-2 border-primary pointer-events-none peer-focus:border-secondary" | |||
/> | |||
) | |||
} | |||
{border && ( | |||
<span | |||
data-testid="border" | |||
className="absolute z-[1] peer-disabled:opacity-50 inset-0 rounded-inherit border-2 border-primary pointer-events-none peer-focus:border-secondary" | |||
/> | |||
)} | |||
</div> | |||
); | |||
}); | |||
@@ -40,7 +40,7 @@ export interface EmailInputProps extends Omit<React.HTMLProps<EmailInputDerivedE | |||
} | |||
/** | |||
* Component for inputting textual values. | |||
* Component for inputting email addresses. | |||
*/ | |||
export const EmailInput = React.forwardRef<EmailInputDerivedElement, EmailInputProps>( | |||
( | |||
@@ -115,33 +115,31 @@ export const EmailInput = React.forwardRef<EmailInputDerivedElement, EmailInputP | |||
}, | |||
)} | |||
/> | |||
{ | |||
label && ( | |||
<label | |||
data-testid="label" | |||
id={labelId} | |||
htmlFor={id} | |||
className={clsx( | |||
'absolute z-[1] w-full top-0.5 left-0 pointer-events-none pl-1 text-xxs font-bold peer-disabled:opacity-50 peer-focus:text-secondary text-primary leading-none bg-negative select-none', | |||
{ | |||
'sr-only': hiddenLabel, | |||
}, | |||
{ | |||
'pr-1': !indicator, | |||
}, | |||
{ | |||
'pr-10': indicator && size === 'small', | |||
'pr-12': indicator && size === 'medium', | |||
'pr-16': indicator && size === 'large', | |||
}, | |||
)} | |||
> | |||
<span className="block w-full whitespace-nowrap h-[1.1em] overflow-hidden text-ellipsis"> | |||
{label} | |||
</span> | |||
</label> | |||
) | |||
} | |||
{label && ( | |||
<label | |||
data-testid="label" | |||
id={labelId} | |||
htmlFor={id} | |||
className={clsx( | |||
'absolute z-[1] w-full top-0.5 left-0 pointer-events-none pl-1 text-xxs font-bold peer-disabled:opacity-50 peer-focus:text-secondary text-primary leading-none bg-negative select-none', | |||
{ | |||
'sr-only': hiddenLabel, | |||
}, | |||
{ | |||
'pr-1': !indicator, | |||
}, | |||
{ | |||
'pr-10': indicator && size === 'small', | |||
'pr-12': indicator && size === 'medium', | |||
'pr-16': indicator && size === 'large', | |||
}, | |||
)} | |||
> | |||
<span className="block w-full whitespace-nowrap h-[1.1em] overflow-hidden text-ellipsis"> | |||
{label} | |||
</span> | |||
</label> | |||
)} | |||
{hint && ( | |||
<div | |||
data-testid="hint" | |||
@@ -187,14 +185,12 @@ export const EmailInput = React.forwardRef<EmailInputDerivedElement, EmailInputP | |||
{indicator} | |||
</div> | |||
)} | |||
{ | |||
border && ( | |||
<span | |||
data-testid="border" | |||
className="absolute z-[1] peer-disabled:opacity-50 inset-0 rounded-inherit border-2 border-primary pointer-events-none peer-focus:border-secondary" | |||
/> | |||
) | |||
} | |||
{border && ( | |||
<span | |||
data-testid="border" | |||
className="absolute z-[1] peer-disabled:opacity-50 inset-0 rounded-inherit border-2 border-primary pointer-events-none peer-focus:border-secondary" | |||
/> | |||
)} | |||
</div> | |||
); | |||
}, | |||
@@ -44,13 +44,13 @@ export interface PhoneNumberInputProps extends Omit<React.HTMLProps<PhoneNumberI | |||
*/ | |||
enhanced?: boolean, | |||
/** | |||
* Default country. | |||
* Country where the phone number should be formatted for. | |||
*/ | |||
defaultCountry?: Country, | |||
country?: Country, | |||
} | |||
/** | |||
* Component for inputting textual values. | |||
* Component for inputting national and international phone numbers. | |||
*/ | |||
export const PhoneNumberInput = React.forwardRef< | |||
PhoneNumberInputDerivedElement, | |||
@@ -69,7 +69,7 @@ export const PhoneNumberInput = React.forwardRef< | |||
id: idProp, | |||
style, | |||
enhanced = true, | |||
defaultCountry = 'PH' as const, | |||
country = 'PH' as const, | |||
value, | |||
onChange, | |||
name, | |||
@@ -154,45 +154,41 @@ export const PhoneNumberInput = React.forwardRef< | |||
tabIndex={clientSide ? -1 : undefined} | |||
className={clsx(commonInputStyles, clientSide && 'sr-only')} | |||
/> | |||
{ | |||
clientSide && ( | |||
<PhoneInput | |||
{...etcProps} | |||
ref={undefined} | |||
value={phoneNumber} | |||
onChange={handlePhoneInputChange} | |||
defaultCountry={defaultCountry} | |||
className={commonInputStyles} | |||
/> | |||
) | |||
} | |||
{ | |||
label && ( | |||
<label | |||
data-testid="label" | |||
id={labelId} | |||
htmlFor={id} | |||
className={clsx( | |||
'absolute z-[1] w-full top-0.5 left-0 pointer-events-none pl-1 text-xxs font-bold peer-disabled:opacity-50 peer-focus:text-secondary text-primary leading-none bg-negative select-none', | |||
{ | |||
'sr-only': hiddenLabel, | |||
}, | |||
{ | |||
'pr-1': !indicator, | |||
}, | |||
{ | |||
'pr-10': indicator && size === 'small', | |||
'pr-12': indicator && size === 'medium', | |||
'pr-16': indicator && size === 'large', | |||
}, | |||
)} | |||
> | |||
<span className="block w-full whitespace-nowrap h-[1.1em] overflow-hidden text-ellipsis"> | |||
{label} | |||
</span> | |||
</label> | |||
) | |||
} | |||
{clientSide && ( | |||
<PhoneInput | |||
{...etcProps} | |||
ref={undefined} | |||
value={phoneNumber} | |||
onChange={handlePhoneInputChange} | |||
defaultCountry={country} | |||
className={commonInputStyles} | |||
/> | |||
)} | |||
{label && ( | |||
<label | |||
data-testid="label" | |||
id={labelId} | |||
htmlFor={id} | |||
className={clsx( | |||
'absolute z-[1] w-full top-0.5 left-0 pointer-events-none pl-1 text-xxs font-bold peer-disabled:opacity-50 peer-focus:text-secondary text-primary leading-none bg-negative select-none', | |||
{ | |||
'sr-only': hiddenLabel, | |||
}, | |||
{ | |||
'pr-1': !indicator, | |||
}, | |||
{ | |||
'pr-10': indicator && size === 'small', | |||
'pr-12': indicator && size === 'medium', | |||
'pr-16': indicator && size === 'large', | |||
}, | |||
)} | |||
> | |||
<span className="block w-full whitespace-nowrap h-[1.1em] overflow-hidden text-ellipsis"> | |||
{label} | |||
</span> | |||
</label> | |||
)} | |||
{hint && ( | |||
<div | |||
data-testid="hint" | |||
@@ -238,14 +234,12 @@ export const PhoneNumberInput = React.forwardRef< | |||
{indicator} | |||
</div> | |||
)} | |||
{ | |||
border && ( | |||
<span | |||
data-testid="border" | |||
className="absolute z-[1] peer-disabled:opacity-50 inset-0 rounded-inherit border-2 border-primary pointer-events-none peer-focus:border-secondary" | |||
/> | |||
) | |||
} | |||
{border && ( | |||
<span | |||
data-testid="border" | |||
className="absolute z-[1] peer-disabled:opacity-50 inset-0 rounded-inherit border-2 border-primary pointer-events-none peer-focus:border-secondary" | |||
/> | |||
)} | |||
</div> | |||
); | |||
}); | |||
@@ -262,5 +256,5 @@ PhoneNumberInput.defaultProps = { | |||
variant: 'default' as const, | |||
hiddenLabel: false as const, | |||
enhanced: false as const, | |||
defaultCountry: 'PH' as const, | |||
country: 'PH' as const, | |||
}; |
@@ -40,7 +40,7 @@ export interface UrlInputProps extends Omit<React.HTMLProps<UrlInputDerivedEleme | |||
} | |||
/** | |||
* Component for inputting textual values. | |||
* Component for inputting URLs. | |||
*/ | |||
export const UrlInput = React.forwardRef<UrlInputDerivedElement, UrlInputProps>(( | |||
{ | |||
@@ -114,33 +114,31 @@ export const UrlInput = React.forwardRef<UrlInputDerivedElement, UrlInputProps>( | |||
}, | |||
)} | |||
/> | |||
{ | |||
label && ( | |||
<label | |||
data-testid="label" | |||
htmlFor={id} | |||
id={labelId} | |||
className={clsx( | |||
'absolute z-[1] w-full top-0.5 left-0 pointer-events-none pl-1 text-xxs font-bold peer-disabled:opacity-50 peer-focus:text-secondary text-primary leading-none bg-negative select-none', | |||
{ | |||
'sr-only': hiddenLabel, | |||
}, | |||
{ | |||
'pr-1': !indicator, | |||
}, | |||
{ | |||
'pr-10': indicator && size === 'small', | |||
'pr-12': indicator && size === 'medium', | |||
'pr-16': indicator && size === 'large', | |||
}, | |||
)} | |||
> | |||
<span className="block w-full whitespace-nowrap h-[1.1em] overflow-hidden text-ellipsis"> | |||
{label} | |||
</span> | |||
</label> | |||
) | |||
} | |||
{label && ( | |||
<label | |||
data-testid="label" | |||
htmlFor={id} | |||
id={labelId} | |||
className={clsx( | |||
'absolute z-[1] w-full top-0.5 left-0 pointer-events-none pl-1 text-xxs font-bold peer-disabled:opacity-50 peer-focus:text-secondary text-primary leading-none bg-negative select-none', | |||
{ | |||
'sr-only': hiddenLabel, | |||
}, | |||
{ | |||
'pr-1': !indicator, | |||
}, | |||
{ | |||
'pr-10': indicator && size === 'small', | |||
'pr-12': indicator && size === 'medium', | |||
'pr-16': indicator && size === 'large', | |||
}, | |||
)} | |||
> | |||
<span className="block w-full whitespace-nowrap h-[1.1em] overflow-hidden text-ellipsis"> | |||
{label} | |||
</span> | |||
</label> | |||
)} | |||
{hint && ( | |||
<div | |||
data-testid="hint" | |||
@@ -186,14 +184,12 @@ export const UrlInput = React.forwardRef<UrlInputDerivedElement, UrlInputProps>( | |||
{indicator} | |||
</div> | |||
)} | |||
{ | |||
border && ( | |||
<span | |||
data-testid="border" | |||
className="absolute z-[1] peer-disabled:opacity-50 inset-0 rounded-inherit border-2 border-primary pointer-events-none peer-focus:border-secondary" | |||
/> | |||
) | |||
} | |||
{border && ( | |||
<span | |||
data-testid="border" | |||
className="absolute z-[1] peer-disabled:opacity-50 inset-0 rounded-inherit border-2 border-primary pointer-events-none peer-focus:border-secondary" | |||
/> | |||
)} | |||
</div> | |||
); | |||
}); | |||
@@ -118,33 +118,31 @@ export const MaskedTextInput = React.forwardRef< | |||
}, | |||
)} | |||
/> | |||
{ | |||
label && ( | |||
<label | |||
data-testid="label" | |||
id={labelId} | |||
htmlFor={id} | |||
className={clsx( | |||
'absolute z-[1] w-full top-0.5 left-0 pointer-events-none pl-1 text-xxs font-bold peer-disabled:opacity-50 peer-focus:text-secondary text-primary leading-none bg-negative select-none', | |||
{ | |||
'sr-only': hiddenLabel, | |||
}, | |||
{ | |||
'pr-1': !indicator, | |||
}, | |||
{ | |||
'pr-10': indicator && size === 'small', | |||
'pr-12': indicator && size === 'medium', | |||
'pr-16': indicator && size === 'large', | |||
}, | |||
)} | |||
> | |||
<span className="block w-full whitespace-nowrap h-[1.1em] overflow-hidden text-ellipsis"> | |||
{label} | |||
</span> | |||
</label> | |||
) | |||
} | |||
{label && ( | |||
<label | |||
data-testid="label" | |||
id={labelId} | |||
htmlFor={id} | |||
className={clsx( | |||
'absolute z-[1] w-full top-0.5 left-0 pointer-events-none pl-1 text-xxs font-bold peer-disabled:opacity-50 peer-focus:text-secondary text-primary leading-none bg-negative select-none', | |||
{ | |||
'sr-only': hiddenLabel, | |||
}, | |||
{ | |||
'pr-1': !indicator, | |||
}, | |||
{ | |||
'pr-10': indicator && size === 'small', | |||
'pr-12': indicator && size === 'medium', | |||
'pr-16': indicator && size === 'large', | |||
}, | |||
)} | |||
> | |||
<span className="block w-full whitespace-nowrap h-[1.1em] overflow-hidden text-ellipsis"> | |||
{label} | |||
</span> | |||
</label> | |||
)} | |||
{hint && ( | |||
<div | |||
data-testid="hint" | |||
@@ -191,14 +189,12 @@ export const MaskedTextInput = React.forwardRef< | |||
{indicator} | |||
</div> | |||
)} | |||
{ | |||
border && ( | |||
<span | |||
data-testid="border" | |||
className="absolute z-[1] peer-disabled:opacity-50 inset-0 rounded-inherit border-2 border-primary pointer-events-none peer-focus:border-secondary" | |||
/> | |||
) | |||
} | |||
{border && ( | |||
<span | |||
data-testid="border" | |||
className="absolute z-[1] peer-disabled:opacity-50 inset-0 rounded-inherit border-2 border-primary pointer-events-none peer-focus:border-secondary" | |||
/> | |||
)} | |||
</div> | |||
); | |||
}); | |||
@@ -133,33 +133,31 @@ export const MultilineTextInput = React.forwardRef< | |||
}, | |||
)} | |||
/> | |||
{ | |||
label && ( | |||
<label | |||
data-testid="label" | |||
id={labelId} | |||
htmlFor={id} | |||
className={clsx( | |||
'absolute z-[1] w-full top-0.5 left-0 pointer-events-none pl-1 text-xxs font-bold peer-disabled:opacity-50 peer-focus:text-secondary text-primary leading-none bg-negative select-none', | |||
{ | |||
'sr-only': hiddenLabel, | |||
}, | |||
{ | |||
'pr-1': !indicator, | |||
}, | |||
{ | |||
'pr-10': indicator && size === 'small', | |||
'pr-12': indicator && size === 'medium', | |||
'pr-16': indicator && size === 'large', | |||
}, | |||
)} | |||
> | |||
<span className="block w-full whitespace-nowrap h-[1.1em] overflow-hidden text-ellipsis"> | |||
{label} | |||
</span> | |||
</label> | |||
) | |||
} | |||
{label && ( | |||
<label | |||
data-testid="label" | |||
id={labelId} | |||
htmlFor={id} | |||
className={clsx( | |||
'absolute z-[1] w-full top-0.5 left-0 pointer-events-none pl-1 text-xxs font-bold peer-disabled:opacity-50 peer-focus:text-secondary text-primary leading-none bg-negative select-none', | |||
{ | |||
'sr-only': hiddenLabel, | |||
}, | |||
{ | |||
'pr-1': !indicator, | |||
}, | |||
{ | |||
'pr-10': indicator && size === 'small', | |||
'pr-12': indicator && size === 'medium', | |||
'pr-16': indicator && size === 'large', | |||
}, | |||
)} | |||
> | |||
<span className="block w-full whitespace-nowrap h-[1.1em] overflow-hidden text-ellipsis"> | |||
{label} | |||
</span> | |||
</label> | |||
)} | |||
{hint && ( | |||
<div | |||
data-testid="hint" | |||
@@ -206,14 +204,12 @@ export const MultilineTextInput = React.forwardRef< | |||
{indicator} | |||
</div> | |||
)} | |||
{ | |||
border && ( | |||
<span | |||
data-testid="border" | |||
className="absolute z-[1] peer-disabled:opacity-50 inset-0 rounded-inherit border-2 border-primary pointer-events-none peer-focus:border-secondary" | |||
/> | |||
) | |||
} | |||
{border && ( | |||
<span | |||
data-testid="border" | |||
className="absolute z-[1] peer-disabled:opacity-50 inset-0 rounded-inherit border-2 border-primary pointer-events-none peer-focus:border-secondary" | |||
/> | |||
)} | |||
</div> | |||
); | |||
}); | |||
@@ -135,33 +135,31 @@ export const TextInput = React.forwardRef<TextInputDerivedElement, TextInputProp | |||
}, | |||
)} | |||
/> | |||
{ | |||
label && ( | |||
<label | |||
data-testid="label" | |||
htmlFor={id} | |||
id={labelId} | |||
className={clsx( | |||
'absolute z-[1] w-full top-0.5 left-0 pointer-events-none pl-1 text-xxs font-bold peer-disabled:opacity-50 peer-focus:text-secondary text-primary leading-none bg-negative select-none', | |||
{ | |||
'sr-only': hiddenLabel, | |||
}, | |||
{ | |||
'pr-1': !indicator, | |||
}, | |||
{ | |||
'pr-10': indicator && size === 'small', | |||
'pr-12': indicator && size === 'medium', | |||
'pr-16': indicator && size === 'large', | |||
}, | |||
)} | |||
> | |||
<span className="block w-full whitespace-nowrap h-[1.1em] overflow-hidden text-ellipsis"> | |||
{label} | |||
</span> | |||
</label> | |||
) | |||
} | |||
{label && ( | |||
<label | |||
data-testid="label" | |||
htmlFor={id} | |||
id={labelId} | |||
className={clsx( | |||
'absolute z-[1] w-full top-0.5 left-0 pointer-events-none pl-1 text-xxs font-bold peer-disabled:opacity-50 peer-focus:text-secondary text-primary leading-none bg-negative select-none', | |||
{ | |||
'sr-only': hiddenLabel, | |||
}, | |||
{ | |||
'pr-1': !indicator, | |||
}, | |||
{ | |||
'pr-10': indicator && size === 'small', | |||
'pr-12': indicator && size === 'medium', | |||
'pr-16': indicator && size === 'large', | |||
}, | |||
)} | |||
> | |||
<span className="block w-full whitespace-nowrap h-[1.1em] overflow-hidden text-ellipsis"> | |||
{label} | |||
</span> | |||
</label> | |||
)} | |||
{hint && ( | |||
<div | |||
data-testid="hint" | |||
@@ -208,14 +206,12 @@ export const TextInput = React.forwardRef<TextInputDerivedElement, TextInputProp | |||
{indicator} | |||
</div> | |||
)} | |||
{ | |||
border && ( | |||
<span | |||
data-testid="border" | |||
className="absolute z-[1] peer-disabled:opacity-50 inset-0 rounded-inherit border-2 border-primary pointer-events-none peer-focus:border-secondary" | |||
/> | |||
) | |||
} | |||
{border && ( | |||
<span | |||
data-testid="border" | |||
className="absolute z-[1] peer-disabled:opacity-50 inset-0 rounded-inherit border-2 border-primary pointer-events-none peer-focus:border-secondary" | |||
/> | |||
)} | |||
</div> | |||
); | |||
}); | |||
@@ -29,31 +29,29 @@ export const KeyValueTable = React.forwardRef<KeyValueTableDerivedElement, KeyVa | |||
)} | |||
ref={forwardedRef} | |||
> | |||
{ | |||
properties.map((property) => typeof property === 'object' && ( | |||
<div | |||
key={property.key} | |||
className={clsx('contents', property.className)} | |||
{properties.map((property) => typeof property === 'object' && ( | |||
<div | |||
key={property.key} | |||
className={clsx('contents', property.className)} | |||
> | |||
<dt | |||
className={clsx(hiddenKeys && 'sr-only', 'pr-4')} | |||
> | |||
<dt | |||
className={clsx(hiddenKeys && 'sr-only', 'pr-4')} | |||
> | |||
{property.key} | |||
</dt> | |||
<dd | |||
{...(property.valueProps ?? {})} | |||
className={clsx( | |||
'm-0 text-ellipsis overflow-hidden', | |||
!hiddenKeys && 'col-span-2', | |||
hiddenKeys && 'col-span-3', | |||
property.valueProps?.className, | |||
)} | |||
> | |||
{property.valueProps?.children} | |||
</dd> | |||
</div> | |||
)) | |||
} | |||
{property.key} | |||
</dt> | |||
<dd | |||
{...(property.valueProps ?? {})} | |||
className={clsx( | |||
'm-0 text-ellipsis overflow-hidden', | |||
!hiddenKeys && 'col-span-2', | |||
hiddenKeys && 'col-span-3', | |||
property.valueProps?.className, | |||
)} | |||
> | |||
{property.valueProps?.children} | |||
</dd> | |||
</div> | |||
))} | |||
</dl> | |||
)); | |||
@@ -0,0 +1,254 @@ | |||
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 { | |||
MenuMultiSelect, | |||
MenuMultiSelectDerivedElement, | |||
} from '.'; | |||
expect.extend(matchers); | |||
describe('MenuMultiSelect', () => { | |||
afterEach(() => { | |||
cleanup(); | |||
}); | |||
it('renders a listbox', () => { | |||
render(<MenuMultiSelect />); | |||
const listbox = screen.getByRole('listbox'); | |||
expect(listbox).toBeInTheDocument(); | |||
}); | |||
it('renders a border', () => { | |||
render( | |||
<MenuMultiSelect | |||
border | |||
/>, | |||
); | |||
const border = screen.getByTestId('border'); | |||
expect(border).toBeInTheDocument(); | |||
}); | |||
it('renders a label', () => { | |||
render( | |||
<MenuMultiSelect | |||
label="foo" | |||
/>, | |||
); | |||
const listbox = screen.getByLabelText('foo'); | |||
expect(listbox).toBeInTheDocument(); | |||
const label = screen.getByTestId('label'); | |||
expect(label).toHaveTextContent('foo'); | |||
}); | |||
it('renders a hidden label', () => { | |||
render( | |||
<MenuMultiSelect | |||
label="foo" | |||
hiddenLabel | |||
/>, | |||
); | |||
const listbox = screen.getByLabelText('foo'); | |||
expect(listbox).toBeInTheDocument(); | |||
const label = screen.getByTestId('label'); | |||
expect(label).toHaveClass('sr-only'); | |||
}); | |||
it('renders a hint', () => { | |||
render( | |||
<MenuMultiSelect | |||
hint="foo" | |||
/>, | |||
); | |||
const hint = screen.getByTestId('hint'); | |||
expect(hint).toBeInTheDocument(); | |||
}); | |||
it('render options with implicit values', () => { | |||
render( | |||
<MenuMultiSelect> | |||
<option>foo</option> | |||
<option>bar</option> | |||
</MenuMultiSelect>, | |||
); | |||
const listbox = screen.getByRole('listbox'); | |||
expect(listbox.children).toHaveLength(2); | |||
}); | |||
it('renders valid options', () => { | |||
render( | |||
<MenuMultiSelect> | |||
<option value="foo">foo</option> | |||
<option value="bar">bar</option> | |||
</MenuMultiSelect>, | |||
); | |||
const listbox = screen.getByRole('listbox'); | |||
expect(listbox.children).toHaveLength(2); | |||
}); | |||
it('renders option groups', () => { | |||
render( | |||
<MenuMultiSelect> | |||
<optgroup label="foo"> | |||
<option value="baz">baz</option> | |||
</optgroup> | |||
<optgroup label="bar"> | |||
<option value="quux">quux</option> | |||
<option value="quuux">quuux</option> | |||
</optgroup> | |||
</MenuMultiSelect>, | |||
); | |||
const listbox = screen.getByRole('listbox'); | |||
expect(listbox.children).toHaveLength(2); | |||
expect(listbox.children[0].children).toHaveLength(1); | |||
expect(listbox.children[1].children).toHaveLength(2); | |||
}); | |||
describe.each` | |||
size | inputClassNames | hintClassNames | indicatorClassNames | |||
${'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, | |||
inputClassNames, | |||
hintClassNames, | |||
indicatorClassNames, | |||
}: { | |||
size: TextControl.Size, | |||
inputClassNames: string, | |||
hintClassNames: string, | |||
indicatorClassNames: string, | |||
}) => { | |||
it('renders input styles', () => { | |||
render( | |||
<MenuMultiSelect | |||
size={size} | |||
/>, | |||
); | |||
const listbox = screen.getByRole('listbox'); | |||
expect(listbox).toHaveClass(inputClassNames); | |||
}); | |||
it('renders hint styles with indicator', () => { | |||
render( | |||
<MenuMultiSelect | |||
size={size} | |||
hint="hint" | |||
indicator="a" | |||
/>, | |||
); | |||
const hint = screen.getByTestId('hint'); | |||
expect(hint).toHaveClass(hintClassNames); | |||
}); | |||
it('renders indicator styles', () => { | |||
render( | |||
<MenuMultiSelect | |||
size={size} | |||
indicator="a" | |||
/>, | |||
); | |||
const indicator = screen.getByTestId('indicator'); | |||
expect(indicator).toHaveClass(indicatorClassNames); | |||
}); | |||
it('renders indicator styles for label', () => { | |||
render( | |||
<MenuMultiSelect | |||
size={size} | |||
label="label" | |||
indicator="a" | |||
/>, | |||
); | |||
const label = screen.getByTestId('label'); | |||
expect(label).toHaveClass(hintClassNames); | |||
}); | |||
}); | |||
it('renders a block input', () => { | |||
render( | |||
<MenuMultiSelect | |||
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-5'} | ${'top-0.5'} | |||
`('on $variant variant', ({ | |||
variant, | |||
inputClassNames, | |||
hintClassNames, | |||
}: { | |||
variant: TextControl.Variant, | |||
inputClassNames: string, | |||
hintClassNames: string, | |||
}) => { | |||
it('renders input styles', () => { | |||
render( | |||
<MenuMultiSelect | |||
variant={variant} | |||
/>, | |||
); | |||
const listbox = screen.getByRole('listbox'); | |||
expect(listbox).toHaveClass(inputClassNames); | |||
}); | |||
it('renders hint styles', () => { | |||
render( | |||
<MenuMultiSelect | |||
variant={variant} | |||
hint="hint" | |||
/>, | |||
); | |||
const hint = screen.getByTestId('hint'); | |||
expect(hint).toHaveClass(hintClassNames); | |||
}); | |||
}); | |||
it('handles change events', async () => { | |||
const onChange = vi.fn().mockImplementationOnce( | |||
(e: React.ChangeEvent<MenuMultiSelectDerivedElement>) => { | |||
e.preventDefault(); | |||
}, | |||
); | |||
render( | |||
<MenuMultiSelect | |||
onChange={onChange} | |||
> | |||
<option value="foo">foo</option> | |||
<option value="bar">bar</option> | |||
</MenuMultiSelect>, | |||
); | |||
const listbox: HTMLSelectElement = screen.getByRole('listbox'); | |||
const [, secondOption] = screen.getAllByRole('option'); | |||
await userEvent.selectOptions(listbox, secondOption); | |||
expect(onChange).toBeCalled(); | |||
}); | |||
}); |
@@ -83,6 +83,7 @@ export const MenuMultiSelect = React.forwardRef< | |||
}, | |||
className, | |||
)} | |||
data-testid="base" | |||
> | |||
<select | |||
{...etcProps} | |||
@@ -137,33 +138,31 @@ export const MenuMultiSelect = React.forwardRef< | |||
}, | |||
)} | |||
/> | |||
{ | |||
label && ( | |||
<label | |||
data-testid="label" | |||
id={labelId} | |||
htmlFor={id} | |||
className={clsx( | |||
'absolute z-[1] w-full top-0.5 left-0 pointer-events-none pl-1 text-xxs font-bold peer-disabled:opacity-50 peer-focus:text-secondary text-primary leading-none bg-negative select-none', | |||
{ | |||
'sr-only': hiddenLabel, | |||
}, | |||
{ | |||
'pr-1': !indicator, | |||
}, | |||
{ | |||
'pr-10': indicator && size === 'small', | |||
'pr-12': indicator && size === 'medium', | |||
'pr-16': indicator && size === 'large', | |||
}, | |||
)} | |||
> | |||
<span className="block w-full whitespace-nowrap h-[1.1em] overflow-hidden text-ellipsis"> | |||
{label} | |||
</span> | |||
</label> | |||
) | |||
} | |||
{label && ( | |||
<label | |||
data-testid="label" | |||
id={labelId} | |||
htmlFor={id} | |||
className={clsx( | |||
'absolute z-[1] w-full top-0.5 left-0 pointer-events-none pl-1 text-xxs font-bold peer-disabled:opacity-50 peer-focus:text-secondary text-primary leading-none bg-negative select-none', | |||
{ | |||
'sr-only': hiddenLabel, | |||
}, | |||
{ | |||
'pr-1': !indicator, | |||
}, | |||
{ | |||
'pr-10': indicator && size === 'small', | |||
'pr-12': indicator && size === 'medium', | |||
'pr-16': indicator && size === 'large', | |||
}, | |||
)} | |||
> | |||
<span className="block w-full whitespace-nowrap h-[1.1em] overflow-hidden text-ellipsis"> | |||
{label} | |||
</span> | |||
</label> | |||
)} | |||
{hint && ( | |||
<div | |||
data-testid="hint" | |||
@@ -197,6 +196,7 @@ export const MenuMultiSelect = React.forwardRef< | |||
)} | |||
{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', | |||
{ | |||
@@ -209,14 +209,12 @@ export const MenuMultiSelect = React.forwardRef< | |||
{indicator} | |||
</div> | |||
)} | |||
{ | |||
border && ( | |||
<span | |||
data-testid="border" | |||
className="absolute z-[1] peer-disabled:opacity-50 inset-0 rounded-inherit border-2 border-primary pointer-events-none peer-focus:border-secondary" | |||
/> | |||
) | |||
} | |||
{border && ( | |||
<span | |||
data-testid="border" | |||
className="absolute z-[1] peer-disabled:opacity-50 inset-0 rounded-inherit border-2 border-primary pointer-events-none peer-focus:border-secondary" | |||
/> | |||
)} | |||
</div> | |||
); | |||
}); | |||
@@ -299,33 +299,31 @@ export const TagInput = React.forwardRef<TagInputDerivedElement, TagInputProps>( | |||
separators={separator.map((s) => TAG_INPUT_SEPARATOR_MAP[s])} | |||
/> | |||
)} | |||
{ | |||
label && ( | |||
<label | |||
data-testid="label" | |||
id={labelId} | |||
htmlFor={id} | |||
className={clsx( | |||
'absolute z-[1] w-full top-0.5 left-0 pointer-events-none pl-1 text-xxs font-bold peer-disabled:opacity-50 group-focus-within:text-secondary text-primary leading-none bg-negative select-none', | |||
{ | |||
'sr-only': hiddenLabel, | |||
}, | |||
{ | |||
'pr-1': !indicator, | |||
}, | |||
{ | |||
'pr-10': indicator && size === 'small', | |||
'pr-12': indicator && size === 'medium', | |||
'pr-16': indicator && size === 'large', | |||
}, | |||
)} | |||
> | |||
<span className="block w-full whitespace-nowrap h-[1.1em] overflow-hidden text-ellipsis"> | |||
{label} | |||
</span> | |||
</label> | |||
) | |||
} | |||
{label && ( | |||
<label | |||
data-testid="label" | |||
id={labelId} | |||
htmlFor={id} | |||
className={clsx( | |||
'absolute z-[1] w-full top-0.5 left-0 pointer-events-none pl-1 text-xxs font-bold peer-disabled:opacity-50 group-focus-within:text-secondary text-primary leading-none bg-negative select-none', | |||
{ | |||
'sr-only': hiddenLabel, | |||
}, | |||
{ | |||
'pr-1': !indicator, | |||
}, | |||
{ | |||
'pr-10': indicator && size === 'small', | |||
'pr-12': indicator && size === 'medium', | |||
'pr-16': indicator && size === 'large', | |||
}, | |||
)} | |||
> | |||
<span className="block w-full whitespace-nowrap h-[1.1em] overflow-hidden text-ellipsis"> | |||
{label} | |||
</span> | |||
</label> | |||
)} | |||
{hint && ( | |||
<div | |||
data-testid="hint" | |||
@@ -371,14 +369,12 @@ export const TagInput = React.forwardRef<TagInputDerivedElement, TagInputProps>( | |||
{indicator} | |||
</div> | |||
)} | |||
{ | |||
border && ( | |||
<span | |||
data-testid="border" | |||
className="absolute z-[1] peer-disabled:opacity-50 inset-0 rounded-inherit border-2 border-primary pointer-events-none group-focus-within:border-secondary" | |||
/> | |||
) | |||
} | |||
{border && ( | |||
<span | |||
data-testid="border" | |||
className="absolute z-[1] peer-disabled:opacity-50 inset-0 rounded-inherit border-2 border-primary pointer-events-none group-focus-within:border-secondary" | |||
/> | |||
)} | |||
</div> | |||
); | |||
}); | |||
@@ -248,16 +248,14 @@ export const Slider = React.forwardRef<SliderDerivedElement, SliderProps>(( | |||
return ( | |||
<> | |||
{ | |||
children | |||
&& ( | |||
<datalist | |||
id={tickMarkId} | |||
> | |||
{children} | |||
</datalist> | |||
) | |||
} | |||
{children | |||
&& ( | |||
<datalist | |||
id={tickMarkId} | |||
> | |||
{children} | |||
</datalist> | |||
)} | |||
<div | |||
className={clsx( | |||
block && orient !== 'vertical' && 'w-full', | |||
@@ -113,33 +113,31 @@ export const Spinner = React.forwardRef<SpinnerDerivedElement, SpinnerProps>(( | |||
}, | |||
)} | |||
/> | |||
{ | |||
label && ( | |||
<label | |||
data-testid="label" | |||
id={labelId} | |||
htmlFor={id} | |||
className={clsx( | |||
'absolute z-[1] w-full top-0.5 left-0 pointer-events-none pl-1 text-xxs font-bold peer-disabled:opacity-50 peer-focus:text-secondary text-primary leading-none bg-negative select-none', | |||
{ | |||
'sr-only': hiddenLabel, | |||
}, | |||
{ | |||
'pr-1': !indicator, | |||
}, | |||
{ | |||
'pr-10': indicator && size === 'small', | |||
'pr-12': indicator && size === 'medium', | |||
'pr-16': indicator && size === 'large', | |||
}, | |||
)} | |||
> | |||
<span className="block w-full whitespace-nowrap h-[1.1em] overflow-hidden text-ellipsis"> | |||
{label} | |||
</span> | |||
</label> | |||
) | |||
} | |||
{label && ( | |||
<label | |||
data-testid="label" | |||
id={labelId} | |||
htmlFor={id} | |||
className={clsx( | |||
'absolute z-[1] w-full top-0.5 left-0 pointer-events-none pl-1 text-xxs font-bold peer-disabled:opacity-50 peer-focus:text-secondary text-primary leading-none bg-negative select-none', | |||
{ | |||
'sr-only': hiddenLabel, | |||
}, | |||
{ | |||
'pr-1': !indicator, | |||
}, | |||
{ | |||
'pr-10': indicator && size === 'small', | |||
'pr-12': indicator && size === 'medium', | |||
'pr-16': indicator && size === 'large', | |||
}, | |||
)} | |||
> | |||
<span className="block w-full whitespace-nowrap h-[1.1em] overflow-hidden text-ellipsis"> | |||
{label} | |||
</span> | |||
</label> | |||
)} | |||
{hint && ( | |||
<div | |||
data-testid="hint" | |||
@@ -185,14 +183,12 @@ export const Spinner = React.forwardRef<SpinnerDerivedElement, SpinnerProps>(( | |||
{indicator} | |||
</div> | |||
)} | |||
{ | |||
border && ( | |||
<span | |||
data-testid="border" | |||
className="absolute z-[1] peer-disabled:opacity-50 inset-0 rounded-inherit border-2 border-primary pointer-events-none peer-focus:border-secondary" | |||
/> | |||
) | |||
} | |||
{border && ( | |||
<span | |||
data-testid="border" | |||
className="absolute z-[1] peer-disabled:opacity-50 inset-0 rounded-inherit border-2 border-primary pointer-events-none peer-focus:border-secondary" | |||
/> | |||
)} | |||
</div> | |||
); | |||
}); | |||
@@ -131,33 +131,31 @@ export const DateDropdown = React.forwardRef< | |||
}, | |||
)} | |||
/> | |||
{ | |||
label && ( | |||
<label | |||
data-testid="label" | |||
id={labelId} | |||
htmlFor={id} | |||
className={clsx( | |||
'absolute z-[1] w-full top-0.5 left-0 pointer-events-none pl-1 text-xxs font-bold peer-disabled:opacity-50 peer-focus:text-secondary text-primary leading-none bg-negative select-none', | |||
{ | |||
'sr-only': hiddenLabel, | |||
}, | |||
{ | |||
'pr-1': !indicator, | |||
}, | |||
{ | |||
'pr-10': indicator && size === 'small', | |||
'pr-12': indicator && size === 'medium', | |||
'pr-16': indicator && size === 'large', | |||
}, | |||
)} | |||
> | |||
<span className="block w-full whitespace-nowrap h-[1.1em] overflow-hidden text-ellipsis"> | |||
{label} | |||
</span> | |||
</label> | |||
) | |||
} | |||
{label && ( | |||
<label | |||
data-testid="label" | |||
id={labelId} | |||
htmlFor={id} | |||
className={clsx( | |||
'absolute z-[1] w-full top-0.5 left-0 pointer-events-none pl-1 text-xxs font-bold peer-disabled:opacity-50 peer-focus:text-secondary text-primary leading-none bg-negative select-none', | |||
{ | |||
'sr-only': hiddenLabel, | |||
}, | |||
{ | |||
'pr-1': !indicator, | |||
}, | |||
{ | |||
'pr-10': indicator && size === 'small', | |||
'pr-12': indicator && size === 'medium', | |||
'pr-16': indicator && size === 'large', | |||
}, | |||
)} | |||
> | |||
<span className="block w-full whitespace-nowrap h-[1.1em] overflow-hidden text-ellipsis"> | |||
{label} | |||
</span> | |||
</label> | |||
)} | |||
{hint && ( | |||
<div | |||
data-testid="hint" | |||
@@ -203,14 +201,12 @@ export const DateDropdown = React.forwardRef< | |||
{indicator} | |||
</div> | |||
)} | |||
{ | |||
border && ( | |||
<span | |||
data-testid="border" | |||
className="absolute z-[1] peer-disabled:opacity-50 inset-0 rounded-inherit border-2 border-primary pointer-events-none peer-focus:border-secondary" | |||
/> | |||
) | |||
} | |||
{border && ( | |||
<span | |||
data-testid="border" | |||
className="absolute z-[1] peer-disabled:opacity-50 inset-0 rounded-inherit border-2 border-primary pointer-events-none peer-focus:border-secondary" | |||
/> | |||
)} | |||
</div> | |||
); | |||
}); | |||