Combo box is just ordinary input with options.pull/1/head
@@ -11,6 +11,11 @@ export enum TextControlStyle { | |||
ALTERNATE = 'alternate', | |||
} | |||
export enum TextControlInputType { | |||
TEXT = 'text', | |||
SEARCH = 'search', | |||
} | |||
export const MIN_HEIGHTS: Record<TextControlSize, string> = { | |||
[TextControlSize.SMALL]: '2.5rem', | |||
[TextControlSize.MEDIUM]: '3rem', | |||
@@ -1,12 +1,7 @@ | |||
import * as React from 'react'; | |||
import * as TextControlBase from '@tesseract-design/web-base-textcontrol'; | |||
export enum TextInputType { | |||
TEXT = 'text', | |||
SEARCH = 'search', | |||
} | |||
export interface TextInputProps extends Omit<React.HTMLProps<HTMLInputElement>, 'size' | 'type' | 'style' | 'label'> { | |||
export interface TextInputProps extends Omit<React.HTMLProps<HTMLInputElement>, 'size' | 'type' | 'style' | 'label' | 'list'> { | |||
/** | |||
* Short textual description indicating the nature of the component's value. | |||
*/ | |||
@@ -34,7 +29,7 @@ export interface TextInputProps extends Omit<React.HTMLProps<HTMLInputElement>, | |||
/** | |||
* Type of the component value. | |||
*/ | |||
type?: TextInputType, | |||
type?: TextControlBase.TextControlInputType, | |||
/** | |||
* Style of the component. | |||
*/ | |||
@@ -59,7 +54,7 @@ export const TextInput = React.forwardRef<HTMLInputElement, TextInputProps>( | |||
size = TextControlBase.TextControlSize.MEDIUM, | |||
border = false, | |||
block = false, | |||
type = TextInputType.TEXT, | |||
type = TextControlBase.TextControlInputType.TEXT, | |||
style = TextControlBase.TextControlStyle.DEFAULT, | |||
hiddenLabel = false, | |||
className: _className, | |||
@@ -0,0 +1,205 @@ | |||
import * as React from 'react'; | |||
import * as TextControlBase from '@tesseract-design/web-base-textcontrol'; | |||
import * as SelectControlBase from '@tesseract-design/web-base-selectcontrol'; | |||
import {useId} from 'react'; | |||
export interface ComboBoxProps extends Omit<React.HTMLProps<HTMLInputElement>, 'size' | 'type' | 'style' | 'label' | 'list'> { | |||
/** | |||
* Short textual description indicating the nature of the component's value. | |||
*/ | |||
label?: React.ReactNode, | |||
/** | |||
* Short textual description as guidelines for valid input values. | |||
*/ | |||
hint?: React.ReactNode, | |||
/** | |||
* Size of the component. | |||
*/ | |||
size?: TextControlBase.TextControlSize, | |||
/** | |||
* Additional description, usually graphical, indicating the nature of the component's value. | |||
*/ | |||
indicator?: React.ReactNode, | |||
/** | |||
* Should the component display a border? | |||
*/ | |||
border?: boolean, | |||
/** | |||
* Should the component occupy the whole width of its parent? | |||
*/ | |||
block?: boolean, | |||
/** | |||
* Type of the component value. | |||
*/ | |||
type?: TextControlBase.TextControlInputType, | |||
/** | |||
* Style of the component. | |||
*/ | |||
style?: TextControlBase.TextControlStyle, | |||
/** | |||
* Is the label hidden? | |||
*/ | |||
hiddenLabel?: boolean, | |||
/** | |||
* Options available for the component's values. | |||
*/ | |||
options?: SelectControlBase.SelectOption[], | |||
} | |||
interface RenderOptionsProps { | |||
options: SelectControlBase.SelectOption[], | |||
optionComponent?: React.ElementType, | |||
optgroupComponent?: React.ElementType, | |||
level?: number, | |||
} | |||
const RenderOptions: React.FC<RenderOptionsProps> = ({ | |||
options, | |||
optionComponent: Option = 'option', | |||
optgroupComponent: Optgroup = 'optgroup', | |||
level = 0, | |||
}: RenderOptionsProps) => ( | |||
<> | |||
{ | |||
options.map((o) => { | |||
if (typeof o.value !== 'undefined') { | |||
return ( | |||
<Option | |||
key={`${o.label}:${o.value.toString()}`} | |||
value={o.value} | |||
> | |||
{o.label} | |||
</Option> | |||
); | |||
} | |||
if (typeof o.children !== 'undefined') { | |||
if (level === 0) { | |||
return ( | |||
<Optgroup | |||
key={o.label} | |||
label={o.label} | |||
> | |||
<RenderOptions | |||
options={o.children} | |||
optionComponent={Option} | |||
optgroupComponent={Optgroup} | |||
level={level + 1} | |||
/> | |||
</Optgroup> | |||
); | |||
} | |||
return ( | |||
<React.Fragment | |||
key={o.label} | |||
> | |||
<Option | |||
disabled | |||
> | |||
{o.label} | |||
</Option> | |||
<RenderOptions | |||
options={o.children} | |||
optionComponent={Option} | |||
optgroupComponent={Optgroup} | |||
level={level + 1} | |||
/> | |||
</React.Fragment> | |||
); | |||
} | |||
return null; | |||
}) | |||
} | |||
</> | |||
); | |||
export const ComboBox = React.forwardRef<HTMLInputElement, ComboBoxProps>(( | |||
{ | |||
label = '', | |||
hint = '', | |||
indicator = null, | |||
size = TextControlBase.TextControlSize.MEDIUM, | |||
border = false, | |||
block = false, | |||
type = TextControlBase.TextControlInputType.TEXT, | |||
style = TextControlBase.TextControlStyle.DEFAULT, | |||
hiddenLabel = false, | |||
className: _className, | |||
placeholder: _placeholder, | |||
as: _as, | |||
options = [], | |||
...etcProps | |||
}: ComboBoxProps, | |||
ref, | |||
) => { | |||
const datalistId = useId(); | |||
const textInputBaseArgs: TextControlBase.TextControlBaseArgs = { | |||
block, | |||
border, | |||
size, | |||
indicator: Boolean(indicator), | |||
style, | |||
resizable: false, | |||
predefinedValues: false, | |||
}; | |||
return ( | |||
<> | |||
<datalist id={datalistId}> | |||
<RenderOptions options={options} /> | |||
</datalist> | |||
<div | |||
className={TextControlBase.Root(textInputBaseArgs)} | |||
> | |||
<input | |||
{...etcProps} | |||
className={TextControlBase.Input(textInputBaseArgs)} | |||
ref={ref} | |||
aria-label={label?.toString()} | |||
type={type} | |||
data-testid="input" | |||
list={datalistId} | |||
/> | |||
{ | |||
border && ( | |||
<span | |||
data-testid="border" | |||
/> | |||
) | |||
} | |||
{ | |||
label && !hiddenLabel && ( | |||
<div | |||
data-testid="label" | |||
className={TextControlBase.LabelWrapper(textInputBaseArgs)} | |||
> | |||
{label} | |||
</div> | |||
) | |||
} | |||
{hint && ( | |||
<div | |||
className={TextControlBase.HintWrapper(textInputBaseArgs)} | |||
data-testid="hint" | |||
> | |||
<div | |||
className={TextControlBase.Hint()} | |||
> | |||
{hint} | |||
</div> | |||
</div> | |||
)} | |||
{indicator && ( | |||
<div | |||
className={TextControlBase.IndicatorWrapper(textInputBaseArgs)} | |||
> | |||
{indicator} | |||
</div> | |||
)} | |||
</div> | |||
</> | |||
); | |||
}); | |||
ComboBox.displayName = 'ComboBox'; |
@@ -39,7 +39,7 @@ export type TagInputProps = Omit<React.HTMLProps<HTMLInputElement>, 'size' | 'st | |||
separator?: string, | |||
} | |||
export const TagInput = React.forwardRef<HTMLInputElement, TagInputProps>((( | |||
export const TagInput = React.forwardRef<HTMLInputElement, TagInputProps>(( | |||
{ | |||
label = '', | |||
hint = '', | |||
@@ -322,6 +322,6 @@ export const TagInput = React.forwardRef<HTMLInputElement, TagInputProps>((( | |||
)} | |||
</div> | |||
); | |||
})); | |||
}); | |||
TagInput.displayName = 'TagInput'; |
@@ -1,3 +1,4 @@ | |||
export * from './components/ComboBox'; | |||
export * from './components/DropdownSelect'; | |||
export * from './components/MenuSelect'; | |||
export * from './components/RadioButton'; | |||
@@ -2,6 +2,7 @@ import * as WebOptionReact from '.'; | |||
describe('web-option-react', () => { | |||
it.each([ | |||
'ComboBox', | |||
'DropdownSelect', | |||
'MenuSelect', | |||
'RadioButton', | |||
@@ -1,9 +1,10 @@ | |||
import Head from 'next/head'; | |||
import { FC } from 'react'; | |||
import {FC, ReactNode} from 'react'; | |||
type DefaultLayoutProps = { | |||
title?: string, | |||
appName?: string, | |||
children?: ReactNode, | |||
} | |||
export const DefaultLayout: FC<DefaultLayoutProps> = ({ | |||
@@ -1414,7 +1414,194 @@ const OptionPage: NextPage<Props> = ({ | |||
ComboBox | |||
</h1> | |||
<div> | |||
TODO input with datalist | |||
<section> | |||
<h2> | |||
Default | |||
</h2> | |||
<div> | |||
<div className="grid md:grid-cols-2 gap-4"> | |||
<div> | |||
<Option.ComboBox | |||
size={TextControlSize.SMALL} | |||
label="MultilineTextInput" | |||
hint="Type anything here…" | |||
indicator="A" | |||
block | |||
options={options} | |||
/> | |||
</div> | |||
<div> | |||
<Option.ComboBox | |||
border | |||
size={TextControlSize.SMALL} | |||
label="MultilineTextInput" | |||
hint="Type anything here…" | |||
indicator="A" | |||
block | |||
options={options} | |||
/> | |||
</div> | |||
<div> | |||
<Option.ComboBox | |||
label="MultilineTextInput" | |||
hint="Type anything here…" | |||
indicator="A" | |||
block | |||
options={options} | |||
/> | |||
</div> | |||
<div> | |||
<Option.ComboBox | |||
border | |||
label="MultilineTextInput" | |||
hint="Type anything here…" | |||
indicator="A" | |||
block | |||
options={options} | |||
/> | |||
</div> | |||
<div> | |||
<Option.ComboBox | |||
size={TextControlSize.LARGE} | |||
label="MultilineTextInput" | |||
hint="Type anything here…" | |||
indicator="A" | |||
block | |||
options={options} | |||
/> | |||
</div> | |||
<div> | |||
<Option.ComboBox | |||
border | |||
size={TextControlSize.LARGE} | |||
label="MultilineTextInput" | |||
hint="Type anything here…" | |||
indicator="A" | |||
block | |||
options={options} | |||
/> | |||
</div> | |||
<div> | |||
<Option.ComboBox | |||
label="MultilineTextInput" | |||
hint="Type anything here…" | |||
indicator="A" | |||
block | |||
disabled | |||
options={options} | |||
/> | |||
</div> | |||
<div> | |||
<Option.ComboBox | |||
border | |||
label="MultilineTextInput" | |||
hint="Type anything here…" | |||
indicator="A" | |||
block | |||
disabled | |||
options={options} | |||
/> | |||
</div> | |||
</div> | |||
</div> | |||
</section> | |||
<section> | |||
<h2>Alternate</h2> | |||
<div> | |||
<div className="grid md:grid-cols-2 gap-4"> | |||
<div> | |||
<Option.ComboBox | |||
style={TextControlStyle.ALTERNATE} | |||
size={TextControlSize.SMALL} | |||
label="MultilineTextInput" | |||
hint="Type anything here…" | |||
indicator="A" | |||
block | |||
options={options} | |||
/> | |||
</div> | |||
<div> | |||
<Option.ComboBox | |||
border | |||
style={TextControlStyle.ALTERNATE} | |||
size={TextControlSize.SMALL} | |||
label="MultilineTextInput" | |||
hint="Type anything here…" | |||
indicator="A" | |||
block | |||
options={options} | |||
/> | |||
</div> | |||
<div> | |||
<Option.ComboBox | |||
style={TextControlStyle.ALTERNATE} | |||
label="MultilineTextInput" | |||
hint="Type anything here…" | |||
indicator="A" | |||
block | |||
options={options} | |||
/> | |||
</div> | |||
<div> | |||
<Option.ComboBox | |||
border | |||
style={TextControlStyle.ALTERNATE} | |||
label="MultilineTextInput" | |||
hint="Type anything here…" | |||
indicator="A" | |||
block | |||
options={options} | |||
/> | |||
</div> | |||
<div> | |||
<Option.ComboBox | |||
style={TextControlStyle.ALTERNATE} | |||
size={TextControlSize.LARGE} | |||
label="MultilineTextInput" | |||
hint="Type anything here…" | |||
indicator="A" | |||
block | |||
options={options} | |||
/> | |||
</div> | |||
<div> | |||
<Option.ComboBox | |||
border | |||
block | |||
style={TextControlStyle.ALTERNATE} | |||
size={TextControlSize.LARGE} | |||
label="MultilineTextInput" | |||
hint="Type anything here…" | |||
indicator="A" | |||
options={options} | |||
/> | |||
</div> | |||
<div> | |||
<Option.ComboBox | |||
style={TextControlStyle.ALTERNATE} | |||
label="MultilineTextInput" | |||
hint="Type anything here…" | |||
indicator="A" | |||
block | |||
disabled | |||
options={options} | |||
/> | |||
</div> | |||
<div> | |||
<Option.ComboBox | |||
style={TextControlStyle.ALTERNATE} | |||
border | |||
label="MultilineTextInput" | |||
hint="Type anything here…" | |||
indicator="A" | |||
block | |||
disabled | |||
options={options} | |||
/> | |||
</div> | |||
</div> | |||
</div> | |||
</section> | |||
</div> | |||
</div> | |||
</section> | |||