@@ -11,6 +11,11 @@ export enum TextControlStyle { | |||||
ALTERNATE = 'alternate', | ALTERNATE = 'alternate', | ||||
} | } | ||||
export enum TextControlInputType { | |||||
TEXT = 'text', | |||||
SEARCH = 'search', | |||||
} | |||||
export const MIN_HEIGHTS: Record<TextControlSize, string> = { | export const MIN_HEIGHTS: Record<TextControlSize, string> = { | ||||
[TextControlSize.SMALL]: '2.5rem', | [TextControlSize.SMALL]: '2.5rem', | ||||
[TextControlSize.MEDIUM]: '3rem', | [TextControlSize.MEDIUM]: '3rem', | ||||
@@ -1,12 +1,7 @@ | |||||
import * as React from 'react'; | import * as React from 'react'; | ||||
import * as TextControlBase from '@tesseract-design/web-base-textcontrol'; | 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. | * 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 of the component value. | ||||
*/ | */ | ||||
type?: TextInputType, | |||||
type?: TextControlBase.TextControlInputType, | |||||
/** | /** | ||||
* Style of the component. | * Style of the component. | ||||
*/ | */ | ||||
@@ -59,7 +54,7 @@ export const TextInput = React.forwardRef<HTMLInputElement, TextInputProps>( | |||||
size = TextControlBase.TextControlSize.MEDIUM, | size = TextControlBase.TextControlSize.MEDIUM, | ||||
border = false, | border = false, | ||||
block = false, | block = false, | ||||
type = TextInputType.TEXT, | |||||
type = TextControlBase.TextControlInputType.TEXT, | |||||
style = TextControlBase.TextControlStyle.DEFAULT, | style = TextControlBase.TextControlStyle.DEFAULT, | ||||
hiddenLabel = false, | hiddenLabel = false, | ||||
className: _className, | 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, | separator?: string, | ||||
} | } | ||||
export const TagInput = React.forwardRef<HTMLInputElement, TagInputProps>((( | |||||
export const TagInput = React.forwardRef<HTMLInputElement, TagInputProps>(( | |||||
{ | { | ||||
label = '', | label = '', | ||||
hint = '', | hint = '', | ||||
@@ -322,6 +322,6 @@ export const TagInput = React.forwardRef<HTMLInputElement, TagInputProps>((( | |||||
)} | )} | ||||
</div> | </div> | ||||
); | ); | ||||
})); | |||||
}); | |||||
TagInput.displayName = 'TagInput'; | TagInput.displayName = 'TagInput'; |
@@ -1,3 +1,4 @@ | |||||
export * from './components/ComboBox'; | |||||
export * from './components/DropdownSelect'; | export * from './components/DropdownSelect'; | ||||
export * from './components/MenuSelect'; | export * from './components/MenuSelect'; | ||||
export * from './components/RadioButton'; | export * from './components/RadioButton'; | ||||
@@ -2,6 +2,7 @@ import * as WebOptionReact from '.'; | |||||
describe('web-option-react', () => { | describe('web-option-react', () => { | ||||
it.each([ | it.each([ | ||||
'ComboBox', | |||||
'DropdownSelect', | 'DropdownSelect', | ||||
'MenuSelect', | 'MenuSelect', | ||||
'RadioButton', | 'RadioButton', | ||||
@@ -1,9 +1,10 @@ | |||||
import Head from 'next/head'; | import Head from 'next/head'; | ||||
import { FC } from 'react'; | |||||
import {FC, ReactNode} from 'react'; | |||||
type DefaultLayoutProps = { | type DefaultLayoutProps = { | ||||
title?: string, | title?: string, | ||||
appName?: string, | appName?: string, | ||||
children?: ReactNode, | |||||
} | } | ||||
export const DefaultLayout: FC<DefaultLayoutProps> = ({ | export const DefaultLayout: FC<DefaultLayoutProps> = ({ | ||||
@@ -1414,7 +1414,194 @@ const OptionPage: NextPage<Props> = ({ | |||||
ComboBox | ComboBox | ||||
</h1> | </h1> | ||||
<div> | <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> | ||||
</div> | </div> | ||||
</section> | </section> | ||||