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 Variant = 'default' | 'alternate'; | ||||
export type InputType = 'text' | 'search'; | export type InputType = 'text' | 'search'; | ||||
export type InputMode = 'none' | 'numeric' | 'decimal' | InputType; |
@@ -1,5 +1,10 @@ | |||||
{ | { | ||||
"root": true, | "root": true, | ||||
"rules": { | |||||
"quote-props": "off", | |||||
"react/jsx-props-no-spreading": "off", | |||||
"react/button-has-type": "off" | |||||
}, | |||||
"extends": [ | "extends": [ | ||||
"lxsmnsyc/typescript/react" | "lxsmnsyc/typescript/react" | ||||
], | ], | ||||
@@ -6,7 +6,7 @@ export type ActionButtonDerivedElement = HTMLButtonElement; | |||||
export interface ActionButtonProps extends Omit<React.HTMLProps<ActionButtonDerivedElement>, 'type' | 'size'> { | export interface ActionButtonProps extends Omit<React.HTMLProps<ActionButtonDerivedElement>, 'type' | 'size'> { | ||||
type?: Button.Type; | type?: Button.Type; | ||||
variant: Button.Variant; | |||||
variant?: Button.Variant; | |||||
block?: boolean; | block?: boolean; | ||||
subtext?: React.ReactNode; | subtext?: React.ReactNode; | ||||
badge?: React.ReactNode; | badge?: React.ReactNode; | ||||
@@ -15,19 +15,22 @@ export interface ActionButtonProps extends Omit<React.HTMLProps<ActionButtonDeri | |||||
compact?: boolean; | 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 | <button | ||||
{...etcProps} | {...etcProps} | ||||
type={type} | type={type} | ||||
@@ -48,7 +51,7 @@ export const ActionButton = React.forwardRef<ActionButtonDerivedElement, ActionB | |||||
'pr-2': compact || menuItem, | '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-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', | '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" | viewBox="0 0 24 24" | ||||
role="presentation" | role="presentation" | ||||
> | > | ||||
<polyline points="9 18 15 12 9 6"/> | |||||
<polyline points="9 18 15 12 9 6" /> | |||||
</svg> | </svg> | ||||
</span> | </span> | ||||
)} | )} | ||||
@@ -118,3 +121,14 @@ export const ActionButton = React.forwardRef<ActionButtonDerivedElement, ActionB | |||||
)); | )); | ||||
ActionButton.displayName = 'ActionButton'; | 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, | "root": true, | ||||
"rules": { | |||||
"quote-props": "off", | |||||
"react/jsx-props-no-spreading": "off" | |||||
}, | |||||
"extends": [ | "extends": [ | ||||
"lxsmnsyc/typescript/react" | "lxsmnsyc/typescript/react" | ||||
], | ], | ||||
@@ -4,7 +4,7 @@ import clsx from 'clsx'; | |||||
export type ComboBoxDerivedElement = HTMLInputElement; | 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. | * 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? | * Is the label hidden? | ||||
*/ | */ | ||||
hiddenLabel?: boolean, | 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. | * 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( | 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( | 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-10': indicator && size === 'small', | ||||
'pr-12': indicator && size === 'medium', | 'pr-12': indicator && size === 'medium', | ||||
'pr-16': indicator && size === 'large', | '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 | <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> | </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.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>( | export const DropdownSelect = React.forwardRef<DropdownSelectDerivedElement, DropdownSelectProps>( | ||||
( | ( | ||||
{ | { | ||||
label = '', | |||||
hint = '', | |||||
label, | |||||
hint, | |||||
indicator, | indicator, | ||||
size = 'medium' as const, | size = 'medium' as const, | ||||
border = false, | border = false, | ||||
@@ -57,11 +57,14 @@ export const DropdownSelect = React.forwardRef<DropdownSelectDerivedElement, Dro | |||||
hiddenLabel = false, | hiddenLabel = false, | ||||
className, | className, | ||||
children, | children, | ||||
id: idProp, | |||||
...etcProps | ...etcProps | ||||
}: DropdownSelectProps, | }: DropdownSelectProps, | ||||
ref, | |||||
forwardedRef, | |||||
) => { | ) => { | ||||
const labelId = React.useId(); | const labelId = React.useId(); | ||||
const defaultId = React.useId(); | |||||
const id = idProp ?? defaultId; | |||||
return ( | return ( | ||||
<div | <div | ||||
@@ -77,7 +80,8 @@ export const DropdownSelect = React.forwardRef<DropdownSelectDerivedElement, Dro | |||||
> | > | ||||
<select | <select | ||||
{...etcProps} | {...etcProps} | ||||
ref={ref} | |||||
ref={forwardedRef} | |||||
id={id} | |||||
aria-labelledby={labelId} | aria-labelledby={labelId} | ||||
data-testid="input" | data-testid="input" | ||||
className={clsx( | className={clsx( | ||||
@@ -116,6 +120,7 @@ export const DropdownSelect = React.forwardRef<DropdownSelectDerivedElement, Dro | |||||
{ | { | ||||
label && ( | label && ( | ||||
<label | <label | ||||
htmlFor={id} | |||||
data-testid="label" | data-testid="label" | ||||
id={labelId} | id={labelId} | ||||
className={clsx( | className={clsx( | ||||
@@ -194,7 +199,18 @@ export const DropdownSelect = React.forwardRef<DropdownSelectDerivedElement, Dro | |||||
} | } | ||||
</div> | </div> | ||||
); | ); | ||||
} | |||||
}, | |||||
); | ); | ||||
DropdownSelect.displayName = 'DropdownSelect'; | 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? | * Is the label hidden? | ||||
*/ | */ | ||||
hiddenLabel?: boolean, | hiddenLabel?: boolean, | ||||
/** | |||||
* Starting height of the component. | |||||
*/ | |||||
startingHeight?: number | string, | startingHeight?: number | string, | ||||
} | } | ||||
@@ -58,11 +61,14 @@ export const MenuSelect = React.forwardRef<MenuSelectDerivedElement, MenuSelectP | |||||
hiddenLabel = false, | hiddenLabel = false, | ||||
className, | className, | ||||
startingHeight = '15rem', | startingHeight = '15rem', | ||||
id: idProp, | |||||
...etcProps | ...etcProps | ||||
}: MenuSelectProps, | }: MenuSelectProps, | ||||
ref, | |||||
forwardedRef, | |||||
) => { | ) => { | ||||
const labelId = React.useId(); | const labelId = React.useId(); | ||||
const defaultId = React.useId(); | |||||
const id = idProp ?? defaultId; | |||||
return ( | return ( | ||||
<div | <div | ||||
@@ -78,7 +84,8 @@ export const MenuSelect = React.forwardRef<MenuSelectDerivedElement, MenuSelectP | |||||
> | > | ||||
<select | <select | ||||
{...etcProps} | {...etcProps} | ||||
ref={ref} | |||||
ref={forwardedRef} | |||||
id={id} | |||||
aria-labelledby={labelId} | aria-labelledby={labelId} | ||||
data-testid="input" | data-testid="input" | ||||
size={2} | size={2} | ||||
@@ -131,6 +138,7 @@ export const MenuSelect = React.forwardRef<MenuSelectDerivedElement, MenuSelectP | |||||
label && ( | label && ( | ||||
<label | <label | ||||
data-testid="label" | data-testid="label" | ||||
htmlFor={id} | |||||
id={labelId} | id={labelId} | ||||
className={clsx( | 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', | ||||
@@ -208,7 +216,19 @@ export const MenuSelect = React.forwardRef<MenuSelectDerivedElement, MenuSelectP | |||||
} | } | ||||
</div> | </div> | ||||
); | ); | ||||
} | |||||
}, | |||||
); | ); | ||||
MenuSelect.displayName = 'MenuSelect'; | 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; | size?: Button.Size; | ||||
subtext?: React.ReactNode; | subtext?: React.ReactNode; | ||||
badge?: 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 defaultId = React.useId(); | ||||
const id = idProp ?? defaultId; | const id = idProp ?? defaultId; | ||||
return ( | return ( | ||||
@@ -55,7 +58,7 @@ export const RadioButton = React.forwardRef<RadioButtonDerivedElement, RadioButt | |||||
'pl-4 pr-4': !compact, | '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', | '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, | '-mr-2': compact, | ||||
'border-current': variant !== 'filled', | 'border-current': variant !== 'filled', | ||||
'border-negative': variant === 'filled', | 'border-negative': variant === 'filled', | ||||
} | |||||
}, | |||||
)} | )} | ||||
> | > | ||||
<span | <span | ||||
@@ -135,7 +138,16 @@ export const RadioButton = React.forwardRef<RadioButtonDerivedElement, RadioButt | |||||
</span> | </span> | ||||
</label> | </label> | ||||
</> | </> | ||||
) | |||||
); | |||||
}); | }); | ||||
RadioButton.displayName = 'RadioButton'; | 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'> { | export interface RadioTickBoxProps extends Omit<React.InputHTMLAttributes<RadioTickBoxDerivedElement>, 'type' | 'size'> { | ||||
block?: boolean; | block?: boolean; | ||||
subtext?: React.ReactNode; | 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 defaultId = React.useId(); | ||||
const id = idProp ?? defaultId; | const id = idProp ?? defaultId; | ||||
return ( | return ( | ||||
@@ -80,7 +81,12 @@ export const RadioTickBox = React.forwardRef<RadioTickBoxDerivedElement, RadioTi | |||||
</div> | </div> | ||||
)} | )} | ||||
</div> | </div> | ||||
) | |||||
); | |||||
}); | }); | ||||
RadioTickBox.displayName = 'RadioTickBox'; | RadioTickBox.displayName = 'RadioTickBox'; | ||||
RadioTickBox.defaultProps = { | |||||
block: false, | |||||
subtext: undefined, | |||||
}; |
@@ -1,5 +1,8 @@ | |||||
{ | { | ||||
"root": true, | "root": true, | ||||
"rules": { | |||||
"react/jsx-props-no-spreading": "off" | |||||
}, | |||||
"extends": [ | "extends": [ | ||||
"lxsmnsyc/typescript/react" | "lxsmnsyc/typescript/react" | ||||
], | ], | ||||
@@ -12,9 +12,10 @@ export interface SwatchProps extends Omit<React.HTMLProps<SwatchDerivedElement>, | |||||
export const useSwatchControls = () => { | export const useSwatchControls = () => { | ||||
const id = React.useId(); | 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; | 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(() => ({ | return React.useMemo(() => ({ | ||||
id, | id, | ||||
@@ -72,23 +73,21 @@ export const Swatch = React.forwardRef<SwatchDerivedElement, SwatchProps>(({ | |||||
</span> | </span> | ||||
<span className="tabular-nums text-xs sr-only"> | <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> | </span> | ||||
</label> | </label> | ||||
@@ -97,3 +96,7 @@ export const Swatch = React.forwardRef<SwatchDerivedElement, SwatchProps>(({ | |||||
}); | }); | ||||
Swatch.displayName = 'Swatch'; | Swatch.displayName = 'Swatch'; | ||||
Swatch.defaultProps = { | |||||
mode: 'rgb', | |||||
}; |
@@ -1,5 +1,9 @@ | |||||
{ | { | ||||
"root": true, | "root": true, | ||||
"rules": { | |||||
"quote-props": "off", | |||||
"react/jsx-props-no-spreading": "off" | |||||
}, | |||||
"extends": [ | "extends": [ | ||||
"lxsmnsyc/typescript/react" | "lxsmnsyc/typescript/react" | ||||
], | ], | ||||
@@ -4,7 +4,7 @@ import clsx from 'clsx'; | |||||
export type EmailInputDerivedElement = HTMLInputElement; | 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. | * 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, | variant = 'default' as const, | ||||
hiddenLabel = false, | hiddenLabel = false, | ||||
className, | className, | ||||
id: idProp, | |||||
style, | |||||
...etcProps | ...etcProps | ||||
}: EmailInputProps, | }: EmailInputProps, | ||||
ref, | |||||
forwardedRef, | |||||
) => { | ) => { | ||||
const labelId = React.useId(); | const labelId = React.useId(); | ||||
const defaultId = React.useId(); | |||||
const id = idProp ?? defaultId; | |||||
return ( | return ( | ||||
<div | <div | ||||
@@ -71,10 +75,12 @@ export const EmailInput = React.forwardRef<EmailInputDerivedElement, EmailInputP | |||||
}, | }, | ||||
className, | className, | ||||
)} | )} | ||||
style={style} | |||||
> | > | ||||
<input | <input | ||||
{...etcProps} | {...etcProps} | ||||
ref={ref} | |||||
ref={forwardedRef} | |||||
id={id} | |||||
aria-labelledby={labelId} | aria-labelledby={labelId} | ||||
type="email" | type="email" | ||||
data-testid="input" | data-testid="input" | ||||
@@ -114,8 +120,9 @@ export const EmailInput = React.forwardRef<EmailInputDerivedElement, EmailInputP | |||||
<label | <label | ||||
data-testid="label" | data-testid="label" | ||||
id={labelId} | id={labelId} | ||||
htmlFor={id} | |||||
className={clsx( | 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, | 'sr-only': hiddenLabel, | ||||
}, | }, | ||||
@@ -139,7 +146,7 @@ export const EmailInput = React.forwardRef<EmailInputDerivedElement, EmailInputP | |||||
<div | <div | ||||
data-testid="hint" | data-testid="hint" | ||||
className={clsx( | 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', | 'bottom-0 pl-4 pb-1': variant === 'default', | ||||
'top-0.5': variant === 'alternate', | 'top-0.5': variant === 'alternate', | ||||
@@ -169,7 +176,7 @@ export const EmailInput = React.forwardRef<EmailInputDerivedElement, EmailInputP | |||||
{indicator && ( | {indicator && ( | ||||
<div | <div | ||||
className={clsx( | 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-10': size === 'small', | ||||
'w-12': size === 'medium', | 'w-12': size === 'medium', | ||||
@@ -194,3 +201,14 @@ export const EmailInput = React.forwardRef<EmailInputDerivedElement, EmailInputP | |||||
); | ); | ||||
EmailInput.displayName = 'EmailInput'; | 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 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. | * 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. | * 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( | 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( | 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-10': indicator && size === 'small', | ||||
'pr-12': indicator && size === 'medium', | 'pr-12': indicator && size === 'medium', | ||||
'pr-16': indicator && size === 'large', | '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 | <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> | </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.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 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. | * 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. | * 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( | 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( | 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-10': indicator && size === 'small', | ||||
'pr-12': indicator && size === 'medium', | 'pr-12': indicator && size === 'medium', | ||||
'pr-16': indicator && size === 'large', | '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 | <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> | </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.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, | "root": true, | ||||
"rules": { | |||||
"quote-props": "off", | |||||
"react/jsx-props-no-spreading": "off" | |||||
}, | |||||
"extends": [ | "extends": [ | ||||
"lxsmnsyc/typescript/react" | "lxsmnsyc/typescript/react" | ||||
], | ], | ||||
@@ -42,155 +42,174 @@ export interface MaskedTextInputProps extends Omit<React.HTMLProps<MaskedTextInp | |||||
/** | /** | ||||
* Component for inputting textual values. | * 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( | 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( | 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-10': indicator && size === 'small', | ||||
'pr-12': indicator && size === 'medium', | 'pr-12': indicator && size === 'medium', | ||||
'pr-16': indicator && size === 'large', | '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 | <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> | </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.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 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. | * 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. | * 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( | 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( | 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-10': indicator && size === 'small', | ||||
'pr-12': indicator && size === 'medium', | 'pr-12': indicator && size === 'medium', | ||||
'pr-16': indicator && size === 'large', | '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 | <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> | </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.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 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. | * 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? | * Is the label hidden? | ||||
*/ | */ | ||||
hiddenLabel?: boolean, | 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. | * 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( | 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( | 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-10': indicator && size === 'small', | ||||
'pr-12': indicator && size === 'medium', | 'pr-12': indicator && size === 'medium', | ||||
'pr-16': indicator && size === 'large', | '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 | <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> | </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.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, | "root": true, | ||||
"rules": { | |||||
"react/jsx-props-no-spreading": "off" | |||||
}, | |||||
"extends": [ | "extends": [ | ||||
"lxsmnsyc/typescript/react" | "lxsmnsyc/typescript/react" | ||||
], | ], | ||||
@@ -7,12 +7,15 @@ export interface BadgeProps extends React.HTMLProps<BadgeDerivedElement> { | |||||
rounded?: boolean; | 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 | <span | ||||
{...etcProps} | {...etcProps} | ||||
ref={forwardedRef} | ref={forwardedRef} | ||||
@@ -26,10 +29,14 @@ export const Badge = React.forwardRef<BadgeDerivedElement, BadgeProps>(({ | |||||
className, | className, | ||||
)} | )} | ||||
> | > | ||||
<span className="relative w-full"> | |||||
{children} | |||||
</span> | |||||
<span className="relative w-full"> | |||||
{children} | |||||
</span> | </span> | ||||
</span> | |||||
)); | )); | ||||
Badge.displayName = 'Badge'; | Badge.displayName = 'Badge'; | ||||
Badge.defaultProps = { | |||||
rounded: false, | |||||
}; |
@@ -14,11 +14,14 @@ export interface KeyValueTableProps extends Omit<React.HTMLProps<KeyValueTableDe | |||||
properties?: (KeyValueProperty | boolean)[]; | 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 | <dl | ||||
{...etcProps} | {...etcProps} | ||||
className={clsx( | className={clsx( | ||||
@@ -55,3 +58,8 @@ export const KeyValueTable = React.forwardRef<KeyValueTableDerivedElement, KeyVa | |||||
)); | )); | ||||
KeyValueTable.displayName = 'KeyValueTable'; | KeyValueTable.displayName = 'KeyValueTable'; | ||||
KeyValueTable.defaultProps = { | |||||
hiddenKeys: false, | |||||
properties: [], | |||||
}; |
@@ -1,5 +1,9 @@ | |||||
{ | { | ||||
"root": true, | "root": true, | ||||
"rules": { | |||||
"quote-props": "off", | |||||
"react/jsx-props-no-spreading": "off" | |||||
}, | |||||
"extends": [ | "extends": [ | ||||
"lxsmnsyc/typescript/react" | "lxsmnsyc/typescript/react" | ||||
], | ], | ||||
@@ -37,6 +37,9 @@ export interface MenuMultiSelectProps extends Omit<React.HTMLProps<MenuMultiSele | |||||
* Is the label hidden? | * Is the label hidden? | ||||
*/ | */ | ||||
hiddenLabel?: boolean, | hiddenLabel?: boolean, | ||||
/** | |||||
* Starting height of the component. | |||||
*/ | |||||
startingHeight?: number | string, | 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. | * 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( | 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( | 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-10': indicator && size === 'small', | ||||
'pr-12': indicator && size === 'medium', | 'pr-12': indicator && size === 'medium', | ||||
'pr-16': indicator && size === 'large', | '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 | <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> | </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.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 * as React from 'react'; | ||||
import { TagsInput } from 'react-tag-input-component'; | import { TagsInput } from 'react-tag-input-component'; | ||||
import clsx from 'clsx'; | 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'; | 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; | export type TagInputDerivedElement = HTMLTextAreaElement; | ||||
@@ -41,7 +47,13 @@ export interface TagInputProps extends Omit<React.HTMLProps<TagInputDerivedEleme | |||||
* Is the label hidden? | * Is the label hidden? | ||||
*/ | */ | ||||
hiddenLabel?: boolean, | hiddenLabel?: boolean, | ||||
/** | |||||
* Should the component be enhanced? | |||||
*/ | |||||
enhanced?: boolean, | enhanced?: boolean, | ||||
/** | |||||
* Separators for splitting the input value into multiple tags. | |||||
*/ | |||||
separator?: TagInputSeparator[], | separator?: TagInputSeparator[], | ||||
} | } | ||||
@@ -50,205 +62,182 @@ export interface TagInputProps extends Omit<React.HTMLProps<TagInputDerivedEleme | |||||
* | * | ||||
* This component supports multiline input and adjusts its layout accordingly. | * 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( | 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-10': size === 'small', | ||||
'min-h-12': size === 'medium', | 'min-h-12': size === 'medium', | ||||
'min-h-16': size === 'large', | '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( | 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', | '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 | <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> | </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.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; | size?: Button.Size; | ||||
subtext?: React.ReactNode; | subtext?: React.ReactNode; | ||||
badge?: React.ReactNode; | badge?: React.ReactNode; | ||||
variant: Button.Variant; | |||||
variant?: Button.Variant; | |||||
indeterminate?: boolean; | 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 defaultRef = React.useRef<ToggleButtonDerivedElement>(null); | ||||
const ref = forwardedRef ?? defaultRef; | const ref = forwardedRef ?? defaultRef; | ||||
const defaultId = React.useId(); | const defaultId = React.useId(); | ||||
@@ -69,7 +72,7 @@ export const ToggleButton = React.forwardRef<ToggleButtonDerivedElement, ToggleB | |||||
'pl-4 pr-4': !compact, | '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', | '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-current': variant !== 'filled', | ||||
'border-negative': variant === 'filled', | 'border-negative': variant === 'filled', | ||||
'-mr-2': compact, | '-mr-2': compact, | ||||
} | |||||
}, | |||||
)} | )} | ||||
> | > | ||||
<svg | <svg | ||||
@@ -96,7 +99,7 @@ export const ToggleButton = React.forwardRef<ToggleButtonDerivedElement, ToggleB | |||||
{ | { | ||||
'stroke-negative': variant === 'filled', | 'stroke-negative': variant === 'filled', | ||||
'stroke-current': variant !== 'filled', | 'stroke-current': variant !== 'filled', | ||||
} | |||||
}, | |||||
)} | )} | ||||
viewBox="0 0 24 24" | viewBox="0 0 24 24" | ||||
role="presentation" | role="presentation" | ||||
@@ -111,7 +114,7 @@ export const ToggleButton = React.forwardRef<ToggleButtonDerivedElement, ToggleB | |||||
{ | { | ||||
'stroke-negative': variant === 'filled', | 'stroke-negative': variant === 'filled', | ||||
'stroke-current': variant !== 'filled', | 'stroke-current': variant !== 'filled', | ||||
} | |||||
}, | |||||
)} | )} | ||||
viewBox="0 0 24 24" | viewBox="0 0 24 24" | ||||
role="presentation" | role="presentation" | ||||
@@ -170,7 +173,17 @@ export const ToggleButton = React.forwardRef<ToggleButtonDerivedElement, ToggleB | |||||
</span> | </span> | ||||
</label> | </label> | ||||
</> | </> | ||||
) | |||||
); | |||||
}); | }); | ||||
ToggleButton.displayName = 'ToggleButton'; | 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'> { | export interface ToggleSwitchProps extends Omit<React.InputHTMLAttributes<ToggleSwitchDerivedElement>, 'type' | 'size'> { | ||||
block?: boolean; | block?: boolean; | ||||
subtext?: React.ReactNode; | subtext?: React.ReactNode; | ||||
badge?: React.ReactNode; | |||||
indeterminate?: boolean; | indeterminate?: boolean; | ||||
checkedLabel?: React.ReactNode; | checkedLabel?: React.ReactNode; | ||||
uncheckedLabel?: 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 defaultRef = React.useRef<ToggleSwitchDerivedElement>(null); | ||||
const ref = forwardedRef ?? defaultRef; | const ref = forwardedRef ?? defaultRef; | ||||
const defaultId = React.useId(); | const defaultId = React.useId(); | ||||
@@ -112,7 +113,15 @@ export const ToggleSwitch = React.forwardRef<ToggleSwitchDerivedElement, ToggleS | |||||
</div> | </div> | ||||
)} | )} | ||||
</div> | </div> | ||||
) | |||||
); | |||||
}); | }); | ||||
ToggleSwitch.displayName = 'ToggleSwitch'; | 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'> { | export interface ToggleTickBoxProps extends Omit<React.InputHTMLAttributes<ToggleTickBoxDerivedElement>, 'type' | 'size'> { | ||||
block?: boolean; | block?: boolean; | ||||
subtext?: React.ReactNode; | subtext?: React.ReactNode; | ||||
badge?: React.ReactNode; | |||||
indeterminate?: boolean; | 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 defaultRef = React.useRef<ToggleTickBoxDerivedElement>(null); | ||||
const ref = forwardedRef ?? defaultRef; | const ref = forwardedRef ?? defaultRef; | ||||
const defaultId = React.useId(); | const defaultId = React.useId(); | ||||
@@ -113,7 +114,13 @@ export const ToggleTickBox = React.forwardRef<ToggleTickBoxDerivedElement, Toggl | |||||
</div> | </div> | ||||
)} | )} | ||||
</div> | </div> | ||||
) | |||||
); | |||||
}); | }); | ||||
ToggleTickBox.displayName = 'ToggleTickBox'; | ToggleTickBox.displayName = 'ToggleTickBox'; | ||||
ToggleTickBox.defaultProps = { | |||||
block: false, | |||||
indeterminate: false, | |||||
subtext: undefined, | |||||
}; |
@@ -1,5 +1,8 @@ | |||||
{ | { | ||||
"root": true, | "root": true, | ||||
"rules": { | |||||
"react/jsx-props-no-spreading": "off" | |||||
}, | |||||
"extends": [ | "extends": [ | ||||
"lxsmnsyc/typescript/react" | "lxsmnsyc/typescript/react" | ||||
], | ], | ||||
@@ -4,30 +4,33 @@ import { Button } from '@tesseract-design/web-base'; | |||||
export type LinkButtonDerivedElement = HTMLAnchorElement; | 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; | block?: boolean; | ||||
variant: Button.Variant; | |||||
variant?: Button.Variant; | |||||
subtext?: React.ReactNode; | subtext?: React.ReactNode; | ||||
badge?: React.ReactNode; | badge?: React.ReactNode; | ||||
menuItem?: boolean; | menuItem?: boolean; | ||||
size?: Button.Size; | size?: Button.Size; | ||||
compact?: boolean; | 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 | <Component | ||||
{...etcProps} | {...etcProps} | ||||
ref={forwardedRef} | ref={forwardedRef} | ||||
@@ -46,7 +49,7 @@ export const LinkButton = React.forwardRef<LinkButtonDerivedElement, LinkButtonP | |||||
'pr-2': compact || menuItem, | '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-negative text-primary focus:text-secondary active:text-tertiary': variant !== 'filled', | ||||
'bg-primary text-negative focus:bg-secondary active:bg-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" | viewBox="0 0 24 24" | ||||
role="presentation" | role="presentation" | ||||
> | > | ||||
<polyline points="9 18 15 12 9 6"/> | |||||
<polyline points="9 18 15 12 9 6" /> | |||||
</svg> | </svg> | ||||
</span> | </span> | ||||
)} | )} | ||||
@@ -116,3 +119,14 @@ export const LinkButton = React.forwardRef<LinkButtonDerivedElement, LinkButtonP | |||||
)); | )); | ||||
LinkButton.displayName = 'LinkButton'; | 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, | "root": true, | ||||
"rules": { | |||||
"quote-props": "off", | |||||
"react/jsx-props-no-spreading": "off" | |||||
}, | |||||
"extends": [ | "extends": [ | ||||
"lxsmnsyc/typescript/react" | "lxsmnsyc/typescript/react" | ||||
], | ], | ||||
@@ -29,8 +29,9 @@ const filterOptions = (children: React.ReactNode): React.ReactNode => { | |||||
/* | /* | ||||
* Caveat for slider: | * 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'; | export type SliderOrientation = 'horizontal' | 'vertical'; | ||||
@@ -43,16 +44,19 @@ export interface SliderProps extends Omit<React.HTMLProps<HTMLInputElement>, 'ty | |||||
length?: React.CSSProperties['width']; | 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>(); | const [browser, setBrowser] = React.useState<string>(); | ||||
React.useEffect(() => { | React.useEffect(() => { | ||||
const isFirefox = typeof (window as unknown as Record<string, unknown>).InstallTrigger !== 'undefined'; | 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(); | const tickMarkId = React.useId(); | ||||
React.useEffect(() => { | 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 () => { | 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'; | grandParent.style.width = 'auto'; | ||||
wrapper.removeAttribute(`data-${browser}`); | wrapper.removeAttribute(`data-${browser}`); | ||||
wrapper.dataset.orient = slider.getAttribute(orient) ?? undefined; | wrapper.dataset.orient = slider.getAttribute(orient) ?? undefined; | ||||
@@ -99,42 +119,63 @@ export const Slider = React.forwardRef<SliderDerivedElement, SliderProps>(({ | |||||
}, [ref, orient, children, browser]); | }, [ref, orient, children, browser]); | ||||
React.useEffect(() => { | 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 () => { | 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) { | 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-height'); | ||||
grandParent.removeAttribute('data-width'); | grandParent.removeAttribute('data-width'); | ||||
} | } | ||||
wrapper.removeAttribute(`data-${browser}`); | |||||
wrapper.removeAttribute(`data-${browser ?? 'unknown'}`); | |||||
}; | }; | ||||
}, [ref, orient, browser]); | }, [ref, orient, browser]); | ||||
@@ -142,7 +183,7 @@ export const Slider = React.forwardRef<SliderDerivedElement, SliderProps>(({ | |||||
if (!(typeof ref === 'object' && ref)) { | if (!(typeof ref === 'object' && ref)) { | ||||
return; | return; | ||||
} | } | ||||
const {current: slider} = ref; | |||||
const { current: slider } = ref; | |||||
if (!slider) { | if (!slider) { | ||||
return; | return; | ||||
} | } | ||||
@@ -266,7 +307,7 @@ export const Slider = React.forwardRef<SliderDerivedElement, SliderProps>(({ | |||||
<div | <div | ||||
key={c.props.value} | key={c.props.value} | ||||
className="absolute w-full h-full box-border" | className="absolute w-full h-full box-border" | ||||
data-offset={`${(value - minValue) / (maxValue - minValue) * 100}%`} | |||||
data-offset={`${((value - minValue) / (maxValue - minValue)) * 100}%`} | |||||
> | > | ||||
<div | <div | ||||
className="flex h-full text-xs items-center justify-between" | 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> | ||||
</div> | </div> | ||||
) | |||||
); | |||||
}) | }) | ||||
} | } | ||||
</div> | </div> | ||||
@@ -291,7 +332,13 @@ export const Slider = React.forwardRef<SliderDerivedElement, SliderProps>(({ | |||||
</div> | </div> | ||||
</div> | </div> | ||||
</> | </> | ||||
) | |||||
); | |||||
}); | }); | ||||
Slider.displayName = 'Slider'; | 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. | * 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( | 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( | 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-10': indicator && size === 'small', | ||||
'pr-12': indicator && size === 'medium', | 'pr-12': indicator && size === 'medium', | ||||
'pr-16': indicator && size === 'large', | '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 | <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> | </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.displayName = 'Spinner'; | ||||
Spinner.defaultProps = { | |||||
label: undefined, | |||||
hint: undefined, | |||||
indicator: undefined, | |||||
border: false, | |||||
block: false, | |||||
hiddenLabel: false, | |||||
size: 'medium', | |||||
variant: 'default', | |||||
}; |