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, | appearance: CheckControlAppearance, | ||||
block: boolean, | block: boolean, | ||||
type: string, | type: string, | ||||
uncheckedLabel: boolean, | |||||
} | } | ||||
export const CheckStateContainer = ({ | export const CheckStateContainer = ({ | ||||
@@ -154,6 +155,7 @@ export const CheckIndicatorArea = ({ | |||||
compact, | compact, | ||||
appearance, | appearance, | ||||
type, | type, | ||||
uncheckedLabel, | |||||
}: CheckControlBaseArgs): string => css.cx( | }: CheckControlBaseArgs): string => css.cx( | ||||
css` | css` | ||||
display: inline-grid; | display: inline-grid; | ||||
@@ -219,7 +221,12 @@ export const CheckIndicatorArea = ({ | |||||
width: 2.5em; | width: 2.5em; | ||||
height: 1.5em; | height: 1.5em; | ||||
border-radius: 0.75em; | border-radius: 0.75em; | ||||
` | |||||
`, | |||||
css.if(uncheckedLabel) ( | |||||
css.dynamic({ | |||||
'margin-left': compact ? '0.375rem' : '0.75rem', | |||||
}) | |||||
), | |||||
), | ), | ||||
css.nest('& + *') ( | css.nest('& + *') ( | ||||
css.dynamic({ | css.dynamic({ | ||||
@@ -256,7 +263,7 @@ export const CheckIndicatorWrapper = ({ | |||||
transition-property: margin-left, margin-right; | transition-property: margin-left, margin-right; | ||||
transition-duration: 150ms; | transition-duration: 150ms; | ||||
transition-timing-function: ease-out; | transition-timing-function: ease-out; | ||||
` | |||||
`, | |||||
), | ), | ||||
); | ); | ||||
@@ -282,6 +289,7 @@ export const CheckIndicator = ({ | |||||
export const ClickAreaWrapper = ({ | export const ClickAreaWrapper = ({ | ||||
block, | block, | ||||
appearance, | appearance, | ||||
uncheckedLabel, | |||||
}: CheckControlBaseArgs) => css.cx( | }: CheckControlBaseArgs) => css.cx( | ||||
css` | css` | ||||
vertical-align: middle; | vertical-align: middle; | ||||
@@ -296,10 +304,12 @@ export const ClickAreaWrapper = ({ | |||||
` | ` | ||||
), | ), | ||||
css.if (appearance === CheckControlAppearance.SWITCH) ( | 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; | position: absolute; | ||||
top: 0; | top: 0; | ||||
left: 0; | left: 0; | ||||
max-width: 100%; | |||||
width: 100%; | |||||
overflow: hidden; | overflow: hidden; | ||||
text-overflow: ellipsis; | text-overflow: ellipsis; | ||||
white-space: nowrap; | white-space: nowrap; | ||||
@@ -1,14 +1,9 @@ | |||||
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 interface SelectOption { | |||||
label: string, | |||||
value?: string | number | readonly string[] | |||||
children?: SelectOption[] | |||||
} | |||||
import * as SelectControlBase from '@tesseract-design/web-base-selectcontrol'; | |||||
type RenderOptionsProps = { | type RenderOptionsProps = { | ||||
options: SelectOption[], | |||||
options: SelectControlBase.SelectOption[], | |||||
optionComponent?: React.ElementType, | optionComponent?: React.ElementType, | ||||
optgroupComponent?: React.ElementType, | optgroupComponent?: React.ElementType, | ||||
level?: number, | level?: number, | ||||
@@ -107,7 +102,7 @@ export type DropdownSelectProps = Omit<React.HTMLProps<HTMLSelectElement>, 'size | |||||
/** | /** | ||||
* Options available for the component's values. | * 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'; | import * as CheckControlBase from '@tesseract-design/web-base-checkcontrol'; | ||||
export type ToggleSwitchProps = Omit<React.HTMLProps<HTMLInputElement>, 'size' | 'type' | 'style'> & { | 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? | * 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. | * Complementary content of the component. | ||||
*/ | */ | ||||
subtext?: React.ReactNode, | 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>( | export const ToggleSwitch = React.forwardRef<HTMLInputElement, ToggleSwitchProps>( | ||||
( | ( | ||||
{ | { | ||||
children, | |||||
checkedLabel, | |||||
uncheckedLabel, | |||||
block = false, | block = false, | ||||
compact = false, | compact = false, | ||||
subtext, | subtext, | ||||
className: _className, | className: _className, | ||||
as: _as, | as: _as, | ||||
children: _children, | |||||
...etcProps | ...etcProps | ||||
}: ToggleSwitchProps, | }: ToggleSwitchProps, | ||||
ref, | ref, | ||||
@@ -41,7 +50,8 @@ export const ToggleSwitch = React.forwardRef<HTMLInputElement, ToggleSwitchProps | |||||
menuItem: false, | menuItem: false, | ||||
appearance: CheckControlBase.CheckControlAppearance.SWITCH, | appearance: CheckControlBase.CheckControlAppearance.SWITCH, | ||||
type: 'checkbox', | type: 'checkbox', | ||||
}), [block, compact]); | |||||
uncheckedLabel: Boolean(uncheckedLabel), | |||||
}), [block, compact, uncheckedLabel]); | |||||
return ( | return ( | ||||
<div | <div | ||||
@@ -57,7 +67,9 @@ export const ToggleSwitch = React.forwardRef<HTMLInputElement, ToggleSwitchProps | |||||
className={CheckControlBase.CheckStateContainer(styleProps)} | className={CheckControlBase.CheckStateContainer(styleProps)} | ||||
/> | /> | ||||
<span> | <span> | ||||
<span /> | |||||
<span> | |||||
{uncheckedLabel} | |||||
</span> | |||||
<span | <span | ||||
className={CheckControlBase.CheckIndicatorArea(styleProps)} | className={CheckControlBase.CheckIndicatorArea(styleProps)} | ||||
> | > | ||||
@@ -85,7 +97,7 @@ export const ToggleSwitch = React.forwardRef<HTMLInputElement, ToggleSwitchProps | |||||
</span> | </span> | ||||
</span> | </span> | ||||
<span> | <span> | ||||
{children} | |||||
{checkedLabel} | |||||
{ | { | ||||
subtext | subtext | ||||
&& ( | && ( | ||||
@@ -1,4 +1,5 @@ | |||||
export * from './components/DropdownSelect'; | export * from './components/DropdownSelect'; | ||||
export * from './components/MenuSelect'; | |||||
export * from './components/RadioButton'; | export * from './components/RadioButton'; | ||||
export * from './components/RadioTickBox'; | export * from './components/RadioTickBox'; | ||||
export * from './components/ToggleButton'; | export * from './components/ToggleButton'; | ||||
@@ -3,6 +3,7 @@ import * as WebOptionReact from '.'; | |||||
describe('web-option-react', () => { | describe('web-option-react', () => { | ||||
it.each([ | it.each([ | ||||
'DropdownSelect', | 'DropdownSelect', | ||||
'MenuSelect', | |||||
'RadioButton', | 'RadioButton', | ||||
'RadioTickBox', | 'RadioTickBox', | ||||
'ToggleButton', | 'ToggleButton', | ||||
@@ -23,6 +23,7 @@ | |||||
"@tesseract-design/web-base-badge": ["./src/modules/base-badge"], | "@tesseract-design/web-base-badge": ["./src/modules/base-badge"], | ||||
"@tesseract-design/web-base-button": ["./src/modules/base-button"], | "@tesseract-design/web-base-button": ["./src/modules/base-button"], | ||||
"@tesseract-design/web-base-checkcontrol": ["./src/modules/base-checkcontrol"], | "@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/web-base-textcontrol": ["./src/modules/base-textcontrol"], | ||||
"@tesseract-design/css-utils": ["./src/modules/css-utils"] | "@tesseract-design/css-utils": ["./src/modules/css-utils"] | ||||
} | } | ||||