|
|
@@ -1,8 +1,5 @@ |
|
|
|
import * as React from 'react' |
|
|
|
import * as PropTypes from 'prop-types' |
|
|
|
import styled from 'styled-components' |
|
|
|
import stringify from '../../utilities/stringify' |
|
|
|
import { Size } from '../../utilities/utilities' |
|
|
|
|
|
|
|
// TODO implement web-client text inputs! |
|
|
|
|
|
|
@@ -21,7 +18,7 @@ const MIN_HEIGHTS: Record<MultilineTextInputSize, string | number> = { |
|
|
|
const LABEL_VERTICAL_PADDING_SIZES: Record<MultilineTextInputSize, string | number> = { |
|
|
|
[MultilineTextInputSize.SMALL]: '0.125rem', |
|
|
|
[MultilineTextInputSize.MEDIUM]: '0.25rem', |
|
|
|
[MultilineTextInputSize.LARGE]: '0.5rem', |
|
|
|
[MultilineTextInputSize.LARGE]: '0.375rem', |
|
|
|
} |
|
|
|
|
|
|
|
const VERTICAL_PADDING_SIZES: Record<MultilineTextInputSize, string | number> = { |
|
|
@@ -43,6 +40,7 @@ const SECONDARY_TEXT_SIZES: Record<MultilineTextInputSize, string | number> = { |
|
|
|
} |
|
|
|
|
|
|
|
const ComponentBase = styled('div')({ |
|
|
|
verticalAlign: 'middle', |
|
|
|
position: 'relative', |
|
|
|
borderRadius: '0.25rem', |
|
|
|
fontFamily: 'var(--font-family-base, sans-serif)', |
|
|
@@ -58,6 +56,7 @@ const CaptureArea = styled('label')({ |
|
|
|
display: 'block', |
|
|
|
borderRadius: 'inherit', |
|
|
|
overflow: 'hidden', |
|
|
|
boxSizing: 'border-box', |
|
|
|
}) |
|
|
|
|
|
|
|
CaptureArea.displayName = 'label' |
|
|
@@ -70,6 +69,7 @@ const LabelWrapper = styled('span')({ |
|
|
|
left: 0, |
|
|
|
fontSize: '0.85em', |
|
|
|
maxWidth: '100%', |
|
|
|
width: '100%', |
|
|
|
overflow: 'hidden', |
|
|
|
textOverflow: 'ellipsis', |
|
|
|
whiteSpace: 'nowrap', |
|
|
@@ -95,7 +95,7 @@ const Border = styled('span')({ |
|
|
|
width: '100%', |
|
|
|
height: '100%', |
|
|
|
borderRadius: 'inherit', |
|
|
|
zIndex: 2, |
|
|
|
zIndex: 3, |
|
|
|
pointerEvents: 'none', |
|
|
|
transitionProperty: 'border-color', |
|
|
|
'::before': { |
|
|
@@ -150,22 +150,24 @@ const TextArea = styled('textarea')({ |
|
|
|
display: 'block', |
|
|
|
verticalAlign: 'top', |
|
|
|
width: '100%', |
|
|
|
height: '100%', |
|
|
|
boxSizing: 'border-box', |
|
|
|
position: 'relative', |
|
|
|
border: 0, |
|
|
|
borderRadius: 'inherit', |
|
|
|
paddingTop: 0, |
|
|
|
paddingBottom: 0, |
|
|
|
margin: 0, |
|
|
|
font: 'inherit', |
|
|
|
minHeight: '4rem', |
|
|
|
minWidth: '3rem', |
|
|
|
maxWidth: '100%', |
|
|
|
zIndex: 1, |
|
|
|
transitionProperty: 'background-color, color', |
|
|
|
resize: 'none', |
|
|
|
':focus': { |
|
|
|
outline: 0, |
|
|
|
}, |
|
|
|
'@media only screen': { |
|
|
|
backgroundColor: 'var(--color-bg, white)', |
|
|
|
color: 'var(--color-fg, black)', |
|
|
|
}, |
|
|
|
}) |
|
|
@@ -177,15 +179,19 @@ const HintWrapper = styled('span')({ |
|
|
|
position: 'absolute', |
|
|
|
left: 0, |
|
|
|
fontSize: '0.85em', |
|
|
|
opacity: 0.5, |
|
|
|
maxWidth: '100%', |
|
|
|
overflow: 'hidden', |
|
|
|
textOverflow: 'ellipsis', |
|
|
|
whiteSpace: 'nowrap', |
|
|
|
zIndex: 2, |
|
|
|
pointerEvents: 'none', |
|
|
|
lineHeight: 1, |
|
|
|
userSelect: 'none', |
|
|
|
width: '100%', |
|
|
|
}) |
|
|
|
|
|
|
|
const HintContent = styled('span')({ |
|
|
|
display: 'block', |
|
|
|
opacity: 0.5, |
|
|
|
}) |
|
|
|
|
|
|
|
const IndicatorWrapper = styled('span')({ |
|
|
@@ -204,84 +210,43 @@ const IndicatorWrapper = styled('span')({ |
|
|
|
userSelect: 'none', |
|
|
|
}) |
|
|
|
|
|
|
|
const propTypes = { |
|
|
|
export type Props = Omit<React.HTMLProps<HTMLTextAreaElement>, 'as' | 'className' | 'style' | 'placeholder' | 'size'> & { |
|
|
|
/** |
|
|
|
* Short textual description indicating the nature of the component's value. |
|
|
|
*/ |
|
|
|
label: PropTypes.any, |
|
|
|
label?: React.ReactNode, |
|
|
|
/** |
|
|
|
* Short textual description as guidelines for valid input values. |
|
|
|
*/ |
|
|
|
hint: PropTypes.any, |
|
|
|
hint?: React.ReactNode, |
|
|
|
/** |
|
|
|
* Size of the component. |
|
|
|
*/ |
|
|
|
size: PropTypes.oneOf<Size>(['small', 'medium', 'large']), |
|
|
|
size?: MultilineTextInputSize, |
|
|
|
/** |
|
|
|
* Additional description, usually graphical, indicating the nature of the component's value. |
|
|
|
*/ |
|
|
|
indicator: PropTypes.node, |
|
|
|
/** |
|
|
|
* Should the component accept multiple lines of input? |
|
|
|
*/ |
|
|
|
multiline: PropTypes.bool, |
|
|
|
/** |
|
|
|
* Is the component active? |
|
|
|
*/ |
|
|
|
disabled: PropTypes.bool, |
|
|
|
/** |
|
|
|
* Should the component resize itself to show all its value? |
|
|
|
*/ |
|
|
|
autoResize: PropTypes.bool, |
|
|
|
/** |
|
|
|
* Placeholder of the component when there is no value. |
|
|
|
*/ |
|
|
|
placeholder: PropTypes.string, |
|
|
|
/** |
|
|
|
* How many rows should the component display if it accepts multiline input? |
|
|
|
*/ |
|
|
|
rows: PropTypes.number, |
|
|
|
/** |
|
|
|
* Does the button display a border? |
|
|
|
*/ |
|
|
|
border: PropTypes.bool, |
|
|
|
/** |
|
|
|
* Event handler triggered when the component changes value. |
|
|
|
*/ |
|
|
|
onChange: PropTypes.func, |
|
|
|
/** |
|
|
|
* Event handler triggered when the component receives focus. |
|
|
|
*/ |
|
|
|
onFocus: PropTypes.func, |
|
|
|
indicator?: React.ReactNode, |
|
|
|
/** |
|
|
|
* Event handler triggered when the component loses focus. |
|
|
|
* Should the component display a border? |
|
|
|
*/ |
|
|
|
onBlur: PropTypes.func, |
|
|
|
border?: boolean, |
|
|
|
/** |
|
|
|
* Should the component be displayed with an alternate appearance? |
|
|
|
*/ |
|
|
|
alternate: PropTypes.bool, |
|
|
|
alternate?: boolean, |
|
|
|
/** |
|
|
|
* Default value of the component. |
|
|
|
* Should the component occupy the whole width of its parent? |
|
|
|
*/ |
|
|
|
defaultValue: PropTypes.any, |
|
|
|
block?: boolean, |
|
|
|
/** |
|
|
|
* Value of the component. |
|
|
|
* How many rows should the component display if it accepts multiline input? |
|
|
|
*/ |
|
|
|
value: PropTypes.any, |
|
|
|
rows?: number, |
|
|
|
/** |
|
|
|
* Name of the form field associated with this component. |
|
|
|
* Should the component resize itself to show all its value? |
|
|
|
*/ |
|
|
|
name: PropTypes.string, |
|
|
|
} |
|
|
|
|
|
|
|
type Props = { |
|
|
|
label?: React.ReactNode, |
|
|
|
hint?: React.ReactNode, |
|
|
|
size?: MultilineTextInputSize, |
|
|
|
indicator?: React.ReactNode, |
|
|
|
border?: boolean, |
|
|
|
alternate?: boolean, |
|
|
|
autoResize?: boolean, |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
@@ -289,7 +254,7 @@ type Props = { |
|
|
|
* |
|
|
|
* This component supports multiline input and adjusts its layout accordingly. |
|
|
|
*/ |
|
|
|
const MultilineTextInput = React.forwardRef<HTMLTextAreaElement, Props>( |
|
|
|
export const MultilineTextInput = React.forwardRef<HTMLTextAreaElement, Props>( |
|
|
|
( |
|
|
|
{ |
|
|
|
label = '', |
|
|
@@ -297,8 +262,6 @@ const MultilineTextInput = React.forwardRef<HTMLTextAreaElement, Props>( |
|
|
|
indicator = null, |
|
|
|
size = MultilineTextInputSize.MEDIUM, |
|
|
|
disabled = false, |
|
|
|
autoResize = false, |
|
|
|
placeholder = '', |
|
|
|
rows = 3, |
|
|
|
border = false, |
|
|
|
onChange, |
|
|
@@ -308,67 +271,79 @@ const MultilineTextInput = React.forwardRef<HTMLTextAreaElement, Props>( |
|
|
|
defaultValue, |
|
|
|
value, |
|
|
|
name, |
|
|
|
block = false, |
|
|
|
...etcProps |
|
|
|
}, |
|
|
|
ref, |
|
|
|
) => ( |
|
|
|
<ComponentBase |
|
|
|
style={{ |
|
|
|
display: block ? 'block' : 'inline-block', |
|
|
|
opacity: disabled ? 0.5 : undefined, |
|
|
|
}} |
|
|
|
> |
|
|
|
{border && <Border />} |
|
|
|
<CaptureArea> |
|
|
|
<LabelWrapper |
|
|
|
style={{ |
|
|
|
paddingLeft: alternate ? '0.5rem' : undefined, |
|
|
|
paddingTop: LABEL_VERTICAL_PADDING_SIZES[size!], |
|
|
|
paddingBottom: LABEL_VERTICAL_PADDING_SIZES[size!], |
|
|
|
paddingRight: indicator ? MIN_HEIGHTS[size!] : '0.5rem', |
|
|
|
fontSize: SECONDARY_TEXT_SIZES[size!], |
|
|
|
}} |
|
|
|
> |
|
|
|
{stringify(label)} |
|
|
|
</LabelWrapper> |
|
|
|
{stringify(label).length > 0 && ' '} |
|
|
|
<CaptureArea |
|
|
|
style={{ |
|
|
|
paddingTop: alternate ? `calc(${SECONDARY_TEXT_SIZES[size!]} * 2)` : `calc(${SECONDARY_TEXT_SIZES[size!]} * 1.25)`, |
|
|
|
paddingBottom: alternate ? '0.375rem' : VERTICAL_PADDING_SIZES[size!], |
|
|
|
height: `calc(${MIN_HEIGHTS[size!]} * ${rows})`, |
|
|
|
minHeight: MIN_HEIGHTS[size!], |
|
|
|
resize: block ? 'vertical' : 'both', |
|
|
|
}} |
|
|
|
> |
|
|
|
{label && ( |
|
|
|
<> |
|
|
|
<LabelWrapper |
|
|
|
style={{ |
|
|
|
paddingLeft: alternate ? (border ? '0.5rem' : undefined) : '0.5rem', |
|
|
|
paddingTop: alternate ? `calc(${LABEL_VERTICAL_PADDING_SIZES[size!]} * 0.25)` : LABEL_VERTICAL_PADDING_SIZES[size!], |
|
|
|
paddingBottom: alternate ? LABEL_VERTICAL_PADDING_SIZES[size!] : undefined, |
|
|
|
paddingRight: indicator ? MIN_HEIGHTS[size!] : (alternate ? (border ? '0.5rem' : undefined) : '0.5rem'), |
|
|
|
fontSize: SECONDARY_TEXT_SIZES[size!], |
|
|
|
}} |
|
|
|
> |
|
|
|
{label} |
|
|
|
</LabelWrapper> |
|
|
|
{' '} |
|
|
|
</> |
|
|
|
)} |
|
|
|
<TextArea |
|
|
|
onChange={onChange as React.ChangeEventHandler} |
|
|
|
onFocus={onFocus as React.FocusEventHandler} |
|
|
|
onBlur={onBlur as React.FocusEventHandler} |
|
|
|
placeholder={placeholder!} |
|
|
|
{...etcProps} |
|
|
|
ref={ref as React.Ref<HTMLTextAreaElement>} |
|
|
|
disabled={disabled!} |
|
|
|
rows={rows!} |
|
|
|
disabled={disabled} |
|
|
|
style={{ |
|
|
|
height: `calc(${MIN_HEIGHTS[size!]} * ${rows})`, |
|
|
|
paddingLeft: alternate ? (border ? '0.5rem' : undefined) : '1rem', |
|
|
|
fontSize: INPUT_FONT_SIZES[size!], |
|
|
|
minHeight: MIN_HEIGHTS[size!], |
|
|
|
paddingTop: alternate ? VERTICAL_PADDING_SIZES[size!] : `calc(${SECONDARY_TEXT_SIZES[size!]} * 2)`, |
|
|
|
paddingBottom: VERTICAL_PADDING_SIZES[size!], |
|
|
|
paddingRight: indicator ? MIN_HEIGHTS[size!] : (alternate ? '1rem' : undefined), |
|
|
|
paddingLeft: alternate ? '1rem' : undefined, |
|
|
|
paddingRight: indicator ? MIN_HEIGHTS[size!] : (alternate ? (border ? '0.5rem' : undefined) : '1rem'), |
|
|
|
}} |
|
|
|
defaultValue={defaultValue} |
|
|
|
value={value} |
|
|
|
name={name} |
|
|
|
/> |
|
|
|
</CaptureArea> |
|
|
|
{stringify(hint).length > 0 && ' '} |
|
|
|
{stringify(hint).length > 0 && ( |
|
|
|
<HintWrapper |
|
|
|
style={{ |
|
|
|
top: alternate ? undefined : '0.75rem', |
|
|
|
bottom: alternate ? 0 : undefined, |
|
|
|
paddingLeft: alternate ? '1rem' : undefined, |
|
|
|
paddingTop: LABEL_VERTICAL_PADDING_SIZES[size!], |
|
|
|
paddingBottom: LABEL_VERTICAL_PADDING_SIZES[size!], |
|
|
|
paddingRight: indicator ? MIN_HEIGHTS[size!] : '1rem', |
|
|
|
fontSize: SECONDARY_TEXT_SIZES[size!], |
|
|
|
}} |
|
|
|
> |
|
|
|
{stringify(hint)} |
|
|
|
</HintWrapper> |
|
|
|
{hint && ( |
|
|
|
<> |
|
|
|
{' '} |
|
|
|
<HintWrapper |
|
|
|
style={{ |
|
|
|
top: alternate ? `calc(${SECONDARY_TEXT_SIZES[size!]} * 0.75)` : undefined, |
|
|
|
bottom: alternate ? undefined : 0, |
|
|
|
paddingLeft: alternate ? (border ? '0.5rem' : undefined) : '1rem', |
|
|
|
paddingTop: alternate ? undefined : LABEL_VERTICAL_PADDING_SIZES[size!], |
|
|
|
paddingBottom:alternate ? undefined : LABEL_VERTICAL_PADDING_SIZES[size!], |
|
|
|
paddingRight: indicator ? MIN_HEIGHTS[size!] : (alternate ? (border ? '0.5rem' : undefined) : '1rem'), |
|
|
|
fontSize: SECONDARY_TEXT_SIZES[size!], |
|
|
|
lineHeight: alternate ? 1.5 : 1, |
|
|
|
}} |
|
|
|
> |
|
|
|
<HintContent> |
|
|
|
{hint} |
|
|
|
</HintContent> |
|
|
|
</HintWrapper> |
|
|
|
</> |
|
|
|
)} |
|
|
|
{(indicator as PropTypes.ReactComponentLike) && ( |
|
|
|
{indicator && ( |
|
|
|
<IndicatorWrapper |
|
|
|
style={{ |
|
|
|
width: MIN_HEIGHTS[size!], |
|
|
@@ -382,8 +357,4 @@ const MultilineTextInput = React.forwardRef<HTMLTextAreaElement, Props>( |
|
|
|
), |
|
|
|
) |
|
|
|
|
|
|
|
MultilineTextInput.propTypes = propTypes |
|
|
|
|
|
|
|
MultilineTextInput.displayName = 'TextInput' |
|
|
|
|
|
|
|
export default MultilineTextInput |
|
|
|
MultilineTextInput.displayName = 'MultilineTextInput' |