The MenuSelect functions to have selected options in a list view. The option demo page now also uses correct components.master
@@ -0,0 +1,69 @@ | |||
# Action | |||
- [X] `ActionButton` | |||
# Color | |||
- [ ] `ColorSlider` | |||
- [ ] `Palette` | |||
- [ ] `Swatch` | |||
# File | |||
- [ ] `FileArea` | |||
- [ ] `FilePicker` | |||
# Formatted | |||
- [ ] `EmailInput` | |||
- [ ] `TelInput` | |||
- [ ] `UrlInput` | |||
# Freeform | |||
- [X] `MaskedTextInput` | |||
- [X] `MultilineTextInput` | |||
- [X] `TextInput` | |||
# Geo | |||
- [ ] `Map` | |||
# Information | |||
- [X] `Badge` | |||
# Navigation | |||
- [ ] `Breadcrumb` | |||
- [X] `LinkButton` | |||
- [ ] `Stepper` | |||
- [ ] `TabPanel` | |||
# Numeric | |||
- [ ] `Knob` | |||
- [ ] `Range2D` | |||
- [ ] `Slider` | |||
- [ ] `Spinner` | |||
# Option | |||
- [X] `DropdownSelect` | |||
- [ ] `MenuSelect` | |||
- [X] `RadioButton` | |||
- [X] `RadioTickBox` | |||
- [ ] `TagInput` | |||
- [X] `ToggleButton` | |||
- [X] `ToggleSwitch` | |||
- [X] `ToggleTickBox` | |||
# Temporal | |||
- [ ] `Calendar` | |||
- [ ] `Clock` | |||
- [ ] `DateDropdown` | |||
- [ ] `DurationInput` | |||
- [ ] `MonthDaySpinner` | |||
- [ ] `TimeSpinner` | |||
- [ ] `YearMonthSpinner` |
@@ -11,6 +11,7 @@ export type CheckControlBaseArgs = { | |||
appearance: CheckControlAppearance, | |||
block: boolean, | |||
type: string, | |||
uncheckedLabel: boolean, | |||
} | |||
export const CheckStateContainer = ({ | |||
@@ -154,6 +155,7 @@ export const CheckIndicatorArea = ({ | |||
compact, | |||
appearance, | |||
type, | |||
uncheckedLabel, | |||
}: CheckControlBaseArgs): string => css.cx( | |||
css` | |||
display: inline-grid; | |||
@@ -219,7 +221,12 @@ export const CheckIndicatorArea = ({ | |||
width: 2.5em; | |||
height: 1.5em; | |||
border-radius: 0.75em; | |||
` | |||
`, | |||
css.if(uncheckedLabel) ( | |||
css.dynamic({ | |||
'margin-left': compact ? '0.375rem' : '0.75rem', | |||
}) | |||
), | |||
), | |||
css.nest('& + *') ( | |||
css.dynamic({ | |||
@@ -256,7 +263,7 @@ export const CheckIndicatorWrapper = ({ | |||
transition-property: margin-left, margin-right; | |||
transition-duration: 150ms; | |||
transition-timing-function: ease-out; | |||
` | |||
`, | |||
), | |||
); | |||
@@ -282,6 +289,7 @@ export const CheckIndicator = ({ | |||
export const ClickAreaWrapper = ({ | |||
block, | |||
appearance, | |||
uncheckedLabel, | |||
}: CheckControlBaseArgs) => css.cx( | |||
css` | |||
vertical-align: middle; | |||
@@ -296,10 +304,12 @@ export const ClickAreaWrapper = ({ | |||
` | |||
), | |||
css.if (appearance === CheckControlAppearance.SWITCH) ( | |||
css` | |||
padding-left: 3.25rem; | |||
text-indent: -3.25rem; | |||
` | |||
css.if (!uncheckedLabel) ( | |||
css` | |||
padding-left: 3.25rem; | |||
text-indent: -3.25rem; | |||
` | |||
), | |||
), | |||
); | |||
@@ -0,0 +1,5 @@ | |||
export interface SelectOption { | |||
label: string, | |||
value?: string | number | readonly string[] | |||
children?: SelectOption[] | |||
} |
@@ -121,7 +121,7 @@ export const LabelWrapper = ({ | |||
position: absolute; | |||
top: 0; | |||
left: 0; | |||
max-width: 100%; | |||
width: 100%; | |||
overflow: hidden; | |||
text-overflow: ellipsis; | |||
white-space: nowrap; | |||
@@ -1,14 +1,9 @@ | |||
import * as React from 'react'; | |||
import * as TextControlBase from '@tesseract-design/web-base-textcontrol'; | |||
export interface SelectOption { | |||
label: string, | |||
value?: string | number | readonly string[] | |||
children?: SelectOption[] | |||
} | |||
import * as SelectControlBase from '@tesseract-design/web-base-selectcontrol'; | |||
type RenderOptionsProps = { | |||
options: SelectOption[], | |||
options: SelectControlBase.SelectOption[], | |||
optionComponent?: React.ElementType, | |||
optgroupComponent?: React.ElementType, | |||
level?: number, | |||
@@ -107,7 +102,7 @@ export type DropdownSelectProps = Omit<React.HTMLProps<HTMLSelectElement>, 'size | |||
/** | |||
* Options available for the component's values. | |||
*/ | |||
options?: SelectOption[], | |||
options?: SelectControlBase.SelectOption[], | |||
} | |||
/** | |||
@@ -0,0 +1,196 @@ | |||
import * as React from 'react'; | |||
import * as TextControlBase from '@tesseract-design/web-base-textcontrol'; | |||
import * as SelectControlBase from '@tesseract-design/web-base-selectcontrol'; | |||
type RenderOptionsProps = { | |||
options: SelectControlBase.SelectOption[], | |||
optionComponent?: React.ElementType, | |||
optgroupComponent?: React.ElementType, | |||
level?: number, | |||
} | |||
const RenderOptions: React.VFC<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 type MenuSelectProps = Omit<React.HTMLProps<HTMLSelectElement>, 'size' | 'style'> & { | |||
/** | |||
* 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, | |||
/** | |||
* Style of the component. | |||
*/ | |||
style?: TextControlBase.TextControlStyle, | |||
/** | |||
* Is the label hidden? | |||
*/ | |||
hiddenLabel?: boolean, | |||
/** | |||
* Options available for the component's values. | |||
*/ | |||
options?: SelectControlBase.SelectOption[], | |||
} | |||
export const MenuSelect = React.forwardRef<HTMLSelectElement, MenuSelectProps>(({ | |||
label = '', | |||
hint = '', | |||
indicator = null, | |||
size = TextControlBase.TextControlSize.MEDIUM, | |||
border = false, | |||
block = false, | |||
style = TextControlBase.TextControlStyle.DEFAULT, | |||
hiddenLabel = false, | |||
options = [], | |||
className: _className, | |||
placeholder: _placeholder, | |||
as: _as, | |||
...etcProps | |||
}: MenuSelectProps, ref) => { | |||
const styleArgs = React.useMemo<TextControlBase.TextControlBaseArgs>(() => ({ | |||
block, | |||
border, | |||
size, | |||
indicator: true, | |||
style, | |||
resizable: true, | |||
predefinedValues: true, | |||
}), [block, border, size, style]); | |||
return ( | |||
<div | |||
className={TextControlBase.Root(styleArgs)} | |||
> | |||
<select | |||
{...etcProps} | |||
className={TextControlBase.Input(styleArgs)} | |||
ref={ref} | |||
aria-label={label} | |||
style={{ | |||
height: TextControlBase.MIN_HEIGHTS[size], | |||
}} | |||
size={2} | |||
data-testid="input" | |||
> | |||
<RenderOptions | |||
options={options} | |||
/> | |||
</select> | |||
{ | |||
border && ( | |||
<span | |||
data-testid="border" | |||
/> | |||
) | |||
} | |||
{ | |||
label && !hiddenLabel && ( | |||
<div | |||
data-testid="label" | |||
className={TextControlBase.LabelWrapper(styleArgs)} | |||
> | |||
{label} | |||
</div> | |||
) | |||
} | |||
{hint && ( | |||
<div | |||
className={TextControlBase.HintWrapper(styleArgs)} | |||
data-testid="hint" | |||
> | |||
<div | |||
className={TextControlBase.Hint()} | |||
> | |||
{hint} | |||
</div> | |||
</div> | |||
)} | |||
{indicator && ( | |||
<div | |||
className={TextControlBase.IndicatorWrapper(styleArgs)} | |||
> | |||
{indicator} | |||
</div> | |||
)} | |||
</div> | |||
); | |||
}); | |||
MenuSelect.displayName = 'MenuSelect'; |
@@ -2,6 +2,14 @@ import * as React from 'react'; | |||
import * as CheckControlBase from '@tesseract-design/web-base-checkcontrol'; | |||
export type ToggleSwitchProps = Omit<React.HTMLProps<HTMLInputElement>, 'size' | 'type' | 'style'> & { | |||
/** | |||
* Label of the component when in the unchecked state. | |||
*/ | |||
uncheckedLabel?: React.ReactNode, | |||
/** | |||
* Label of the component when in the checked state. | |||
*/ | |||
checkedLabel?: React.ReactNode, | |||
/** | |||
* Should the component occupy the whole width of its parent? | |||
*/ | |||
@@ -14,7 +22,6 @@ export type ToggleSwitchProps = Omit<React.HTMLProps<HTMLInputElement>, 'size' | | |||
* Complementary content of the component. | |||
*/ | |||
subtext?: React.ReactNode, | |||
// TODO - opposite children - label used for "off" value | |||
} | |||
/** | |||
@@ -25,12 +32,14 @@ export type ToggleSwitchProps = Omit<React.HTMLProps<HTMLInputElement>, 'size' | | |||
export const ToggleSwitch = React.forwardRef<HTMLInputElement, ToggleSwitchProps>( | |||
( | |||
{ | |||
children, | |||
checkedLabel, | |||
uncheckedLabel, | |||
block = false, | |||
compact = false, | |||
subtext, | |||
className: _className, | |||
as: _as, | |||
children: _children, | |||
...etcProps | |||
}: ToggleSwitchProps, | |||
ref, | |||
@@ -41,7 +50,8 @@ export const ToggleSwitch = React.forwardRef<HTMLInputElement, ToggleSwitchProps | |||
menuItem: false, | |||
appearance: CheckControlBase.CheckControlAppearance.SWITCH, | |||
type: 'checkbox', | |||
}), [block, compact]); | |||
uncheckedLabel: Boolean(uncheckedLabel), | |||
}), [block, compact, uncheckedLabel]); | |||
return ( | |||
<div | |||
@@ -57,7 +67,9 @@ export const ToggleSwitch = React.forwardRef<HTMLInputElement, ToggleSwitchProps | |||
className={CheckControlBase.CheckStateContainer(styleProps)} | |||
/> | |||
<span> | |||
<span /> | |||
<span> | |||
{uncheckedLabel} | |||
</span> | |||
<span | |||
className={CheckControlBase.CheckIndicatorArea(styleProps)} | |||
> | |||
@@ -85,7 +97,7 @@ export const ToggleSwitch = React.forwardRef<HTMLInputElement, ToggleSwitchProps | |||
</span> | |||
</span> | |||
<span> | |||
{children} | |||
{checkedLabel} | |||
{ | |||
subtext | |||
&& ( | |||
@@ -1,4 +1,5 @@ | |||
export * from './components/DropdownSelect'; | |||
export * from './components/MenuSelect'; | |||
export * from './components/RadioButton'; | |||
export * from './components/RadioTickBox'; | |||
export * from './components/ToggleButton'; | |||
@@ -3,6 +3,7 @@ import * as WebOptionReact from '.'; | |||
describe('web-option-react', () => { | |||
it.each([ | |||
'DropdownSelect', | |||
'MenuSelect', | |||
'RadioButton', | |||
'RadioTickBox', | |||
'ToggleButton', | |||
@@ -23,6 +23,7 @@ | |||
"@tesseract-design/web-base-badge": ["./src/modules/base-badge"], | |||
"@tesseract-design/web-base-button": ["./src/modules/base-button"], | |||
"@tesseract-design/web-base-checkcontrol": ["./src/modules/base-checkcontrol"], | |||
"@tesseract-design/web-base-selectcontrol": ["./src/modules/base-selectcontrol"], | |||
"@tesseract-design/web-base-textcontrol": ["./src/modules/base-textcontrol"], | |||
"@tesseract-design/css-utils": ["./src/modules/css-utils"] | |||
} | |||