@@ -13,6 +13,10 @@ | |||
- Color | |||
- [ ] ColorPicker | |||
- [X] Swatch | |||
- Code | |||
- [ ] CodeInput (extract to own package) | |||
- [X] CodeBlock (`react-refractor`) | |||
- [ ] VerifyCodeInput (for OTP inputs) | |||
- Formatted | |||
- [X] EmailInput | |||
- [X] PhoneNumberInput | |||
@@ -61,4 +65,4 @@ | |||
- [ ] YearInput | |||
# Others | |||
- [ ] Add `select-none` to input labels, etc. | |||
- [X] Add `select-none` to input labels, etc. |
@@ -105,3 +105,4 @@ dist | |||
.tern-port | |||
.npmrc | |||
types/ |
@@ -15,6 +15,7 @@ | |||
"devDependencies": { | |||
"@testing-library/jest-dom": "^5.16.5", | |||
"@testing-library/react": "^13.4.0", | |||
"@testing-library/user-event": "^14.4.3", | |||
"@types/node": "^18.14.1", | |||
"@types/react": "^18.0.27", | |||
"eslint": "^8.35.0", | |||
@@ -58,8 +59,8 @@ | |||
"access": "public" | |||
}, | |||
"dependencies": { | |||
"clsx": "^1.2.1", | |||
"@tesseract-design/web-base": "workspace:*" | |||
"@tesseract-design/web-base": "workspace:*", | |||
"clsx": "^1.2.1" | |||
}, | |||
"types": "./dist/types/index.d.ts", | |||
"main": "./dist/cjs/production/index.js", | |||
@@ -0,0 +1,4 @@ | |||
import matchers from '@testing-library/jest-dom/matchers'; | |||
import { expect } from 'vitest'; | |||
expect.extend(matchers); |
@@ -0,0 +1,186 @@ | |||
import * as React from 'react'; | |||
import { | |||
render, | |||
screen, | |||
cleanup, | |||
} from '@testing-library/react'; | |||
import userEvent from '@testing-library/user-event'; | |||
import { Button } from '@tesseract-design/web-base'; | |||
import { | |||
vi, | |||
describe, | |||
it, | |||
expect, afterEach, | |||
} from 'vitest'; | |||
import { | |||
ActionButton, | |||
} from '.'; | |||
import matchers from '@testing-library/jest-dom/matchers'; | |||
expect.extend(matchers); | |||
vi.mock('@tesseract-design/web-base-button'); | |||
describe('ActionButton', () => { | |||
afterEach(() => { | |||
cleanup(); | |||
}); | |||
it('renders a button', () => { | |||
render( | |||
<ActionButton />, | |||
); | |||
const button: HTMLButtonElement = screen.getByRole('button'); | |||
expect(button).toBeInTheDocument(); | |||
expect(button).toHaveProperty('type', 'button'); | |||
}); | |||
it('renders a subtext', () => { | |||
render( | |||
<ActionButton | |||
subtext="subtext" | |||
/>, | |||
); | |||
const subtext: HTMLElement = screen.getByTestId('subtext'); | |||
expect(subtext).toBeInTheDocument(); | |||
}); | |||
it('renders a badge', () => { | |||
render( | |||
<ActionButton | |||
badge="badge" | |||
/>, | |||
); | |||
const badge: HTMLElement = screen.getByTestId('badge'); | |||
expect(badge).toBeInTheDocument(); | |||
}); | |||
it('renders as a menu item', () => { | |||
render( | |||
<ActionButton | |||
menuItem | |||
/>, | |||
); | |||
const menuItemIndicator: HTMLElement = screen.getByTestId('menuItemIndicator'); | |||
expect(menuItemIndicator).toBeInTheDocument(); | |||
}); | |||
it('handles click events', async () => { | |||
const onClick = vi.fn().mockImplementationOnce((e) => { e.preventDefault() }); | |||
render( | |||
<ActionButton | |||
onClick={onClick} | |||
/>, | |||
); | |||
const button: HTMLButtonElement = screen.getByRole('button'); | |||
await userEvent.click(button); | |||
expect(onClick).toBeCalled(); | |||
}); | |||
it('renders a compact button', () => { | |||
render( | |||
<ActionButton | |||
compact | |||
/>, | |||
); | |||
const button: HTMLButtonElement = screen.getByRole('button'); | |||
expect(button).toHaveClass('pl-2'); | |||
expect(button).toHaveClass('gap-2'); | |||
expect(button).toHaveClass('pr-2'); | |||
}); | |||
describe.each` | |||
size | className | |||
${'small'} | ${'h-10'} | |||
${'medium'} | ${'h-12'} | |||
${'large'} | ${'h-16'} | |||
`('on %s size', ({ | |||
size, | |||
className, | |||
}: { size: Button.Size, className: string }) => { | |||
it('renders button styles', () => { | |||
render( | |||
<ActionButton | |||
size={size} | |||
/>, | |||
); | |||
const button: HTMLButtonElement = screen.getByRole('button'); | |||
expect(button).toHaveClass(className); | |||
}); | |||
it('renders badge styles', () => { | |||
render( | |||
<ActionButton | |||
size={size} | |||
badge="badge" | |||
/>, | |||
); | |||
const badge = screen.getByTestId('badge'); | |||
expect(badge).toBeInTheDocument(); | |||
}); | |||
}); | |||
it.each` | |||
variant | className | |||
${'bare'} | ${'bg-negative'} | |||
${'outline'} | ${'border-2'} | |||
${'filled'} | ${'bg-primary'} | |||
`('renders a button with variant %s', ({ | |||
variant, | |||
className, | |||
}: { variant: Button.Variant, className: string }) => { | |||
render( | |||
<ActionButton | |||
variant={variant} | |||
/>, | |||
); | |||
const button: HTMLButtonElement = screen.getByRole('button'); | |||
expect(button).toHaveClass(className); | |||
}); | |||
it('renders a block button', () => { | |||
render( | |||
<ActionButton | |||
block | |||
/>, | |||
); | |||
const button: HTMLButtonElement = screen.getByRole('button'); | |||
expect(button).toHaveClass('w-full'); | |||
expect(button).toHaveClass('flex'); | |||
}); | |||
it('renders children', () => { | |||
render( | |||
<ActionButton> | |||
Foo | |||
</ActionButton> | |||
); | |||
const children: HTMLElement = screen.getByTestId('children'); | |||
expect(children).toHaveTextContent('Foo'); | |||
}); | |||
it.each(['button', 'submit'] as const)('renders a button with type %s', (buttonType) => { | |||
render( | |||
<ActionButton | |||
type={buttonType} | |||
/> | |||
); | |||
const button: HTMLButtonElement = screen.getByRole('button'); | |||
expect(button).toHaveProperty('type', buttonType); | |||
}); | |||
it('renders a disabled button', () => { | |||
render( | |||
<ActionButton | |||
disabled | |||
/> | |||
); | |||
const button: HTMLButtonElement = screen.getByTestId('button'); | |||
expect(button).toBeDisabled(); | |||
}); | |||
}); |
@@ -2,37 +2,73 @@ import * as React from 'react'; | |||
import clsx from 'clsx'; | |||
import { Button } from '@tesseract-design/web-base'; | |||
/** | |||
* Derived HTML element of the {@link ActionButton} component. | |||
*/ | |||
export type ActionButtonDerivedElement = HTMLButtonElement; | |||
/** | |||
* Props of the {@link ActionButton} component. | |||
*/ | |||
export interface ActionButtonProps extends Omit<React.HTMLProps<ActionButtonDerivedElement>, 'type' | 'size'> { | |||
/** | |||
* Type of the component. | |||
*/ | |||
type?: Button.Type; | |||
/** | |||
* Variant of the component. | |||
*/ | |||
variant?: Button.Variant; | |||
/** | |||
* Should the component occupy the whole width of its parent? | |||
*/ | |||
block?: boolean; | |||
/** | |||
* Complementary content of the component. | |||
*/ | |||
subtext?: React.ReactNode; | |||
/** | |||
* Short complementary content displayed at the edge of the component. | |||
*/ | |||
badge?: React.ReactNode; | |||
/** | |||
* Is this component part of a menu? | |||
*/ | |||
menuItem?: boolean; | |||
/** | |||
* Size of the component. | |||
*/ | |||
size?: Button.Size; | |||
/** | |||
* Should the component's content use minimal space? | |||
*/ | |||
compact?: boolean; | |||
} | |||
/** | |||
* Component for performing an action upon activation (e.g. when clicked). | |||
* | |||
* This component functions as a regular button. | |||
*/ | |||
export const ActionButton = React.forwardRef<ActionButtonDerivedElement, ActionButtonProps>(( | |||
{ | |||
type = 'button' as const, | |||
variant = 'bare' as const, | |||
subtext, | |||
badge, | |||
menuItem = false, | |||
menuItem = false as const, | |||
children, | |||
size = 'medium' as const, | |||
compact = false, | |||
compact = false as const, | |||
className, | |||
block = false, | |||
block = false as const, | |||
...etcProps | |||
}, | |||
forwardedRef, | |||
) => ( | |||
<button | |||
{...etcProps} | |||
data-testid="button" | |||
type={type} | |||
ref={forwardedRef} | |||
className={clsx( | |||
@@ -120,15 +156,15 @@ export const ActionButton = React.forwardRef<ActionButtonDerivedElement, ActionB | |||
</button> | |||
)); | |||
ActionButton.displayName = 'ActionButton'; | |||
ActionButton.displayName = 'ActionButton' as const; | |||
ActionButton.defaultProps = { | |||
type: 'button', | |||
variant: 'bare', | |||
block: false, | |||
type: 'button' as const, | |||
variant: 'bare' as const, | |||
block: false as const, | |||
subtext: undefined, | |||
badge: undefined, | |||
menuItem: false, | |||
size: 'medium', | |||
compact: false, | |||
menuItem: false as const, | |||
size: 'medium' as const, | |||
compact: false as const, | |||
}; |
@@ -0,0 +1,10 @@ | |||
import { describe, it, expect } from 'vitest'; | |||
import * as WebActionReact from '.'; | |||
describe('web-action-react', () => { | |||
it.each([ | |||
'ActionButton', | |||
])('exports %s', (namedExport) => { | |||
expect(WebActionReact).toHaveProperty(namedExport); | |||
}); | |||
}); |
@@ -2,8 +2,14 @@ import * as React from 'react'; | |||
import { TextControl } from '@tesseract-design/web-base'; | |||
import clsx from 'clsx'; | |||
/** | |||
* Derived HTML element of the {@link ComboBox} component. | |||
*/ | |||
export type ComboBoxDerivedElement = HTMLInputElement; | |||
/** | |||
* Props of the {@link ComboBox} component. | |||
*/ | |||
export interface ComboBoxProps extends Omit<React.HTMLProps<ComboBoxDerivedElement>, 'size' | 'type' | 'style' | 'label' | 'list' | 'inputMode'> { | |||
/** | |||
* Short textual description indicating the nature of the component's value. | |||
@@ -58,11 +64,11 @@ export const ComboBox = React.forwardRef<ComboBoxDerivedElement, ComboBoxProps>( | |||
hint, | |||
indicator, | |||
size = 'medium' as const, | |||
border = false, | |||
block = false, | |||
border = false as const, | |||
block = false as const, | |||
type = 'text' as const, | |||
variant = 'default' as const, | |||
hiddenLabel = false, | |||
hiddenLabel = false as const, | |||
className, | |||
children, | |||
inputMode = 'text' as const, | |||
@@ -226,17 +232,17 @@ export const ComboBox = React.forwardRef<ComboBoxDerivedElement, ComboBoxProps>( | |||
); | |||
}); | |||
ComboBox.displayName = 'ComboBox'; | |||
ComboBox.displayName = 'ComboBox' as const; | |||
ComboBox.defaultProps = { | |||
label: undefined, | |||
hint: undefined, | |||
indicator: undefined, | |||
size: 'medium', | |||
border: false, | |||
block: false, | |||
type: 'text', | |||
variant: 'default', | |||
hiddenLabel: false, | |||
inputMode: 'text', | |||
size: 'medium' as const, | |||
border: false as const, | |||
block: false as const, | |||
type: 'text' as const, | |||
variant: 'default' as const, | |||
hiddenLabel: false as const, | |||
inputMode: 'text' as const, | |||
}; |
@@ -2,8 +2,14 @@ import * as React from 'react'; | |||
import { TextControl } from '@tesseract-design/web-base'; | |||
import clsx from 'clsx'; | |||
/** | |||
* Derived HTML element of the {@link DropdownSelect} component. | |||
*/ | |||
export type DropdownSelectDerivedElement = HTMLSelectElement; | |||
/** | |||
* Props of the {@link DropdownSelect} component. | |||
*/ | |||
export interface DropdownSelectProps extends Omit<React.HTMLProps<DropdownSelectDerivedElement>, 'size' | 'type' | 'style' | 'label' | 'list' | 'multiple'> { | |||
/** | |||
* Short textual description indicating the nature of the component's value. | |||
@@ -40,177 +46,173 @@ export interface DropdownSelectProps extends Omit<React.HTMLProps<DropdownSelect | |||
} | |||
/** | |||
* Component for inputting textual values. | |||
* | |||
* This component supports multiline input and adjusts its layout accordingly. | |||
* Component for selecting a single value from a dropdown. | |||
*/ | |||
export const DropdownSelect = React.forwardRef<DropdownSelectDerivedElement, DropdownSelectProps>( | |||
( | |||
{ | |||
label, | |||
hint, | |||
indicator, | |||
size = 'medium' as const, | |||
border = false, | |||
block = false, | |||
variant = 'default' as const, | |||
hiddenLabel = false, | |||
className, | |||
children, | |||
id: idProp, | |||
...etcProps | |||
}: DropdownSelectProps, | |||
forwardedRef, | |||
) => { | |||
const labelId = React.useId(); | |||
const defaultId = React.useId(); | |||
const id = idProp ?? defaultId; | |||
export const DropdownSelect = React.forwardRef<DropdownSelectDerivedElement, DropdownSelectProps>(( | |||
{ | |||
label, | |||
hint, | |||
indicator, | |||
size = 'medium' as const, | |||
border = false as const, | |||
block = false as const, | |||
variant = 'default' as const, | |||
hiddenLabel = false as const, | |||
className, | |||
children, | |||
id: idProp, | |||
...etcProps | |||
}: DropdownSelectProps, | |||
forwardedRef, | |||
) => { | |||
const labelId = React.useId(); | |||
const defaultId = React.useId(); | |||
const id = idProp ?? defaultId; | |||
return ( | |||
<div | |||
return ( | |||
<div | |||
className={clsx( | |||
'relative rounded ring-secondary/50 min-w-48', | |||
'focus-within:ring-4', | |||
{ | |||
'block': block, | |||
'inline-block align-middle': !block, | |||
}, | |||
className, | |||
)} | |||
> | |||
<select | |||
{...etcProps} | |||
ref={forwardedRef} | |||
id={id} | |||
aria-labelledby={labelId} | |||
data-testid="input" | |||
className={clsx( | |||
'relative rounded ring-secondary/50 min-w-48', | |||
'focus-within:ring-4', | |||
'tesseract-design-dropdown-select bg-negative rounded-inherit w-full peer block appearance-none cursor-pointer select-none', | |||
'focus:outline-0', | |||
'disabled:opacity-50 disabled:cursor-not-allowed', | |||
{ | |||
'text-xxs': size === 'small', | |||
'text-xs': size === 'medium', | |||
}, | |||
{ | |||
'pl-4': variant === 'default', | |||
'pl-1.5': variant === 'alternate', | |||
}, | |||
{ | |||
'pt-4': variant === 'alternate', | |||
}, | |||
{ | |||
'pr-4': variant === 'default' && !indicator, | |||
'pr-1.5': variant === 'alternate' && !indicator, | |||
}, | |||
{ | |||
'pr-10': indicator && size === 'small', | |||
'pr-12': indicator && size === 'medium', | |||
'pr-16': indicator && size === 'large', | |||
}, | |||
{ | |||
'block': block, | |||
'inline-block align-middle': !block, | |||
'h-10': size === 'small', | |||
'h-12': size === 'medium', | |||
'h-16': size === 'large', | |||
}, | |||
className, | |||
)} | |||
> | |||
<select | |||
{...etcProps} | |||
ref={forwardedRef} | |||
id={id} | |||
aria-labelledby={labelId} | |||
data-testid="input" | |||
{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> | |||
) | |||
} | |||
{hint && ( | |||
<div | |||
data-testid="hint" | |||
className={clsx( | |||
'tesseract-design-dropdown-select bg-negative rounded-inherit w-full peer block appearance-none cursor-pointer select-none', | |||
'focus:outline-0', | |||
'disabled:opacity-50 disabled:cursor-not-allowed', | |||
'absolute left-0 px-1 pointer-events-none text-xxs peer-disabled:opacity-50 leading-none w-full bg-negative select-none', | |||
{ | |||
'text-xxs': size === 'small', | |||
'text-xs': size === 'medium', | |||
'bottom-0 pl-4 pb-1': variant === 'default', | |||
'top-0.5': variant === 'alternate', | |||
}, | |||
{ | |||
'pl-4': variant === 'default', | |||
'pl-1.5': variant === 'alternate', | |||
'pt-2': variant === 'alternate' && size === 'small', | |||
'pt-3': variant === 'alternate' && size !== 'small', | |||
}, | |||
{ | |||
'pt-4': variant === 'alternate', | |||
}, | |||
{ | |||
'pr-4': variant === 'default' && !indicator, | |||
'pr-1.5': variant === 'alternate' && !indicator, | |||
'pr-4': !indicator && variant === 'default', | |||
'pr-1': !indicator && variant === 'alternate', | |||
}, | |||
{ | |||
'pr-10': indicator && size === 'small', | |||
'pr-12': indicator && size === 'medium', | |||
'pr-16': indicator && size === 'large', | |||
}, | |||
{ | |||
'h-10': size === 'small', | |||
'h-12': size === 'medium', | |||
'h-16': size === 'large', | |||
}, | |||
)} | |||
> | |||
{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> | |||
) | |||
} | |||
{hint && ( | |||
<div | |||
data-testid="hint" | |||
className={clsx( | |||
'absolute left-0 px-1 pointer-events-none text-xxs peer-disabled:opacity-50 leading-none w-full bg-negative select-none', | |||
{ | |||
'bottom-0 pl-4 pb-1': variant === 'default', | |||
'top-0.5': variant === 'alternate', | |||
}, | |||
{ | |||
'pt-2': variant === 'alternate' && size === 'small', | |||
'pt-3': variant === 'alternate' && size !== 'small', | |||
}, | |||
{ | |||
'pr-4': !indicator && variant === 'default', | |||
'pr-1': !indicator && variant === 'alternate', | |||
}, | |||
{ | |||
'pr-10': indicator && size === 'small', | |||
'pr-12': indicator && size === 'medium', | |||
'pr-16': indicator && size === 'large', | |||
}, | |||
)} | |||
> | |||
<div | |||
className="opacity-50 whitespace-nowrap w-full h-[1.1em] overflow-hidden text-ellipsis" | |||
> | |||
{hint} | |||
</div> | |||
</div> | |||
)} | |||
{indicator && ( | |||
<div | |||
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', | |||
{ | |||
'w-10': size === 'small', | |||
'w-12': size === 'medium', | |||
'w-16': size === 'large', | |||
}, | |||
)} | |||
className="opacity-50 whitespace-nowrap w-full h-[1.1em] overflow-hidden text-ellipsis" | |||
> | |||
{indicator} | |||
{hint} | |||
</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" | |||
/> | |||
) | |||
} | |||
</div> | |||
); | |||
}, | |||
); | |||
</div> | |||
)} | |||
{indicator && ( | |||
<div | |||
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', | |||
{ | |||
'w-10': size === 'small', | |||
'w-12': size === 'medium', | |||
'w-16': size === 'large', | |||
}, | |||
)} | |||
> | |||
{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" | |||
/> | |||
) | |||
} | |||
</div> | |||
); | |||
}); | |||
DropdownSelect.displayName = 'DropdownSelect'; | |||
DropdownSelect.displayName = 'DropdownSelect' as const; | |||
DropdownSelect.defaultProps = { | |||
label: undefined, | |||
hint: undefined, | |||
indicator: undefined, | |||
size: 'medium', | |||
border: false, | |||
block: false, | |||
variant: 'default', | |||
hiddenLabel: false, | |||
size: 'medium' as const, | |||
border: false as const, | |||
block: false as const, | |||
variant: 'default' as const, | |||
hiddenLabel: false as const, | |||
}; |
@@ -2,8 +2,14 @@ import * as React from 'react'; | |||
import { TextControl } from '@tesseract-design/web-base'; | |||
import clsx from 'clsx'; | |||
/** | |||
* Derived HTML element of the {@link MenuSelect} component. | |||
*/ | |||
export type MenuSelectDerivedElement = HTMLSelectElement; | |||
/** | |||
* Props of the {@link MenuSelect} component. | |||
*/ | |||
export interface MenuSelectProps extends Omit<React.HTMLProps<MenuSelectDerivedElement>, 'size' | 'style' | 'label' | 'multiple'> { | |||
/** | |||
* Short textual description indicating the nature of the component's value. | |||
@@ -44,191 +50,187 @@ export interface MenuSelectProps extends Omit<React.HTMLProps<MenuSelectDerivedE | |||
} | |||
/** | |||
* Component for inputting textual values. | |||
* | |||
* This component supports multiline input and adjusts its layout accordingly. | |||
* Component for selecting a single value from a menu. | |||
*/ | |||
export const MenuSelect = React.forwardRef<MenuSelectDerivedElement, MenuSelectProps>( | |||
( | |||
{ | |||
label, | |||
hint, | |||
indicator, | |||
size = 'medium' as const, | |||
border = false, | |||
block = false, | |||
variant = 'default' as const, | |||
hiddenLabel = false, | |||
className, | |||
startingHeight = '15rem', | |||
id: idProp, | |||
...etcProps | |||
}: MenuSelectProps, | |||
forwardedRef, | |||
) => { | |||
const labelId = React.useId(); | |||
const defaultId = React.useId(); | |||
const id = idProp ?? defaultId; | |||
export const MenuSelect = React.forwardRef<MenuSelectDerivedElement, MenuSelectProps>(( | |||
{ | |||
label, | |||
hint, | |||
indicator, | |||
size = 'medium' as const, | |||
border = false, | |||
block = false, | |||
variant = 'default' as const, | |||
hiddenLabel = false, | |||
className, | |||
startingHeight = '15rem', | |||
id: idProp, | |||
...etcProps | |||
}: MenuSelectProps, | |||
forwardedRef, | |||
) => { | |||
const labelId = React.useId(); | |||
const defaultId = React.useId(); | |||
const id = idProp ?? defaultId; | |||
return ( | |||
<div | |||
return ( | |||
<div | |||
className={clsx( | |||
'relative rounded ring-secondary/50', | |||
'focus-within:ring-4', | |||
{ | |||
'block': block, | |||
'inline-block align-middle': !block, | |||
}, | |||
className, | |||
)} | |||
> | |||
<select | |||
{...etcProps} | |||
ref={forwardedRef} | |||
id={id} | |||
aria-labelledby={labelId} | |||
data-testid="input" | |||
size={2} | |||
style={{ | |||
height: startingHeight, | |||
}} | |||
className={clsx( | |||
'relative rounded ring-secondary/50', | |||
'focus-within:ring-4', | |||
'tesseract-design-menu-select bg-negative rounded-inherit w-full peer block overflow-auto cursor-pointer', | |||
'focus:outline-0', | |||
'disabled:opacity-50 disabled:cursor-not-allowed', | |||
{ | |||
'resize': !block, | |||
'resize-y': block, | |||
}, | |||
{ | |||
'text-xxs': size === 'small', | |||
'text-xs': size === 'medium', | |||
}, | |||
{ | |||
'pl-4': variant === 'default', | |||
'pl-1.5': variant === 'alternate', | |||
}, | |||
{ | |||
'pt-4': variant === 'alternate' && size === 'small', | |||
'pt-5': variant === 'alternate' && size === 'medium', | |||
'pt-8': variant === 'alternate' && size === 'large', | |||
}, | |||
{ | |||
'py-2.5': variant === 'default' && size === 'small', | |||
'py-3': variant === 'default' && size === 'medium', | |||
'py-5': variant === 'default' && size === 'large', | |||
}, | |||
{ | |||
'pr-4': variant === 'default' && !indicator, | |||
'pr-1.5': variant === 'alternate' && !indicator, | |||
}, | |||
{ | |||
'block': block, | |||
'inline-block align-middle': !block, | |||
'pr-10': indicator && size === 'small', | |||
'pr-12': indicator && size === 'medium', | |||
'pr-16': indicator && size === 'large', | |||
}, | |||
{ | |||
'min-h-10': size === 'small', | |||
'min-h-12': size === 'medium', | |||
'min-h-16': size === 'large', | |||
}, | |||
className, | |||
)} | |||
> | |||
<select | |||
{...etcProps} | |||
ref={forwardedRef} | |||
id={id} | |||
aria-labelledby={labelId} | |||
data-testid="input" | |||
size={2} | |||
style={{ | |||
height: startingHeight, | |||
}} | |||
/> | |||
{ | |||
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" | |||
className={clsx( | |||
'tesseract-design-menu-select bg-negative rounded-inherit w-full peer block overflow-auto cursor-pointer', | |||
'focus:outline-0', | |||
'disabled:opacity-50 disabled:cursor-not-allowed', | |||
{ | |||
'resize': !block, | |||
'resize-y': block, | |||
}, | |||
'absolute left-0 px-1 pointer-events-none text-xxs peer-disabled:opacity-50 leading-none w-full bg-negative select-none', | |||
{ | |||
'text-xxs': size === 'small', | |||
'text-xs': size === 'medium', | |||
'bottom-0 pl-4 pb-1': variant === 'default', | |||
'top-0.5': variant === 'alternate', | |||
}, | |||
{ | |||
'pl-4': variant === 'default', | |||
'pl-1.5': variant === 'alternate', | |||
'pt-2': variant === 'alternate' && size === 'small', | |||
'pt-3': variant === 'alternate' && size !== 'small', | |||
}, | |||
{ | |||
'pt-4': variant === 'alternate' && size === 'small', | |||
'pt-5': variant === 'alternate' && size === 'medium', | |||
'pt-8': variant === 'alternate' && size === 'large', | |||
}, | |||
{ | |||
'py-2.5': variant === 'default' && size === 'small', | |||
'py-3': variant === 'default' && size === 'medium', | |||
'py-5': variant === 'default' && size === 'large', | |||
}, | |||
{ | |||
'pr-4': variant === 'default' && !indicator, | |||
'pr-1.5': variant === 'alternate' && !indicator, | |||
'pr-4': !indicator && variant === 'default', | |||
'pr-1': !indicator && variant === 'alternate', | |||
}, | |||
{ | |||
'pr-10': indicator && size === 'small', | |||
'pr-12': indicator && size === 'medium', | |||
'pr-16': indicator && size === 'large', | |||
}, | |||
{ | |||
'min-h-10': size === 'small', | |||
'min-h-12': size === 'medium', | |||
'min-h-16': size === 'large', | |||
}, | |||
)} | |||
/> | |||
{ | |||
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" | |||
className={clsx( | |||
'absolute left-0 px-1 pointer-events-none text-xxs peer-disabled:opacity-50 leading-none w-full bg-negative select-none', | |||
{ | |||
'bottom-0 pl-4 pb-1': variant === 'default', | |||
'top-0.5': variant === 'alternate', | |||
}, | |||
{ | |||
'pt-2': variant === 'alternate' && size === 'small', | |||
'pt-3': variant === 'alternate' && size !== 'small', | |||
}, | |||
{ | |||
'pr-4': !indicator && variant === 'default', | |||
'pr-1': !indicator && variant === 'alternate', | |||
}, | |||
{ | |||
'pr-10': indicator && size === 'small', | |||
'pr-12': indicator && size === 'medium', | |||
'pr-16': indicator && size === 'large', | |||
}, | |||
)} | |||
> | |||
<div | |||
className="opacity-50 whitespace-nowrap w-full h-[1.1em] overflow-hidden text-ellipsis" | |||
> | |||
{hint} | |||
</div> | |||
</div> | |||
)} | |||
{indicator && ( | |||
<div | |||
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', | |||
{ | |||
'w-10': size === 'small', | |||
'w-12': size === 'medium', | |||
'w-16': size === 'large', | |||
}, | |||
)} | |||
className="opacity-50 whitespace-nowrap w-full h-[1.1em] overflow-hidden text-ellipsis" | |||
> | |||
{indicator} | |||
{hint} | |||
</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" | |||
/> | |||
) | |||
} | |||
</div> | |||
); | |||
}, | |||
); | |||
</div> | |||
)} | |||
{indicator && ( | |||
<div | |||
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', | |||
{ | |||
'w-10': size === 'small', | |||
'w-12': size === 'medium', | |||
'w-16': size === 'large', | |||
}, | |||
)} | |||
> | |||
{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" | |||
/> | |||
) | |||
} | |||
</div> | |||
); | |||
}); | |||
MenuSelect.displayName = 'MenuSelect'; | |||
MenuSelect.displayName = 'MenuSelect' as const; | |||
MenuSelect.defaultProps = { | |||
label: undefined, | |||
hint: undefined, | |||
indicator: undefined, | |||
size: 'medium', | |||
border: false, | |||
block: false, | |||
variant: 'default', | |||
hiddenLabel: false, | |||
startingHeight: '15rem', | |||
size: 'medium' as const, | |||
border: false as const, | |||
block: false as const, | |||
variant: 'default' as const, | |||
hiddenLabel: false as const, | |||
startingHeight: '15rem' as const, | |||
}; |
@@ -2,22 +2,51 @@ import * as React from 'react'; | |||
import clsx from 'clsx'; | |||
import { Button } from '@tesseract-design/web-base'; | |||
/** | |||
* Derived HTML element of the {@link RadioButton} component. | |||
*/ | |||
export type RadioButtonDerivedElement = HTMLInputElement; | |||
/** | |||
* Props of the {@link RadioButton} component. | |||
*/ | |||
export interface RadioButtonProps extends Omit<React.InputHTMLAttributes<RadioButtonDerivedElement>, 'type' | 'size'> { | |||
/** | |||
* Should the component occupy the whole width of its parent? | |||
*/ | |||
block?: boolean; | |||
/** | |||
* Should the component's content use minimal space? | |||
*/ | |||
compact?: boolean; | |||
/** | |||
* Size of the component. | |||
*/ | |||
size?: Button.Size; | |||
/** | |||
* Complementary content of the component. | |||
*/ | |||
subtext?: React.ReactNode; | |||
/** | |||
* Short complementary content displayed at the edge of the component. | |||
*/ | |||
badge?: React.ReactNode; | |||
/** | |||
* Variant of the component. | |||
*/ | |||
variant?: Button.Variant; | |||
} | |||
/** | |||
* Component for selecting a single value from an array of choices grouped by name. | |||
* | |||
* This component is displayed as a regular button. | |||
*/ | |||
export const RadioButton = React.forwardRef<RadioButtonDerivedElement, RadioButtonProps>(( | |||
{ | |||
children, | |||
block = false, | |||
compact = false, | |||
block = false as const, | |||
compact = false as const, | |||
size = 'medium' as const, | |||
id: idProp, | |||
className, | |||
@@ -141,13 +170,13 @@ export const RadioButton = React.forwardRef<RadioButtonDerivedElement, RadioButt | |||
); | |||
}); | |||
RadioButton.displayName = 'RadioButton'; | |||
RadioButton.displayName = 'RadioButton' as const; | |||
RadioButton.defaultProps = { | |||
badge: undefined, | |||
block: false, | |||
compact: false, | |||
block: false as const, | |||
compact: false as const, | |||
subtext: undefined, | |||
size: 'medium', | |||
variant: 'filled', | |||
size: 'medium' as const, | |||
variant: 'bare' as const, | |||
}; |
@@ -1,17 +1,34 @@ | |||
import * as React from 'react'; | |||
import clsx from 'clsx'; | |||
/** | |||
* Derived HTML element of the {@link RadioTickBox} component. | |||
*/ | |||
export type RadioTickBoxDerivedElement = HTMLInputElement; | |||
/** | |||
* Props of the {@link RadioTickBox} component. | |||
*/ | |||
export interface RadioTickBoxProps extends Omit<React.InputHTMLAttributes<RadioTickBoxDerivedElement>, 'type' | 'size'> { | |||
/** | |||
* Should the component occupy the whole width of its parent? | |||
*/ | |||
block?: boolean; | |||
/** | |||
* Complementary content of the component. | |||
*/ | |||
subtext?: React.ReactNode; | |||
} | |||
/** | |||
* Component for selecting a single value from an array of choices grouped by name. | |||
* | |||
* This component is displayed as a tick box, i.e. a typical radio button. | |||
*/ | |||
export const RadioTickBox = React.forwardRef<RadioTickBoxDerivedElement, RadioTickBoxProps>(( | |||
{ | |||
children, | |||
block = false, | |||
block = false as const, | |||
id: idProp, | |||
className, | |||
subtext, | |||
@@ -84,9 +101,9 @@ export const RadioTickBox = React.forwardRef<RadioTickBoxDerivedElement, RadioTi | |||
); | |||
}); | |||
RadioTickBox.displayName = 'RadioTickBox'; | |||
RadioTickBox.displayName = 'RadioTickBox' as const; | |||
RadioTickBox.defaultProps = { | |||
block: false, | |||
block: false as const, | |||
subtext: undefined, | |||
}; |
@@ -45,6 +45,9 @@ importers: | |||
'@testing-library/react': | |||
specifier: ^13.4.0 | |||
version: 13.4.0(react-dom@18.2.0)(react@18.2.0) | |||
'@testing-library/user-event': | |||
specifier: ^14.4.3 | |||
version: 14.4.3(@testing-library/dom@8.20.1) | |||
'@types/node': | |||
specifier: ^18.14.1 | |||
version: 18.14.1 | |||
@@ -1926,6 +1929,15 @@ packages: | |||
react-dom: 18.2.0(react@18.2.0) | |||
dev: true | |||
/@testing-library/user-event@14.4.3(@testing-library/dom@8.20.1): | |||
resolution: {integrity: sha512-kCUc5MEwaEMakkO5x7aoD+DLi02ehmEM2QCGWvNqAS1dV/fAvORWEjnjsEIvml59M7Y5kCkWN6fCCyPOe8OL6Q==} | |||
engines: {node: '>=12', npm: '>=6'} | |||
peerDependencies: | |||
'@testing-library/dom': '>=7.21.4' | |||
dependencies: | |||
'@testing-library/dom': 8.20.1 | |||
dev: true | |||
/@theoryofnekomata/formxtra@1.0.3: | |||
resolution: {integrity: sha512-xOzE07Slttpx7vbOWqXfatJ+k44TN4zUjI57A5/sNqUDtHzp3pz94A+AVPGVoBY0QXiwzMjeN4DPMp6U1qlkyg==} | |||
engines: {node: '>=10'} | |||
@@ -1,45 +0,0 @@ | |||
import { NextPage } from 'next'; | |||
import { DefaultLayout } from '@/components/DefaultLayout'; | |||
const CodePage: NextPage = () => { | |||
return ( | |||
<DefaultLayout | |||
title="Code" | |||
> | |||
<main className="mt-8 mb-16 md:mt-16 md:mb-32"> | |||
<section> | |||
<div className="container mx-auto px-4"> | |||
<h2> | |||
VerifyCodeInput | |||
</h2> | |||
<div> | |||
TODO | |||
</div> | |||
</div> | |||
</section> | |||
<section> | |||
<div className="container mx-auto px-4"> | |||
<h2> | |||
CodeBlock | |||
</h2> | |||
<div> | |||
TODO | |||
</div> | |||
</div> | |||
</section> | |||
<section> | |||
<div className="container mx-auto px-4"> | |||
<h2> | |||
CodeInput | |||
</h2> | |||
<div> | |||
TODO | |||
</div> | |||
</div> | |||
</section> | |||
</main> | |||
</DefaultLayout> | |||
) | |||
} | |||
export default CodePage; |