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, | className, | ||||
)} | )} | ||||
style={style} | style={style} | ||||
data-testid="base" | |||||
> | > | ||||
<input | <input | ||||
{...etcProps} | {...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 && ( | {hint && ( | ||||
<div | <div | ||||
data-testid="hint" | data-testid="hint" | ||||
@@ -209,6 +208,7 @@ export const ComboBox = React.forwardRef<ComboBoxDerivedElement, ComboBoxProps>( | |||||
)} | )} | ||||
{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', | ||||
{ | { | ||||
@@ -221,14 +221,12 @@ export const ComboBox = React.forwardRef<ComboBoxDerivedElement, ComboBoxProps>( | |||||
{indicator} | {indicator} | ||||
</div> | </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> | </div> | ||||
</> | </> | ||||
); | ); | ||||
@@ -15,7 +15,8 @@ import { | |||||
} from 'vitest'; | } from 'vitest'; | ||||
import matchers from '@testing-library/jest-dom/matchers'; | import matchers from '@testing-library/jest-dom/matchers'; | ||||
import { | import { | ||||
DropdownSelect, DropdownSelectDerivedElement, | |||||
DropdownSelect, | |||||
DropdownSelectDerivedElement, | |||||
} from '.'; | } from '.'; | ||||
expect.extend(matchers); | expect.extend(matchers); | ||||
@@ -124,33 +124,31 @@ export const DropdownSelect = React.forwardRef<DropdownSelectDerivedElement, Dro | |||||
> | > | ||||
{children} | {children} | ||||
</select> | </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 && ( | {hint && ( | ||||
<div | <div | ||||
data-testid="hint" | data-testid="hint" | ||||
@@ -197,14 +195,12 @@ export const DropdownSelect = React.forwardRef<DropdownSelectDerivedElement, Dro | |||||
{indicator} | {indicator} | ||||
</div> | </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> | </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, | className, | ||||
)} | )} | ||||
data-testid="base" | |||||
> | > | ||||
<select | <select | ||||
{...etcProps} | {...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 && ( | {hint && ( | ||||
<div | <div | ||||
data-testid="hint" | data-testid="hint" | ||||
@@ -197,6 +196,7 @@ export const MenuSelect = React.forwardRef<MenuSelectDerivedElement, MenuSelectP | |||||
)} | )} | ||||
{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', | ||||
{ | { | ||||
@@ -209,14 +209,12 @@ export const MenuSelect = React.forwardRef<MenuSelectDerivedElement, MenuSelectP | |||||
{indicator} | {indicator} | ||||
</div> | </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> | </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>( | 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 && ( | {hint && ( | ||||
<div | <div | ||||
data-testid="hint" | data-testid="hint" | ||||
@@ -187,14 +185,12 @@ export const EmailInput = React.forwardRef<EmailInputDerivedElement, EmailInputP | |||||
{indicator} | {indicator} | ||||
</div> | </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> | </div> | ||||
); | ); | ||||
}, | }, | ||||
@@ -44,13 +44,13 @@ export interface PhoneNumberInputProps extends Omit<React.HTMLProps<PhoneNumberI | |||||
*/ | */ | ||||
enhanced?: boolean, | 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< | export const PhoneNumberInput = React.forwardRef< | ||||
PhoneNumberInputDerivedElement, | PhoneNumberInputDerivedElement, | ||||
@@ -69,7 +69,7 @@ export const PhoneNumberInput = React.forwardRef< | |||||
id: idProp, | id: idProp, | ||||
style, | style, | ||||
enhanced = true, | enhanced = true, | ||||
defaultCountry = 'PH' as const, | |||||
country = 'PH' as const, | |||||
value, | value, | ||||
onChange, | onChange, | ||||
name, | name, | ||||
@@ -154,45 +154,41 @@ export const PhoneNumberInput = React.forwardRef< | |||||
tabIndex={clientSide ? -1 : undefined} | tabIndex={clientSide ? -1 : undefined} | ||||
className={clsx(commonInputStyles, clientSide && 'sr-only')} | 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 && ( | {hint && ( | ||||
<div | <div | ||||
data-testid="hint" | data-testid="hint" | ||||
@@ -238,14 +234,12 @@ export const PhoneNumberInput = React.forwardRef< | |||||
{indicator} | {indicator} | ||||
</div> | </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> | </div> | ||||
); | ); | ||||
}); | }); | ||||
@@ -262,5 +256,5 @@ PhoneNumberInput.defaultProps = { | |||||
variant: 'default' as const, | variant: 'default' as const, | ||||
hiddenLabel: false as const, | hiddenLabel: false as const, | ||||
enhanced: 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>(( | 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 && ( | {hint && ( | ||||
<div | <div | ||||
data-testid="hint" | data-testid="hint" | ||||
@@ -186,14 +184,12 @@ export const UrlInput = React.forwardRef<UrlInputDerivedElement, UrlInputProps>( | |||||
{indicator} | {indicator} | ||||
</div> | </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> | </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 && ( | {hint && ( | ||||
<div | <div | ||||
data-testid="hint" | data-testid="hint" | ||||
@@ -191,14 +189,12 @@ export const MaskedTextInput = React.forwardRef< | |||||
{indicator} | {indicator} | ||||
</div> | </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> | </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 && ( | {hint && ( | ||||
<div | <div | ||||
data-testid="hint" | data-testid="hint" | ||||
@@ -206,14 +204,12 @@ export const MultilineTextInput = React.forwardRef< | |||||
{indicator} | {indicator} | ||||
</div> | </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> | </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 && ( | {hint && ( | ||||
<div | <div | ||||
data-testid="hint" | data-testid="hint" | ||||
@@ -208,14 +206,12 @@ export const TextInput = React.forwardRef<TextInputDerivedElement, TextInputProp | |||||
{indicator} | {indicator} | ||||
</div> | </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> | </div> | ||||
); | ); | ||||
}); | }); | ||||
@@ -29,31 +29,29 @@ export const KeyValueTable = React.forwardRef<KeyValueTableDerivedElement, KeyVa | |||||
)} | )} | ||||
ref={forwardedRef} | 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> | </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, | className, | ||||
)} | )} | ||||
data-testid="base" | |||||
> | > | ||||
<select | <select | ||||
{...etcProps} | {...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 && ( | {hint && ( | ||||
<div | <div | ||||
data-testid="hint" | data-testid="hint" | ||||
@@ -197,6 +196,7 @@ export const MenuMultiSelect = 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', | ||||
{ | { | ||||
@@ -209,14 +209,12 @@ export const MenuMultiSelect = React.forwardRef< | |||||
{indicator} | {indicator} | ||||
</div> | </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> | </div> | ||||
); | ); | ||||
}); | }); | ||||
@@ -299,33 +299,31 @@ export const TagInput = React.forwardRef<TagInputDerivedElement, TagInputProps>( | |||||
separators={separator.map((s) => TAG_INPUT_SEPARATOR_MAP[s])} | 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 && ( | {hint && ( | ||||
<div | <div | ||||
data-testid="hint" | data-testid="hint" | ||||
@@ -371,14 +369,12 @@ export const TagInput = React.forwardRef<TagInputDerivedElement, TagInputProps>( | |||||
{indicator} | {indicator} | ||||
</div> | </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> | </div> | ||||
); | ); | ||||
}); | }); | ||||
@@ -248,16 +248,14 @@ export const Slider = React.forwardRef<SliderDerivedElement, SliderProps>(( | |||||
return ( | return ( | ||||
<> | <> | ||||
{ | |||||
children | |||||
&& ( | |||||
<datalist | |||||
id={tickMarkId} | |||||
> | |||||
{children} | |||||
</datalist> | |||||
) | |||||
} | |||||
{children | |||||
&& ( | |||||
<datalist | |||||
id={tickMarkId} | |||||
> | |||||
{children} | |||||
</datalist> | |||||
)} | |||||
<div | <div | ||||
className={clsx( | className={clsx( | ||||
block && orient !== 'vertical' && 'w-full', | 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 && ( | {hint && ( | ||||
<div | <div | ||||
data-testid="hint" | data-testid="hint" | ||||
@@ -185,14 +183,12 @@ export const Spinner = React.forwardRef<SpinnerDerivedElement, SpinnerProps>(( | |||||
{indicator} | {indicator} | ||||
</div> | </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> | </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 && ( | {hint && ( | ||||
<div | <div | ||||
data-testid="hint" | data-testid="hint" | ||||
@@ -203,14 +201,12 @@ export const DateDropdown = React.forwardRef< | |||||
{indicator} | {indicator} | ||||
</div> | </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> | </div> | ||||
); | ); | ||||
}); | }); | ||||