Manage props directly and fix some linting errors across all categories.pull/1/head
@@ -3,3 +3,5 @@ export type Size = 'small' | 'medium' | 'large'; | |||
export type Variant = 'default' | 'alternate'; | |||
export type InputType = 'text' | 'search'; | |||
export type InputMode = 'none' | 'numeric' | 'decimal' | InputType; |
@@ -1,5 +1,10 @@ | |||
{ | |||
"root": true, | |||
"rules": { | |||
"quote-props": "off", | |||
"react/jsx-props-no-spreading": "off", | |||
"react/button-has-type": "off" | |||
}, | |||
"extends": [ | |||
"lxsmnsyc/typescript/react" | |||
], | |||
@@ -6,7 +6,7 @@ export type ActionButtonDerivedElement = HTMLButtonElement; | |||
export interface ActionButtonProps extends Omit<React.HTMLProps<ActionButtonDerivedElement>, 'type' | 'size'> { | |||
type?: Button.Type; | |||
variant: Button.Variant; | |||
variant?: Button.Variant; | |||
block?: boolean; | |||
subtext?: React.ReactNode; | |||
badge?: React.ReactNode; | |||
@@ -15,19 +15,22 @@ export interface ActionButtonProps extends Omit<React.HTMLProps<ActionButtonDeri | |||
compact?: boolean; | |||
} | |||
export const ActionButton = React.forwardRef<ActionButtonDerivedElement, ActionButtonProps>(({ | |||
type = 'button' as const, | |||
variant, | |||
subtext, | |||
badge, | |||
menuItem = false, | |||
children, | |||
size = 'medium' as const, | |||
compact = false, | |||
className, | |||
block = false, | |||
...etcProps | |||
}, forwardedRef) => ( | |||
export const ActionButton = React.forwardRef<ActionButtonDerivedElement, ActionButtonProps>(( | |||
{ | |||
type = 'button' as const, | |||
variant = 'bare' as const, | |||
subtext, | |||
badge, | |||
menuItem = false, | |||
children, | |||
size = 'medium' as const, | |||
compact = false, | |||
className, | |||
block = false, | |||
...etcProps | |||
}, | |||
forwardedRef, | |||
) => ( | |||
<button | |||
{...etcProps} | |||
type={type} | |||
@@ -48,7 +51,7 @@ export const ActionButton = React.forwardRef<ActionButtonDerivedElement, ActionB | |||
'pr-2': compact || menuItem, | |||
}, | |||
{ | |||
'border-2 border-primary disabled:border-primary focus:border-secondary active:border-tertiary' : variant !== 'bare', | |||
'border-2 border-primary disabled:border-primary focus:border-secondary active:border-tertiary': variant !== 'bare', | |||
'bg-negative text-primary disabled:text-primary focus:text-secondary active:text-tertiary': variant !== 'filled', | |||
'bg-primary text-negative disabled:bg-primary focus:bg-secondary active:bg-tertiary': variant === 'filled', | |||
}, | |||
@@ -110,7 +113,7 @@ export const ActionButton = React.forwardRef<ActionButtonDerivedElement, ActionB | |||
viewBox="0 0 24 24" | |||
role="presentation" | |||
> | |||
<polyline points="9 18 15 12 9 6"/> | |||
<polyline points="9 18 15 12 9 6" /> | |||
</svg> | |||
</span> | |||
)} | |||
@@ -118,3 +121,14 @@ export const ActionButton = React.forwardRef<ActionButtonDerivedElement, ActionB | |||
)); | |||
ActionButton.displayName = 'ActionButton'; | |||
ActionButton.defaultProps = { | |||
type: 'button', | |||
variant: 'bare', | |||
block: false, | |||
subtext: undefined, | |||
badge: undefined, | |||
menuItem: false, | |||
size: 'medium', | |||
compact: false, | |||
}; |
@@ -1,5 +1,9 @@ | |||
{ | |||
"root": true, | |||
"rules": { | |||
"quote-props": "off", | |||
"react/jsx-props-no-spreading": "off" | |||
}, | |||
"extends": [ | |||
"lxsmnsyc/typescript/react" | |||
], | |||
@@ -4,7 +4,7 @@ import clsx from 'clsx'; | |||
export type ComboBoxDerivedElement = HTMLInputElement; | |||
export interface ComboBoxProps extends Omit<React.HTMLProps<ComboBoxDerivedElement>, 'size' | 'type' | 'style' | 'label' | 'list'> { | |||
export interface ComboBoxProps extends Omit<React.HTMLProps<ComboBoxDerivedElement>, 'size' | 'type' | 'style' | 'label' | 'list' | 'inputMode'> { | |||
/** | |||
* Short textual description indicating the nature of the component's value. | |||
*/ | |||
@@ -41,6 +41,10 @@ export interface ComboBoxProps extends Omit<React.HTMLProps<ComboBoxDerivedEleme | |||
* Is the label hidden? | |||
*/ | |||
hiddenLabel?: boolean, | |||
/** | |||
* Input mode of the component. | |||
*/ | |||
inputMode?: TextControl.InputMode, | |||
} | |||
/** | |||
@@ -48,166 +52,191 @@ export interface ComboBoxProps extends Omit<React.HTMLProps<ComboBoxDerivedEleme | |||
* | |||
* This component supports multiline input and adjusts its layout accordingly. | |||
*/ | |||
export const ComboBox = React.forwardRef<ComboBoxDerivedElement, ComboBoxProps>( | |||
( | |||
{ | |||
label = '', | |||
hint = '', | |||
indicator, | |||
size = 'medium' as const, | |||
border = false, | |||
block = false, | |||
type = 'text' as const, | |||
variant = 'default' as const, | |||
hiddenLabel = false, | |||
className, | |||
children, | |||
...etcProps | |||
}: ComboBoxProps, | |||
ref, | |||
) => { | |||
const labelId = React.useId(); | |||
const datalistId = React.useId(); | |||
export const ComboBox = React.forwardRef<ComboBoxDerivedElement, ComboBoxProps>(( | |||
{ | |||
label, | |||
hint, | |||
indicator, | |||
size = 'medium' as const, | |||
border = false, | |||
block = false, | |||
type = 'text' as const, | |||
variant = 'default' as const, | |||
hiddenLabel = false, | |||
className, | |||
children, | |||
inputMode = 'text' as const, | |||
id: idProp, | |||
...etcProps | |||
}: ComboBoxProps, | |||
forwardedRef, | |||
) => { | |||
const labelId = React.useId(); | |||
const datalistId = React.useId(); | |||
const defaultId = React.useId(); | |||
const id = idProp ?? defaultId; | |||
let resultInputMode = inputMode as React.HTMLProps<ComboBoxDerivedElement>['inputMode']; | |||
if (type === 'text' && resultInputMode === 'search') { | |||
resultInputMode = 'text'; | |||
} else if (type === 'search' && resultInputMode === 'text') { | |||
resultInputMode = 'search'; | |||
} | |||
return ( | |||
<> | |||
<datalist | |||
id={datalistId} | |||
> | |||
{children} | |||
</datalist> | |||
<div | |||
return ( | |||
<> | |||
<datalist | |||
id={datalistId} | |||
> | |||
{children} | |||
</datalist> | |||
<div | |||
className={clsx( | |||
'relative rounded ring-secondary/50', | |||
'focus-within:ring-4', | |||
{ | |||
'block': block, | |||
'inline-block align-middle': !block, | |||
}, | |||
className, | |||
)} | |||
> | |||
<input | |||
{...etcProps} | |||
ref={forwardedRef} | |||
id={id} | |||
aria-labelledby={labelId} | |||
type={type} | |||
inputMode={resultInputMode} | |||
list={datalistId} | |||
data-testid="input" | |||
className={clsx( | |||
'relative rounded ring-secondary/50', | |||
'focus-within:ring-4', | |||
'bg-negative rounded-inherit w-full peer block', | |||
'focus:outline-0', | |||
'disabled:opacity-50 disabled:cursor-not-allowed', | |||
{ | |||
'text-xxs': size === 'small', | |||
'text-xs': size === 'medium', | |||
}, | |||
{ | |||
'pl-4': variant === 'default', | |||
'pl-1.5': variant === 'alternate', | |||
}, | |||
{ | |||
'pt-4': variant === 'alternate', | |||
}, | |||
{ | |||
'pr-4': variant === 'default' && !indicator, | |||
'pr-1.5': variant === 'alternate' && !indicator, | |||
}, | |||
{ | |||
'pr-10': indicator && size === 'small', | |||
'pr-12': indicator && size === 'medium', | |||
'pr-16': indicator && size === 'large', | |||
}, | |||
{ | |||
'block': block, | |||
'inline-block align-middle': !block, | |||
'h-10': size === 'small', | |||
'h-12': size === 'medium', | |||
'h-16': size === 'large', | |||
}, | |||
className, | |||
)} | |||
> | |||
<input | |||
{...etcProps} | |||
ref={ref} | |||
aria-labelledby={labelId} | |||
type={type} | |||
list={datalistId} | |||
data-testid="input" | |||
/> | |||
{ | |||
label && ( | |||
<label | |||
data-testid="label" | |||
id={labelId} | |||
htmlFor={id} | |||
className={clsx( | |||
'absolute z-[1] w-full top-0.5 left-0 pointer-events-none pl-1 text-xxs font-bold peer-disabled:opacity-50 peer-focus:text-secondary text-primary leading-none bg-negative', | |||
{ | |||
'sr-only': hiddenLabel, | |||
}, | |||
{ | |||
'pr-1': !indicator, | |||
}, | |||
{ | |||
'pr-10': indicator && size === 'small', | |||
'pr-12': indicator && size === 'medium', | |||
'pr-16': indicator && size === 'large', | |||
}, | |||
)} | |||
> | |||
<span className="w-full whitespace-nowrap h-[1.1em] overflow-hidden text-ellipsis"> | |||
{label} | |||
</span> | |||
</label> | |||
) | |||
} | |||
{hint && ( | |||
<div | |||
data-testid="hint" | |||
className={clsx( | |||
'bg-negative rounded-inherit w-full peer block', | |||
'focus:outline-0', | |||
'disabled:opacity-50 disabled:cursor-not-allowed', | |||
'absolute left-0 px-1 pointer-events-none text-xxs peer-disabled:opacity-50 leading-none w-full bg-negative', | |||
{ | |||
'text-xxs': size === 'small', | |||
'text-xs': size === 'medium', | |||
'bottom-0 pl-4 pb-1': variant === 'default', | |||
'top-0.5': variant === 'alternate', | |||
}, | |||
{ | |||
'pl-4': variant === 'default', | |||
'pl-1.5': variant === 'alternate', | |||
'pt-2': variant === 'alternate' && size === 'small', | |||
'pt-3': variant === 'alternate' && size !== 'small', | |||
}, | |||
{ | |||
'pt-4': variant === 'alternate', | |||
}, | |||
{ | |||
'pr-4': variant === 'default' && !indicator, | |||
'pr-1.5': variant === 'alternate' && !indicator, | |||
'pr-4': !indicator && variant === 'default', | |||
'pr-1': !indicator && variant === 'alternate', | |||
}, | |||
{ | |||
'pr-10': indicator && size === 'small', | |||
'pr-12': indicator && size === 'medium', | |||
'pr-16': indicator && size === 'large', | |||
}, | |||
{ | |||
'h-10': size === 'small', | |||
'h-12': size === 'medium', | |||
'h-16': size === 'large', | |||
}, | |||
)} | |||
/> | |||
{ | |||
label && ( | |||
<label | |||
data-testid="label" | |||
id={labelId} | |||
className={clsx( | |||
'absolute z-[1] w-full top-0.5 left-0 pointer-events-none pl-1 text-xxs font-bold peer-disabled:opacity-50 peer-focus:text-secondary text-primary leading-none bg-negative', | |||
{ | |||
'sr-only': hiddenLabel, | |||
}, | |||
{ | |||
'pr-1': !indicator, | |||
}, | |||
{ | |||
'pr-10': indicator && size === 'small', | |||
'pr-12': indicator && size === 'medium', | |||
'pr-16': indicator && size === 'large', | |||
}, | |||
)} | |||
> | |||
<span className="w-full whitespace-nowrap h-[1.1em] overflow-hidden text-ellipsis"> | |||
{label} | |||
</span> | |||
</label> | |||
) | |||
} | |||
{hint && ( | |||
<div | |||
data-testid="hint" | |||
className={clsx( | |||
'absolute left-0 px-1 pointer-events-none text-xxs peer-disabled:opacity-50 leading-none w-full bg-negative', | |||
{ | |||
'bottom-0 pl-4 pb-1': variant === 'default', | |||
'top-0.5': variant === 'alternate', | |||
}, | |||
{ | |||
'pt-2': variant === 'alternate' && size === 'small', | |||
'pt-3': variant === 'alternate' && size !== 'small', | |||
}, | |||
{ | |||
'pr-4': !indicator && variant === 'default', | |||
'pr-1': !indicator && variant === 'alternate', | |||
}, | |||
{ | |||
'pr-10': indicator && size === 'small', | |||
'pr-12': indicator && size === 'medium', | |||
'pr-16': indicator && size === 'large', | |||
}, | |||
)} | |||
> | |||
<div | |||
className="opacity-50 whitespace-nowrap w-full h-[1.1em] overflow-hidden text-ellipsis" | |||
> | |||
{hint} | |||
</div> | |||
</div> | |||
)} | |||
{indicator && ( | |||
> | |||
<div | |||
className={clsx( | |||
'text-center flex items-center justify-center peer-disabled:opacity-50 aspect-square absolute bottom-0 right-0 pointer-events-none', | |||
{ | |||
'w-10': size === 'small', | |||
'w-12': size === 'medium', | |||
'w-16': size === 'large', | |||
}, | |||
)} | |||
className="opacity-50 whitespace-nowrap w-full h-[1.1em] overflow-hidden text-ellipsis" | |||
> | |||
{indicator} | |||
{hint} | |||
</div> | |||
)} | |||
{ | |||
border && ( | |||
<span | |||
data-testid="border" | |||
className="absolute z-[1] peer-disabled:opacity-50 inset-0 rounded-inherit border-2 border-primary pointer-events-none peer-focus:border-secondary" | |||
/> | |||
) | |||
} | |||
</div> | |||
</> | |||
); | |||
} | |||
); | |||
</div> | |||
)} | |||
{indicator && ( | |||
<div | |||
className={clsx( | |||
'text-center flex items-center justify-center peer-disabled:opacity-50 aspect-square absolute bottom-0 right-0 pointer-events-none', | |||
{ | |||
'w-10': size === 'small', | |||
'w-12': size === 'medium', | |||
'w-16': size === 'large', | |||
}, | |||
)} | |||
> | |||
{indicator} | |||
</div> | |||
)} | |||
{ | |||
border && ( | |||
<span | |||
data-testid="border" | |||
className="absolute z-[1] peer-disabled:opacity-50 inset-0 rounded-inherit border-2 border-primary pointer-events-none peer-focus:border-secondary" | |||
/> | |||
) | |||
} | |||
</div> | |||
</> | |||
); | |||
}); | |||
ComboBox.displayName = 'ComboBox'; | |||
ComboBox.defaultProps = { | |||
label: undefined, | |||
hint: undefined, | |||
indicator: undefined, | |||
size: 'medium', | |||
border: false, | |||
block: false, | |||
type: 'text', | |||
variant: 'default', | |||
hiddenLabel: false, | |||
inputMode: 'text', | |||
}; |
@@ -47,8 +47,8 @@ export interface DropdownSelectProps extends Omit<React.HTMLProps<DropdownSelect | |||
export const DropdownSelect = React.forwardRef<DropdownSelectDerivedElement, DropdownSelectProps>( | |||
( | |||
{ | |||
label = '', | |||
hint = '', | |||
label, | |||
hint, | |||
indicator, | |||
size = 'medium' as const, | |||
border = false, | |||
@@ -57,11 +57,14 @@ export const DropdownSelect = React.forwardRef<DropdownSelectDerivedElement, Dro | |||
hiddenLabel = false, | |||
className, | |||
children, | |||
id: idProp, | |||
...etcProps | |||
}: DropdownSelectProps, | |||
ref, | |||
forwardedRef, | |||
) => { | |||
const labelId = React.useId(); | |||
const defaultId = React.useId(); | |||
const id = idProp ?? defaultId; | |||
return ( | |||
<div | |||
@@ -77,7 +80,8 @@ export const DropdownSelect = React.forwardRef<DropdownSelectDerivedElement, Dro | |||
> | |||
<select | |||
{...etcProps} | |||
ref={ref} | |||
ref={forwardedRef} | |||
id={id} | |||
aria-labelledby={labelId} | |||
data-testid="input" | |||
className={clsx( | |||
@@ -116,6 +120,7 @@ export const DropdownSelect = React.forwardRef<DropdownSelectDerivedElement, Dro | |||
{ | |||
label && ( | |||
<label | |||
htmlFor={id} | |||
data-testid="label" | |||
id={labelId} | |||
className={clsx( | |||
@@ -194,7 +199,18 @@ export const DropdownSelect = React.forwardRef<DropdownSelectDerivedElement, Dro | |||
} | |||
</div> | |||
); | |||
} | |||
}, | |||
); | |||
DropdownSelect.displayName = 'DropdownSelect'; | |||
DropdownSelect.defaultProps = { | |||
label: undefined, | |||
hint: undefined, | |||
indicator: undefined, | |||
size: 'medium', | |||
border: false, | |||
block: false, | |||
variant: 'default', | |||
hiddenLabel: false, | |||
}; |
@@ -37,6 +37,9 @@ export interface MenuSelectProps extends Omit<React.HTMLProps<MenuSelectDerivedE | |||
* Is the label hidden? | |||
*/ | |||
hiddenLabel?: boolean, | |||
/** | |||
* Starting height of the component. | |||
*/ | |||
startingHeight?: number | string, | |||
} | |||
@@ -58,11 +61,14 @@ export const MenuSelect = React.forwardRef<MenuSelectDerivedElement, MenuSelectP | |||
hiddenLabel = false, | |||
className, | |||
startingHeight = '15rem', | |||
id: idProp, | |||
...etcProps | |||
}: MenuSelectProps, | |||
ref, | |||
forwardedRef, | |||
) => { | |||
const labelId = React.useId(); | |||
const defaultId = React.useId(); | |||
const id = idProp ?? defaultId; | |||
return ( | |||
<div | |||
@@ -78,7 +84,8 @@ export const MenuSelect = React.forwardRef<MenuSelectDerivedElement, MenuSelectP | |||
> | |||
<select | |||
{...etcProps} | |||
ref={ref} | |||
ref={forwardedRef} | |||
id={id} | |||
aria-labelledby={labelId} | |||
data-testid="input" | |||
size={2} | |||
@@ -131,6 +138,7 @@ export const MenuSelect = React.forwardRef<MenuSelectDerivedElement, MenuSelectP | |||
label && ( | |||
<label | |||
data-testid="label" | |||
htmlFor={id} | |||
id={labelId} | |||
className={clsx( | |||
'absolute z-[1] w-full top-0.5 left-0 pointer-events-none pl-1 text-xxs font-bold peer-disabled:opacity-50 peer-focus:text-secondary text-primary leading-none bg-negative', | |||
@@ -208,7 +216,19 @@ export const MenuSelect = React.forwardRef<MenuSelectDerivedElement, MenuSelectP | |||
} | |||
</div> | |||
); | |||
} | |||
}, | |||
); | |||
MenuSelect.displayName = 'MenuSelect'; | |||
MenuSelect.defaultProps = { | |||
label: undefined, | |||
hint: undefined, | |||
indicator: undefined, | |||
size: 'medium', | |||
border: false, | |||
block: false, | |||
variant: 'default', | |||
hiddenLabel: false, | |||
startingHeight: '15rem', | |||
}; |
@@ -10,22 +10,25 @@ export interface RadioButtonProps extends Omit<React.InputHTMLAttributes<RadioBu | |||
size?: Button.Size; | |||
subtext?: React.ReactNode; | |||
badge?: React.ReactNode; | |||
variant: Button.Variant; | |||
variant?: Button.Variant; | |||
} | |||
export const RadioButton = React.forwardRef<RadioButtonDerivedElement, RadioButtonProps>(({ | |||
children, | |||
block = false, | |||
compact = false, | |||
size = 'medium', | |||
id: idProp, | |||
className, | |||
subtext, | |||
badge, | |||
variant, | |||
style, | |||
...etcProps | |||
}, forwardedRef) => { | |||
export const RadioButton = React.forwardRef<RadioButtonDerivedElement, RadioButtonProps>(( | |||
{ | |||
children, | |||
block = false, | |||
compact = false, | |||
size = 'medium' as const, | |||
id: idProp, | |||
className, | |||
subtext, | |||
badge, | |||
variant = 'bare' as const, | |||
style, | |||
...etcProps | |||
}, | |||
forwardedRef, | |||
) => { | |||
const defaultId = React.useId(); | |||
const id = idProp ?? defaultId; | |||
return ( | |||
@@ -55,7 +58,7 @@ export const RadioButton = React.forwardRef<RadioButtonDerivedElement, RadioButt | |||
'pl-4 pr-4': !compact, | |||
}, | |||
{ | |||
'border-2 border-primary peer-disabled:border-primary peer-focus:border-secondary peer-checked:border-tertiary active:border-tertiary' : variant !== 'bare', | |||
'border-2 border-primary peer-disabled:border-primary peer-focus:border-secondary peer-checked:border-tertiary active:border-tertiary': variant !== 'bare', | |||
'bg-primary text-negative peer-disabled:bg-primary peer-focus:bg-secondary peer-checked:bg-tertiary active:bg-tertiary': variant === 'filled', | |||
}, | |||
{ | |||
@@ -73,7 +76,7 @@ export const RadioButton = React.forwardRef<RadioButtonDerivedElement, RadioButt | |||
'-mr-2': compact, | |||
'border-current': variant !== 'filled', | |||
'border-negative': variant === 'filled', | |||
} | |||
}, | |||
)} | |||
> | |||
<span | |||
@@ -135,7 +138,16 @@ export const RadioButton = React.forwardRef<RadioButtonDerivedElement, RadioButt | |||
</span> | |||
</label> | |||
</> | |||
) | |||
); | |||
}); | |||
RadioButton.displayName = 'RadioButton'; | |||
RadioButton.defaultProps = { | |||
badge: undefined, | |||
block: false, | |||
compact: false, | |||
subtext: undefined, | |||
size: 'medium', | |||
variant: 'filled', | |||
}; |
@@ -6,19 +6,20 @@ export type RadioTickBoxDerivedElement = HTMLInputElement; | |||
export interface RadioTickBoxProps extends Omit<React.InputHTMLAttributes<RadioTickBoxDerivedElement>, 'type' | 'size'> { | |||
block?: boolean; | |||
subtext?: React.ReactNode; | |||
badge?: React.ReactNode; | |||
} | |||
export const RadioTickBox = React.forwardRef<RadioTickBoxDerivedElement, RadioTickBoxProps>(({ | |||
children, | |||
block = false, | |||
id: idProp, | |||
className, | |||
subtext, | |||
badge, | |||
style, | |||
...etcProps | |||
}, forwardedRef) => { | |||
export const RadioTickBox = React.forwardRef<RadioTickBoxDerivedElement, RadioTickBoxProps>(( | |||
{ | |||
children, | |||
block = false, | |||
id: idProp, | |||
className, | |||
subtext, | |||
style, | |||
...etcProps | |||
}, | |||
forwardedRef, | |||
) => { | |||
const defaultId = React.useId(); | |||
const id = idProp ?? defaultId; | |||
return ( | |||
@@ -80,7 +81,12 @@ export const RadioTickBox = React.forwardRef<RadioTickBoxDerivedElement, RadioTi | |||
</div> | |||
)} | |||
</div> | |||
) | |||
); | |||
}); | |||
RadioTickBox.displayName = 'RadioTickBox'; | |||
RadioTickBox.defaultProps = { | |||
block: false, | |||
subtext: undefined, | |||
}; |
@@ -1,5 +1,8 @@ | |||
{ | |||
"root": true, | |||
"rules": { | |||
"react/jsx-props-no-spreading": "off" | |||
}, | |||
"extends": [ | |||
"lxsmnsyc/typescript/react" | |||
], | |||
@@ -12,9 +12,10 @@ export interface SwatchProps extends Omit<React.HTMLProps<SwatchDerivedElement>, | |||
export const useSwatchControls = () => { | |||
const id = React.useId(); | |||
const copyColor: React.ReactEventHandler<SwatchDerivedElement> = React.useCallback(async (e) => { | |||
const copyColor: React.ReactEventHandler<SwatchDerivedElement> = React.useCallback((e) => { | |||
const { value } = e.currentTarget; | |||
await window.navigator.clipboard.writeText(value); | |||
// eslint-disable-next-line no-void | |||
void window.navigator.clipboard.writeText(value); | |||
}, []); | |||
return React.useMemo(() => ({ | |||
id, | |||
@@ -72,23 +73,21 @@ export const Swatch = React.forwardRef<SwatchDerivedElement, SwatchProps>(({ | |||
</span> | |||
<span className="tabular-nums text-xs sr-only"> | |||
{ | |||
color | |||
.map((c) => c | |||
.toString() | |||
.padStart(4, ' ') | |||
.split('') | |||
.map((cc, j) => ( | |||
<span | |||
key={`${cc}:${j}`} | |||
className={clsx({ | |||
'opacity-0': cc === ' ', | |||
})} | |||
> | |||
{j === 0 && ' '} | |||
{cc === ' ' && j > 0 ? '0' : cc} | |||
</span> | |||
)) | |||
) | |||
color.map((c) => c | |||
.toString() | |||
.padStart(4, ' ') | |||
.split('') | |||
.map((cc, j) => ( | |||
<span | |||
key={`${cc}:${j}`} | |||
className={clsx({ | |||
'opacity-0': cc === ' ', | |||
})} | |||
> | |||
{j === 0 && ' '} | |||
{cc === ' ' && j > 0 ? '0' : cc} | |||
</span> | |||
))) | |||
} | |||
</span> | |||
</label> | |||
@@ -97,3 +96,7 @@ export const Swatch = React.forwardRef<SwatchDerivedElement, SwatchProps>(({ | |||
}); | |||
Swatch.displayName = 'Swatch'; | |||
Swatch.defaultProps = { | |||
mode: 'rgb', | |||
}; |
@@ -1,5 +1,9 @@ | |||
{ | |||
"root": true, | |||
"rules": { | |||
"quote-props": "off", | |||
"react/jsx-props-no-spreading": "off" | |||
}, | |||
"extends": [ | |||
"lxsmnsyc/typescript/react" | |||
], | |||
@@ -4,7 +4,7 @@ import clsx from 'clsx'; | |||
export type EmailInputDerivedElement = HTMLInputElement; | |||
export interface EmailInputProps extends Omit<React.HTMLProps<EmailInputDerivedElement>, 'size' | 'type' | 'label'> { | |||
export interface EmailInputProps extends Omit<React.HTMLProps<EmailInputDerivedElement>, 'size' | 'type' | 'label' | 'inputMode'> { | |||
/** | |||
* Short textual description indicating the nature of the component's value. | |||
*/ | |||
@@ -54,11 +54,15 @@ export const EmailInput = React.forwardRef<EmailInputDerivedElement, EmailInputP | |||
variant = 'default' as const, | |||
hiddenLabel = false, | |||
className, | |||
id: idProp, | |||
style, | |||
...etcProps | |||
}: EmailInputProps, | |||
ref, | |||
forwardedRef, | |||
) => { | |||
const labelId = React.useId(); | |||
const defaultId = React.useId(); | |||
const id = idProp ?? defaultId; | |||
return ( | |||
<div | |||
@@ -71,10 +75,12 @@ export const EmailInput = React.forwardRef<EmailInputDerivedElement, EmailInputP | |||
}, | |||
className, | |||
)} | |||
style={style} | |||
> | |||
<input | |||
{...etcProps} | |||
ref={ref} | |||
ref={forwardedRef} | |||
id={id} | |||
aria-labelledby={labelId} | |||
type="email" | |||
data-testid="input" | |||
@@ -114,8 +120,9 @@ export const EmailInput = React.forwardRef<EmailInputDerivedElement, EmailInputP | |||
<label | |||
data-testid="label" | |||
id={labelId} | |||
htmlFor={id} | |||
className={clsx( | |||
'absolute z-[1] w-full top-0.5 left-0 pointer-events-none pl-1 text-xxs font-bold peer-disabled:opacity-50 peer-focus:text-secondary text-primary leading-none bg-negative', | |||
'absolute z-[1] w-full top-0.5 left-0 pointer-events-none pl-1 text-xxs font-bold peer-disabled:opacity-50 peer-focus:text-secondary text-primary leading-none bg-negative select-none', | |||
{ | |||
'sr-only': hiddenLabel, | |||
}, | |||
@@ -139,7 +146,7 @@ export const EmailInput = React.forwardRef<EmailInputDerivedElement, EmailInputP | |||
<div | |||
data-testid="hint" | |||
className={clsx( | |||
'absolute left-0 px-1 pointer-events-none text-xxs peer-disabled:opacity-50 leading-none w-full bg-negative', | |||
'absolute left-0 px-1 pointer-events-none text-xxs peer-disabled:opacity-50 leading-none w-full bg-negative select-none', | |||
{ | |||
'bottom-0 pl-4 pb-1': variant === 'default', | |||
'top-0.5': variant === 'alternate', | |||
@@ -169,7 +176,7 @@ export const EmailInput = React.forwardRef<EmailInputDerivedElement, EmailInputP | |||
{indicator && ( | |||
<div | |||
className={clsx( | |||
'text-center flex items-center justify-center peer-disabled:opacity-50 aspect-square absolute bottom-0 right-0 pointer-events-none', | |||
'text-center flex items-center justify-center peer-disabled:opacity-50 aspect-square absolute bottom-0 right-0 pointer-events-none select-none', | |||
{ | |||
'w-10': size === 'small', | |||
'w-12': size === 'medium', | |||
@@ -194,3 +201,14 @@ export const EmailInput = React.forwardRef<EmailInputDerivedElement, EmailInputP | |||
); | |||
EmailInput.displayName = 'EmailInput'; | |||
EmailInput.defaultProps = { | |||
label: undefined, | |||
hint: undefined, | |||
indicator: undefined, | |||
size: 'medium', | |||
border: false, | |||
block: false, | |||
variant: 'default', | |||
hiddenLabel: false, | |||
}; |
@@ -4,7 +4,7 @@ import clsx from 'clsx'; | |||
export type PhoneNumberInputDerivedElement = HTMLInputElement; | |||
export interface PhoneNumberInputProps extends Omit<React.HTMLProps<PhoneNumberInputDerivedElement>, 'size' | 'type' | 'label'> { | |||
export interface PhoneNumberInputProps extends Omit<React.HTMLProps<PhoneNumberInputDerivedElement>, 'size' | 'type' | 'label' | 'inputMode'> { | |||
/** | |||
* Short textual description indicating the nature of the component's value. | |||
*/ | |||
@@ -42,155 +42,174 @@ export interface PhoneNumberInputProps extends Omit<React.HTMLProps<PhoneNumberI | |||
/** | |||
* Component for inputting textual values. | |||
*/ | |||
export const PhoneNumberInput = React.forwardRef<PhoneNumberInputDerivedElement, PhoneNumberInputProps>( | |||
( | |||
{ | |||
label, | |||
hint, | |||
indicator, | |||
size = 'medium' as const, | |||
border = false, | |||
block = false, | |||
variant = 'default' as const, | |||
hiddenLabel = false, | |||
className, | |||
...etcProps | |||
}: PhoneNumberInputProps, | |||
ref, | |||
) => { | |||
const labelId = React.useId(); | |||
export const PhoneNumberInput = React.forwardRef< | |||
PhoneNumberInputDerivedElement, | |||
PhoneNumberInputProps | |||
>(( | |||
{ | |||
label, | |||
hint, | |||
indicator, | |||
size = 'medium' as const, | |||
border = false, | |||
block = false, | |||
variant = 'default' as const, | |||
hiddenLabel = false, | |||
className, | |||
id: idProp, | |||
style, | |||
...etcProps | |||
}: PhoneNumberInputProps, | |||
forwardedRef, | |||
) => { | |||
const labelId = React.useId(); | |||
const defaultId = React.useId(); | |||
const id = idProp ?? defaultId; | |||
return ( | |||
<div | |||
return ( | |||
<div | |||
className={clsx( | |||
'relative rounded ring-secondary/50', | |||
'focus-within:ring-4', | |||
{ | |||
'block': block, | |||
'inline-block align-middle': !block, | |||
}, | |||
className, | |||
)} | |||
style={style} | |||
> | |||
<input | |||
{...etcProps} | |||
ref={forwardedRef} | |||
aria-labelledby={labelId} | |||
type="tel" | |||
id={id} | |||
data-testid="input" | |||
className={clsx( | |||
'relative rounded ring-secondary/50', | |||
'focus-within:ring-4', | |||
'bg-negative rounded-inherit w-full peer block', | |||
'focus:outline-0', | |||
'disabled:opacity-50 disabled:cursor-not-allowed', | |||
{ | |||
'text-xxs': size === 'small', | |||
'text-xs': size === 'medium', | |||
}, | |||
{ | |||
'pl-4': variant === 'default', | |||
'pl-1.5': variant === 'alternate', | |||
}, | |||
{ | |||
'pt-4': variant === 'alternate', | |||
}, | |||
{ | |||
'block': block, | |||
'inline-block align-middle': !block, | |||
'pr-4': variant === 'default' && !indicator, | |||
'pr-1.5': variant === 'alternate' && !indicator, | |||
}, | |||
{ | |||
'pr-10': indicator && size === 'small', | |||
'pr-12': indicator && size === 'medium', | |||
'pr-16': indicator && size === 'large', | |||
}, | |||
{ | |||
'h-10': size === 'small', | |||
'h-12': size === 'medium', | |||
'h-16': size === 'large', | |||
}, | |||
className, | |||
)} | |||
> | |||
<input | |||
{...etcProps} | |||
ref={ref} | |||
aria-labelledby={labelId} | |||
type="tel" | |||
data-testid="input" | |||
/> | |||
{ | |||
label && ( | |||
<label | |||
data-testid="label" | |||
id={labelId} | |||
htmlFor={id} | |||
className={clsx( | |||
'absolute z-[1] w-full top-0.5 left-0 pointer-events-none pl-1 text-xxs font-bold peer-disabled:opacity-50 peer-focus:text-secondary text-primary leading-none bg-negative select-none', | |||
{ | |||
'sr-only': hiddenLabel, | |||
}, | |||
{ | |||
'pr-1': !indicator, | |||
}, | |||
{ | |||
'pr-10': indicator && size === 'small', | |||
'pr-12': indicator && size === 'medium', | |||
'pr-16': indicator && size === 'large', | |||
}, | |||
)} | |||
> | |||
<span className="block w-full whitespace-nowrap h-[1.1em] overflow-hidden text-ellipsis"> | |||
{label} | |||
</span> | |||
</label> | |||
) | |||
} | |||
{hint && ( | |||
<div | |||
data-testid="hint" | |||
className={clsx( | |||
'bg-negative rounded-inherit w-full peer block', | |||
'focus:outline-0', | |||
'disabled:opacity-50 disabled:cursor-not-allowed', | |||
{ | |||
'text-xxs': size === 'small', | |||
'text-xs': size === 'medium', | |||
}, | |||
'absolute left-0 px-1 pointer-events-none text-xxs peer-disabled:opacity-50 leading-none w-full bg-negative select-none', | |||
{ | |||
'pl-4': variant === 'default', | |||
'pl-1.5': variant === 'alternate', | |||
'bottom-0 pl-4 pb-1': variant === 'default', | |||
'top-0.5': variant === 'alternate', | |||
}, | |||
{ | |||
'pt-4': variant === 'alternate', | |||
'pt-2': variant === 'alternate' && size === 'small', | |||
'pt-3': variant === 'alternate' && size !== 'small', | |||
}, | |||
{ | |||
'pr-4': variant === 'default' && !indicator, | |||
'pr-1.5': variant === 'alternate' && !indicator, | |||
'pr-4': !indicator && variant === 'default', | |||
'pr-1': !indicator && variant === 'alternate', | |||
}, | |||
{ | |||
'pr-10': indicator && size === 'small', | |||
'pr-12': indicator && size === 'medium', | |||
'pr-16': indicator && size === 'large', | |||
}, | |||
{ | |||
'h-10': size === 'small', | |||
'h-12': size === 'medium', | |||
'h-16': size === 'large', | |||
}, | |||
)} | |||
/> | |||
{ | |||
label && ( | |||
<label | |||
data-testid="label" | |||
id={labelId} | |||
className={clsx( | |||
'absolute z-[1] w-full top-0.5 left-0 pointer-events-none pl-1 text-xxs font-bold peer-disabled:opacity-50 peer-focus:text-secondary text-primary leading-none bg-negative', | |||
{ | |||
'sr-only': hiddenLabel, | |||
}, | |||
{ | |||
'pr-1': !indicator, | |||
}, | |||
{ | |||
'pr-10': indicator && size === 'small', | |||
'pr-12': indicator && size === 'medium', | |||
'pr-16': indicator && size === 'large', | |||
}, | |||
)} | |||
> | |||
<span className="block w-full whitespace-nowrap h-[1.1em] overflow-hidden text-ellipsis"> | |||
{label} | |||
</span> | |||
</label> | |||
) | |||
} | |||
{hint && ( | |||
> | |||
<div | |||
data-testid="hint" | |||
className={clsx( | |||
'absolute left-0 px-1 pointer-events-none text-xxs peer-disabled:opacity-50 leading-none w-full bg-negative', | |||
{ | |||
'bottom-0 pl-4 pb-1': variant === 'default', | |||
'top-0.5': variant === 'alternate', | |||
}, | |||
{ | |||
'pt-2': variant === 'alternate' && size === 'small', | |||
'pt-3': variant === 'alternate' && size !== 'small', | |||
}, | |||
{ | |||
'pr-4': !indicator && variant === 'default', | |||
'pr-1': !indicator && variant === 'alternate', | |||
}, | |||
{ | |||
'pr-10': indicator && size === 'small', | |||
'pr-12': indicator && size === 'medium', | |||
'pr-16': indicator && size === 'large', | |||
}, | |||
)} | |||
> | |||
<div | |||
className="opacity-50 whitespace-nowrap w-full h-[1.1em] overflow-hidden text-ellipsis" | |||
> | |||
{hint} | |||
</div> | |||
</div> | |||
)} | |||
{indicator && ( | |||
<div | |||
className={clsx( | |||
'text-center flex items-center justify-center peer-disabled:opacity-50 aspect-square absolute bottom-0 right-0 pointer-events-none', | |||
{ | |||
'w-10': size === 'small', | |||
'w-12': size === 'medium', | |||
'w-16': size === 'large', | |||
}, | |||
)} | |||
className="opacity-50 whitespace-nowrap w-full h-[1.1em] overflow-hidden text-ellipsis" | |||
> | |||
{indicator} | |||
{hint} | |||
</div> | |||
)} | |||
{ | |||
border && ( | |||
<span | |||
data-testid="border" | |||
className="absolute z-[1] peer-disabled:opacity-50 inset-0 rounded-inherit border-2 border-primary pointer-events-none peer-focus:border-secondary" | |||
/> | |||
) | |||
} | |||
</div> | |||
); | |||
}, | |||
); | |||
</div> | |||
)} | |||
{indicator && ( | |||
<div | |||
className={clsx( | |||
'text-center flex items-center justify-center peer-disabled:opacity-50 aspect-square absolute bottom-0 right-0 pointer-events-none select-none', | |||
{ | |||
'w-10': size === 'small', | |||
'w-12': size === 'medium', | |||
'w-16': size === 'large', | |||
}, | |||
)} | |||
> | |||
{indicator} | |||
</div> | |||
)} | |||
{ | |||
border && ( | |||
<span | |||
data-testid="border" | |||
className="absolute z-[1] peer-disabled:opacity-50 inset-0 rounded-inherit border-2 border-primary pointer-events-none peer-focus:border-secondary" | |||
/> | |||
) | |||
} | |||
</div> | |||
); | |||
}); | |||
PhoneNumberInput.displayName = 'PhoneNumberInput'; | |||
PhoneNumberInput.defaultProps = { | |||
label: undefined, | |||
hint: undefined, | |||
indicator: undefined, | |||
size: 'medium', | |||
border: false, | |||
block: false, | |||
variant: 'default', | |||
hiddenLabel: false, | |||
}; |
@@ -4,7 +4,7 @@ import clsx from 'clsx'; | |||
export type UrlInputDerivedElement = HTMLInputElement; | |||
export interface UrlInputProps extends Omit<React.HTMLProps<UrlInputDerivedElement>, 'size' | 'type' | 'label'> { | |||
export interface UrlInputProps extends Omit<React.HTMLProps<UrlInputDerivedElement>, 'size' | 'type' | 'label' | 'inputMode'> { | |||
/** | |||
* Short textual description indicating the nature of the component's value. | |||
*/ | |||
@@ -42,155 +42,171 @@ export interface UrlInputProps extends Omit<React.HTMLProps<UrlInputDerivedEleme | |||
/** | |||
* Component for inputting textual values. | |||
*/ | |||
export const UrlInput = React.forwardRef<UrlInputDerivedElement, UrlInputProps>( | |||
( | |||
{ | |||
label, | |||
hint, | |||
indicator, | |||
size = 'medium' as const, | |||
border = false, | |||
block = false, | |||
variant = 'default' as const, | |||
hiddenLabel = false, | |||
className, | |||
...etcProps | |||
}: UrlInputProps, | |||
ref, | |||
) => { | |||
const labelId = React.useId(); | |||
export const UrlInput = React.forwardRef<UrlInputDerivedElement, UrlInputProps>(( | |||
{ | |||
label, | |||
hint, | |||
indicator, | |||
size = 'medium' as const, | |||
border = false, | |||
block = false, | |||
variant = 'default' as const, | |||
hiddenLabel = false, | |||
className, | |||
style, | |||
id: idProp, | |||
...etcProps | |||
}: UrlInputProps, | |||
forwardedRef, | |||
) => { | |||
const labelId = React.useId(); | |||
const defaultId = React.useId(); | |||
const id = idProp ?? defaultId; | |||
return ( | |||
<div | |||
return ( | |||
<div | |||
className={clsx( | |||
'relative rounded ring-secondary/50', | |||
'focus-within:ring-4', | |||
{ | |||
'block': block, | |||
'inline-block align-middle': !block, | |||
}, | |||
className, | |||
)} | |||
style={style} | |||
> | |||
<input | |||
{...etcProps} | |||
ref={forwardedRef} | |||
id={id} | |||
aria-labelledby={labelId} | |||
type="url" | |||
data-testid="input" | |||
className={clsx( | |||
'relative rounded ring-secondary/50', | |||
'focus-within:ring-4', | |||
'bg-negative rounded-inherit w-full peer block', | |||
'focus:outline-0', | |||
'disabled:opacity-50 disabled:cursor-not-allowed', | |||
{ | |||
'text-xxs': size === 'small', | |||
'text-xs': size === 'medium', | |||
}, | |||
{ | |||
'pl-4': variant === 'default', | |||
'pl-1.5': variant === 'alternate', | |||
}, | |||
{ | |||
'pt-4': variant === 'alternate', | |||
}, | |||
{ | |||
'block': block, | |||
'inline-block align-middle': !block, | |||
'pr-4': variant === 'default' && !indicator, | |||
'pr-1.5': variant === 'alternate' && !indicator, | |||
}, | |||
{ | |||
'pr-10': indicator && size === 'small', | |||
'pr-12': indicator && size === 'medium', | |||
'pr-16': indicator && size === 'large', | |||
}, | |||
{ | |||
'h-10': size === 'small', | |||
'h-12': size === 'medium', | |||
'h-16': size === 'large', | |||
}, | |||
className, | |||
)} | |||
> | |||
<input | |||
{...etcProps} | |||
ref={ref} | |||
aria-labelledby={labelId} | |||
type="url" | |||
data-testid="input" | |||
/> | |||
{ | |||
label && ( | |||
<label | |||
data-testid="label" | |||
htmlFor={id} | |||
id={labelId} | |||
className={clsx( | |||
'absolute z-[1] w-full top-0.5 left-0 pointer-events-none pl-1 text-xxs font-bold peer-disabled:opacity-50 peer-focus:text-secondary text-primary leading-none bg-negative select-none', | |||
{ | |||
'sr-only': hiddenLabel, | |||
}, | |||
{ | |||
'pr-1': !indicator, | |||
}, | |||
{ | |||
'pr-10': indicator && size === 'small', | |||
'pr-12': indicator && size === 'medium', | |||
'pr-16': indicator && size === 'large', | |||
}, | |||
)} | |||
> | |||
<span className="block w-full whitespace-nowrap h-[1.1em] overflow-hidden text-ellipsis"> | |||
{label} | |||
</span> | |||
</label> | |||
) | |||
} | |||
{hint && ( | |||
<div | |||
data-testid="hint" | |||
className={clsx( | |||
'bg-negative rounded-inherit w-full peer block', | |||
'focus:outline-0', | |||
'disabled:opacity-50 disabled:cursor-not-allowed', | |||
{ | |||
'text-xxs': size === 'small', | |||
'text-xs': size === 'medium', | |||
}, | |||
'absolute left-0 px-1 pointer-events-none text-xxs peer-disabled:opacity-50 leading-none w-full bg-negative select-none', | |||
{ | |||
'pl-4': variant === 'default', | |||
'pl-1.5': variant === 'alternate', | |||
'bottom-0 pl-4 pb-1': variant === 'default', | |||
'top-0.5': variant === 'alternate', | |||
}, | |||
{ | |||
'pt-4': variant === 'alternate', | |||
'pt-2': variant === 'alternate' && size === 'small', | |||
'pt-3': variant === 'alternate' && size !== 'small', | |||
}, | |||
{ | |||
'pr-4': variant === 'default' && !indicator, | |||
'pr-1.5': variant === 'alternate' && !indicator, | |||
'pr-4': !indicator && variant === 'default', | |||
'pr-1': !indicator && variant === 'alternate', | |||
}, | |||
{ | |||
'pr-10': indicator && size === 'small', | |||
'pr-12': indicator && size === 'medium', | |||
'pr-16': indicator && size === 'large', | |||
}, | |||
{ | |||
'h-10': size === 'small', | |||
'h-12': size === 'medium', | |||
'h-16': size === 'large', | |||
}, | |||
)} | |||
/> | |||
{ | |||
label && ( | |||
<label | |||
data-testid="label" | |||
id={labelId} | |||
className={clsx( | |||
'absolute z-[1] w-full top-0.5 left-0 pointer-events-none pl-1 text-xxs font-bold peer-disabled:opacity-50 peer-focus:text-secondary text-primary leading-none bg-negative', | |||
{ | |||
'sr-only': hiddenLabel, | |||
}, | |||
{ | |||
'pr-1': !indicator, | |||
}, | |||
{ | |||
'pr-10': indicator && size === 'small', | |||
'pr-12': indicator && size === 'medium', | |||
'pr-16': indicator && size === 'large', | |||
}, | |||
)} | |||
> | |||
<span className="block w-full whitespace-nowrap h-[1.1em] overflow-hidden text-ellipsis"> | |||
{label} | |||
</span> | |||
</label> | |||
) | |||
} | |||
{hint && ( | |||
> | |||
<div | |||
data-testid="hint" | |||
className={clsx( | |||
'absolute left-0 px-1 pointer-events-none text-xxs peer-disabled:opacity-50 leading-none w-full bg-negative', | |||
{ | |||
'bottom-0 pl-4 pb-1': variant === 'default', | |||
'top-0.5': variant === 'alternate', | |||
}, | |||
{ | |||
'pt-2': variant === 'alternate' && size === 'small', | |||
'pt-3': variant === 'alternate' && size !== 'small', | |||
}, | |||
{ | |||
'pr-4': !indicator && variant === 'default', | |||
'pr-1': !indicator && variant === 'alternate', | |||
}, | |||
{ | |||
'pr-10': indicator && size === 'small', | |||
'pr-12': indicator && size === 'medium', | |||
'pr-16': indicator && size === 'large', | |||
}, | |||
)} | |||
> | |||
<div | |||
className="opacity-50 whitespace-nowrap w-full h-[1.1em] overflow-hidden text-ellipsis" | |||
> | |||
{hint} | |||
</div> | |||
</div> | |||
)} | |||
{indicator && ( | |||
<div | |||
className={clsx( | |||
'text-center flex items-center justify-center peer-disabled:opacity-50 aspect-square absolute bottom-0 right-0 pointer-events-none', | |||
{ | |||
'w-10': size === 'small', | |||
'w-12': size === 'medium', | |||
'w-16': size === 'large', | |||
}, | |||
)} | |||
className="opacity-50 whitespace-nowrap w-full h-[1.1em] overflow-hidden text-ellipsis" | |||
> | |||
{indicator} | |||
{hint} | |||
</div> | |||
)} | |||
{ | |||
border && ( | |||
<span | |||
data-testid="border" | |||
className="absolute z-[1] peer-disabled:opacity-50 inset-0 rounded-inherit border-2 border-primary pointer-events-none peer-focus:border-secondary" | |||
/> | |||
) | |||
} | |||
</div> | |||
); | |||
}, | |||
); | |||
</div> | |||
)} | |||
{indicator && ( | |||
<div | |||
className={clsx( | |||
'text-center flex items-center justify-center peer-disabled:opacity-50 aspect-square absolute bottom-0 right-0 pointer-events-none select-none', | |||
{ | |||
'w-10': size === 'small', | |||
'w-12': size === 'medium', | |||
'w-16': size === 'large', | |||
}, | |||
)} | |||
> | |||
{indicator} | |||
</div> | |||
)} | |||
{ | |||
border && ( | |||
<span | |||
data-testid="border" | |||
className="absolute z-[1] peer-disabled:opacity-50 inset-0 rounded-inherit border-2 border-primary pointer-events-none peer-focus:border-secondary" | |||
/> | |||
) | |||
} | |||
</div> | |||
); | |||
}); | |||
UrlInput.displayName = 'UrlInput'; | |||
UrlInput.defaultProps = { | |||
label: undefined, | |||
hint: undefined, | |||
indicator: undefined, | |||
size: 'medium', | |||
border: false, | |||
block: false, | |||
variant: 'default', | |||
hiddenLabel: false, | |||
}; |
@@ -1,5 +1,9 @@ | |||
{ | |||
"root": true, | |||
"rules": { | |||
"quote-props": "off", | |||
"react/jsx-props-no-spreading": "off" | |||
}, | |||
"extends": [ | |||
"lxsmnsyc/typescript/react" | |||
], | |||
@@ -42,155 +42,174 @@ export interface MaskedTextInputProps extends Omit<React.HTMLProps<MaskedTextInp | |||
/** | |||
* Component for inputting textual values. | |||
*/ | |||
export const MaskedTextInput = React.forwardRef<MaskedTextInputDerivedElement, MaskedTextInputProps>( | |||
( | |||
{ | |||
label, | |||
hint, | |||
indicator, | |||
size = 'medium' as const, | |||
border = false, | |||
block = false, | |||
variant = 'default' as const, | |||
hiddenLabel = false, | |||
className, | |||
...etcProps | |||
}: MaskedTextInputProps, | |||
ref, | |||
) => { | |||
const labelId = React.useId(); | |||
export const MaskedTextInput = React.forwardRef< | |||
MaskedTextInputDerivedElement, | |||
MaskedTextInputProps | |||
>(( | |||
{ | |||
label, | |||
hint, | |||
indicator, | |||
size = 'medium' as const, | |||
border = false, | |||
block = false, | |||
variant = 'default' as const, | |||
hiddenLabel = false, | |||
className, | |||
id: idProp, | |||
style, | |||
...etcProps | |||
}: MaskedTextInputProps, | |||
forwardedRef, | |||
) => { | |||
const labelId = React.useId(); | |||
const defaultId = React.useId(); | |||
const id = idProp ?? defaultId; | |||
return ( | |||
<div | |||
return ( | |||
<div | |||
className={clsx( | |||
'relative rounded ring-secondary/50 overflow-hidden', | |||
'focus-within:ring-4', | |||
{ | |||
'block': block, | |||
'inline-block align-middle': !block, | |||
}, | |||
className, | |||
)} | |||
style={style} | |||
> | |||
<input | |||
{...etcProps} | |||
ref={forwardedRef} | |||
id={id} | |||
aria-labelledby={labelId} | |||
type="password" | |||
data-testid="input" | |||
className={clsx( | |||
'relative rounded ring-secondary/50', | |||
'focus-within:ring-4', | |||
'bg-negative rounded-inherit w-full peer block', | |||
'focus:outline-0', | |||
'disabled:opacity-50 disabled:cursor-not-allowed', | |||
{ | |||
'text-xxs': size === 'small', | |||
'text-xs': size === 'medium', | |||
}, | |||
{ | |||
'pl-4': variant === 'default', | |||
'pl-1.5': variant === 'alternate', | |||
}, | |||
{ | |||
'pt-4': variant === 'alternate', | |||
}, | |||
{ | |||
'block': block, | |||
'inline-block align-middle': !block, | |||
'pr-4': variant === 'default' && !indicator, | |||
'pr-1.5': variant === 'alternate' && !indicator, | |||
}, | |||
{ | |||
'pr-10': indicator && size === 'small', | |||
'pr-12': indicator && size === 'medium', | |||
'pr-16': indicator && size === 'large', | |||
}, | |||
{ | |||
'h-10': size === 'small', | |||
'h-12': size === 'medium', | |||
'h-16': size === 'large', | |||
}, | |||
className, | |||
)} | |||
> | |||
<input | |||
{...etcProps} | |||
ref={ref} | |||
aria-labelledby={labelId} | |||
type="password" | |||
data-testid="input" | |||
/> | |||
{ | |||
label && ( | |||
<label | |||
data-testid="label" | |||
id={labelId} | |||
htmlFor={id} | |||
className={clsx( | |||
'absolute z-[1] w-full top-0.5 left-0 pointer-events-none pl-1 text-xxs font-bold peer-disabled:opacity-50 peer-focus:text-secondary text-primary leading-none bg-negative select-none', | |||
{ | |||
'sr-only': hiddenLabel, | |||
}, | |||
{ | |||
'pr-1': !indicator, | |||
}, | |||
{ | |||
'pr-10': indicator && size === 'small', | |||
'pr-12': indicator && size === 'medium', | |||
'pr-16': indicator && size === 'large', | |||
}, | |||
)} | |||
> | |||
<span className="block w-full whitespace-nowrap h-[1.1em] overflow-hidden text-ellipsis"> | |||
{label} | |||
</span> | |||
</label> | |||
) | |||
} | |||
{hint && ( | |||
<div | |||
data-testid="hint" | |||
className={clsx( | |||
'bg-negative rounded-inherit w-full peer block', | |||
'focus:outline-0', | |||
'disabled:opacity-50 disabled:cursor-not-allowed', | |||
{ | |||
'text-xxs': size === 'small', | |||
'text-xs': size === 'medium', | |||
}, | |||
'absolute left-0 px-1 pointer-events-none text-xxs peer-disabled:opacity-50 leading-none w-full bg-negative select-none', | |||
{ | |||
'pl-4': variant === 'default', | |||
'pl-1.5': variant === 'alternate', | |||
'bottom-0 pl-4 pb-1': variant === 'default', | |||
'top-0.5': variant === 'alternate', | |||
}, | |||
{ | |||
'pt-4': variant === 'alternate', | |||
'pt-2': variant === 'alternate' && size === 'small', | |||
'pt-3': variant === 'alternate' && size !== 'small', | |||
}, | |||
{ | |||
'pr-4': variant === 'default' && !indicator, | |||
'pr-1.5': variant === 'alternate' && !indicator, | |||
'pr-4': !indicator && variant === 'default', | |||
'pr-1': !indicator && variant === 'alternate', | |||
}, | |||
{ | |||
'pr-10': indicator && size === 'small', | |||
'pr-12': indicator && size === 'medium', | |||
'pr-16': indicator && size === 'large', | |||
}, | |||
{ | |||
'h-10': size === 'small', | |||
'h-12': size === 'medium', | |||
'h-16': size === 'large', | |||
}, | |||
)} | |||
/> | |||
{ | |||
label && ( | |||
<label | |||
data-testid="label" | |||
id={labelId} | |||
className={clsx( | |||
'absolute z-[1] w-full top-0.5 left-0 pointer-events-none pl-1 text-xxs font-bold peer-disabled:opacity-50 peer-focus:text-secondary text-primary leading-none bg-negative', | |||
{ | |||
'sr-only': hiddenLabel, | |||
}, | |||
{ | |||
'pr-1': !indicator, | |||
}, | |||
{ | |||
'pr-10': indicator && size === 'small', | |||
'pr-12': indicator && size === 'medium', | |||
'pr-16': indicator && size === 'large', | |||
}, | |||
)} | |||
> | |||
<span className="block w-full whitespace-nowrap h-[1.1em] overflow-hidden text-ellipsis"> | |||
{label} | |||
</span> | |||
</label> | |||
) | |||
} | |||
{hint && ( | |||
> | |||
<div | |||
data-testid="hint" | |||
className={clsx( | |||
'absolute left-0 px-1 pointer-events-none text-xxs peer-disabled:opacity-50 leading-none w-full bg-negative', | |||
{ | |||
'bottom-0 pl-4 pb-1': variant === 'default', | |||
'top-0.5': variant === 'alternate', | |||
}, | |||
{ | |||
'pt-2': variant === 'alternate' && size === 'small', | |||
'pt-3': variant === 'alternate' && size !== 'small', | |||
}, | |||
{ | |||
'pr-4': !indicator && variant === 'default', | |||
'pr-1': !indicator && variant === 'alternate', | |||
}, | |||
{ | |||
'pr-10': indicator && size === 'small', | |||
'pr-12': indicator && size === 'medium', | |||
'pr-16': indicator && size === 'large', | |||
}, | |||
)} | |||
> | |||
<div | |||
className="opacity-50 whitespace-nowrap w-full h-[1.1em] overflow-hidden text-ellipsis" | |||
> | |||
{hint} | |||
</div> | |||
</div> | |||
)} | |||
{indicator && ( | |||
<div | |||
className={clsx( | |||
'text-center flex items-center justify-center peer-disabled:opacity-50 aspect-square absolute bottom-0 right-0 pointer-events-none', | |||
{ | |||
'w-10': size === 'small', | |||
'w-12': size === 'medium', | |||
'w-16': size === 'large', | |||
}, | |||
)} | |||
className="opacity-50 whitespace-nowrap w-full h-[1.1em] overflow-hidden text-ellipsis" | |||
> | |||
{indicator} | |||
{hint} | |||
</div> | |||
)} | |||
{ | |||
border && ( | |||
<span | |||
data-testid="border" | |||
className="absolute z-[1] peer-disabled:opacity-50 inset-0 rounded-inherit border-2 border-primary pointer-events-none peer-focus:border-secondary" | |||
/> | |||
) | |||
} | |||
</div> | |||
); | |||
}, | |||
); | |||
</div> | |||
)} | |||
{indicator && ( | |||
<div | |||
className={clsx( | |||
'text-center flex items-center justify-center peer-disabled:opacity-50 aspect-square absolute bottom-0 right-0 pointer-events-none select-none', | |||
{ | |||
'w-10': size === 'small', | |||
'w-12': size === 'medium', | |||
'w-16': size === 'large', | |||
}, | |||
)} | |||
> | |||
{indicator} | |||
</div> | |||
)} | |||
{ | |||
border && ( | |||
<span | |||
data-testid="border" | |||
className="absolute z-[1] peer-disabled:opacity-50 inset-0 rounded-inherit border-2 border-primary pointer-events-none peer-focus:border-secondary" | |||
/> | |||
) | |||
} | |||
</div> | |||
); | |||
}); | |||
MaskedTextInput.displayName = 'MaskedTextInput'; | |||
MaskedTextInput.defaultProps = { | |||
label: undefined, | |||
hint: undefined, | |||
size: 'medium', | |||
indicator: undefined, | |||
border: false, | |||
block: false, | |||
variant: 'default', | |||
hiddenLabel: false, | |||
}; |
@@ -4,7 +4,7 @@ import clsx from 'clsx'; | |||
export type MultilineTextInputDerivedElement = HTMLTextAreaElement; | |||
export interface MultilineTextInputProps extends Omit<React.HTMLProps<MultilineTextInputDerivedElement>, 'size' | 'style' | 'label'> { | |||
export interface MultilineTextInputProps extends Omit<React.HTMLProps<MultilineTextInputDerivedElement>, 'size' | 'label' | 'inputMode'> { | |||
/** | |||
* Short textual description indicating the nature of the component's value. | |||
*/ | |||
@@ -44,168 +44,187 @@ export interface MultilineTextInputProps extends Omit<React.HTMLProps<MultilineT | |||
* | |||
* This component supports multiline input and adjusts its layout accordingly. | |||
*/ | |||
export const MultilineTextInput = React.forwardRef<MultilineTextInputDerivedElement, MultilineTextInputProps>( | |||
( | |||
{ | |||
label, | |||
hint, | |||
indicator, | |||
size = 'medium' as const, | |||
border = false, | |||
block = false, | |||
variant = 'default' as const, | |||
hiddenLabel = false, | |||
className, | |||
...etcProps | |||
}: MultilineTextInputProps, | |||
ref, | |||
) => { | |||
const labelId = React.useId(); | |||
export const MultilineTextInput = React.forwardRef< | |||
MultilineTextInputDerivedElement, | |||
MultilineTextInputProps | |||
>(( | |||
{ | |||
label, | |||
hint, | |||
indicator, | |||
size = 'medium' as const, | |||
border = false, | |||
block = false, | |||
variant = 'default' as const, | |||
hiddenLabel = false, | |||
className, | |||
style, | |||
id: idProp, | |||
...etcProps | |||
}: MultilineTextInputProps, | |||
forwardedRef, | |||
) => { | |||
const labelId = React.useId(); | |||
const defaultId = React.useId(); | |||
const id = idProp ?? defaultId; | |||
return ( | |||
<div | |||
return ( | |||
<div | |||
className={clsx( | |||
'relative rounded ring-secondary/50 overflow-hidden', | |||
'focus-within:ring-4', | |||
{ | |||
'block': block, | |||
'inline-block align-middle': !block, | |||
}, | |||
className, | |||
)} | |||
style={style} | |||
> | |||
<textarea | |||
{...etcProps} | |||
ref={forwardedRef} | |||
aria-labelledby={labelId} | |||
id={id} | |||
data-testid="input" | |||
style={{ | |||
height: 0, | |||
}} | |||
className={clsx( | |||
'relative rounded ring-secondary/50', | |||
'focus-within:ring-4', | |||
'bg-negative rounded-inherit w-full peer block', | |||
'focus:outline-0', | |||
'disabled:opacity-50 disabled:cursor-not-allowed', | |||
{ | |||
'resize': !block, | |||
'resize-y': block, | |||
}, | |||
{ | |||
'text-xxs': size === 'small', | |||
'text-xs': size === 'medium', | |||
}, | |||
{ | |||
'pl-4': variant === 'default', | |||
'pl-1.5': variant === 'alternate', | |||
}, | |||
{ | |||
'pt-4': variant === 'alternate' && size === 'small', | |||
'pt-5': variant === 'alternate' && size === 'medium', | |||
'pt-8': variant === 'alternate' && size === 'large', | |||
}, | |||
{ | |||
'py-2.5': variant === 'default' && size === 'small', | |||
'py-3': variant === 'default' && size === 'medium', | |||
'py-5': variant === 'default' && size === 'large', | |||
}, | |||
{ | |||
'pr-4': variant === 'default' && !indicator, | |||
'pr-1.5': variant === 'alternate' && !indicator, | |||
}, | |||
{ | |||
'pr-10': indicator && size === 'small', | |||
'pr-12': indicator && size === 'medium', | |||
'pr-16': indicator && size === 'large', | |||
}, | |||
{ | |||
'block': block, | |||
'inline-block align-middle': !block, | |||
'min-h-10': size === 'small', | |||
'min-h-12': size === 'medium', | |||
'min-h-16': size === 'large', | |||
}, | |||
className, | |||
)} | |||
> | |||
<textarea | |||
{...etcProps} | |||
ref={ref} | |||
aria-labelledby={labelId} | |||
data-testid="input" | |||
style={{ | |||
height: 0, | |||
}} | |||
/> | |||
{ | |||
label && ( | |||
<label | |||
data-testid="label" | |||
id={labelId} | |||
htmlFor={id} | |||
className={clsx( | |||
'absolute z-[1] w-full top-0.5 left-0 pointer-events-none pl-1 text-xxs font-bold peer-disabled:opacity-50 peer-focus:text-secondary text-primary leading-none bg-negative select-none', | |||
{ | |||
'sr-only': hiddenLabel, | |||
}, | |||
{ | |||
'pr-1': !indicator, | |||
}, | |||
{ | |||
'pr-10': indicator && size === 'small', | |||
'pr-12': indicator && size === 'medium', | |||
'pr-16': indicator && size === 'large', | |||
}, | |||
)} | |||
> | |||
<span className="block w-full whitespace-nowrap h-[1.1em] overflow-hidden text-ellipsis"> | |||
{label} | |||
</span> | |||
</label> | |||
) | |||
} | |||
{hint && ( | |||
<div | |||
data-testid="hint" | |||
className={clsx( | |||
'bg-negative rounded-inherit w-full peer block', | |||
'focus:outline-0', | |||
'disabled:opacity-50 disabled:cursor-not-allowed', | |||
{ | |||
'resize': !block, | |||
'resize-y': block, | |||
}, | |||
{ | |||
'text-xxs': size === 'small', | |||
'text-xs': size === 'medium', | |||
}, | |||
{ | |||
'pl-4': variant === 'default', | |||
'pl-1.5': variant === 'alternate', | |||
}, | |||
'absolute left-0 px-1 pointer-events-none text-xxs peer-disabled:opacity-50 leading-none w-full bg-negative select-none', | |||
{ | |||
'pt-4': variant === 'alternate' && size === 'small', | |||
'pt-5': variant === 'alternate' && size === 'medium', | |||
'pt-8': variant === 'alternate' && size === 'large', | |||
'bottom-0 pl-4 pb-1': variant === 'default', | |||
'top-0.5': variant === 'alternate', | |||
}, | |||
{ | |||
'py-2.5': variant === 'default' && size === 'small', | |||
'py-3': variant === 'default' && size === 'medium', | |||
'py-5': variant === 'default' && size === 'large', | |||
'pt-2': variant === 'alternate' && size === 'small', | |||
'pt-3': variant === 'alternate' && size !== 'small', | |||
}, | |||
{ | |||
'pr-4': variant === 'default' && !indicator, | |||
'pr-1.5': variant === 'alternate' && !indicator, | |||
'pr-4': !indicator && variant === 'default', | |||
'pr-1': !indicator && variant === 'alternate', | |||
}, | |||
{ | |||
'pr-10': indicator && size === 'small', | |||
'pr-12': indicator && size === 'medium', | |||
'pr-16': indicator && size === 'large', | |||
}, | |||
{ | |||
'min-h-10': size === 'small', | |||
'min-h-12': size === 'medium', | |||
'min-h-16': size === 'large', | |||
}, | |||
)} | |||
/> | |||
{ | |||
label && ( | |||
<label | |||
data-testid="label" | |||
id={labelId} | |||
className={clsx( | |||
'absolute z-[1] w-full top-0.5 left-0 pointer-events-none pl-1 text-xxs font-bold peer-disabled:opacity-50 peer-focus:text-secondary text-primary leading-none bg-negative', | |||
{ | |||
'sr-only': hiddenLabel, | |||
}, | |||
{ | |||
'pr-1': !indicator, | |||
}, | |||
{ | |||
'pr-10': indicator && size === 'small', | |||
'pr-12': indicator && size === 'medium', | |||
'pr-16': indicator && size === 'large', | |||
}, | |||
)} | |||
> | |||
<span className="block w-full whitespace-nowrap h-[1.1em] overflow-hidden text-ellipsis"> | |||
{label} | |||
</span> | |||
</label> | |||
) | |||
} | |||
{hint && ( | |||
<div | |||
data-testid="hint" | |||
className={clsx( | |||
'absolute left-0 px-1 pointer-events-none text-xxs peer-disabled:opacity-50 leading-none w-full bg-negative', | |||
{ | |||
'bottom-0 pl-4 pb-1': variant === 'default', | |||
'top-0.5': variant === 'alternate', | |||
}, | |||
{ | |||
'pt-2': variant === 'alternate' && size === 'small', | |||
'pt-3': variant === 'alternate' && size !== 'small', | |||
}, | |||
{ | |||
'pr-4': !indicator && variant === 'default', | |||
'pr-1': !indicator && variant === 'alternate', | |||
}, | |||
{ | |||
'pr-10': indicator && size === 'small', | |||
'pr-12': indicator && size === 'medium', | |||
'pr-16': indicator && size === 'large', | |||
}, | |||
)} | |||
> | |||
<div | |||
className="opacity-50 whitespace-nowrap w-full h-[1.1em] overflow-hidden text-ellipsis" | |||
> | |||
{hint} | |||
</div> | |||
</div> | |||
)} | |||
{indicator && ( | |||
> | |||
<div | |||
className={clsx( | |||
'text-center flex items-center justify-center peer-disabled:opacity-50 aspect-square absolute bottom-0 right-0 pointer-events-none', | |||
{ | |||
'w-10': size === 'small', | |||
'w-12': size === 'medium', | |||
'w-16': size === 'large', | |||
}, | |||
)} | |||
className="opacity-50 whitespace-nowrap w-full h-[1.1em] overflow-hidden text-ellipsis" | |||
> | |||
{indicator} | |||
{hint} | |||
</div> | |||
)} | |||
{ | |||
border && ( | |||
<span | |||
data-testid="border" | |||
className="absolute z-[1] peer-disabled:opacity-50 inset-0 rounded-inherit border-2 border-primary pointer-events-none peer-focus:border-secondary" | |||
/> | |||
) | |||
} | |||
</div> | |||
); | |||
} | |||
); | |||
</div> | |||
)} | |||
{indicator && ( | |||
<div | |||
className={clsx( | |||
'text-center flex items-center justify-center peer-disabled:opacity-50 aspect-square absolute bottom-0 right-0 pointer-events-none select-none', | |||
{ | |||
'w-10': size === 'small', | |||
'w-12': size === 'medium', | |||
'w-16': size === 'large', | |||
}, | |||
)} | |||
> | |||
{indicator} | |||
</div> | |||
)} | |||
{ | |||
border && ( | |||
<span | |||
data-testid="border" | |||
className="absolute z-[1] peer-disabled:opacity-50 inset-0 rounded-inherit border-2 border-primary pointer-events-none peer-focus:border-secondary" | |||
/> | |||
) | |||
} | |||
</div> | |||
); | |||
}); | |||
MultilineTextInput.displayName = 'MultilineTextInput'; | |||
MultilineTextInput.defaultProps = { | |||
label: undefined, | |||
hint: undefined, | |||
size: 'medium', | |||
indicator: undefined, | |||
border: false, | |||
block: false, | |||
variant: 'default', | |||
hiddenLabel: false, | |||
}; |
@@ -4,7 +4,7 @@ import clsx from 'clsx'; | |||
export type TextInputDerivedElement = HTMLInputElement; | |||
export interface TextInputProps extends Omit<React.HTMLProps<TextInputDerivedElement>, 'size' | 'type' | 'style' | 'label' | 'list'> { | |||
export interface TextInputProps extends Omit<React.HTMLProps<TextInputDerivedElement>, 'size' | 'type' | 'label' | 'list' | 'inputMode'> { | |||
/** | |||
* Short textual description indicating the nature of the component's value. | |||
*/ | |||
@@ -41,6 +41,10 @@ export interface TextInputProps extends Omit<React.HTMLProps<TextInputDerivedEle | |||
* Is the label hidden? | |||
*/ | |||
hiddenLabel?: boolean, | |||
/** | |||
* Input mode of the component. | |||
*/ | |||
inputMode?: TextControl.InputMode, | |||
} | |||
/** | |||
@@ -48,156 +52,183 @@ export interface TextInputProps extends Omit<React.HTMLProps<TextInputDerivedEle | |||
* | |||
* This component supports multiline input and adjusts its layout accordingly. | |||
*/ | |||
export const TextInput = React.forwardRef<TextInputDerivedElement, TextInputProps>( | |||
( | |||
{ | |||
label = '', | |||
hint = '', | |||
indicator, | |||
size = 'medium' as const, | |||
border = false, | |||
block = false, | |||
type = 'text' as const, | |||
variant = 'default' as const, | |||
hiddenLabel = false, | |||
className, | |||
...etcProps | |||
}: TextInputProps, | |||
ref, | |||
) => { | |||
const labelId = React.useId(); | |||
export const TextInput = React.forwardRef<TextInputDerivedElement, TextInputProps>(( | |||
{ | |||
label, | |||
hint, | |||
indicator, | |||
size = 'medium' as const, | |||
border = false, | |||
block = false, | |||
type = 'text' as const, | |||
variant = 'default' as const, | |||
hiddenLabel = false, | |||
className, | |||
id: idProp, | |||
style, | |||
inputMode = type, | |||
...etcProps | |||
}, | |||
forwardedRef, | |||
) => { | |||
const labelId = React.useId(); | |||
const defaultId = React.useId(); | |||
const id = idProp ?? defaultId; | |||
let resultInputMode = inputMode as React.HTMLProps<TextInputDerivedElement>['inputMode']; | |||
if (type === 'text' && resultInputMode === 'search') { | |||
resultInputMode = 'text'; | |||
} else if (type === 'search' && resultInputMode === 'text') { | |||
resultInputMode = 'search'; | |||
} | |||
return ( | |||
<div | |||
return ( | |||
<div | |||
className={clsx( | |||
'relative rounded ring-secondary/50 overflow-hidden', | |||
'focus-within:ring-4', | |||
{ | |||
'block': block, | |||
'inline-block align-middle': !block, | |||
}, | |||
className, | |||
)} | |||
style={style} | |||
> | |||
<input | |||
{...etcProps} | |||
ref={forwardedRef} | |||
aria-labelledby={labelId} | |||
id={id} | |||
type={type} | |||
inputMode={resultInputMode} | |||
data-testid="input" | |||
className={clsx( | |||
'relative rounded ring-secondary/50', | |||
'focus-within:ring-4', | |||
'bg-negative rounded-inherit w-full peer block', | |||
'focus:outline-0', | |||
'disabled:opacity-50 disabled:cursor-not-allowed', | |||
{ | |||
'text-xxs': size === 'small', | |||
'text-xs': size === 'medium', | |||
}, | |||
{ | |||
'pl-4': variant === 'default', | |||
'pl-1.5': variant === 'alternate', | |||
}, | |||
{ | |||
'pt-4': variant === 'alternate', | |||
}, | |||
{ | |||
'pr-4': variant === 'default' && !indicator, | |||
'pr-1.5': variant === 'alternate' && !indicator, | |||
}, | |||
{ | |||
'pr-10': indicator && size === 'small', | |||
'pr-12': indicator && size === 'medium', | |||
'pr-16': indicator && size === 'large', | |||
}, | |||
{ | |||
'block': block, | |||
'inline-block align-middle': !block, | |||
'h-10': size === 'small', | |||
'h-12': size === 'medium', | |||
'h-16': size === 'large', | |||
}, | |||
className, | |||
)} | |||
> | |||
<input | |||
{...etcProps} | |||
ref={ref} | |||
aria-labelledby={labelId} | |||
type={type} | |||
data-testid="input" | |||
/> | |||
{ | |||
label && ( | |||
<label | |||
data-testid="label" | |||
htmlFor={id} | |||
id={labelId} | |||
className={clsx( | |||
'absolute z-[1] w-full top-0.5 left-0 pointer-events-none pl-1 text-xxs font-bold peer-disabled:opacity-50 peer-focus:text-secondary text-primary leading-none bg-negative select-none', | |||
{ | |||
'sr-only': hiddenLabel, | |||
}, | |||
{ | |||
'pr-1': !indicator, | |||
}, | |||
{ | |||
'pr-10': indicator && size === 'small', | |||
'pr-12': indicator && size === 'medium', | |||
'pr-16': indicator && size === 'large', | |||
}, | |||
)} | |||
> | |||
<span className="block w-full whitespace-nowrap h-[1.1em] overflow-hidden text-ellipsis"> | |||
{label} | |||
</span> | |||
</label> | |||
) | |||
} | |||
{hint && ( | |||
<div | |||
data-testid="hint" | |||
className={clsx( | |||
'bg-negative rounded-inherit w-full peer block', | |||
'focus:outline-0', | |||
'disabled:opacity-50 disabled:cursor-not-allowed', | |||
'absolute left-0 px-1 pointer-events-none text-xxs peer-disabled:opacity-50 leading-none w-full bg-negative select-none', | |||
{ | |||
'text-xxs': size === 'small', | |||
'text-xs': size === 'medium', | |||
'bottom-0 pl-4 pb-1': variant === 'default', | |||
'top-0.5': variant === 'alternate', | |||
}, | |||
{ | |||
'pl-4': variant === 'default', | |||
'pl-1.5': variant === 'alternate', | |||
'pt-2': variant === 'alternate' && size === 'small', | |||
'pt-3': variant === 'alternate' && size !== 'small', | |||
}, | |||
{ | |||
'pt-4': variant === 'alternate', | |||
}, | |||
{ | |||
'pr-4': variant === 'default' && !indicator, | |||
'pr-1.5': variant === 'alternate' && !indicator, | |||
'pr-4': !indicator && variant === 'default', | |||
'pr-1': !indicator && variant === 'alternate', | |||
}, | |||
{ | |||
'pr-10': indicator && size === 'small', | |||
'pr-12': indicator && size === 'medium', | |||
'pr-16': indicator && size === 'large', | |||
}, | |||
{ | |||
'h-10': size === 'small', | |||
'h-12': size === 'medium', | |||
'h-16': size === 'large', | |||
}, | |||
)} | |||
/> | |||
{ | |||
label && ( | |||
<label | |||
data-testid="label" | |||
id={labelId} | |||
className={clsx( | |||
'absolute z-[1] w-full top-0.5 left-0 pointer-events-none pl-1 text-xxs font-bold peer-disabled:opacity-50 peer-focus:text-secondary text-primary leading-none bg-negative', | |||
{ | |||
'sr-only': hiddenLabel, | |||
}, | |||
{ | |||
'pr-1': !indicator, | |||
}, | |||
{ | |||
'pr-10': indicator && size === 'small', | |||
'pr-12': indicator && size === 'medium', | |||
'pr-16': indicator && size === 'large', | |||
}, | |||
)} | |||
> | |||
<span className="block w-full whitespace-nowrap h-[1.1em] overflow-hidden text-ellipsis"> | |||
{label} | |||
</span> | |||
</label> | |||
) | |||
} | |||
{hint && ( | |||
<div | |||
data-testid="hint" | |||
className={clsx( | |||
'absolute left-0 px-1 pointer-events-none text-xxs peer-disabled:opacity-50 leading-none w-full bg-negative', | |||
{ | |||
'bottom-0 pl-4 pb-1': variant === 'default', | |||
'top-0.5': variant === 'alternate', | |||
}, | |||
{ | |||
'pt-2': variant === 'alternate' && size === 'small', | |||
'pt-3': variant === 'alternate' && size !== 'small', | |||
}, | |||
{ | |||
'pr-4': !indicator && variant === 'default', | |||
'pr-1': !indicator && variant === 'alternate', | |||
}, | |||
{ | |||
'pr-10': indicator && size === 'small', | |||
'pr-12': indicator && size === 'medium', | |||
'pr-16': indicator && size === 'large', | |||
}, | |||
)} | |||
> | |||
<div | |||
className="opacity-50 whitespace-nowrap w-full h-[1.1em] overflow-hidden text-ellipsis" | |||
> | |||
{hint} | |||
</div> | |||
</div> | |||
)} | |||
{indicator && ( | |||
> | |||
<div | |||
className={clsx( | |||
'text-center flex items-center justify-center peer-disabled:opacity-50 aspect-square absolute bottom-0 right-0 pointer-events-none', | |||
{ | |||
'w-10': size === 'small', | |||
'w-12': size === 'medium', | |||
'w-16': size === 'large', | |||
}, | |||
)} | |||
className="opacity-50 whitespace-nowrap w-full h-[1.1em] overflow-hidden text-ellipsis" | |||
> | |||
{indicator} | |||
{hint} | |||
</div> | |||
)} | |||
{ | |||
border && ( | |||
<span | |||
data-testid="border" | |||
className="absolute z-[1] peer-disabled:opacity-50 inset-0 rounded-inherit border-2 border-primary pointer-events-none peer-focus:border-secondary" | |||
/> | |||
) | |||
} | |||
</div> | |||
); | |||
} | |||
); | |||
</div> | |||
)} | |||
{indicator && ( | |||
<div | |||
className={clsx( | |||
'text-center flex items-center justify-center peer-disabled:opacity-50 aspect-square absolute bottom-0 right-0 pointer-events-none select-none', | |||
{ | |||
'w-10': size === 'small', | |||
'w-12': size === 'medium', | |||
'w-16': size === 'large', | |||
}, | |||
)} | |||
> | |||
{indicator} | |||
</div> | |||
)} | |||
{ | |||
border && ( | |||
<span | |||
data-testid="border" | |||
className="absolute z-[1] peer-disabled:opacity-50 inset-0 rounded-inherit border-2 border-primary pointer-events-none peer-focus:border-secondary" | |||
/> | |||
) | |||
} | |||
</div> | |||
); | |||
}); | |||
TextInput.displayName = 'TextInput'; | |||
TextInput.defaultProps = { | |||
label: undefined, | |||
hint: undefined, | |||
size: 'medium', | |||
indicator: undefined, | |||
border: false, | |||
block: false, | |||
type: 'text', | |||
variant: 'default', | |||
hiddenLabel: false, | |||
inputMode: 'text', | |||
}; |
@@ -1,5 +1,8 @@ | |||
{ | |||
"root": true, | |||
"rules": { | |||
"react/jsx-props-no-spreading": "off" | |||
}, | |||
"extends": [ | |||
"lxsmnsyc/typescript/react" | |||
], | |||
@@ -7,12 +7,15 @@ export interface BadgeProps extends React.HTMLProps<BadgeDerivedElement> { | |||
rounded?: boolean; | |||
} | |||
export const Badge = React.forwardRef<BadgeDerivedElement, BadgeProps>(({ | |||
children, | |||
rounded = false, | |||
className, | |||
...etcProps | |||
}, forwardedRef) => ( | |||
export const Badge = React.forwardRef<BadgeDerivedElement, BadgeProps>(( | |||
{ | |||
children, | |||
rounded = false, | |||
className, | |||
...etcProps | |||
}, | |||
forwardedRef, | |||
) => ( | |||
<span | |||
{...etcProps} | |||
ref={forwardedRef} | |||
@@ -26,10 +29,14 @@ export const Badge = React.forwardRef<BadgeDerivedElement, BadgeProps>(({ | |||
className, | |||
)} | |||
> | |||
<span className="relative w-full"> | |||
{children} | |||
</span> | |||
<span className="relative w-full"> | |||
{children} | |||
</span> | |||
</span> | |||
)); | |||
Badge.displayName = 'Badge'; | |||
Badge.defaultProps = { | |||
rounded: false, | |||
}; |
@@ -14,11 +14,14 @@ export interface KeyValueTableProps extends Omit<React.HTMLProps<KeyValueTableDe | |||
properties?: (KeyValueProperty | boolean)[]; | |||
} | |||
export const KeyValueTable = React.forwardRef<KeyValueTableDerivedElement, KeyValueTableProps>(({ | |||
hiddenKeys = false, | |||
properties = [], | |||
...etcProps | |||
}, forwardedRef) => ( | |||
export const KeyValueTable = React.forwardRef<KeyValueTableDerivedElement, KeyValueTableProps>(( | |||
{ | |||
hiddenKeys = false, | |||
properties = [], | |||
...etcProps | |||
}, | |||
forwardedRef, | |||
) => ( | |||
<dl | |||
{...etcProps} | |||
className={clsx( | |||
@@ -55,3 +58,8 @@ export const KeyValueTable = React.forwardRef<KeyValueTableDerivedElement, KeyVa | |||
)); | |||
KeyValueTable.displayName = 'KeyValueTable'; | |||
KeyValueTable.defaultProps = { | |||
hiddenKeys: false, | |||
properties: [], | |||
}; |
@@ -1,5 +1,9 @@ | |||
{ | |||
"root": true, | |||
"rules": { | |||
"quote-props": "off", | |||
"react/jsx-props-no-spreading": "off" | |||
}, | |||
"extends": [ | |||
"lxsmnsyc/typescript/react" | |||
], | |||
@@ -37,6 +37,9 @@ export interface MenuMultiSelectProps extends Omit<React.HTMLProps<MenuMultiSele | |||
* Is the label hidden? | |||
*/ | |||
hiddenLabel?: boolean, | |||
/** | |||
* Starting height of the component. | |||
*/ | |||
startingHeight?: number | string, | |||
} | |||
@@ -45,171 +48,189 @@ export interface MenuMultiSelectProps extends Omit<React.HTMLProps<MenuMultiSele | |||
* | |||
* This component supports multiline input and adjusts its layout accordingly. | |||
*/ | |||
export const MenuMultiSelect = React.forwardRef<MenuMultiSelectDerivedElement, MenuMultiSelectProps>( | |||
( | |||
{ | |||
label, | |||
hint, | |||
indicator, | |||
size = 'medium' as const, | |||
border = false, | |||
block = false, | |||
variant = 'default' as const, | |||
hiddenLabel = false, | |||
className, | |||
startingHeight = '15rem', | |||
...etcProps | |||
}: MenuMultiSelectProps, | |||
ref, | |||
) => { | |||
const labelId = React.useId(); | |||
export const MenuMultiSelect = React.forwardRef< | |||
MenuMultiSelectDerivedElement, | |||
MenuMultiSelectProps | |||
>(( | |||
{ | |||
label, | |||
hint, | |||
indicator, | |||
size = 'medium' as const, | |||
border = false, | |||
block = false, | |||
variant = 'default' as const, | |||
hiddenLabel = false, | |||
className, | |||
startingHeight = '15rem', | |||
id: idProp, | |||
...etcProps | |||
}, | |||
forwardedRef, | |||
) => { | |||
const labelId = React.useId(); | |||
const defaultId = React.useId(); | |||
const id = idProp ?? defaultId; | |||
return ( | |||
<div | |||
return ( | |||
<div | |||
className={clsx( | |||
'relative rounded ring-secondary/50', | |||
'focus-within:ring-4', | |||
{ | |||
'block': block, | |||
'inline-block align-middle': !block, | |||
}, | |||
className, | |||
)} | |||
> | |||
<select | |||
{...etcProps} | |||
ref={forwardedRef} | |||
id={id} | |||
aria-labelledby={labelId} | |||
data-testid="input" | |||
size={2} | |||
multiple | |||
style={{ | |||
height: startingHeight, | |||
}} | |||
className={clsx( | |||
'relative rounded ring-secondary/50', | |||
'focus-within:ring-4', | |||
'bg-negative rounded-inherit w-full peer block overflow-auto', | |||
'focus:outline-0', | |||
'disabled:opacity-50 disabled:cursor-not-allowed', | |||
{ | |||
'resize': !block, | |||
'resize-y': block, | |||
}, | |||
{ | |||
'text-xxs': size === 'small', | |||
'text-xs': size === 'medium', | |||
}, | |||
{ | |||
'pl-4': variant === 'default', | |||
'pl-1.5': variant === 'alternate', | |||
}, | |||
{ | |||
'pt-4': variant === 'alternate' && size === 'small', | |||
'pt-5': variant === 'alternate' && size === 'medium', | |||
'pt-8': variant === 'alternate' && size === 'large', | |||
}, | |||
{ | |||
'py-2.5': variant === 'default' && size === 'small', | |||
'py-3': variant === 'default' && size === 'medium', | |||
'py-5': variant === 'default' && size === 'large', | |||
}, | |||
{ | |||
'pr-4': variant === 'default' && !indicator, | |||
'pr-1.5': variant === 'alternate' && !indicator, | |||
}, | |||
{ | |||
'pr-10': indicator && size === 'small', | |||
'pr-12': indicator && size === 'medium', | |||
'pr-16': indicator && size === 'large', | |||
}, | |||
{ | |||
'block': block, | |||
'inline-block align-middle': !block, | |||
'min-h-10': size === 'small', | |||
'min-h-12': size === 'medium', | |||
'min-h-16': size === 'large', | |||
}, | |||
className, | |||
)} | |||
> | |||
<select | |||
{...etcProps} | |||
ref={ref} | |||
aria-labelledby={labelId} | |||
data-testid="input" | |||
size={2} | |||
multiple | |||
style={{ | |||
height: startingHeight, | |||
}} | |||
/> | |||
{ | |||
label && ( | |||
<label | |||
data-testid="label" | |||
id={labelId} | |||
htmlFor={id} | |||
className={clsx( | |||
'absolute z-[1] w-full top-0.5 left-0 pointer-events-none pl-1 text-xxs font-bold peer-disabled:opacity-50 peer-focus:text-secondary text-primary leading-none bg-negative', | |||
{ | |||
'sr-only': hiddenLabel, | |||
}, | |||
{ | |||
'pr-1': !indicator, | |||
}, | |||
{ | |||
'pr-10': indicator && size === 'small', | |||
'pr-12': indicator && size === 'medium', | |||
'pr-16': indicator && size === 'large', | |||
}, | |||
)} | |||
> | |||
<span className="block w-full whitespace-nowrap h-[1.1em] overflow-hidden text-ellipsis"> | |||
{label} | |||
</span> | |||
</label> | |||
) | |||
} | |||
{hint && ( | |||
<div | |||
data-testid="hint" | |||
className={clsx( | |||
'bg-negative rounded-inherit w-full peer block overflow-auto', | |||
'focus:outline-0', | |||
'disabled:opacity-50 disabled:cursor-not-allowed', | |||
{ | |||
'resize': !block, | |||
'resize-y': block, | |||
}, | |||
{ | |||
'text-xxs': size === 'small', | |||
'text-xs': size === 'medium', | |||
}, | |||
'absolute left-0 px-1 pointer-events-none text-xxs peer-disabled:opacity-50 leading-none w-full bg-negative', | |||
{ | |||
'pl-4': variant === 'default', | |||
'pl-1.5': variant === 'alternate', | |||
'bottom-0 pl-4 pb-1': variant === 'default', | |||
'top-0.5': variant === 'alternate', | |||
}, | |||
{ | |||
'pt-4': variant === 'alternate' && size === 'small', | |||
'pt-5': variant === 'alternate' && size === 'medium', | |||
'pt-8': variant === 'alternate' && size === 'large', | |||
'pt-2': variant === 'alternate' && size === 'small', | |||
'pt-3': variant === 'alternate' && size !== 'small', | |||
}, | |||
{ | |||
'py-2.5': variant === 'default' && size === 'small', | |||
'py-3': variant === 'default' && size === 'medium', | |||
'py-5': variant === 'default' && size === 'large', | |||
}, | |||
{ | |||
'pr-4': variant === 'default' && !indicator, | |||
'pr-1.5': variant === 'alternate' && !indicator, | |||
'pr-4': !indicator && variant === 'default', | |||
'pr-1': !indicator && variant === 'alternate', | |||
}, | |||
{ | |||
'pr-10': indicator && size === 'small', | |||
'pr-12': indicator && size === 'medium', | |||
'pr-16': indicator && size === 'large', | |||
}, | |||
{ | |||
'min-h-10': size === 'small', | |||
'min-h-12': size === 'medium', | |||
'min-h-16': size === 'large', | |||
}, | |||
)} | |||
/> | |||
{ | |||
label && ( | |||
<label | |||
data-testid="label" | |||
id={labelId} | |||
className={clsx( | |||
'absolute z-[1] w-full top-0.5 left-0 pointer-events-none pl-1 text-xxs font-bold peer-disabled:opacity-50 peer-focus:text-secondary text-primary leading-none bg-negative', | |||
{ | |||
'sr-only': hiddenLabel, | |||
}, | |||
{ | |||
'pr-1': !indicator, | |||
}, | |||
{ | |||
'pr-10': indicator && size === 'small', | |||
'pr-12': indicator && size === 'medium', | |||
'pr-16': indicator && size === 'large', | |||
}, | |||
)} | |||
> | |||
<span className="block w-full whitespace-nowrap h-[1.1em] overflow-hidden text-ellipsis"> | |||
{label} | |||
</span> | |||
</label> | |||
) | |||
} | |||
{hint && ( | |||
> | |||
<div | |||
data-testid="hint" | |||
className={clsx( | |||
'absolute left-0 px-1 pointer-events-none text-xxs peer-disabled:opacity-50 leading-none w-full bg-negative', | |||
{ | |||
'bottom-0 pl-4 pb-1': variant === 'default', | |||
'top-0.5': variant === 'alternate', | |||
}, | |||
{ | |||
'pt-2': variant === 'alternate' && size === 'small', | |||
'pt-3': variant === 'alternate' && size !== 'small', | |||
}, | |||
{ | |||
'pr-4': !indicator && variant === 'default', | |||
'pr-1': !indicator && variant === 'alternate', | |||
}, | |||
{ | |||
'pr-10': indicator && size === 'small', | |||
'pr-12': indicator && size === 'medium', | |||
'pr-16': indicator && size === 'large', | |||
}, | |||
)} | |||
> | |||
<div | |||
className="opacity-50 whitespace-nowrap w-full h-[1.1em] overflow-hidden text-ellipsis" | |||
> | |||
{hint} | |||
</div> | |||
</div> | |||
)} | |||
{indicator && ( | |||
<div | |||
className={clsx( | |||
'text-center flex items-center justify-center peer-disabled:opacity-50 aspect-square absolute bottom-0 right-0 pointer-events-none', | |||
{ | |||
'w-10': size === 'small', | |||
'w-12': size === 'medium', | |||
'w-16': size === 'large', | |||
}, | |||
)} | |||
className="opacity-50 whitespace-nowrap w-full h-[1.1em] overflow-hidden text-ellipsis" | |||
> | |||
{indicator} | |||
{hint} | |||
</div> | |||
)} | |||
{ | |||
border && ( | |||
<span | |||
data-testid="border" | |||
className="absolute z-[1] peer-disabled:opacity-50 inset-0 rounded-inherit border-2 border-primary pointer-events-none peer-focus:border-secondary" | |||
/> | |||
) | |||
} | |||
</div> | |||
); | |||
} | |||
); | |||
</div> | |||
)} | |||
{indicator && ( | |||
<div | |||
className={clsx( | |||
'text-center flex items-center justify-center peer-disabled:opacity-50 aspect-square absolute bottom-0 right-0 pointer-events-none', | |||
{ | |||
'w-10': size === 'small', | |||
'w-12': size === 'medium', | |||
'w-16': size === 'large', | |||
}, | |||
)} | |||
> | |||
{indicator} | |||
</div> | |||
)} | |||
{ | |||
border && ( | |||
<span | |||
data-testid="border" | |||
className="absolute z-[1] peer-disabled:opacity-50 inset-0 rounded-inherit border-2 border-primary pointer-events-none peer-focus:border-secondary" | |||
/> | |||
) | |||
} | |||
</div> | |||
); | |||
}); | |||
MenuMultiSelect.displayName = 'MenuMultiSelect'; | |||
MenuMultiSelect.defaultProps = { | |||
label: undefined, | |||
hint: undefined, | |||
indicator: undefined, | |||
size: 'medium', | |||
border: false, | |||
block: false, | |||
variant: 'default', | |||
hiddenLabel: false, | |||
startingHeight: '15rem', | |||
}; |
@@ -1,10 +1,16 @@ | |||
import * as React from 'react'; | |||
import { TagsInput } from 'react-tag-input-component'; | |||
import clsx from 'clsx'; | |||
import {useClientSide, delegateTriggerEvent} from '@modal-sh/react-utils'; | |||
import { useClientSide, delegateTriggerEvent } from '@modal-sh/react-utils'; | |||
import { TextControl } from '@tesseract-design/web-base'; | |||
export type TagInputSeparator = ',' | '\n'; | |||
const TAG_INPUT_SEPARATOR_MAP = { | |||
'comma': ',', | |||
'newline': 'Enter', | |||
'semicolon': ';', | |||
} as const; | |||
export type TagInputSeparator = keyof typeof TAG_INPUT_SEPARATOR_MAP | |||
export type TagInputDerivedElement = HTMLTextAreaElement; | |||
@@ -41,7 +47,13 @@ export interface TagInputProps extends Omit<React.HTMLProps<TagInputDerivedEleme | |||
* Is the label hidden? | |||
*/ | |||
hiddenLabel?: boolean, | |||
/** | |||
* Should the component be enhanced? | |||
*/ | |||
enhanced?: boolean, | |||
/** | |||
* Separators for splitting the input value into multiple tags. | |||
*/ | |||
separator?: TagInputSeparator[], | |||
} | |||
@@ -50,205 +62,182 @@ export interface TagInputProps extends Omit<React.HTMLProps<TagInputDerivedEleme | |||
* | |||
* This component supports multiline input and adjusts its layout accordingly. | |||
*/ | |||
export const TagInput = React.forwardRef<TagInputDerivedElement, TagInputProps>( | |||
( | |||
{ | |||
label = '', | |||
hint = '', | |||
indicator, | |||
size = 'medium' as const, | |||
border = false, | |||
block = false, | |||
variant = 'default' as const, | |||
hiddenLabel = false, | |||
className, | |||
enhanced: enhancedProp = false, | |||
separator = ['\n'], | |||
defaultValue, | |||
disabled, | |||
...etcProps | |||
}: TagInputProps, | |||
forwardedRef, | |||
) => { | |||
const {clientSide} = useClientSide({ clientSide: enhancedProp }); | |||
const defaultRef = React.useRef<TagInputDerivedElement>(null); | |||
const ref = forwardedRef ?? defaultRef; | |||
const labelId = React.useId(); | |||
const tags = React.useMemo(() => { | |||
if (defaultValue === undefined) { | |||
return []; | |||
} | |||
if (typeof defaultValue === 'string') { | |||
return separator.reduce((theDefaultValues, separator) => { | |||
return theDefaultValues.join(separator).split(separator); | |||
}, [defaultValue]); | |||
} | |||
if (typeof defaultValue === 'number') { | |||
return [defaultValue.toString()]; | |||
} | |||
return defaultValue as string[]; | |||
}, [defaultValue, separator]); | |||
export const TagInput = React.forwardRef<TagInputDerivedElement, TagInputProps>(( | |||
{ | |||
label, | |||
hint, | |||
indicator, | |||
size = 'medium' as const, | |||
border = false, | |||
block = false, | |||
variant = 'default' as const, | |||
hiddenLabel = false, | |||
className, | |||
enhanced: enhancedProp = false, | |||
separator = ['newline'], | |||
defaultValue, | |||
disabled, | |||
id: idProp, | |||
...etcProps | |||
}, | |||
forwardedRef, | |||
) => { | |||
const { clientSide } = useClientSide({ clientSide: enhancedProp }); | |||
const defaultRef = React.useRef<TagInputDerivedElement>(null); | |||
const ref = forwardedRef ?? defaultRef; | |||
const labelId = React.useId(); | |||
const defaultId = React.useId(); | |||
const id = idProp ?? defaultId; | |||
const tags = React.useMemo(() => { | |||
if (defaultValue === undefined) { | |||
return []; | |||
} | |||
if (typeof defaultValue === 'string') { | |||
return separator.reduce((theDefaultValues, s) => { | |||
if (s === 'newline') { | |||
return theDefaultValues.join('\n').split('\n'); | |||
} | |||
return theDefaultValues.join(s).split(s); | |||
}, [defaultValue]); | |||
} | |||
if (typeof defaultValue === 'number') { | |||
return [defaultValue.toString()]; | |||
} | |||
return defaultValue as string[]; | |||
}, [defaultValue, separator]); | |||
const handleTagsInputChange = (tags: string[]) => { | |||
if (!(typeof ref === 'object' && ref)) { | |||
return; | |||
} | |||
const { current: input } = ref; | |||
if (!input) { | |||
return; | |||
} | |||
setTimeout(() => { | |||
delegateTriggerEvent('change', input, tags.map((t => t.trim())).join('\n')); | |||
}); | |||
}; | |||
const handleTagsInputChange = (newTags: string[]) => { | |||
if (!(typeof ref === 'object' && ref)) { | |||
return; | |||
} | |||
const { current: input } = ref; | |||
if (!input) { | |||
return; | |||
} | |||
setTimeout(() => { | |||
delegateTriggerEvent('change', input, newTags.map(((t) => t.trim())).join('\n')); | |||
}); | |||
}; | |||
const handleBlur = () => { | |||
if (!(typeof ref === 'object' && ref)) { | |||
return; | |||
} | |||
const { current: input } = ref; | |||
if (!input) { | |||
return; | |||
} | |||
setTimeout(() => { | |||
delegateTriggerEvent('blur', input); | |||
}); | |||
const handleBlur = () => { | |||
if (!(typeof ref === 'object' && ref)) { | |||
return; | |||
} | |||
const { current: input } = ref; | |||
if (!input) { | |||
return; | |||
} | |||
setTimeout(() => { | |||
delegateTriggerEvent('blur', input); | |||
}); | |||
}; | |||
return ( | |||
<div | |||
return ( | |||
<div | |||
className={clsx( | |||
'relative rounded ring-secondary/50 group', | |||
'focus-within:ring-4', | |||
{ | |||
'block': block, | |||
'inline-block align-middle': !block, | |||
}, | |||
clientSide && { | |||
'min-h-10': size === 'small', | |||
'min-h-12': size === 'medium', | |||
'min-h-16': size === 'large', | |||
}, | |||
'tesseract-design-tag-input', | |||
size === 'small' && 'tag-input-small', | |||
size === 'medium' && 'tag-input-medium', | |||
size === 'large' && 'tag-input-large', | |||
indicator && 'tag-input-indicator', | |||
variant === 'default' && 'tag-input-default', | |||
variant === 'alternate' && 'tag-input-alternate', | |||
className, | |||
)} | |||
> | |||
<textarea | |||
{...etcProps} | |||
disabled={disabled} | |||
ref={ref} | |||
id={id} | |||
aria-labelledby={labelId} | |||
data-testid="input" | |||
defaultValue={defaultValue} | |||
style={{ | |||
height: clientSide ? undefined : 0, | |||
}} | |||
className={clsx( | |||
'relative rounded ring-secondary/50 group', | |||
'focus-within:ring-4', | |||
'bg-negative rounded-inherit peer block', | |||
'focus:outline-0', | |||
'disabled:opacity-50 disabled:cursor-not-allowed', | |||
{ | |||
'block': block, | |||
'inline-block align-middle': !block, | |||
'resize': !block, | |||
'resize-y': block, | |||
}, | |||
clientSide && { | |||
{ | |||
'text-xxs': size === 'small', | |||
'text-xs': size === 'medium', | |||
}, | |||
!clientSide && { | |||
'pl-4': variant === 'default', | |||
'pl-1.5': variant === 'alternate', | |||
}, | |||
!clientSide && { | |||
'pt-4': variant === 'alternate' && size === 'small', | |||
'pt-5': variant === 'alternate' && size === 'medium', | |||
'pt-8': variant === 'alternate' && size === 'large', | |||
}, | |||
!clientSide && { | |||
'py-2.5': variant === 'default' && size === 'small', | |||
'py-3': variant === 'default' && size === 'medium', | |||
'py-5': variant === 'default' && size === 'large', | |||
}, | |||
!clientSide && { | |||
'pr-4': variant === 'default' && !indicator, | |||
'pr-1.5': variant === 'alternate' && !indicator, | |||
}, | |||
!clientSide && { | |||
'pr-10': indicator && size === 'small', | |||
'pr-12': indicator && size === 'medium', | |||
'pr-16': indicator && size === 'large', | |||
}, | |||
!clientSide && { | |||
'min-h-10': size === 'small', | |||
'min-h-12': size === 'medium', | |||
'min-h-16': size === 'large', | |||
}, | |||
'tesseract-design-tag-input', | |||
size === 'small' && 'tag-input-small', | |||
size === 'medium' && 'tag-input-medium', | |||
size === 'large' && 'tag-input-large', | |||
indicator && 'tag-input-indicator', | |||
variant === 'default' && 'tag-input-default', | |||
variant === 'alternate' && 'tag-input-alternate', | |||
className, | |||
!clientSide && 'peer', | |||
!clientSide && 'w-full', | |||
clientSide && 'sr-only', | |||
)} | |||
> | |||
<textarea | |||
{...etcProps} | |||
disabled={disabled} | |||
ref={ref} | |||
aria-labelledby={labelId} | |||
data-testid="input" | |||
defaultValue={defaultValue} | |||
style={{ | |||
height: clientSide ? undefined : 0, | |||
/> | |||
{clientSide && ( | |||
<TagsInput | |||
value={tags} | |||
classNames={{ | |||
input: 'peer bg-transparent', | |||
tag: 'text-xs p-2', | |||
}} | |||
className={clsx( | |||
'bg-negative rounded-inherit peer block', | |||
'focus:outline-0', | |||
'disabled:opacity-50 disabled:cursor-not-allowed', | |||
{ | |||
'resize': !block, | |||
'resize-y': block, | |||
}, | |||
{ | |||
'text-xxs': size === 'small', | |||
'text-xs': size === 'medium', | |||
}, | |||
!clientSide && { | |||
'pl-4': variant === 'default', | |||
'pl-1.5': variant === 'alternate', | |||
}, | |||
!clientSide && { | |||
'pt-4': variant === 'alternate' && size === 'small', | |||
'pt-5': variant === 'alternate' && size === 'medium', | |||
'pt-8': variant === 'alternate' && size === 'large', | |||
}, | |||
!clientSide && { | |||
'py-2.5': variant === 'default' && size === 'small', | |||
'py-3': variant === 'default' && size === 'medium', | |||
'py-5': variant === 'default' && size === 'large', | |||
}, | |||
!clientSide && { | |||
'pr-4': variant === 'default' && !indicator, | |||
'pr-1.5': variant === 'alternate' && !indicator, | |||
}, | |||
!clientSide && { | |||
'pr-10': indicator && size === 'small', | |||
'pr-12': indicator && size === 'medium', | |||
'pr-16': indicator && size === 'large', | |||
}, | |||
!clientSide && { | |||
'min-h-10': size === 'small', | |||
'min-h-12': size === 'medium', | |||
'min-h-16': size === 'large', | |||
}, | |||
!clientSide && 'peer', | |||
!clientSide && 'w-full', | |||
clientSide && 'sr-only', | |||
)} | |||
disabled={disabled} | |||
onChange={handleTagsInputChange} | |||
onBlur={handleBlur} | |||
separators={separator.map((s) => TAG_INPUT_SEPARATOR_MAP[s])} | |||
/> | |||
{clientSide && ( | |||
<TagsInput | |||
value={tags} | |||
classNames={{ | |||
input: 'peer bg-transparent', | |||
tag: 'text-xs p-2', | |||
}} | |||
disabled={disabled} | |||
onChange={handleTagsInputChange} | |||
onBlur={handleBlur} | |||
separators={separator.map((separator) => separator === '\n' ? 'Enter' : separator)} | |||
/> | |||
)} | |||
{ | |||
label && ( | |||
<label | |||
data-testid="label" | |||
id={labelId} | |||
className={clsx( | |||
'absolute z-[1] w-full top-0.5 left-0 pointer-events-none pl-1 text-xxs font-bold peer-disabled:opacity-50 group-focus-within:text-secondary text-primary leading-none bg-negative', | |||
{ | |||
'sr-only': hiddenLabel, | |||
}, | |||
{ | |||
'pr-1': !indicator, | |||
}, | |||
{ | |||
'pr-10': indicator && size === 'small', | |||
'pr-12': indicator && size === 'medium', | |||
'pr-16': indicator && size === 'large', | |||
}, | |||
)} | |||
> | |||
<span className="block w-full whitespace-nowrap h-[1.1em] overflow-hidden text-ellipsis"> | |||
{label} | |||
</span> | |||
</label> | |||
) | |||
} | |||
{hint && ( | |||
<div | |||
data-testid="hint" | |||
)} | |||
{ | |||
label && ( | |||
<label | |||
data-testid="label" | |||
id={labelId} | |||
htmlFor={id} | |||
className={clsx( | |||
'absolute left-0 px-1 pointer-events-none text-xxs peer-disabled:opacity-50 leading-none w-full bg-negative', | |||
{ | |||
'bottom-0 pl-4 pb-1': variant === 'default', | |||
'top-0.5': variant === 'alternate', | |||
}, | |||
'absolute z-[1] w-full top-0.5 left-0 pointer-events-none pl-1 text-xxs font-bold peer-disabled:opacity-50 group-focus-within:text-secondary text-primary leading-none bg-negative', | |||
{ | |||
'pt-2': variant === 'alternate' && size === 'small', | |||
'pt-3': variant === 'alternate' && size !== 'small', | |||
'sr-only': hiddenLabel, | |||
}, | |||
{ | |||
'pr-4': !indicator && variant === 'default', | |||
'pr-1': !indicator && variant === 'alternate', | |||
'pr-1': !indicator, | |||
}, | |||
{ | |||
'pr-10': indicator && size === 'small', | |||
@@ -257,38 +246,80 @@ export const TagInput = React.forwardRef<TagInputDerivedElement, TagInputProps>( | |||
}, | |||
)} | |||
> | |||
<div | |||
className="opacity-50 whitespace-nowrap w-full h-[1.1em] overflow-hidden text-ellipsis" | |||
> | |||
{hint} | |||
</div> | |||
</div> | |||
)} | |||
{indicator && ( | |||
<span className="block w-full whitespace-nowrap h-[1.1em] overflow-hidden text-ellipsis"> | |||
{label} | |||
</span> | |||
</label> | |||
) | |||
} | |||
{hint && ( | |||
<div | |||
data-testid="hint" | |||
className={clsx( | |||
'absolute left-0 px-1 pointer-events-none text-xxs peer-disabled:opacity-50 leading-none w-full bg-negative', | |||
{ | |||
'bottom-0 pl-4 pb-1': variant === 'default', | |||
'top-0.5': variant === 'alternate', | |||
}, | |||
{ | |||
'pt-2': variant === 'alternate' && size === 'small', | |||
'pt-3': variant === 'alternate' && size !== 'small', | |||
}, | |||
{ | |||
'pr-4': !indicator && variant === 'default', | |||
'pr-1': !indicator && variant === 'alternate', | |||
}, | |||
{ | |||
'pr-10': indicator && size === 'small', | |||
'pr-12': indicator && size === 'medium', | |||
'pr-16': indicator && size === 'large', | |||
}, | |||
)} | |||
> | |||
<div | |||
className={clsx( | |||
'text-center flex items-center justify-center peer-disabled:opacity-50 aspect-square absolute bottom-0 right-0 pointer-events-none', | |||
{ | |||
'w-10': size === 'small', | |||
'w-12': size === 'medium', | |||
'w-16': size === 'large', | |||
}, | |||
)} | |||
className="opacity-50 whitespace-nowrap w-full h-[1.1em] overflow-hidden text-ellipsis" | |||
> | |||
{indicator} | |||
{hint} | |||
</div> | |||
)} | |||
{ | |||
border && ( | |||
<span | |||
data-testid="border" | |||
className="absolute z-[1] peer-disabled:opacity-50 inset-0 rounded-inherit border-2 border-primary pointer-events-none group-focus-within:border-secondary" | |||
/> | |||
) | |||
} | |||
</div> | |||
); | |||
} | |||
); | |||
</div> | |||
)} | |||
{indicator && ( | |||
<div | |||
className={clsx( | |||
'text-center flex items-center justify-center peer-disabled:opacity-50 aspect-square absolute bottom-0 right-0 pointer-events-none', | |||
{ | |||
'w-10': size === 'small', | |||
'w-12': size === 'medium', | |||
'w-16': size === 'large', | |||
}, | |||
)} | |||
> | |||
{indicator} | |||
</div> | |||
)} | |||
{ | |||
border && ( | |||
<span | |||
data-testid="border" | |||
className="absolute z-[1] peer-disabled:opacity-50 inset-0 rounded-inherit border-2 border-primary pointer-events-none group-focus-within:border-secondary" | |||
/> | |||
) | |||
} | |||
</div> | |||
); | |||
}); | |||
TagInput.displayName = 'TagInput'; | |||
TagInput.defaultProps = { | |||
label: undefined, | |||
hint: undefined, | |||
indicator: undefined, | |||
size: 'medium', | |||
variant: 'default', | |||
separator: ['newline'], | |||
border: false, | |||
block: false, | |||
hiddenLabel: false, | |||
enhanced: false, | |||
}; |
@@ -10,23 +10,26 @@ export interface ToggleButtonProps extends Omit<React.InputHTMLAttributes<Toggle | |||
size?: Button.Size; | |||
subtext?: React.ReactNode; | |||
badge?: React.ReactNode; | |||
variant: Button.Variant; | |||
variant?: Button.Variant; | |||
indeterminate?: boolean; | |||
} | |||
export const ToggleButton = React.forwardRef<ToggleButtonDerivedElement, ToggleButtonProps>(({ | |||
children, | |||
block = false, | |||
compact = false, | |||
size = 'medium', | |||
id: idProp, | |||
className, | |||
subtext, | |||
badge, | |||
variant, | |||
indeterminate = false, | |||
...etcProps | |||
}, forwardedRef) => { | |||
export const ToggleButton = React.forwardRef<ToggleButtonDerivedElement, ToggleButtonProps>(( | |||
{ | |||
children, | |||
block = false, | |||
compact = false, | |||
size = 'medium' as const, | |||
id: idProp, | |||
className, | |||
subtext, | |||
badge, | |||
variant = 'bare' as const, | |||
indeterminate = false, | |||
...etcProps | |||
}, | |||
forwardedRef, | |||
) => { | |||
const defaultRef = React.useRef<ToggleButtonDerivedElement>(null); | |||
const ref = forwardedRef ?? defaultRef; | |||
const defaultId = React.useId(); | |||
@@ -69,7 +72,7 @@ export const ToggleButton = React.forwardRef<ToggleButtonDerivedElement, ToggleB | |||
'pl-4 pr-4': !compact, | |||
}, | |||
{ | |||
'border-2 border-primary peer-disabled:border-primary peer-focus:border-secondary peer-checked:border-tertiary active:border-tertiary' : variant !== 'bare', | |||
'border-2 border-primary peer-disabled:border-primary peer-focus:border-secondary peer-checked:border-tertiary active:border-tertiary': variant !== 'bare', | |||
'bg-primary text-negative peer-disabled:bg-primary peer-focus:bg-secondary peer-checked:bg-tertiary active:bg-tertiary': variant === 'filled', | |||
}, | |||
{ | |||
@@ -87,7 +90,7 @@ export const ToggleButton = React.forwardRef<ToggleButtonDerivedElement, ToggleB | |||
'border-current': variant !== 'filled', | |||
'border-negative': variant === 'filled', | |||
'-mr-2': compact, | |||
} | |||
}, | |||
)} | |||
> | |||
<svg | |||
@@ -96,7 +99,7 @@ export const ToggleButton = React.forwardRef<ToggleButtonDerivedElement, ToggleB | |||
{ | |||
'stroke-negative': variant === 'filled', | |||
'stroke-current': variant !== 'filled', | |||
} | |||
}, | |||
)} | |||
viewBox="0 0 24 24" | |||
role="presentation" | |||
@@ -111,7 +114,7 @@ export const ToggleButton = React.forwardRef<ToggleButtonDerivedElement, ToggleB | |||
{ | |||
'stroke-negative': variant === 'filled', | |||
'stroke-current': variant !== 'filled', | |||
} | |||
}, | |||
)} | |||
viewBox="0 0 24 24" | |||
role="presentation" | |||
@@ -170,7 +173,17 @@ export const ToggleButton = React.forwardRef<ToggleButtonDerivedElement, ToggleB | |||
</span> | |||
</label> | |||
</> | |||
) | |||
); | |||
}); | |||
ToggleButton.displayName = 'ToggleButton'; | |||
ToggleButton.defaultProps = { | |||
block: false, | |||
compact: false, | |||
size: 'medium', | |||
subtext: undefined, | |||
badge: undefined, | |||
indeterminate: false, | |||
variant: 'bare', | |||
}; |
@@ -6,24 +6,25 @@ export type ToggleSwitchDerivedElement = HTMLInputElement; | |||
export interface ToggleSwitchProps extends Omit<React.InputHTMLAttributes<ToggleSwitchDerivedElement>, 'type' | 'size'> { | |||
block?: boolean; | |||
subtext?: React.ReactNode; | |||
badge?: React.ReactNode; | |||
indeterminate?: boolean; | |||
checkedLabel?: React.ReactNode; | |||
uncheckedLabel?: React.ReactNode; | |||
} | |||
export const ToggleSwitch = React.forwardRef<ToggleSwitchDerivedElement, ToggleSwitchProps>(({ | |||
uncheckedLabel, | |||
checkedLabel, | |||
block = false, | |||
id: idProp, | |||
className, | |||
subtext, | |||
badge, | |||
style, | |||
indeterminate = false, | |||
...etcProps | |||
}, forwardedRef) => { | |||
export const ToggleSwitch = React.forwardRef<ToggleSwitchDerivedElement, ToggleSwitchProps>(( | |||
{ | |||
uncheckedLabel, | |||
checkedLabel, | |||
block = false, | |||
id: idProp, | |||
className, | |||
subtext, | |||
style, | |||
indeterminate = false, | |||
...etcProps | |||
}, | |||
forwardedRef, | |||
) => { | |||
const defaultRef = React.useRef<ToggleSwitchDerivedElement>(null); | |||
const ref = forwardedRef ?? defaultRef; | |||
const defaultId = React.useId(); | |||
@@ -112,7 +113,15 @@ export const ToggleSwitch = React.forwardRef<ToggleSwitchDerivedElement, ToggleS | |||
</div> | |||
)} | |||
</div> | |||
) | |||
); | |||
}); | |||
ToggleSwitch.displayName = 'ToggleSwitch'; | |||
ToggleSwitch.defaultProps = { | |||
block: false, | |||
subtext: undefined, | |||
indeterminate: false, | |||
checkedLabel: undefined, | |||
uncheckedLabel: undefined, | |||
}; |
@@ -6,21 +6,22 @@ export type ToggleTickBoxDerivedElement = HTMLInputElement; | |||
export interface ToggleTickBoxProps extends Omit<React.InputHTMLAttributes<ToggleTickBoxDerivedElement>, 'type' | 'size'> { | |||
block?: boolean; | |||
subtext?: React.ReactNode; | |||
badge?: React.ReactNode; | |||
indeterminate?: boolean; | |||
} | |||
export const ToggleTickBox = React.forwardRef<ToggleTickBoxDerivedElement, ToggleTickBoxProps>(({ | |||
children, | |||
block = false, | |||
id: idProp, | |||
className, | |||
subtext, | |||
badge, | |||
style, | |||
indeterminate = false, | |||
...etcProps | |||
}, forwardedRef) => { | |||
export const ToggleTickBox = React.forwardRef<ToggleTickBoxDerivedElement, ToggleTickBoxProps>(( | |||
{ | |||
children, | |||
block = false, | |||
id: idProp, | |||
className, | |||
subtext, | |||
style, | |||
indeterminate = false, | |||
...etcProps | |||
}, | |||
forwardedRef, | |||
) => { | |||
const defaultRef = React.useRef<ToggleTickBoxDerivedElement>(null); | |||
const ref = forwardedRef ?? defaultRef; | |||
const defaultId = React.useId(); | |||
@@ -113,7 +114,13 @@ export const ToggleTickBox = React.forwardRef<ToggleTickBoxDerivedElement, Toggl | |||
</div> | |||
)} | |||
</div> | |||
) | |||
); | |||
}); | |||
ToggleTickBox.displayName = 'ToggleTickBox'; | |||
ToggleTickBox.defaultProps = { | |||
block: false, | |||
indeterminate: false, | |||
subtext: undefined, | |||
}; |
@@ -1,5 +1,8 @@ | |||
{ | |||
"root": true, | |||
"rules": { | |||
"react/jsx-props-no-spreading": "off" | |||
}, | |||
"extends": [ | |||
"lxsmnsyc/typescript/react" | |||
], | |||
@@ -4,30 +4,33 @@ import { Button } from '@tesseract-design/web-base'; | |||
export type LinkButtonDerivedElement = HTMLAnchorElement; | |||
export interface LinkButtonProps extends Omit<React.HTMLProps<LinkButtonDerivedElement>, 'size'> { | |||
export interface LinkButtonProps<T = any> extends Omit<React.HTMLProps<LinkButtonDerivedElement>, 'size'> { | |||
block?: boolean; | |||
variant: Button.Variant; | |||
variant?: Button.Variant; | |||
subtext?: React.ReactNode; | |||
badge?: React.ReactNode; | |||
menuItem?: boolean; | |||
size?: Button.Size; | |||
compact?: boolean; | |||
component?: React.ElementType; | |||
component?: React.ElementType<T>; | |||
} | |||
export const LinkButton = React.forwardRef<LinkButtonDerivedElement, LinkButtonProps>(({ | |||
variant, | |||
subtext, | |||
badge, | |||
menuItem = false, | |||
children, | |||
size = 'medium' as const, | |||
compact = false, | |||
className, | |||
block = false, | |||
component: Component = 'a', | |||
...etcProps | |||
}, forwardedRef) => ( | |||
export const LinkButton = React.forwardRef<LinkButtonDerivedElement, LinkButtonProps>(( | |||
{ | |||
variant = 'bare' as const, | |||
subtext, | |||
badge, | |||
menuItem = false, | |||
children, | |||
size = 'medium' as const, | |||
compact = false, | |||
className, | |||
block = false, | |||
component: Component = 'a', | |||
...etcProps | |||
}, | |||
forwardedRef, | |||
) => ( | |||
<Component | |||
{...etcProps} | |||
ref={forwardedRef} | |||
@@ -46,7 +49,7 @@ export const LinkButton = React.forwardRef<LinkButtonDerivedElement, LinkButtonP | |||
'pr-2': compact || menuItem, | |||
}, | |||
{ | |||
'border-2 border-primary focus:border-secondary active:border-tertiary' : variant !== 'bare', | |||
'border-2 border-primary focus:border-secondary active:border-tertiary': variant !== 'bare', | |||
'bg-negative text-primary focus:text-secondary active:text-tertiary': variant !== 'filled', | |||
'bg-primary text-negative focus:bg-secondary active:bg-tertiary': variant === 'filled', | |||
}, | |||
@@ -108,7 +111,7 @@ export const LinkButton = React.forwardRef<LinkButtonDerivedElement, LinkButtonP | |||
viewBox="0 0 24 24" | |||
role="presentation" | |||
> | |||
<polyline points="9 18 15 12 9 6"/> | |||
<polyline points="9 18 15 12 9 6" /> | |||
</svg> | |||
</span> | |||
)} | |||
@@ -116,3 +119,14 @@ export const LinkButton = React.forwardRef<LinkButtonDerivedElement, LinkButtonP | |||
)); | |||
LinkButton.displayName = 'LinkButton'; | |||
LinkButton.defaultProps = { | |||
variant: 'bare', | |||
size: 'medium', | |||
compact: false, | |||
menuItem: false, | |||
component: 'a', | |||
badge: undefined, | |||
subtext: undefined, | |||
block: false, | |||
}; |
@@ -1,5 +1,9 @@ | |||
{ | |||
"root": true, | |||
"rules": { | |||
"quote-props": "off", | |||
"react/jsx-props-no-spreading": "off" | |||
}, | |||
"extends": [ | |||
"lxsmnsyc/typescript/react" | |||
], | |||
@@ -29,8 +29,9 @@ const filterOptions = (children: React.ReactNode): React.ReactNode => { | |||
/* | |||
* Caveat for slider: | |||
* | |||
* Since sliders are not as customizable as other components, especially with orientations, prefer using sliders where | |||
* using horizontal sliders would be as acceptable as vertical sliders and vv. | |||
* Since sliders are not as customizable as other components, especially with orientations, | |||
* prefer using sliders where using horizontal sliders would be as acceptable as vertical | |||
* sliders and vv. | |||
*/ | |||
export type SliderOrientation = 'horizontal' | 'vertical'; | |||
@@ -43,16 +44,19 @@ export interface SliderProps extends Omit<React.HTMLProps<HTMLInputElement>, 'ty | |||
length?: React.CSSProperties['width']; | |||
} | |||
export const Slider = React.forwardRef<SliderDerivedElement, SliderProps>(({ | |||
className, | |||
style, | |||
children, | |||
orient = 'horizontal', | |||
length, | |||
min = 0, | |||
max = 100, | |||
...etcProps | |||
}, forwardedRef) => { | |||
export const Slider = React.forwardRef<SliderDerivedElement, SliderProps>(( | |||
{ | |||
className, | |||
style, | |||
children, | |||
orient = 'horizontal', | |||
length, | |||
min = 0, | |||
max = 100, | |||
...etcProps | |||
}, | |||
forwardedRef, | |||
) => { | |||
const [browser, setBrowser] = React.useState<string>(); | |||
React.useEffect(() => { | |||
const isFirefox = typeof (window as unknown as Record<string, unknown>).InstallTrigger !== 'undefined'; | |||
@@ -69,27 +73,43 @@ export const Slider = React.forwardRef<SliderDerivedElement, SliderProps>(({ | |||
const tickMarkId = React.useId(); | |||
React.useEffect(() => { | |||
if (!(typeof ref === 'object' && ref)) { | |||
return; | |||
} | |||
const {current: slider} = ref; | |||
if (!slider) { | |||
return; | |||
} | |||
const setupGecko = (theOrient: string, theChildren: unknown, theBrowser?: string) => { | |||
if (!(typeof ref === 'object' && ref)) { | |||
return; | |||
} | |||
const { current: slider } = ref; | |||
if (!slider) { | |||
return; | |||
} | |||
const isFirefox = browser === 'firefox'; | |||
const wrapper = slider?.parentElement as HTMLElement; | |||
const parent = wrapper?.parentElement as HTMLElement; | |||
const grandParent = parent?.parentElement as HTMLElement; | |||
if (isFirefox) { | |||
slider.setAttribute('orient', orient); | |||
wrapper.dataset[browser] = orient; | |||
wrapper.removeAttribute('data-orient'); | |||
grandParent.style.width = children ? '2.5em' : '1em'; | |||
} | |||
const isFirefox = theBrowser === 'firefox'; | |||
const wrapper = slider?.parentElement as HTMLElement; | |||
const parent = wrapper?.parentElement as HTMLElement; | |||
const grandParent = parent?.parentElement as HTMLElement; | |||
if (isFirefox) { | |||
slider.setAttribute('orient', theOrient); | |||
wrapper.dataset[theBrowser] = theOrient; | |||
wrapper.removeAttribute('data-orient'); | |||
grandParent.style.width = theChildren ? '2.5em' : '1em'; | |||
} | |||
}; | |||
setupGecko(orient, children, browser); | |||
return () => { | |||
if (slider && isFirefox) { | |||
if (!(typeof ref === 'object' && ref)) { | |||
return; | |||
} | |||
const { current: slider } = ref; | |||
if (!slider) { | |||
return; | |||
} | |||
const isFirefox = browser === 'firefox'; | |||
const wrapper = slider?.parentElement as HTMLElement; | |||
const parent = wrapper?.parentElement as HTMLElement; | |||
const grandParent = parent?.parentElement as HTMLElement; | |||
if (slider && isFirefox && parent && grandParent && wrapper) { | |||
grandParent.style.width = 'auto'; | |||
wrapper.removeAttribute(`data-${browser}`); | |||
wrapper.dataset.orient = slider.getAttribute(orient) ?? undefined; | |||
@@ -99,42 +119,63 @@ export const Slider = React.forwardRef<SliderDerivedElement, SliderProps>(({ | |||
}, [ref, orient, children, browser]); | |||
React.useEffect(() => { | |||
if (!(typeof ref === 'object' && ref)) { | |||
return; | |||
} | |||
const {current: slider} = ref; | |||
if (!slider) { | |||
return; | |||
} | |||
let shouldEffectExecute: boolean; | |||
const setupNonGecko = (theOrient: string, theBrowser?: string) => { | |||
if (!(typeof ref === 'object' && ref)) { | |||
return; | |||
} | |||
const { current: slider } = ref; | |||
if (!slider) { | |||
return; | |||
} | |||
const wrapper = slider?.parentElement as HTMLElement; | |||
const parent = wrapper?.parentElement as HTMLElement; | |||
const grandParent = parent?.parentElement as HTMLElement; | |||
if (!theBrowser) { | |||
return; | |||
} | |||
const isNotFirefox = typeof browser === 'string' && browser !== 'firefox'; | |||
if (isNotFirefox) { | |||
wrapper.dataset[browser] = orient; | |||
} | |||
const wrapper = slider?.parentElement as HTMLElement; | |||
const parent = wrapper?.parentElement as HTMLElement; | |||
const grandParent = parent?.parentElement as HTMLElement; | |||
const shouldEffectExecute = isNotFirefox && orient === 'vertical' && slider && parent && grandParent; | |||
if (shouldEffectExecute) { | |||
const trueHeight = parent.clientWidth; | |||
const trueWidth = parent.clientHeight; | |||
const isNotFirefox = theBrowser !== 'firefox'; | |||
if (isNotFirefox) { | |||
wrapper.dataset[theBrowser] = theOrient; | |||
} | |||
grandParent.dataset.height = grandParent.clientHeight.toString(); | |||
grandParent.dataset.width = grandParent.clientWidth.toString(); | |||
grandParent.style.height = trueHeight + 'px'; | |||
grandParent.style.width = trueWidth + 'px'; | |||
} | |||
shouldEffectExecute = isNotFirefox && theOrient === 'vertical' && Boolean(slider) && Boolean(parent) && Boolean(grandParent); | |||
if (shouldEffectExecute) { | |||
const trueHeight = parent.clientWidth; | |||
const trueWidth = parent.clientHeight; | |||
grandParent.dataset.height = grandParent.clientHeight.toString(); | |||
grandParent.dataset.width = grandParent.clientWidth.toString(); | |||
grandParent.style.height = `${trueHeight}px`; | |||
grandParent.style.width = `${trueWidth}px`; | |||
} | |||
}; | |||
setupNonGecko(orient, browser); | |||
return () => { | |||
if (!(typeof ref === 'object' && ref)) { | |||
return; | |||
} | |||
const { current: slider } = ref; | |||
if (!slider) { | |||
return; | |||
} | |||
const wrapper = slider?.parentElement as HTMLElement; | |||
const parent = wrapper?.parentElement as HTMLElement; | |||
const grandParent = parent?.parentElement as HTMLElement; | |||
if (shouldEffectExecute) { | |||
grandParent.style.height = grandParent.dataset.height + 'px'; | |||
grandParent.style.width = grandParent.dataset.width + 'px'; | |||
grandParent.style.height = `${grandParent.dataset.height ?? 0}px`; | |||
grandParent.style.width = `${grandParent.dataset.width ?? 0}px`; | |||
grandParent.removeAttribute('data-height'); | |||
grandParent.removeAttribute('data-width'); | |||
} | |||
wrapper.removeAttribute(`data-${browser}`); | |||
wrapper.removeAttribute(`data-${browser ?? 'unknown'}`); | |||
}; | |||
}, [ref, orient, browser]); | |||
@@ -142,7 +183,7 @@ export const Slider = React.forwardRef<SliderDerivedElement, SliderProps>(({ | |||
if (!(typeof ref === 'object' && ref)) { | |||
return; | |||
} | |||
const {current: slider} = ref; | |||
const { current: slider } = ref; | |||
if (!slider) { | |||
return; | |||
} | |||
@@ -266,7 +307,7 @@ export const Slider = React.forwardRef<SliderDerivedElement, SliderProps>(({ | |||
<div | |||
key={c.props.value} | |||
className="absolute w-full h-full box-border" | |||
data-offset={`${(value - minValue) / (maxValue - minValue) * 100}%`} | |||
data-offset={`${((value - minValue) / (maxValue - minValue)) * 100}%`} | |||
> | |||
<div | |||
className="flex h-full text-xs items-center justify-between" | |||
@@ -281,7 +322,7 @@ export const Slider = React.forwardRef<SliderDerivedElement, SliderProps>(({ | |||
</div> | |||
</div> | |||
</div> | |||
) | |||
); | |||
}) | |||
} | |||
</div> | |||
@@ -291,7 +332,13 @@ export const Slider = React.forwardRef<SliderDerivedElement, SliderProps>(({ | |||
</div> | |||
</div> | |||
</> | |||
) | |||
); | |||
}); | |||
Slider.displayName = 'Slider'; | |||
Slider.defaultProps = { | |||
orient: 'horizontal', | |||
length: undefined, | |||
children: undefined, | |||
}; |
@@ -42,156 +42,170 @@ export interface SpinnerProps extends Omit<React.HTMLProps<SpinnerDerivedElement | |||
/** | |||
* Component for inputting numeric values. | |||
*/ | |||
export const Spinner = React.forwardRef<SpinnerDerivedElement, SpinnerProps>( | |||
( | |||
{ | |||
label, | |||
hint, | |||
indicator, | |||
size = 'medium' as const, | |||
border = false, | |||
block = false, | |||
variant = 'default' as const, | |||
hiddenLabel = false, | |||
className, | |||
...etcProps | |||
}: SpinnerProps, | |||
ref, | |||
) => { | |||
const labelId = React.useId(); | |||
export const Spinner = React.forwardRef<SpinnerDerivedElement, SpinnerProps>(( | |||
{ | |||
label, | |||
hint, | |||
indicator, | |||
size = 'medium' as const, | |||
border = false, | |||
block = false, | |||
variant = 'default' as const, | |||
hiddenLabel = false, | |||
className, | |||
id: idProp, | |||
...etcProps | |||
}: SpinnerProps, | |||
forwardedRef, | |||
) => { | |||
const labelId = React.useId(); | |||
const defaultId = React.useId(); | |||
const id = idProp ?? defaultId; | |||
return ( | |||
<div | |||
return ( | |||
<div | |||
className={clsx( | |||
'relative rounded ring-secondary/50', | |||
'focus-within:ring-4', | |||
{ | |||
'block': block, | |||
'inline-block align-middle': !block, | |||
}, | |||
className, | |||
)} | |||
> | |||
<input | |||
{...etcProps} | |||
ref={forwardedRef} | |||
id={id} | |||
aria-labelledby={labelId} | |||
type="number" | |||
data-testid="input" | |||
className={clsx( | |||
'relative rounded ring-secondary/50', | |||
'focus-within:ring-4', | |||
'bg-negative rounded-inherit w-full peer block tabular-nums', | |||
'focus:outline-0', | |||
'disabled:opacity-50 disabled:cursor-not-allowed', | |||
'tesseract-design-spinner', | |||
{ | |||
'text-xxs': size === 'small', | |||
'text-xs': size === 'medium', | |||
}, | |||
{ | |||
'pl-4': variant === 'default', | |||
'pl-1.5': variant === 'alternate', | |||
}, | |||
{ | |||
'pt-4': variant === 'alternate', | |||
}, | |||
{ | |||
'block': block, | |||
'inline-block align-middle': !block, | |||
'pr-4': variant === 'default' && !indicator, | |||
'pr-1.5': variant === 'alternate' && !indicator, | |||
}, | |||
{ | |||
'pr-10': indicator && size === 'small', | |||
'pr-12': indicator && size === 'medium', | |||
'pr-16': indicator && size === 'large', | |||
}, | |||
{ | |||
'h-10': size === 'small', | |||
'h-12': size === 'medium', | |||
'h-16': size === 'large', | |||
}, | |||
className, | |||
)} | |||
> | |||
<input | |||
{...etcProps} | |||
ref={ref} | |||
aria-labelledby={labelId} | |||
type="number" | |||
data-testid="input" | |||
/> | |||
{ | |||
label && ( | |||
<label | |||
data-testid="label" | |||
id={labelId} | |||
htmlFor={id} | |||
className={clsx( | |||
'absolute z-[1] w-full top-0.5 left-0 pointer-events-none pl-1 text-xxs font-bold peer-disabled:opacity-50 peer-focus:text-secondary text-primary leading-none bg-negative select-none', | |||
{ | |||
'sr-only': hiddenLabel, | |||
}, | |||
{ | |||
'pr-1': !indicator, | |||
}, | |||
{ | |||
'pr-10': indicator && size === 'small', | |||
'pr-12': indicator && size === 'medium', | |||
'pr-16': indicator && size === 'large', | |||
}, | |||
)} | |||
> | |||
<span className="block w-full whitespace-nowrap h-[1.1em] overflow-hidden text-ellipsis"> | |||
{label} | |||
</span> | |||
</label> | |||
) | |||
} | |||
{hint && ( | |||
<div | |||
data-testid="hint" | |||
className={clsx( | |||
'bg-negative rounded-inherit w-full peer block tabular-nums', | |||
'focus:outline-0', | |||
'disabled:opacity-50 disabled:cursor-not-allowed', | |||
'tesseract-design-spinner', | |||
{ | |||
'text-xxs': size === 'small', | |||
'text-xs': size === 'medium', | |||
}, | |||
'absolute left-0 px-1 pointer-events-none text-xxs peer-disabled:opacity-50 leading-none w-full bg-negative select-none text-primary', | |||
{ | |||
'pl-4': variant === 'default', | |||
'pl-1.5': variant === 'alternate', | |||
'bottom-0 pl-4 pb-1': variant === 'default', | |||
'top-0.5': variant === 'alternate', | |||
}, | |||
{ | |||
'pt-4': variant === 'alternate', | |||
'pt-2': variant === 'alternate' && size === 'small', | |||
'pt-3': variant === 'alternate' && size !== 'small', | |||
}, | |||
{ | |||
'pr-4': variant === 'default' && !indicator, | |||
'pr-1.5': variant === 'alternate' && !indicator, | |||
'pr-4': !indicator && variant === 'default', | |||
'pr-1': !indicator && variant === 'alternate', | |||
}, | |||
{ | |||
'pr-10': indicator && size === 'small', | |||
'pr-12': indicator && size === 'medium', | |||
'pr-16': indicator && size === 'large', | |||
}, | |||
{ | |||
'h-10': size === 'small', | |||
'h-12': size === 'medium', | |||
'h-16': size === 'large', | |||
}, | |||
)} | |||
/> | |||
{ | |||
label && ( | |||
<label | |||
data-testid="label" | |||
id={labelId} | |||
className={clsx( | |||
'absolute z-[1] w-full top-0.5 left-0 pointer-events-none pl-1 text-xxs font-bold peer-disabled:opacity-50 peer-focus:text-secondary text-primary leading-none bg-negative', | |||
{ | |||
'sr-only': hiddenLabel, | |||
}, | |||
{ | |||
'pr-1': !indicator, | |||
}, | |||
{ | |||
'pr-10': indicator && size === 'small', | |||
'pr-12': indicator && size === 'medium', | |||
'pr-16': indicator && size === 'large', | |||
}, | |||
)} | |||
> | |||
<span className="block w-full whitespace-nowrap h-[1.1em] overflow-hidden text-ellipsis"> | |||
{label} | |||
</span> | |||
</label> | |||
) | |||
} | |||
{hint && ( | |||
> | |||
<div | |||
data-testid="hint" | |||
className={clsx( | |||
'absolute left-0 px-1 pointer-events-none text-xxs peer-disabled:opacity-50 leading-none w-full bg-negative', | |||
{ | |||
'bottom-0 pl-4 pb-1': variant === 'default', | |||
'top-0.5': variant === 'alternate', | |||
}, | |||
{ | |||
'pt-2': variant === 'alternate' && size === 'small', | |||
'pt-3': variant === 'alternate' && size !== 'small', | |||
}, | |||
{ | |||
'pr-4': !indicator && variant === 'default', | |||
'pr-1': !indicator && variant === 'alternate', | |||
}, | |||
{ | |||
'pr-10': indicator && size === 'small', | |||
'pr-12': indicator && size === 'medium', | |||
'pr-16': indicator && size === 'large', | |||
}, | |||
)} | |||
> | |||
<div | |||
className="opacity-50 whitespace-nowrap w-full h-[1.1em] overflow-hidden text-ellipsis" | |||
> | |||
{hint} | |||
</div> | |||
</div> | |||
)} | |||
{indicator && ( | |||
<div | |||
className={clsx( | |||
'text-center flex items-center justify-center peer-disabled:opacity-50 aspect-square absolute bottom-0 right-0 pointer-events-none', | |||
{ | |||
'w-10': size === 'small', | |||
'w-12': size === 'medium', | |||
'w-16': size === 'large', | |||
}, | |||
)} | |||
className="opacity-50 whitespace-nowrap w-full h-[1.1em] overflow-hidden text-ellipsis" | |||
> | |||
{indicator} | |||
{hint} | |||
</div> | |||
)} | |||
{ | |||
border && ( | |||
<span | |||
data-testid="border" | |||
className="absolute z-[1] peer-disabled:opacity-50 inset-0 rounded-inherit border-2 border-primary pointer-events-none peer-focus:border-secondary" | |||
/> | |||
) | |||
} | |||
</div> | |||
); | |||
}, | |||
); | |||
</div> | |||
)} | |||
{indicator && ( | |||
<div | |||
className={clsx( | |||
'text-center flex items-center justify-center peer-disabled:opacity-50 aspect-square absolute bottom-0 right-0 pointer-events-none select-none', | |||
{ | |||
'w-10': size === 'small', | |||
'w-12': size === 'medium', | |||
'w-16': size === 'large', | |||
}, | |||
)} | |||
> | |||
{indicator} | |||
</div> | |||
)} | |||
{ | |||
border && ( | |||
<span | |||
data-testid="border" | |||
className="absolute z-[1] peer-disabled:opacity-50 inset-0 rounded-inherit border-2 border-primary pointer-events-none peer-focus:border-secondary" | |||
/> | |||
) | |||
} | |||
</div> | |||
); | |||
}); | |||
Spinner.displayName = 'Spinner'; | |||
Spinner.defaultProps = { | |||
label: undefined, | |||
hint: undefined, | |||
indicator: undefined, | |||
border: false, | |||
block: false, | |||
hiddenLabel: false, | |||
size: 'medium', | |||
variant: 'default', | |||
}; |