Browse Source

Make components consistent

Manage props directly and fix some linting errors across all categories.
pull/1/head
TheoryOfNekomata 1 year ago
parent
commit
a73d093533
33 changed files with 1937 additions and 1508 deletions
  1. +2
    -0
      base/src/text-control.ts
  2. +5
    -0
      categories/action/react/.eslintrc
  3. +30
    -16
      categories/action/react/src/components/ActionButton/index.tsx
  4. +4
    -0
      categories/choice/react/.eslintrc
  5. +170
    -141
      categories/choice/react/src/components/ComboBox/index.tsx
  6. +21
    -5
      categories/choice/react/src/components/DropdownSelect/index.tsx
  7. +23
    -3
      categories/choice/react/src/components/MenuSelect/index.tsx
  8. +29
    -17
      categories/choice/react/src/components/RadioButton/index.tsx
  9. +18
    -12
      categories/choice/react/src/components/RadioTickBox/index.tsx
  10. +3
    -0
      categories/color/react/.eslintrc
  11. +22
    -19
      categories/color/react/src/components/Swatch/index.tsx
  12. +4
    -0
      categories/formatted/react/.eslintrc
  13. +24
    -6
      categories/formatted/react/src/components/EmailInput/index.tsx
  14. +149
    -130
      categories/formatted/react/src/components/PhoneNumberInput/index.tsx
  15. +146
    -130
      categories/formatted/react/src/components/UrlInput/index.tsx
  16. +4
    -0
      categories/freeform/react/.eslintrc
  17. +148
    -129
      categories/freeform/react/src/components/MaskedTextInput/index.tsx
  18. +162
    -143
      categories/freeform/react/src/components/MultilineTextInput/index.tsx
  19. +162
    -131
      categories/freeform/react/src/components/TextInput/index.tsx
  20. +3
    -0
      categories/information/react/.eslintrc
  21. +16
    -9
      categories/information/react/src/components/Badge/index.tsx
  22. +13
    -5
      categories/information/react/src/components/KeyValueTable/index.tsx
  23. +4
    -0
      categories/multichoice/react/.eslintrc
  24. +166
    -145
      categories/multichoice/react/src/components/MenuMultiSelect/index.tsx
  25. +244
    -213
      categories/multichoice/react/src/components/TagInput/index.tsx
  26. +32
    -19
      categories/multichoice/react/src/components/ToggleButton/index.tsx
  27. +23
    -14
      categories/multichoice/react/src/components/ToggleSwitch/index.tsx
  28. +20
    -13
      categories/multichoice/react/src/components/ToggleTickBox/index.tsx
  29. +3
    -0
      categories/navigation/react/.eslintrc
  30. +32
    -18
      categories/navigation/react/src/components/LinkButton/index.tsx
  31. +4
    -0
      categories/number/react/.eslintrc
  32. +107
    -60
      categories/number/react/src/components/Slider/index.tsx
  33. +144
    -130
      categories/number/react/src/components/Spinner/index.tsx

+ 2
- 0
base/src/text-control.ts View File

@@ -3,3 +3,5 @@ export type Size = 'small' | 'medium' | 'large';
export type Variant = 'default' | 'alternate';

export type InputType = 'text' | 'search';

export type InputMode = 'none' | 'numeric' | 'decimal' | InputType;

+ 5
- 0
categories/action/react/.eslintrc View File

@@ -1,5 +1,10 @@
{
"root": true,
"rules": {
"quote-props": "off",
"react/jsx-props-no-spreading": "off",
"react/button-has-type": "off"
},
"extends": [
"lxsmnsyc/typescript/react"
],


+ 30
- 16
categories/action/react/src/components/ActionButton/index.tsx View File

@@ -6,7 +6,7 @@ export type ActionButtonDerivedElement = HTMLButtonElement;

export interface ActionButtonProps extends Omit<React.HTMLProps<ActionButtonDerivedElement>, 'type' | 'size'> {
type?: Button.Type;
variant: Button.Variant;
variant?: Button.Variant;
block?: boolean;
subtext?: React.ReactNode;
badge?: React.ReactNode;
@@ -15,19 +15,22 @@ export interface ActionButtonProps extends Omit<React.HTMLProps<ActionButtonDeri
compact?: boolean;
}

export const ActionButton = React.forwardRef<ActionButtonDerivedElement, ActionButtonProps>(({
type = 'button' as const,
variant,
subtext,
badge,
menuItem = false,
children,
size = 'medium' as const,
compact = false,
className,
block = false,
...etcProps
}, forwardedRef) => (
export const ActionButton = React.forwardRef<ActionButtonDerivedElement, ActionButtonProps>((
{
type = 'button' as const,
variant = 'bare' as const,
subtext,
badge,
menuItem = false,
children,
size = 'medium' as const,
compact = false,
className,
block = false,
...etcProps
},
forwardedRef,
) => (
<button
{...etcProps}
type={type}
@@ -48,7 +51,7 @@ export const ActionButton = React.forwardRef<ActionButtonDerivedElement, ActionB
'pr-2': compact || menuItem,
},
{
'border-2 border-primary disabled:border-primary focus:border-secondary active:border-tertiary' : variant !== 'bare',
'border-2 border-primary disabled:border-primary focus:border-secondary active:border-tertiary': variant !== 'bare',
'bg-negative text-primary disabled:text-primary focus:text-secondary active:text-tertiary': variant !== 'filled',
'bg-primary text-negative disabled:bg-primary focus:bg-secondary active:bg-tertiary': variant === 'filled',
},
@@ -110,7 +113,7 @@ export const ActionButton = React.forwardRef<ActionButtonDerivedElement, ActionB
viewBox="0 0 24 24"
role="presentation"
>
<polyline points="9 18 15 12 9 6"/>
<polyline points="9 18 15 12 9 6" />
</svg>
</span>
)}
@@ -118,3 +121,14 @@ export const ActionButton = React.forwardRef<ActionButtonDerivedElement, ActionB
));

ActionButton.displayName = 'ActionButton';

ActionButton.defaultProps = {
type: 'button',
variant: 'bare',
block: false,
subtext: undefined,
badge: undefined,
menuItem: false,
size: 'medium',
compact: false,
};

+ 4
- 0
categories/choice/react/.eslintrc View File

@@ -1,5 +1,9 @@
{
"root": true,
"rules": {
"quote-props": "off",
"react/jsx-props-no-spreading": "off"
},
"extends": [
"lxsmnsyc/typescript/react"
],


+ 170
- 141
categories/choice/react/src/components/ComboBox/index.tsx View File

@@ -4,7 +4,7 @@ import clsx from 'clsx';

export type ComboBoxDerivedElement = HTMLInputElement;

export interface ComboBoxProps extends Omit<React.HTMLProps<ComboBoxDerivedElement>, 'size' | 'type' | 'style' | 'label' | 'list'> {
export interface ComboBoxProps extends Omit<React.HTMLProps<ComboBoxDerivedElement>, 'size' | 'type' | 'style' | 'label' | 'list' | 'inputMode'> {
/**
* Short textual description indicating the nature of the component's value.
*/
@@ -41,6 +41,10 @@ export interface ComboBoxProps extends Omit<React.HTMLProps<ComboBoxDerivedEleme
* Is the label hidden?
*/
hiddenLabel?: boolean,
/**
* Input mode of the component.
*/
inputMode?: TextControl.InputMode,
}

/**
@@ -48,166 +52,191 @@ export interface ComboBoxProps extends Omit<React.HTMLProps<ComboBoxDerivedEleme
*
* This component supports multiline input and adjusts its layout accordingly.
*/
export const ComboBox = React.forwardRef<ComboBoxDerivedElement, ComboBoxProps>(
(
{
label = '',
hint = '',
indicator,
size = 'medium' as const,
border = false,
block = false,
type = 'text' as const,
variant = 'default' as const,
hiddenLabel = false,
className,
children,
...etcProps
}: ComboBoxProps,
ref,
) => {
const labelId = React.useId();
const datalistId = React.useId();
export const ComboBox = React.forwardRef<ComboBoxDerivedElement, ComboBoxProps>((
{
label,
hint,
indicator,
size = 'medium' as const,
border = false,
block = false,
type = 'text' as const,
variant = 'default' as const,
hiddenLabel = false,
className,
children,
inputMode = 'text' as const,
id: idProp,
...etcProps
}: ComboBoxProps,
forwardedRef,
) => {
const labelId = React.useId();
const datalistId = React.useId();
const defaultId = React.useId();
const id = idProp ?? defaultId;

let resultInputMode = inputMode as React.HTMLProps<ComboBoxDerivedElement>['inputMode'];
if (type === 'text' && resultInputMode === 'search') {
resultInputMode = 'text';
} else if (type === 'search' && resultInputMode === 'text') {
resultInputMode = 'search';
}

return (
<>
<datalist
id={datalistId}
>
{children}
</datalist>
<div
return (
<>
<datalist
id={datalistId}
>
{children}
</datalist>
<div
className={clsx(
'relative rounded ring-secondary/50',
'focus-within:ring-4',
{
'block': block,
'inline-block align-middle': !block,
},
className,
)}
>
<input
{...etcProps}
ref={forwardedRef}
id={id}
aria-labelledby={labelId}
type={type}
inputMode={resultInputMode}
list={datalistId}
data-testid="input"
className={clsx(
'relative rounded ring-secondary/50',
'focus-within:ring-4',
'bg-negative rounded-inherit w-full peer block',
'focus:outline-0',
'disabled:opacity-50 disabled:cursor-not-allowed',
{
'text-xxs': size === 'small',
'text-xs': size === 'medium',
},
{
'pl-4': variant === 'default',
'pl-1.5': variant === 'alternate',
},
{
'pt-4': variant === 'alternate',
},
{
'pr-4': variant === 'default' && !indicator,
'pr-1.5': variant === 'alternate' && !indicator,
},
{
'pr-10': indicator && size === 'small',
'pr-12': indicator && size === 'medium',
'pr-16': indicator && size === 'large',
},
{
'block': block,
'inline-block align-middle': !block,
'h-10': size === 'small',
'h-12': size === 'medium',
'h-16': size === 'large',
},
className,
)}
>
<input
{...etcProps}
ref={ref}
aria-labelledby={labelId}
type={type}
list={datalistId}
data-testid="input"
/>
{
label && (
<label
data-testid="label"
id={labelId}
htmlFor={id}
className={clsx(
'absolute z-[1] w-full top-0.5 left-0 pointer-events-none pl-1 text-xxs font-bold peer-disabled:opacity-50 peer-focus:text-secondary text-primary leading-none bg-negative',
{
'sr-only': hiddenLabel,
},
{
'pr-1': !indicator,
},
{
'pr-10': indicator && size === 'small',
'pr-12': indicator && size === 'medium',
'pr-16': indicator && size === 'large',
},
)}
>
<span className="w-full whitespace-nowrap h-[1.1em] overflow-hidden text-ellipsis">
{label}
</span>
</label>
)
}
{hint && (
<div
data-testid="hint"
className={clsx(
'bg-negative rounded-inherit w-full peer block',
'focus:outline-0',
'disabled:opacity-50 disabled:cursor-not-allowed',
'absolute left-0 px-1 pointer-events-none text-xxs peer-disabled:opacity-50 leading-none w-full bg-negative',
{
'text-xxs': size === 'small',
'text-xs': size === 'medium',
'bottom-0 pl-4 pb-1': variant === 'default',
'top-0.5': variant === 'alternate',
},
{
'pl-4': variant === 'default',
'pl-1.5': variant === 'alternate',
'pt-2': variant === 'alternate' && size === 'small',
'pt-3': variant === 'alternate' && size !== 'small',
},
{
'pt-4': variant === 'alternate',
},
{
'pr-4': variant === 'default' && !indicator,
'pr-1.5': variant === 'alternate' && !indicator,
'pr-4': !indicator && variant === 'default',
'pr-1': !indicator && variant === 'alternate',
},
{
'pr-10': indicator && size === 'small',
'pr-12': indicator && size === 'medium',
'pr-16': indicator && size === 'large',
},
{
'h-10': size === 'small',
'h-12': size === 'medium',
'h-16': size === 'large',
},
)}
/>
{
label && (
<label
data-testid="label"
id={labelId}
className={clsx(
'absolute z-[1] w-full top-0.5 left-0 pointer-events-none pl-1 text-xxs font-bold peer-disabled:opacity-50 peer-focus:text-secondary text-primary leading-none bg-negative',
{
'sr-only': hiddenLabel,
},
{
'pr-1': !indicator,
},
{
'pr-10': indicator && size === 'small',
'pr-12': indicator && size === 'medium',
'pr-16': indicator && size === 'large',
},
)}
>
<span className="w-full whitespace-nowrap h-[1.1em] overflow-hidden text-ellipsis">
{label}
</span>
</label>
)
}
{hint && (
<div
data-testid="hint"
className={clsx(
'absolute left-0 px-1 pointer-events-none text-xxs peer-disabled:opacity-50 leading-none w-full bg-negative',
{
'bottom-0 pl-4 pb-1': variant === 'default',
'top-0.5': variant === 'alternate',
},
{
'pt-2': variant === 'alternate' && size === 'small',
'pt-3': variant === 'alternate' && size !== 'small',
},
{
'pr-4': !indicator && variant === 'default',
'pr-1': !indicator && variant === 'alternate',
},
{
'pr-10': indicator && size === 'small',
'pr-12': indicator && size === 'medium',
'pr-16': indicator && size === 'large',
},
)}
>
<div
className="opacity-50 whitespace-nowrap w-full h-[1.1em] overflow-hidden text-ellipsis"
>
{hint}
</div>
</div>
)}
{indicator && (
>
<div
className={clsx(
'text-center flex items-center justify-center peer-disabled:opacity-50 aspect-square absolute bottom-0 right-0 pointer-events-none',
{
'w-10': size === 'small',
'w-12': size === 'medium',
'w-16': size === 'large',
},
)}
className="opacity-50 whitespace-nowrap w-full h-[1.1em] overflow-hidden text-ellipsis"
>
{indicator}
{hint}
</div>
)}
{
border && (
<span
data-testid="border"
className="absolute z-[1] peer-disabled:opacity-50 inset-0 rounded-inherit border-2 border-primary pointer-events-none peer-focus:border-secondary"
/>
)
}
</div>
</>
);
}
);
</div>
)}
{indicator && (
<div
className={clsx(
'text-center flex items-center justify-center peer-disabled:opacity-50 aspect-square absolute bottom-0 right-0 pointer-events-none',
{
'w-10': size === 'small',
'w-12': size === 'medium',
'w-16': size === 'large',
},
)}
>
{indicator}
</div>
)}
{
border && (
<span
data-testid="border"
className="absolute z-[1] peer-disabled:opacity-50 inset-0 rounded-inherit border-2 border-primary pointer-events-none peer-focus:border-secondary"
/>
)
}
</div>
</>
);
});

ComboBox.displayName = 'ComboBox';

ComboBox.defaultProps = {
label: undefined,
hint: undefined,
indicator: undefined,
size: 'medium',
border: false,
block: false,
type: 'text',
variant: 'default',
hiddenLabel: false,
inputMode: 'text',
};

+ 21
- 5
categories/choice/react/src/components/DropdownSelect/index.tsx View File

@@ -47,8 +47,8 @@ export interface DropdownSelectProps extends Omit<React.HTMLProps<DropdownSelect
export const DropdownSelect = React.forwardRef<DropdownSelectDerivedElement, DropdownSelectProps>(
(
{
label = '',
hint = '',
label,
hint,
indicator,
size = 'medium' as const,
border = false,
@@ -57,11 +57,14 @@ export const DropdownSelect = React.forwardRef<DropdownSelectDerivedElement, Dro
hiddenLabel = false,
className,
children,
id: idProp,
...etcProps
}: DropdownSelectProps,
ref,
forwardedRef,
) => {
const labelId = React.useId();
const defaultId = React.useId();
const id = idProp ?? defaultId;

return (
<div
@@ -77,7 +80,8 @@ export const DropdownSelect = React.forwardRef<DropdownSelectDerivedElement, Dro
>
<select
{...etcProps}
ref={ref}
ref={forwardedRef}
id={id}
aria-labelledby={labelId}
data-testid="input"
className={clsx(
@@ -116,6 +120,7 @@ export const DropdownSelect = React.forwardRef<DropdownSelectDerivedElement, Dro
{
label && (
<label
htmlFor={id}
data-testid="label"
id={labelId}
className={clsx(
@@ -194,7 +199,18 @@ export const DropdownSelect = React.forwardRef<DropdownSelectDerivedElement, Dro
}
</div>
);
}
},
);

DropdownSelect.displayName = 'DropdownSelect';

DropdownSelect.defaultProps = {
label: undefined,
hint: undefined,
indicator: undefined,
size: 'medium',
border: false,
block: false,
variant: 'default',
hiddenLabel: false,
};

+ 23
- 3
categories/choice/react/src/components/MenuSelect/index.tsx View File

@@ -37,6 +37,9 @@ export interface MenuSelectProps extends Omit<React.HTMLProps<MenuSelectDerivedE
* Is the label hidden?
*/
hiddenLabel?: boolean,
/**
* Starting height of the component.
*/
startingHeight?: number | string,
}

@@ -58,11 +61,14 @@ export const MenuSelect = React.forwardRef<MenuSelectDerivedElement, MenuSelectP
hiddenLabel = false,
className,
startingHeight = '15rem',
id: idProp,
...etcProps
}: MenuSelectProps,
ref,
forwardedRef,
) => {
const labelId = React.useId();
const defaultId = React.useId();
const id = idProp ?? defaultId;

return (
<div
@@ -78,7 +84,8 @@ export const MenuSelect = React.forwardRef<MenuSelectDerivedElement, MenuSelectP
>
<select
{...etcProps}
ref={ref}
ref={forwardedRef}
id={id}
aria-labelledby={labelId}
data-testid="input"
size={2}
@@ -131,6 +138,7 @@ export const MenuSelect = React.forwardRef<MenuSelectDerivedElement, MenuSelectP
label && (
<label
data-testid="label"
htmlFor={id}
id={labelId}
className={clsx(
'absolute z-[1] w-full top-0.5 left-0 pointer-events-none pl-1 text-xxs font-bold peer-disabled:opacity-50 peer-focus:text-secondary text-primary leading-none bg-negative',
@@ -208,7 +216,19 @@ export const MenuSelect = React.forwardRef<MenuSelectDerivedElement, MenuSelectP
}
</div>
);
}
},
);

MenuSelect.displayName = 'MenuSelect';

MenuSelect.defaultProps = {
label: undefined,
hint: undefined,
indicator: undefined,
size: 'medium',
border: false,
block: false,
variant: 'default',
hiddenLabel: false,
startingHeight: '15rem',
};

+ 29
- 17
categories/choice/react/src/components/RadioButton/index.tsx View File

@@ -10,22 +10,25 @@ export interface RadioButtonProps extends Omit<React.InputHTMLAttributes<RadioBu
size?: Button.Size;
subtext?: React.ReactNode;
badge?: React.ReactNode;
variant: Button.Variant;
variant?: Button.Variant;
}

export const RadioButton = React.forwardRef<RadioButtonDerivedElement, RadioButtonProps>(({
children,
block = false,
compact = false,
size = 'medium',
id: idProp,
className,
subtext,
badge,
variant,
style,
...etcProps
}, forwardedRef) => {
export const RadioButton = React.forwardRef<RadioButtonDerivedElement, RadioButtonProps>((
{
children,
block = false,
compact = false,
size = 'medium' as const,
id: idProp,
className,
subtext,
badge,
variant = 'bare' as const,
style,
...etcProps
},
forwardedRef,
) => {
const defaultId = React.useId();
const id = idProp ?? defaultId;
return (
@@ -55,7 +58,7 @@ export const RadioButton = React.forwardRef<RadioButtonDerivedElement, RadioButt
'pl-4 pr-4': !compact,
},
{
'border-2 border-primary peer-disabled:border-primary peer-focus:border-secondary peer-checked:border-tertiary active:border-tertiary' : variant !== 'bare',
'border-2 border-primary peer-disabled:border-primary peer-focus:border-secondary peer-checked:border-tertiary active:border-tertiary': variant !== 'bare',
'bg-primary text-negative peer-disabled:bg-primary peer-focus:bg-secondary peer-checked:bg-tertiary active:bg-tertiary': variant === 'filled',
},
{
@@ -73,7 +76,7 @@ export const RadioButton = React.forwardRef<RadioButtonDerivedElement, RadioButt
'-mr-2': compact,
'border-current': variant !== 'filled',
'border-negative': variant === 'filled',
}
},
)}
>
<span
@@ -135,7 +138,16 @@ export const RadioButton = React.forwardRef<RadioButtonDerivedElement, RadioButt
</span>
</label>
</>
)
);
});

RadioButton.displayName = 'RadioButton';

RadioButton.defaultProps = {
badge: undefined,
block: false,
compact: false,
subtext: undefined,
size: 'medium',
variant: 'filled',
};

+ 18
- 12
categories/choice/react/src/components/RadioTickBox/index.tsx View File

@@ -6,19 +6,20 @@ export type RadioTickBoxDerivedElement = HTMLInputElement;
export interface RadioTickBoxProps extends Omit<React.InputHTMLAttributes<RadioTickBoxDerivedElement>, 'type' | 'size'> {
block?: boolean;
subtext?: React.ReactNode;
badge?: React.ReactNode;
}

export const RadioTickBox = React.forwardRef<RadioTickBoxDerivedElement, RadioTickBoxProps>(({
children,
block = false,
id: idProp,
className,
subtext,
badge,
style,
...etcProps
}, forwardedRef) => {
export const RadioTickBox = React.forwardRef<RadioTickBoxDerivedElement, RadioTickBoxProps>((
{
children,
block = false,
id: idProp,
className,
subtext,
style,
...etcProps
},
forwardedRef,
) => {
const defaultId = React.useId();
const id = idProp ?? defaultId;
return (
@@ -80,7 +81,12 @@ export const RadioTickBox = React.forwardRef<RadioTickBoxDerivedElement, RadioTi
</div>
)}
</div>
)
);
});

RadioTickBox.displayName = 'RadioTickBox';

RadioTickBox.defaultProps = {
block: false,
subtext: undefined,
};

+ 3
- 0
categories/color/react/.eslintrc View File

@@ -1,5 +1,8 @@
{
"root": true,
"rules": {
"react/jsx-props-no-spreading": "off"
},
"extends": [
"lxsmnsyc/typescript/react"
],


+ 22
- 19
categories/color/react/src/components/Swatch/index.tsx View File

@@ -12,9 +12,10 @@ export interface SwatchProps extends Omit<React.HTMLProps<SwatchDerivedElement>,

export const useSwatchControls = () => {
const id = React.useId();
const copyColor: React.ReactEventHandler<SwatchDerivedElement> = React.useCallback(async (e) => {
const copyColor: React.ReactEventHandler<SwatchDerivedElement> = React.useCallback((e) => {
const { value } = e.currentTarget;
await window.navigator.clipboard.writeText(value);
// eslint-disable-next-line no-void
void window.navigator.clipboard.writeText(value);
}, []);
return React.useMemo(() => ({
id,
@@ -72,23 +73,21 @@ export const Swatch = React.forwardRef<SwatchDerivedElement, SwatchProps>(({
</span>
<span className="tabular-nums text-xs sr-only">
{
color
.map((c) => c
.toString()
.padStart(4, ' ')
.split('')
.map((cc, j) => (
<span
key={`${cc}:${j}`}
className={clsx({
'opacity-0': cc === ' ',
})}
>
{j === 0 && ' '}
{cc === ' ' && j > 0 ? '0' : cc}
</span>
))
)
color.map((c) => c
.toString()
.padStart(4, ' ')
.split('')
.map((cc, j) => (
<span
key={`${cc}:${j}`}
className={clsx({
'opacity-0': cc === ' ',
})}
>
{j === 0 && ' '}
{cc === ' ' && j > 0 ? '0' : cc}
</span>
)))
}
</span>
</label>
@@ -97,3 +96,7 @@ export const Swatch = React.forwardRef<SwatchDerivedElement, SwatchProps>(({
});

Swatch.displayName = 'Swatch';

Swatch.defaultProps = {
mode: 'rgb',
};

+ 4
- 0
categories/formatted/react/.eslintrc View File

@@ -1,5 +1,9 @@
{
"root": true,
"rules": {
"quote-props": "off",
"react/jsx-props-no-spreading": "off"
},
"extends": [
"lxsmnsyc/typescript/react"
],


+ 24
- 6
categories/formatted/react/src/components/EmailInput/index.tsx View File

@@ -4,7 +4,7 @@ import clsx from 'clsx';

export type EmailInputDerivedElement = HTMLInputElement;

export interface EmailInputProps extends Omit<React.HTMLProps<EmailInputDerivedElement>, 'size' | 'type' | 'label'> {
export interface EmailInputProps extends Omit<React.HTMLProps<EmailInputDerivedElement>, 'size' | 'type' | 'label' | 'inputMode'> {
/**
* Short textual description indicating the nature of the component's value.
*/
@@ -54,11 +54,15 @@ export const EmailInput = React.forwardRef<EmailInputDerivedElement, EmailInputP
variant = 'default' as const,
hiddenLabel = false,
className,
id: idProp,
style,
...etcProps
}: EmailInputProps,
ref,
forwardedRef,
) => {
const labelId = React.useId();
const defaultId = React.useId();
const id = idProp ?? defaultId;

return (
<div
@@ -71,10 +75,12 @@ export const EmailInput = React.forwardRef<EmailInputDerivedElement, EmailInputP
},
className,
)}
style={style}
>
<input
{...etcProps}
ref={ref}
ref={forwardedRef}
id={id}
aria-labelledby={labelId}
type="email"
data-testid="input"
@@ -114,8 +120,9 @@ export const EmailInput = React.forwardRef<EmailInputDerivedElement, EmailInputP
<label
data-testid="label"
id={labelId}
htmlFor={id}
className={clsx(
'absolute z-[1] w-full top-0.5 left-0 pointer-events-none pl-1 text-xxs font-bold peer-disabled:opacity-50 peer-focus:text-secondary text-primary leading-none bg-negative',
'absolute z-[1] w-full top-0.5 left-0 pointer-events-none pl-1 text-xxs font-bold peer-disabled:opacity-50 peer-focus:text-secondary text-primary leading-none bg-negative select-none',
{
'sr-only': hiddenLabel,
},
@@ -139,7 +146,7 @@ export const EmailInput = React.forwardRef<EmailInputDerivedElement, EmailInputP
<div
data-testid="hint"
className={clsx(
'absolute left-0 px-1 pointer-events-none text-xxs peer-disabled:opacity-50 leading-none w-full bg-negative',
'absolute left-0 px-1 pointer-events-none text-xxs peer-disabled:opacity-50 leading-none w-full bg-negative select-none',
{
'bottom-0 pl-4 pb-1': variant === 'default',
'top-0.5': variant === 'alternate',
@@ -169,7 +176,7 @@ export const EmailInput = React.forwardRef<EmailInputDerivedElement, EmailInputP
{indicator && (
<div
className={clsx(
'text-center flex items-center justify-center peer-disabled:opacity-50 aspect-square absolute bottom-0 right-0 pointer-events-none',
'text-center flex items-center justify-center peer-disabled:opacity-50 aspect-square absolute bottom-0 right-0 pointer-events-none select-none',
{
'w-10': size === 'small',
'w-12': size === 'medium',
@@ -194,3 +201,14 @@ export const EmailInput = React.forwardRef<EmailInputDerivedElement, EmailInputP
);

EmailInput.displayName = 'EmailInput';

EmailInput.defaultProps = {
label: undefined,
hint: undefined,
indicator: undefined,
size: 'medium',
border: false,
block: false,
variant: 'default',
hiddenLabel: false,
};

+ 149
- 130
categories/formatted/react/src/components/PhoneNumberInput/index.tsx View File

@@ -4,7 +4,7 @@ import clsx from 'clsx';

export type PhoneNumberInputDerivedElement = HTMLInputElement;

export interface PhoneNumberInputProps extends Omit<React.HTMLProps<PhoneNumberInputDerivedElement>, 'size' | 'type' | 'label'> {
export interface PhoneNumberInputProps extends Omit<React.HTMLProps<PhoneNumberInputDerivedElement>, 'size' | 'type' | 'label' | 'inputMode'> {
/**
* Short textual description indicating the nature of the component's value.
*/
@@ -42,155 +42,174 @@ export interface PhoneNumberInputProps extends Omit<React.HTMLProps<PhoneNumberI
/**
* Component for inputting textual values.
*/
export const PhoneNumberInput = React.forwardRef<PhoneNumberInputDerivedElement, PhoneNumberInputProps>(
(
{
label,
hint,
indicator,
size = 'medium' as const,
border = false,
block = false,
variant = 'default' as const,
hiddenLabel = false,
className,
...etcProps
}: PhoneNumberInputProps,
ref,
) => {
const labelId = React.useId();
export const PhoneNumberInput = React.forwardRef<
PhoneNumberInputDerivedElement,
PhoneNumberInputProps
>((
{
label,
hint,
indicator,
size = 'medium' as const,
border = false,
block = false,
variant = 'default' as const,
hiddenLabel = false,
className,
id: idProp,
style,
...etcProps
}: PhoneNumberInputProps,
forwardedRef,
) => {
const labelId = React.useId();
const defaultId = React.useId();
const id = idProp ?? defaultId;

return (
<div
return (
<div
className={clsx(
'relative rounded ring-secondary/50',
'focus-within:ring-4',
{
'block': block,
'inline-block align-middle': !block,
},
className,
)}
style={style}
>
<input
{...etcProps}
ref={forwardedRef}
aria-labelledby={labelId}
type="tel"
id={id}
data-testid="input"
className={clsx(
'relative rounded ring-secondary/50',
'focus-within:ring-4',
'bg-negative rounded-inherit w-full peer block',
'focus:outline-0',
'disabled:opacity-50 disabled:cursor-not-allowed',
{
'text-xxs': size === 'small',
'text-xs': size === 'medium',
},
{
'pl-4': variant === 'default',
'pl-1.5': variant === 'alternate',
},
{
'pt-4': variant === 'alternate',
},
{
'block': block,
'inline-block align-middle': !block,
'pr-4': variant === 'default' && !indicator,
'pr-1.5': variant === 'alternate' && !indicator,
},
{
'pr-10': indicator && size === 'small',
'pr-12': indicator && size === 'medium',
'pr-16': indicator && size === 'large',
},
{
'h-10': size === 'small',
'h-12': size === 'medium',
'h-16': size === 'large',
},
className,
)}
>
<input
{...etcProps}
ref={ref}
aria-labelledby={labelId}
type="tel"
data-testid="input"
/>
{
label && (
<label
data-testid="label"
id={labelId}
htmlFor={id}
className={clsx(
'absolute z-[1] w-full top-0.5 left-0 pointer-events-none pl-1 text-xxs font-bold peer-disabled:opacity-50 peer-focus:text-secondary text-primary leading-none bg-negative select-none',
{
'sr-only': hiddenLabel,
},
{
'pr-1': !indicator,
},
{
'pr-10': indicator && size === 'small',
'pr-12': indicator && size === 'medium',
'pr-16': indicator && size === 'large',
},
)}
>
<span className="block w-full whitespace-nowrap h-[1.1em] overflow-hidden text-ellipsis">
{label}
</span>
</label>
)
}
{hint && (
<div
data-testid="hint"
className={clsx(
'bg-negative rounded-inherit w-full peer block',
'focus:outline-0',
'disabled:opacity-50 disabled:cursor-not-allowed',
{
'text-xxs': size === 'small',
'text-xs': size === 'medium',
},
'absolute left-0 px-1 pointer-events-none text-xxs peer-disabled:opacity-50 leading-none w-full bg-negative select-none',
{
'pl-4': variant === 'default',
'pl-1.5': variant === 'alternate',
'bottom-0 pl-4 pb-1': variant === 'default',
'top-0.5': variant === 'alternate',
},
{
'pt-4': variant === 'alternate',
'pt-2': variant === 'alternate' && size === 'small',
'pt-3': variant === 'alternate' && size !== 'small',
},
{
'pr-4': variant === 'default' && !indicator,
'pr-1.5': variant === 'alternate' && !indicator,
'pr-4': !indicator && variant === 'default',
'pr-1': !indicator && variant === 'alternate',
},
{
'pr-10': indicator && size === 'small',
'pr-12': indicator && size === 'medium',
'pr-16': indicator && size === 'large',
},
{
'h-10': size === 'small',
'h-12': size === 'medium',
'h-16': size === 'large',
},
)}
/>
{
label && (
<label
data-testid="label"
id={labelId}
className={clsx(
'absolute z-[1] w-full top-0.5 left-0 pointer-events-none pl-1 text-xxs font-bold peer-disabled:opacity-50 peer-focus:text-secondary text-primary leading-none bg-negative',
{
'sr-only': hiddenLabel,
},
{
'pr-1': !indicator,
},
{
'pr-10': indicator && size === 'small',
'pr-12': indicator && size === 'medium',
'pr-16': indicator && size === 'large',
},
)}
>
<span className="block w-full whitespace-nowrap h-[1.1em] overflow-hidden text-ellipsis">
{label}
</span>
</label>
)
}
{hint && (
>
<div
data-testid="hint"
className={clsx(
'absolute left-0 px-1 pointer-events-none text-xxs peer-disabled:opacity-50 leading-none w-full bg-negative',
{
'bottom-0 pl-4 pb-1': variant === 'default',
'top-0.5': variant === 'alternate',
},
{
'pt-2': variant === 'alternate' && size === 'small',
'pt-3': variant === 'alternate' && size !== 'small',
},
{
'pr-4': !indicator && variant === 'default',
'pr-1': !indicator && variant === 'alternate',
},
{
'pr-10': indicator && size === 'small',
'pr-12': indicator && size === 'medium',
'pr-16': indicator && size === 'large',
},
)}
>
<div
className="opacity-50 whitespace-nowrap w-full h-[1.1em] overflow-hidden text-ellipsis"
>
{hint}
</div>
</div>
)}
{indicator && (
<div
className={clsx(
'text-center flex items-center justify-center peer-disabled:opacity-50 aspect-square absolute bottom-0 right-0 pointer-events-none',
{
'w-10': size === 'small',
'w-12': size === 'medium',
'w-16': size === 'large',
},
)}
className="opacity-50 whitespace-nowrap w-full h-[1.1em] overflow-hidden text-ellipsis"
>
{indicator}
{hint}
</div>
)}
{
border && (
<span
data-testid="border"
className="absolute z-[1] peer-disabled:opacity-50 inset-0 rounded-inherit border-2 border-primary pointer-events-none peer-focus:border-secondary"
/>
)
}
</div>
);
},
);
</div>
)}
{indicator && (
<div
className={clsx(
'text-center flex items-center justify-center peer-disabled:opacity-50 aspect-square absolute bottom-0 right-0 pointer-events-none select-none',
{
'w-10': size === 'small',
'w-12': size === 'medium',
'w-16': size === 'large',
},
)}
>
{indicator}
</div>
)}
{
border && (
<span
data-testid="border"
className="absolute z-[1] peer-disabled:opacity-50 inset-0 rounded-inherit border-2 border-primary pointer-events-none peer-focus:border-secondary"
/>
)
}
</div>
);
});

PhoneNumberInput.displayName = 'PhoneNumberInput';

PhoneNumberInput.defaultProps = {
label: undefined,
hint: undefined,
indicator: undefined,
size: 'medium',
border: false,
block: false,
variant: 'default',
hiddenLabel: false,
};

+ 146
- 130
categories/formatted/react/src/components/UrlInput/index.tsx View File

@@ -4,7 +4,7 @@ import clsx from 'clsx';

export type UrlInputDerivedElement = HTMLInputElement;

export interface UrlInputProps extends Omit<React.HTMLProps<UrlInputDerivedElement>, 'size' | 'type' | 'label'> {
export interface UrlInputProps extends Omit<React.HTMLProps<UrlInputDerivedElement>, 'size' | 'type' | 'label' | 'inputMode'> {
/**
* Short textual description indicating the nature of the component's value.
*/
@@ -42,155 +42,171 @@ export interface UrlInputProps extends Omit<React.HTMLProps<UrlInputDerivedEleme
/**
* Component for inputting textual values.
*/
export const UrlInput = React.forwardRef<UrlInputDerivedElement, UrlInputProps>(
(
{
label,
hint,
indicator,
size = 'medium' as const,
border = false,
block = false,
variant = 'default' as const,
hiddenLabel = false,
className,
...etcProps
}: UrlInputProps,
ref,
) => {
const labelId = React.useId();
export const UrlInput = React.forwardRef<UrlInputDerivedElement, UrlInputProps>((
{
label,
hint,
indicator,
size = 'medium' as const,
border = false,
block = false,
variant = 'default' as const,
hiddenLabel = false,
className,
style,
id: idProp,
...etcProps
}: UrlInputProps,
forwardedRef,
) => {
const labelId = React.useId();
const defaultId = React.useId();
const id = idProp ?? defaultId;

return (
<div
return (
<div
className={clsx(
'relative rounded ring-secondary/50',
'focus-within:ring-4',
{
'block': block,
'inline-block align-middle': !block,
},
className,
)}
style={style}
>
<input
{...etcProps}
ref={forwardedRef}
id={id}
aria-labelledby={labelId}
type="url"
data-testid="input"
className={clsx(
'relative rounded ring-secondary/50',
'focus-within:ring-4',
'bg-negative rounded-inherit w-full peer block',
'focus:outline-0',
'disabled:opacity-50 disabled:cursor-not-allowed',
{
'text-xxs': size === 'small',
'text-xs': size === 'medium',
},
{
'pl-4': variant === 'default',
'pl-1.5': variant === 'alternate',
},
{
'pt-4': variant === 'alternate',
},
{
'block': block,
'inline-block align-middle': !block,
'pr-4': variant === 'default' && !indicator,
'pr-1.5': variant === 'alternate' && !indicator,
},
{
'pr-10': indicator && size === 'small',
'pr-12': indicator && size === 'medium',
'pr-16': indicator && size === 'large',
},
{
'h-10': size === 'small',
'h-12': size === 'medium',
'h-16': size === 'large',
},
className,
)}
>
<input
{...etcProps}
ref={ref}
aria-labelledby={labelId}
type="url"
data-testid="input"
/>
{
label && (
<label
data-testid="label"
htmlFor={id}
id={labelId}
className={clsx(
'absolute z-[1] w-full top-0.5 left-0 pointer-events-none pl-1 text-xxs font-bold peer-disabled:opacity-50 peer-focus:text-secondary text-primary leading-none bg-negative select-none',
{
'sr-only': hiddenLabel,
},
{
'pr-1': !indicator,
},
{
'pr-10': indicator && size === 'small',
'pr-12': indicator && size === 'medium',
'pr-16': indicator && size === 'large',
},
)}
>
<span className="block w-full whitespace-nowrap h-[1.1em] overflow-hidden text-ellipsis">
{label}
</span>
</label>
)
}
{hint && (
<div
data-testid="hint"
className={clsx(
'bg-negative rounded-inherit w-full peer block',
'focus:outline-0',
'disabled:opacity-50 disabled:cursor-not-allowed',
{
'text-xxs': size === 'small',
'text-xs': size === 'medium',
},
'absolute left-0 px-1 pointer-events-none text-xxs peer-disabled:opacity-50 leading-none w-full bg-negative select-none',
{
'pl-4': variant === 'default',
'pl-1.5': variant === 'alternate',
'bottom-0 pl-4 pb-1': variant === 'default',
'top-0.5': variant === 'alternate',
},
{
'pt-4': variant === 'alternate',
'pt-2': variant === 'alternate' && size === 'small',
'pt-3': variant === 'alternate' && size !== 'small',
},
{
'pr-4': variant === 'default' && !indicator,
'pr-1.5': variant === 'alternate' && !indicator,
'pr-4': !indicator && variant === 'default',
'pr-1': !indicator && variant === 'alternate',
},
{
'pr-10': indicator && size === 'small',
'pr-12': indicator && size === 'medium',
'pr-16': indicator && size === 'large',
},
{
'h-10': size === 'small',
'h-12': size === 'medium',
'h-16': size === 'large',
},
)}
/>
{
label && (
<label
data-testid="label"
id={labelId}
className={clsx(
'absolute z-[1] w-full top-0.5 left-0 pointer-events-none pl-1 text-xxs font-bold peer-disabled:opacity-50 peer-focus:text-secondary text-primary leading-none bg-negative',
{
'sr-only': hiddenLabel,
},
{
'pr-1': !indicator,
},
{
'pr-10': indicator && size === 'small',
'pr-12': indicator && size === 'medium',
'pr-16': indicator && size === 'large',
},
)}
>
<span className="block w-full whitespace-nowrap h-[1.1em] overflow-hidden text-ellipsis">
{label}
</span>
</label>
)
}
{hint && (
>
<div
data-testid="hint"
className={clsx(
'absolute left-0 px-1 pointer-events-none text-xxs peer-disabled:opacity-50 leading-none w-full bg-negative',
{
'bottom-0 pl-4 pb-1': variant === 'default',
'top-0.5': variant === 'alternate',
},
{
'pt-2': variant === 'alternate' && size === 'small',
'pt-3': variant === 'alternate' && size !== 'small',
},
{
'pr-4': !indicator && variant === 'default',
'pr-1': !indicator && variant === 'alternate',
},
{
'pr-10': indicator && size === 'small',
'pr-12': indicator && size === 'medium',
'pr-16': indicator && size === 'large',
},
)}
>
<div
className="opacity-50 whitespace-nowrap w-full h-[1.1em] overflow-hidden text-ellipsis"
>
{hint}
</div>
</div>
)}
{indicator && (
<div
className={clsx(
'text-center flex items-center justify-center peer-disabled:opacity-50 aspect-square absolute bottom-0 right-0 pointer-events-none',
{
'w-10': size === 'small',
'w-12': size === 'medium',
'w-16': size === 'large',
},
)}
className="opacity-50 whitespace-nowrap w-full h-[1.1em] overflow-hidden text-ellipsis"
>
{indicator}
{hint}
</div>
)}
{
border && (
<span
data-testid="border"
className="absolute z-[1] peer-disabled:opacity-50 inset-0 rounded-inherit border-2 border-primary pointer-events-none peer-focus:border-secondary"
/>
)
}
</div>
);
},
);
</div>
)}
{indicator && (
<div
className={clsx(
'text-center flex items-center justify-center peer-disabled:opacity-50 aspect-square absolute bottom-0 right-0 pointer-events-none select-none',
{
'w-10': size === 'small',
'w-12': size === 'medium',
'w-16': size === 'large',
},
)}
>
{indicator}
</div>
)}
{
border && (
<span
data-testid="border"
className="absolute z-[1] peer-disabled:opacity-50 inset-0 rounded-inherit border-2 border-primary pointer-events-none peer-focus:border-secondary"
/>
)
}
</div>
);
});

UrlInput.displayName = 'UrlInput';

UrlInput.defaultProps = {
label: undefined,
hint: undefined,
indicator: undefined,
size: 'medium',
border: false,
block: false,
variant: 'default',
hiddenLabel: false,
};

+ 4
- 0
categories/freeform/react/.eslintrc View File

@@ -1,5 +1,9 @@
{
"root": true,
"rules": {
"quote-props": "off",
"react/jsx-props-no-spreading": "off"
},
"extends": [
"lxsmnsyc/typescript/react"
],


+ 148
- 129
categories/freeform/react/src/components/MaskedTextInput/index.tsx View File

@@ -42,155 +42,174 @@ export interface MaskedTextInputProps extends Omit<React.HTMLProps<MaskedTextInp
/**
* Component for inputting textual values.
*/
export const MaskedTextInput = React.forwardRef<MaskedTextInputDerivedElement, MaskedTextInputProps>(
(
{
label,
hint,
indicator,
size = 'medium' as const,
border = false,
block = false,
variant = 'default' as const,
hiddenLabel = false,
className,
...etcProps
}: MaskedTextInputProps,
ref,
) => {
const labelId = React.useId();
export const MaskedTextInput = React.forwardRef<
MaskedTextInputDerivedElement,
MaskedTextInputProps
>((
{
label,
hint,
indicator,
size = 'medium' as const,
border = false,
block = false,
variant = 'default' as const,
hiddenLabel = false,
className,
id: idProp,
style,
...etcProps
}: MaskedTextInputProps,
forwardedRef,
) => {
const labelId = React.useId();
const defaultId = React.useId();
const id = idProp ?? defaultId;

return (
<div
return (
<div
className={clsx(
'relative rounded ring-secondary/50 overflow-hidden',
'focus-within:ring-4',
{
'block': block,
'inline-block align-middle': !block,
},
className,
)}
style={style}
>
<input
{...etcProps}
ref={forwardedRef}
id={id}
aria-labelledby={labelId}
type="password"
data-testid="input"
className={clsx(
'relative rounded ring-secondary/50',
'focus-within:ring-4',
'bg-negative rounded-inherit w-full peer block',
'focus:outline-0',
'disabled:opacity-50 disabled:cursor-not-allowed',
{
'text-xxs': size === 'small',
'text-xs': size === 'medium',
},
{
'pl-4': variant === 'default',
'pl-1.5': variant === 'alternate',
},
{
'pt-4': variant === 'alternate',
},
{
'block': block,
'inline-block align-middle': !block,
'pr-4': variant === 'default' && !indicator,
'pr-1.5': variant === 'alternate' && !indicator,
},
{
'pr-10': indicator && size === 'small',
'pr-12': indicator && size === 'medium',
'pr-16': indicator && size === 'large',
},
{
'h-10': size === 'small',
'h-12': size === 'medium',
'h-16': size === 'large',
},
className,
)}
>
<input
{...etcProps}
ref={ref}
aria-labelledby={labelId}
type="password"
data-testid="input"
/>
{
label && (
<label
data-testid="label"
id={labelId}
htmlFor={id}
className={clsx(
'absolute z-[1] w-full top-0.5 left-0 pointer-events-none pl-1 text-xxs font-bold peer-disabled:opacity-50 peer-focus:text-secondary text-primary leading-none bg-negative select-none',
{
'sr-only': hiddenLabel,
},
{
'pr-1': !indicator,
},
{
'pr-10': indicator && size === 'small',
'pr-12': indicator && size === 'medium',
'pr-16': indicator && size === 'large',
},
)}
>
<span className="block w-full whitespace-nowrap h-[1.1em] overflow-hidden text-ellipsis">
{label}
</span>
</label>
)
}
{hint && (
<div
data-testid="hint"
className={clsx(
'bg-negative rounded-inherit w-full peer block',
'focus:outline-0',
'disabled:opacity-50 disabled:cursor-not-allowed',
{
'text-xxs': size === 'small',
'text-xs': size === 'medium',
},
'absolute left-0 px-1 pointer-events-none text-xxs peer-disabled:opacity-50 leading-none w-full bg-negative select-none',
{
'pl-4': variant === 'default',
'pl-1.5': variant === 'alternate',
'bottom-0 pl-4 pb-1': variant === 'default',
'top-0.5': variant === 'alternate',
},
{
'pt-4': variant === 'alternate',
'pt-2': variant === 'alternate' && size === 'small',
'pt-3': variant === 'alternate' && size !== 'small',
},
{
'pr-4': variant === 'default' && !indicator,
'pr-1.5': variant === 'alternate' && !indicator,
'pr-4': !indicator && variant === 'default',
'pr-1': !indicator && variant === 'alternate',
},
{
'pr-10': indicator && size === 'small',
'pr-12': indicator && size === 'medium',
'pr-16': indicator && size === 'large',
},
{
'h-10': size === 'small',
'h-12': size === 'medium',
'h-16': size === 'large',
},
)}
/>
{
label && (
<label
data-testid="label"
id={labelId}
className={clsx(
'absolute z-[1] w-full top-0.5 left-0 pointer-events-none pl-1 text-xxs font-bold peer-disabled:opacity-50 peer-focus:text-secondary text-primary leading-none bg-negative',
{
'sr-only': hiddenLabel,
},
{
'pr-1': !indicator,
},
{
'pr-10': indicator && size === 'small',
'pr-12': indicator && size === 'medium',
'pr-16': indicator && size === 'large',
},
)}
>
<span className="block w-full whitespace-nowrap h-[1.1em] overflow-hidden text-ellipsis">
{label}
</span>
</label>
)
}
{hint && (
>
<div
data-testid="hint"
className={clsx(
'absolute left-0 px-1 pointer-events-none text-xxs peer-disabled:opacity-50 leading-none w-full bg-negative',
{
'bottom-0 pl-4 pb-1': variant === 'default',
'top-0.5': variant === 'alternate',
},
{
'pt-2': variant === 'alternate' && size === 'small',
'pt-3': variant === 'alternate' && size !== 'small',
},
{
'pr-4': !indicator && variant === 'default',
'pr-1': !indicator && variant === 'alternate',
},
{
'pr-10': indicator && size === 'small',
'pr-12': indicator && size === 'medium',
'pr-16': indicator && size === 'large',
},
)}
>
<div
className="opacity-50 whitespace-nowrap w-full h-[1.1em] overflow-hidden text-ellipsis"
>
{hint}
</div>
</div>
)}
{indicator && (
<div
className={clsx(
'text-center flex items-center justify-center peer-disabled:opacity-50 aspect-square absolute bottom-0 right-0 pointer-events-none',
{
'w-10': size === 'small',
'w-12': size === 'medium',
'w-16': size === 'large',
},
)}
className="opacity-50 whitespace-nowrap w-full h-[1.1em] overflow-hidden text-ellipsis"
>
{indicator}
{hint}
</div>
)}
{
border && (
<span
data-testid="border"
className="absolute z-[1] peer-disabled:opacity-50 inset-0 rounded-inherit border-2 border-primary pointer-events-none peer-focus:border-secondary"
/>
)
}
</div>
);
},
);
</div>
)}
{indicator && (
<div
className={clsx(
'text-center flex items-center justify-center peer-disabled:opacity-50 aspect-square absolute bottom-0 right-0 pointer-events-none select-none',
{
'w-10': size === 'small',
'w-12': size === 'medium',
'w-16': size === 'large',
},
)}
>
{indicator}
</div>
)}
{
border && (
<span
data-testid="border"
className="absolute z-[1] peer-disabled:opacity-50 inset-0 rounded-inherit border-2 border-primary pointer-events-none peer-focus:border-secondary"
/>
)
}
</div>
);
});

MaskedTextInput.displayName = 'MaskedTextInput';

MaskedTextInput.defaultProps = {
label: undefined,
hint: undefined,
size: 'medium',
indicator: undefined,
border: false,
block: false,
variant: 'default',
hiddenLabel: false,
};

+ 162
- 143
categories/freeform/react/src/components/MultilineTextInput/index.tsx View File

@@ -4,7 +4,7 @@ import clsx from 'clsx';

export type MultilineTextInputDerivedElement = HTMLTextAreaElement;

export interface MultilineTextInputProps extends Omit<React.HTMLProps<MultilineTextInputDerivedElement>, 'size' | 'style' | 'label'> {
export interface MultilineTextInputProps extends Omit<React.HTMLProps<MultilineTextInputDerivedElement>, 'size' | 'label' | 'inputMode'> {
/**
* Short textual description indicating the nature of the component's value.
*/
@@ -44,168 +44,187 @@ export interface MultilineTextInputProps extends Omit<React.HTMLProps<MultilineT
*
* This component supports multiline input and adjusts its layout accordingly.
*/
export const MultilineTextInput = React.forwardRef<MultilineTextInputDerivedElement, MultilineTextInputProps>(
(
{
label,
hint,
indicator,
size = 'medium' as const,
border = false,
block = false,
variant = 'default' as const,
hiddenLabel = false,
className,
...etcProps
}: MultilineTextInputProps,
ref,
) => {
const labelId = React.useId();
export const MultilineTextInput = React.forwardRef<
MultilineTextInputDerivedElement,
MultilineTextInputProps
>((
{
label,
hint,
indicator,
size = 'medium' as const,
border = false,
block = false,
variant = 'default' as const,
hiddenLabel = false,
className,
style,
id: idProp,
...etcProps
}: MultilineTextInputProps,
forwardedRef,
) => {
const labelId = React.useId();
const defaultId = React.useId();
const id = idProp ?? defaultId;

return (
<div
return (
<div
className={clsx(
'relative rounded ring-secondary/50 overflow-hidden',
'focus-within:ring-4',
{
'block': block,
'inline-block align-middle': !block,
},
className,
)}
style={style}
>
<textarea
{...etcProps}
ref={forwardedRef}
aria-labelledby={labelId}
id={id}
data-testid="input"
style={{
height: 0,
}}
className={clsx(
'relative rounded ring-secondary/50',
'focus-within:ring-4',
'bg-negative rounded-inherit w-full peer block',
'focus:outline-0',
'disabled:opacity-50 disabled:cursor-not-allowed',
{
'resize': !block,
'resize-y': block,
},
{
'text-xxs': size === 'small',
'text-xs': size === 'medium',
},
{
'pl-4': variant === 'default',
'pl-1.5': variant === 'alternate',
},
{
'pt-4': variant === 'alternate' && size === 'small',
'pt-5': variant === 'alternate' && size === 'medium',
'pt-8': variant === 'alternate' && size === 'large',
},
{
'py-2.5': variant === 'default' && size === 'small',
'py-3': variant === 'default' && size === 'medium',
'py-5': variant === 'default' && size === 'large',
},
{
'pr-4': variant === 'default' && !indicator,
'pr-1.5': variant === 'alternate' && !indicator,
},
{
'pr-10': indicator && size === 'small',
'pr-12': indicator && size === 'medium',
'pr-16': indicator && size === 'large',
},
{
'block': block,
'inline-block align-middle': !block,
'min-h-10': size === 'small',
'min-h-12': size === 'medium',
'min-h-16': size === 'large',
},
className,
)}
>
<textarea
{...etcProps}
ref={ref}
aria-labelledby={labelId}
data-testid="input"
style={{
height: 0,
}}
/>
{
label && (
<label
data-testid="label"
id={labelId}
htmlFor={id}
className={clsx(
'absolute z-[1] w-full top-0.5 left-0 pointer-events-none pl-1 text-xxs font-bold peer-disabled:opacity-50 peer-focus:text-secondary text-primary leading-none bg-negative select-none',
{
'sr-only': hiddenLabel,
},
{
'pr-1': !indicator,
},
{
'pr-10': indicator && size === 'small',
'pr-12': indicator && size === 'medium',
'pr-16': indicator && size === 'large',
},
)}
>
<span className="block w-full whitespace-nowrap h-[1.1em] overflow-hidden text-ellipsis">
{label}
</span>
</label>
)
}
{hint && (
<div
data-testid="hint"
className={clsx(
'bg-negative rounded-inherit w-full peer block',
'focus:outline-0',
'disabled:opacity-50 disabled:cursor-not-allowed',
{
'resize': !block,
'resize-y': block,
},
{
'text-xxs': size === 'small',
'text-xs': size === 'medium',
},
{
'pl-4': variant === 'default',
'pl-1.5': variant === 'alternate',
},
'absolute left-0 px-1 pointer-events-none text-xxs peer-disabled:opacity-50 leading-none w-full bg-negative select-none',
{
'pt-4': variant === 'alternate' && size === 'small',
'pt-5': variant === 'alternate' && size === 'medium',
'pt-8': variant === 'alternate' && size === 'large',
'bottom-0 pl-4 pb-1': variant === 'default',
'top-0.5': variant === 'alternate',
},
{
'py-2.5': variant === 'default' && size === 'small',
'py-3': variant === 'default' && size === 'medium',
'py-5': variant === 'default' && size === 'large',
'pt-2': variant === 'alternate' && size === 'small',
'pt-3': variant === 'alternate' && size !== 'small',
},
{
'pr-4': variant === 'default' && !indicator,
'pr-1.5': variant === 'alternate' && !indicator,
'pr-4': !indicator && variant === 'default',
'pr-1': !indicator && variant === 'alternate',
},
{
'pr-10': indicator && size === 'small',
'pr-12': indicator && size === 'medium',
'pr-16': indicator && size === 'large',
},
{
'min-h-10': size === 'small',
'min-h-12': size === 'medium',
'min-h-16': size === 'large',
},
)}
/>
{
label && (
<label
data-testid="label"
id={labelId}
className={clsx(
'absolute z-[1] w-full top-0.5 left-0 pointer-events-none pl-1 text-xxs font-bold peer-disabled:opacity-50 peer-focus:text-secondary text-primary leading-none bg-negative',
{
'sr-only': hiddenLabel,
},
{
'pr-1': !indicator,
},
{
'pr-10': indicator && size === 'small',
'pr-12': indicator && size === 'medium',
'pr-16': indicator && size === 'large',
},
)}
>
<span className="block w-full whitespace-nowrap h-[1.1em] overflow-hidden text-ellipsis">
{label}
</span>
</label>
)
}
{hint && (
<div
data-testid="hint"
className={clsx(
'absolute left-0 px-1 pointer-events-none text-xxs peer-disabled:opacity-50 leading-none w-full bg-negative',
{
'bottom-0 pl-4 pb-1': variant === 'default',
'top-0.5': variant === 'alternate',
},
{
'pt-2': variant === 'alternate' && size === 'small',
'pt-3': variant === 'alternate' && size !== 'small',
},
{
'pr-4': !indicator && variant === 'default',
'pr-1': !indicator && variant === 'alternate',
},
{
'pr-10': indicator && size === 'small',
'pr-12': indicator && size === 'medium',
'pr-16': indicator && size === 'large',
},
)}
>
<div
className="opacity-50 whitespace-nowrap w-full h-[1.1em] overflow-hidden text-ellipsis"
>
{hint}
</div>
</div>
)}
{indicator && (
>
<div
className={clsx(
'text-center flex items-center justify-center peer-disabled:opacity-50 aspect-square absolute bottom-0 right-0 pointer-events-none',
{
'w-10': size === 'small',
'w-12': size === 'medium',
'w-16': size === 'large',
},
)}
className="opacity-50 whitespace-nowrap w-full h-[1.1em] overflow-hidden text-ellipsis"
>
{indicator}
{hint}
</div>
)}
{
border && (
<span
data-testid="border"
className="absolute z-[1] peer-disabled:opacity-50 inset-0 rounded-inherit border-2 border-primary pointer-events-none peer-focus:border-secondary"
/>
)
}
</div>
);
}
);
</div>
)}
{indicator && (
<div
className={clsx(
'text-center flex items-center justify-center peer-disabled:opacity-50 aspect-square absolute bottom-0 right-0 pointer-events-none select-none',
{
'w-10': size === 'small',
'w-12': size === 'medium',
'w-16': size === 'large',
},
)}
>
{indicator}
</div>
)}
{
border && (
<span
data-testid="border"
className="absolute z-[1] peer-disabled:opacity-50 inset-0 rounded-inherit border-2 border-primary pointer-events-none peer-focus:border-secondary"
/>
)
}
</div>
);
});

MultilineTextInput.displayName = 'MultilineTextInput';

MultilineTextInput.defaultProps = {
label: undefined,
hint: undefined,
size: 'medium',
indicator: undefined,
border: false,
block: false,
variant: 'default',
hiddenLabel: false,
};

+ 162
- 131
categories/freeform/react/src/components/TextInput/index.tsx View File

@@ -4,7 +4,7 @@ import clsx from 'clsx';

export type TextInputDerivedElement = HTMLInputElement;

export interface TextInputProps extends Omit<React.HTMLProps<TextInputDerivedElement>, 'size' | 'type' | 'style' | 'label' | 'list'> {
export interface TextInputProps extends Omit<React.HTMLProps<TextInputDerivedElement>, 'size' | 'type' | 'label' | 'list' | 'inputMode'> {
/**
* Short textual description indicating the nature of the component's value.
*/
@@ -41,6 +41,10 @@ export interface TextInputProps extends Omit<React.HTMLProps<TextInputDerivedEle
* Is the label hidden?
*/
hiddenLabel?: boolean,
/**
* Input mode of the component.
*/
inputMode?: TextControl.InputMode,
}

/**
@@ -48,156 +52,183 @@ export interface TextInputProps extends Omit<React.HTMLProps<TextInputDerivedEle
*
* This component supports multiline input and adjusts its layout accordingly.
*/
export const TextInput = React.forwardRef<TextInputDerivedElement, TextInputProps>(
(
{
label = '',
hint = '',
indicator,
size = 'medium' as const,
border = false,
block = false,
type = 'text' as const,
variant = 'default' as const,
hiddenLabel = false,
className,
...etcProps
}: TextInputProps,
ref,
) => {
const labelId = React.useId();
export const TextInput = React.forwardRef<TextInputDerivedElement, TextInputProps>((
{
label,
hint,
indicator,
size = 'medium' as const,
border = false,
block = false,
type = 'text' as const,
variant = 'default' as const,
hiddenLabel = false,
className,
id: idProp,
style,
inputMode = type,
...etcProps
},
forwardedRef,
) => {
const labelId = React.useId();
const defaultId = React.useId();
const id = idProp ?? defaultId;

let resultInputMode = inputMode as React.HTMLProps<TextInputDerivedElement>['inputMode'];
if (type === 'text' && resultInputMode === 'search') {
resultInputMode = 'text';
} else if (type === 'search' && resultInputMode === 'text') {
resultInputMode = 'search';
}

return (
<div
return (
<div
className={clsx(
'relative rounded ring-secondary/50 overflow-hidden',
'focus-within:ring-4',
{
'block': block,
'inline-block align-middle': !block,
},
className,
)}
style={style}
>
<input
{...etcProps}
ref={forwardedRef}
aria-labelledby={labelId}
id={id}
type={type}
inputMode={resultInputMode}
data-testid="input"
className={clsx(
'relative rounded ring-secondary/50',
'focus-within:ring-4',
'bg-negative rounded-inherit w-full peer block',
'focus:outline-0',
'disabled:opacity-50 disabled:cursor-not-allowed',
{
'text-xxs': size === 'small',
'text-xs': size === 'medium',
},
{
'pl-4': variant === 'default',
'pl-1.5': variant === 'alternate',
},
{
'pt-4': variant === 'alternate',
},
{
'pr-4': variant === 'default' && !indicator,
'pr-1.5': variant === 'alternate' && !indicator,
},
{
'pr-10': indicator && size === 'small',
'pr-12': indicator && size === 'medium',
'pr-16': indicator && size === 'large',
},
{
'block': block,
'inline-block align-middle': !block,
'h-10': size === 'small',
'h-12': size === 'medium',
'h-16': size === 'large',
},
className,
)}
>
<input
{...etcProps}
ref={ref}
aria-labelledby={labelId}
type={type}
data-testid="input"
/>
{
label && (
<label
data-testid="label"
htmlFor={id}
id={labelId}
className={clsx(
'absolute z-[1] w-full top-0.5 left-0 pointer-events-none pl-1 text-xxs font-bold peer-disabled:opacity-50 peer-focus:text-secondary text-primary leading-none bg-negative select-none',
{
'sr-only': hiddenLabel,
},
{
'pr-1': !indicator,
},
{
'pr-10': indicator && size === 'small',
'pr-12': indicator && size === 'medium',
'pr-16': indicator && size === 'large',
},
)}
>
<span className="block w-full whitespace-nowrap h-[1.1em] overflow-hidden text-ellipsis">
{label}
</span>
</label>
)
}
{hint && (
<div
data-testid="hint"
className={clsx(
'bg-negative rounded-inherit w-full peer block',
'focus:outline-0',
'disabled:opacity-50 disabled:cursor-not-allowed',
'absolute left-0 px-1 pointer-events-none text-xxs peer-disabled:opacity-50 leading-none w-full bg-negative select-none',
{
'text-xxs': size === 'small',
'text-xs': size === 'medium',
'bottom-0 pl-4 pb-1': variant === 'default',
'top-0.5': variant === 'alternate',
},
{
'pl-4': variant === 'default',
'pl-1.5': variant === 'alternate',
'pt-2': variant === 'alternate' && size === 'small',
'pt-3': variant === 'alternate' && size !== 'small',
},
{
'pt-4': variant === 'alternate',
},
{
'pr-4': variant === 'default' && !indicator,
'pr-1.5': variant === 'alternate' && !indicator,
'pr-4': !indicator && variant === 'default',
'pr-1': !indicator && variant === 'alternate',
},
{
'pr-10': indicator && size === 'small',
'pr-12': indicator && size === 'medium',
'pr-16': indicator && size === 'large',
},
{
'h-10': size === 'small',
'h-12': size === 'medium',
'h-16': size === 'large',
},
)}
/>
{
label && (
<label
data-testid="label"
id={labelId}
className={clsx(
'absolute z-[1] w-full top-0.5 left-0 pointer-events-none pl-1 text-xxs font-bold peer-disabled:opacity-50 peer-focus:text-secondary text-primary leading-none bg-negative',
{
'sr-only': hiddenLabel,
},
{
'pr-1': !indicator,
},
{
'pr-10': indicator && size === 'small',
'pr-12': indicator && size === 'medium',
'pr-16': indicator && size === 'large',
},
)}
>
<span className="block w-full whitespace-nowrap h-[1.1em] overflow-hidden text-ellipsis">
{label}
</span>
</label>
)
}
{hint && (
<div
data-testid="hint"
className={clsx(
'absolute left-0 px-1 pointer-events-none text-xxs peer-disabled:opacity-50 leading-none w-full bg-negative',
{
'bottom-0 pl-4 pb-1': variant === 'default',
'top-0.5': variant === 'alternate',
},
{
'pt-2': variant === 'alternate' && size === 'small',
'pt-3': variant === 'alternate' && size !== 'small',
},
{
'pr-4': !indicator && variant === 'default',
'pr-1': !indicator && variant === 'alternate',
},
{
'pr-10': indicator && size === 'small',
'pr-12': indicator && size === 'medium',
'pr-16': indicator && size === 'large',
},
)}
>
<div
className="opacity-50 whitespace-nowrap w-full h-[1.1em] overflow-hidden text-ellipsis"
>
{hint}
</div>
</div>
)}
{indicator && (
>
<div
className={clsx(
'text-center flex items-center justify-center peer-disabled:opacity-50 aspect-square absolute bottom-0 right-0 pointer-events-none',
{
'w-10': size === 'small',
'w-12': size === 'medium',
'w-16': size === 'large',
},
)}
className="opacity-50 whitespace-nowrap w-full h-[1.1em] overflow-hidden text-ellipsis"
>
{indicator}
{hint}
</div>
)}
{
border && (
<span
data-testid="border"
className="absolute z-[1] peer-disabled:opacity-50 inset-0 rounded-inherit border-2 border-primary pointer-events-none peer-focus:border-secondary"
/>
)
}
</div>
);
}
);
</div>
)}
{indicator && (
<div
className={clsx(
'text-center flex items-center justify-center peer-disabled:opacity-50 aspect-square absolute bottom-0 right-0 pointer-events-none select-none',
{
'w-10': size === 'small',
'w-12': size === 'medium',
'w-16': size === 'large',
},
)}
>
{indicator}
</div>
)}
{
border && (
<span
data-testid="border"
className="absolute z-[1] peer-disabled:opacity-50 inset-0 rounded-inherit border-2 border-primary pointer-events-none peer-focus:border-secondary"
/>
)
}
</div>
);
});

TextInput.displayName = 'TextInput';

TextInput.defaultProps = {
label: undefined,
hint: undefined,
size: 'medium',
indicator: undefined,
border: false,
block: false,
type: 'text',
variant: 'default',
hiddenLabel: false,
inputMode: 'text',
};

+ 3
- 0
categories/information/react/.eslintrc View File

@@ -1,5 +1,8 @@
{
"root": true,
"rules": {
"react/jsx-props-no-spreading": "off"
},
"extends": [
"lxsmnsyc/typescript/react"
],


+ 16
- 9
categories/information/react/src/components/Badge/index.tsx View File

@@ -7,12 +7,15 @@ export interface BadgeProps extends React.HTMLProps<BadgeDerivedElement> {
rounded?: boolean;
}

export const Badge = React.forwardRef<BadgeDerivedElement, BadgeProps>(({
children,
rounded = false,
className,
...etcProps
}, forwardedRef) => (
export const Badge = React.forwardRef<BadgeDerivedElement, BadgeProps>((
{
children,
rounded = false,
className,
...etcProps
},
forwardedRef,
) => (
<span
{...etcProps}
ref={forwardedRef}
@@ -26,10 +29,14 @@ export const Badge = React.forwardRef<BadgeDerivedElement, BadgeProps>(({
className,
)}
>
<span className="relative w-full">
{children}
</span>
<span className="relative w-full">
{children}
</span>
</span>
));

Badge.displayName = 'Badge';

Badge.defaultProps = {
rounded: false,
};

+ 13
- 5
categories/information/react/src/components/KeyValueTable/index.tsx View File

@@ -14,11 +14,14 @@ export interface KeyValueTableProps extends Omit<React.HTMLProps<KeyValueTableDe
properties?: (KeyValueProperty | boolean)[];
}

export const KeyValueTable = React.forwardRef<KeyValueTableDerivedElement, KeyValueTableProps>(({
hiddenKeys = false,
properties = [],
...etcProps
}, forwardedRef) => (
export const KeyValueTable = React.forwardRef<KeyValueTableDerivedElement, KeyValueTableProps>((
{
hiddenKeys = false,
properties = [],
...etcProps
},
forwardedRef,
) => (
<dl
{...etcProps}
className={clsx(
@@ -55,3 +58,8 @@ export const KeyValueTable = React.forwardRef<KeyValueTableDerivedElement, KeyVa
));

KeyValueTable.displayName = 'KeyValueTable';

KeyValueTable.defaultProps = {
hiddenKeys: false,
properties: [],
};

+ 4
- 0
categories/multichoice/react/.eslintrc View File

@@ -1,5 +1,9 @@
{
"root": true,
"rules": {
"quote-props": "off",
"react/jsx-props-no-spreading": "off"
},
"extends": [
"lxsmnsyc/typescript/react"
],


+ 166
- 145
categories/multichoice/react/src/components/MenuMultiSelect/index.tsx View File

@@ -37,6 +37,9 @@ export interface MenuMultiSelectProps extends Omit<React.HTMLProps<MenuMultiSele
* Is the label hidden?
*/
hiddenLabel?: boolean,
/**
* Starting height of the component.
*/
startingHeight?: number | string,
}

@@ -45,171 +48,189 @@ export interface MenuMultiSelectProps extends Omit<React.HTMLProps<MenuMultiSele
*
* This component supports multiline input and adjusts its layout accordingly.
*/
export const MenuMultiSelect = React.forwardRef<MenuMultiSelectDerivedElement, MenuMultiSelectProps>(
(
{
label,
hint,
indicator,
size = 'medium' as const,
border = false,
block = false,
variant = 'default' as const,
hiddenLabel = false,
className,
startingHeight = '15rem',
...etcProps
}: MenuMultiSelectProps,
ref,
) => {
const labelId = React.useId();
export const MenuMultiSelect = React.forwardRef<
MenuMultiSelectDerivedElement,
MenuMultiSelectProps
>((
{
label,
hint,
indicator,
size = 'medium' as const,
border = false,
block = false,
variant = 'default' as const,
hiddenLabel = false,
className,
startingHeight = '15rem',
id: idProp,
...etcProps
},
forwardedRef,
) => {
const labelId = React.useId();
const defaultId = React.useId();
const id = idProp ?? defaultId;

return (
<div
return (
<div
className={clsx(
'relative rounded ring-secondary/50',
'focus-within:ring-4',
{
'block': block,
'inline-block align-middle': !block,
},
className,
)}
>
<select
{...etcProps}
ref={forwardedRef}
id={id}
aria-labelledby={labelId}
data-testid="input"
size={2}
multiple
style={{
height: startingHeight,
}}
className={clsx(
'relative rounded ring-secondary/50',
'focus-within:ring-4',
'bg-negative rounded-inherit w-full peer block overflow-auto',
'focus:outline-0',
'disabled:opacity-50 disabled:cursor-not-allowed',
{
'resize': !block,
'resize-y': block,
},
{
'text-xxs': size === 'small',
'text-xs': size === 'medium',
},
{
'pl-4': variant === 'default',
'pl-1.5': variant === 'alternate',
},
{
'pt-4': variant === 'alternate' && size === 'small',
'pt-5': variant === 'alternate' && size === 'medium',
'pt-8': variant === 'alternate' && size === 'large',
},
{
'py-2.5': variant === 'default' && size === 'small',
'py-3': variant === 'default' && size === 'medium',
'py-5': variant === 'default' && size === 'large',
},
{
'pr-4': variant === 'default' && !indicator,
'pr-1.5': variant === 'alternate' && !indicator,
},
{
'pr-10': indicator && size === 'small',
'pr-12': indicator && size === 'medium',
'pr-16': indicator && size === 'large',
},
{
'block': block,
'inline-block align-middle': !block,
'min-h-10': size === 'small',
'min-h-12': size === 'medium',
'min-h-16': size === 'large',
},
className,
)}
>
<select
{...etcProps}
ref={ref}
aria-labelledby={labelId}
data-testid="input"
size={2}
multiple
style={{
height: startingHeight,
}}
/>
{
label && (
<label
data-testid="label"
id={labelId}
htmlFor={id}
className={clsx(
'absolute z-[1] w-full top-0.5 left-0 pointer-events-none pl-1 text-xxs font-bold peer-disabled:opacity-50 peer-focus:text-secondary text-primary leading-none bg-negative',
{
'sr-only': hiddenLabel,
},
{
'pr-1': !indicator,
},
{
'pr-10': indicator && size === 'small',
'pr-12': indicator && size === 'medium',
'pr-16': indicator && size === 'large',
},
)}
>
<span className="block w-full whitespace-nowrap h-[1.1em] overflow-hidden text-ellipsis">
{label}
</span>
</label>
)
}
{hint && (
<div
data-testid="hint"
className={clsx(
'bg-negative rounded-inherit w-full peer block overflow-auto',
'focus:outline-0',
'disabled:opacity-50 disabled:cursor-not-allowed',
{
'resize': !block,
'resize-y': block,
},
{
'text-xxs': size === 'small',
'text-xs': size === 'medium',
},
'absolute left-0 px-1 pointer-events-none text-xxs peer-disabled:opacity-50 leading-none w-full bg-negative',
{
'pl-4': variant === 'default',
'pl-1.5': variant === 'alternate',
'bottom-0 pl-4 pb-1': variant === 'default',
'top-0.5': variant === 'alternate',
},
{
'pt-4': variant === 'alternate' && size === 'small',
'pt-5': variant === 'alternate' && size === 'medium',
'pt-8': variant === 'alternate' && size === 'large',
'pt-2': variant === 'alternate' && size === 'small',
'pt-3': variant === 'alternate' && size !== 'small',
},
{
'py-2.5': variant === 'default' && size === 'small',
'py-3': variant === 'default' && size === 'medium',
'py-5': variant === 'default' && size === 'large',
},
{
'pr-4': variant === 'default' && !indicator,
'pr-1.5': variant === 'alternate' && !indicator,
'pr-4': !indicator && variant === 'default',
'pr-1': !indicator && variant === 'alternate',
},
{
'pr-10': indicator && size === 'small',
'pr-12': indicator && size === 'medium',
'pr-16': indicator && size === 'large',
},
{
'min-h-10': size === 'small',
'min-h-12': size === 'medium',
'min-h-16': size === 'large',
},
)}
/>
{
label && (
<label
data-testid="label"
id={labelId}
className={clsx(
'absolute z-[1] w-full top-0.5 left-0 pointer-events-none pl-1 text-xxs font-bold peer-disabled:opacity-50 peer-focus:text-secondary text-primary leading-none bg-negative',
{
'sr-only': hiddenLabel,
},
{
'pr-1': !indicator,
},
{
'pr-10': indicator && size === 'small',
'pr-12': indicator && size === 'medium',
'pr-16': indicator && size === 'large',
},
)}
>
<span className="block w-full whitespace-nowrap h-[1.1em] overflow-hidden text-ellipsis">
{label}
</span>
</label>
)
}
{hint && (
>
<div
data-testid="hint"
className={clsx(
'absolute left-0 px-1 pointer-events-none text-xxs peer-disabled:opacity-50 leading-none w-full bg-negative',
{
'bottom-0 pl-4 pb-1': variant === 'default',
'top-0.5': variant === 'alternate',
},
{
'pt-2': variant === 'alternate' && size === 'small',
'pt-3': variant === 'alternate' && size !== 'small',
},
{
'pr-4': !indicator && variant === 'default',
'pr-1': !indicator && variant === 'alternate',
},
{
'pr-10': indicator && size === 'small',
'pr-12': indicator && size === 'medium',
'pr-16': indicator && size === 'large',
},
)}
>
<div
className="opacity-50 whitespace-nowrap w-full h-[1.1em] overflow-hidden text-ellipsis"
>
{hint}
</div>
</div>
)}
{indicator && (
<div
className={clsx(
'text-center flex items-center justify-center peer-disabled:opacity-50 aspect-square absolute bottom-0 right-0 pointer-events-none',
{
'w-10': size === 'small',
'w-12': size === 'medium',
'w-16': size === 'large',
},
)}
className="opacity-50 whitespace-nowrap w-full h-[1.1em] overflow-hidden text-ellipsis"
>
{indicator}
{hint}
</div>
)}
{
border && (
<span
data-testid="border"
className="absolute z-[1] peer-disabled:opacity-50 inset-0 rounded-inherit border-2 border-primary pointer-events-none peer-focus:border-secondary"
/>
)
}
</div>
);
}
);
</div>
)}
{indicator && (
<div
className={clsx(
'text-center flex items-center justify-center peer-disabled:opacity-50 aspect-square absolute bottom-0 right-0 pointer-events-none',
{
'w-10': size === 'small',
'w-12': size === 'medium',
'w-16': size === 'large',
},
)}
>
{indicator}
</div>
)}
{
border && (
<span
data-testid="border"
className="absolute z-[1] peer-disabled:opacity-50 inset-0 rounded-inherit border-2 border-primary pointer-events-none peer-focus:border-secondary"
/>
)
}
</div>
);
});

MenuMultiSelect.displayName = 'MenuMultiSelect';

MenuMultiSelect.defaultProps = {
label: undefined,
hint: undefined,
indicator: undefined,
size: 'medium',
border: false,
block: false,
variant: 'default',
hiddenLabel: false,
startingHeight: '15rem',
};

+ 244
- 213
categories/multichoice/react/src/components/TagInput/index.tsx View File

@@ -1,10 +1,16 @@
import * as React from 'react';
import { TagsInput } from 'react-tag-input-component';
import clsx from 'clsx';
import {useClientSide, delegateTriggerEvent} from '@modal-sh/react-utils';
import { useClientSide, delegateTriggerEvent } from '@modal-sh/react-utils';
import { TextControl } from '@tesseract-design/web-base';

export type TagInputSeparator = ',' | '\n';
const TAG_INPUT_SEPARATOR_MAP = {
'comma': ',',
'newline': 'Enter',
'semicolon': ';',
} as const;

export type TagInputSeparator = keyof typeof TAG_INPUT_SEPARATOR_MAP

export type TagInputDerivedElement = HTMLTextAreaElement;

@@ -41,7 +47,13 @@ export interface TagInputProps extends Omit<React.HTMLProps<TagInputDerivedEleme
* Is the label hidden?
*/
hiddenLabel?: boolean,
/**
* Should the component be enhanced?
*/
enhanced?: boolean,
/**
* Separators for splitting the input value into multiple tags.
*/
separator?: TagInputSeparator[],
}

@@ -50,205 +62,182 @@ export interface TagInputProps extends Omit<React.HTMLProps<TagInputDerivedEleme
*
* This component supports multiline input and adjusts its layout accordingly.
*/
export const TagInput = React.forwardRef<TagInputDerivedElement, TagInputProps>(
(
{
label = '',
hint = '',
indicator,
size = 'medium' as const,
border = false,
block = false,
variant = 'default' as const,
hiddenLabel = false,
className,
enhanced: enhancedProp = false,
separator = ['\n'],
defaultValue,
disabled,
...etcProps
}: TagInputProps,
forwardedRef,
) => {
const {clientSide} = useClientSide({ clientSide: enhancedProp });
const defaultRef = React.useRef<TagInputDerivedElement>(null);
const ref = forwardedRef ?? defaultRef;
const labelId = React.useId();
const tags = React.useMemo(() => {
if (defaultValue === undefined) {
return [];
}
if (typeof defaultValue === 'string') {
return separator.reduce((theDefaultValues, separator) => {
return theDefaultValues.join(separator).split(separator);
}, [defaultValue]);
}
if (typeof defaultValue === 'number') {
return [defaultValue.toString()];
}
return defaultValue as string[];
}, [defaultValue, separator]);
export const TagInput = React.forwardRef<TagInputDerivedElement, TagInputProps>((
{
label,
hint,
indicator,
size = 'medium' as const,
border = false,
block = false,
variant = 'default' as const,
hiddenLabel = false,
className,
enhanced: enhancedProp = false,
separator = ['newline'],
defaultValue,
disabled,
id: idProp,
...etcProps
},
forwardedRef,
) => {
const { clientSide } = useClientSide({ clientSide: enhancedProp });
const defaultRef = React.useRef<TagInputDerivedElement>(null);
const ref = forwardedRef ?? defaultRef;
const labelId = React.useId();
const defaultId = React.useId();
const id = idProp ?? defaultId;
const tags = React.useMemo(() => {
if (defaultValue === undefined) {
return [];
}
if (typeof defaultValue === 'string') {
return separator.reduce((theDefaultValues, s) => {
if (s === 'newline') {
return theDefaultValues.join('\n').split('\n');
}
return theDefaultValues.join(s).split(s);
}, [defaultValue]);
}
if (typeof defaultValue === 'number') {
return [defaultValue.toString()];
}
return defaultValue as string[];
}, [defaultValue, separator]);

const handleTagsInputChange = (tags: string[]) => {
if (!(typeof ref === 'object' && ref)) {
return;
}
const { current: input } = ref;
if (!input) {
return;
}
setTimeout(() => {
delegateTriggerEvent('change', input, tags.map((t => t.trim())).join('\n'));
});
};
const handleTagsInputChange = (newTags: string[]) => {
if (!(typeof ref === 'object' && ref)) {
return;
}
const { current: input } = ref;
if (!input) {
return;
}
setTimeout(() => {
delegateTriggerEvent('change', input, newTags.map(((t) => t.trim())).join('\n'));
});
};

const handleBlur = () => {
if (!(typeof ref === 'object' && ref)) {
return;
}
const { current: input } = ref;
if (!input) {
return;
}
setTimeout(() => {
delegateTriggerEvent('blur', input);
});
const handleBlur = () => {
if (!(typeof ref === 'object' && ref)) {
return;
}
const { current: input } = ref;
if (!input) {
return;
}
setTimeout(() => {
delegateTriggerEvent('blur', input);
});
};

return (
<div
return (
<div
className={clsx(
'relative rounded ring-secondary/50 group',
'focus-within:ring-4',
{
'block': block,
'inline-block align-middle': !block,
},
clientSide && {
'min-h-10': size === 'small',
'min-h-12': size === 'medium',
'min-h-16': size === 'large',
},
'tesseract-design-tag-input',
size === 'small' && 'tag-input-small',
size === 'medium' && 'tag-input-medium',
size === 'large' && 'tag-input-large',
indicator && 'tag-input-indicator',
variant === 'default' && 'tag-input-default',
variant === 'alternate' && 'tag-input-alternate',
className,
)}
>
<textarea
{...etcProps}
disabled={disabled}
ref={ref}
id={id}
aria-labelledby={labelId}
data-testid="input"
defaultValue={defaultValue}
style={{
height: clientSide ? undefined : 0,
}}
className={clsx(
'relative rounded ring-secondary/50 group',
'focus-within:ring-4',
'bg-negative rounded-inherit peer block',
'focus:outline-0',
'disabled:opacity-50 disabled:cursor-not-allowed',
{
'block': block,
'inline-block align-middle': !block,
'resize': !block,
'resize-y': block,
},
clientSide && {
{
'text-xxs': size === 'small',
'text-xs': size === 'medium',
},
!clientSide && {
'pl-4': variant === 'default',
'pl-1.5': variant === 'alternate',
},
!clientSide && {
'pt-4': variant === 'alternate' && size === 'small',
'pt-5': variant === 'alternate' && size === 'medium',
'pt-8': variant === 'alternate' && size === 'large',
},
!clientSide && {
'py-2.5': variant === 'default' && size === 'small',
'py-3': variant === 'default' && size === 'medium',
'py-5': variant === 'default' && size === 'large',
},
!clientSide && {
'pr-4': variant === 'default' && !indicator,
'pr-1.5': variant === 'alternate' && !indicator,
},
!clientSide && {
'pr-10': indicator && size === 'small',
'pr-12': indicator && size === 'medium',
'pr-16': indicator && size === 'large',
},
!clientSide && {
'min-h-10': size === 'small',
'min-h-12': size === 'medium',
'min-h-16': size === 'large',
},
'tesseract-design-tag-input',
size === 'small' && 'tag-input-small',
size === 'medium' && 'tag-input-medium',
size === 'large' && 'tag-input-large',
indicator && 'tag-input-indicator',
variant === 'default' && 'tag-input-default',
variant === 'alternate' && 'tag-input-alternate',
className,
!clientSide && 'peer',
!clientSide && 'w-full',
clientSide && 'sr-only',
)}
>
<textarea
{...etcProps}
disabled={disabled}
ref={ref}
aria-labelledby={labelId}
data-testid="input"
defaultValue={defaultValue}
style={{
height: clientSide ? undefined : 0,
/>
{clientSide && (
<TagsInput
value={tags}
classNames={{
input: 'peer bg-transparent',
tag: 'text-xs p-2',
}}
className={clsx(
'bg-negative rounded-inherit peer block',
'focus:outline-0',
'disabled:opacity-50 disabled:cursor-not-allowed',
{
'resize': !block,
'resize-y': block,
},
{
'text-xxs': size === 'small',
'text-xs': size === 'medium',
},
!clientSide && {
'pl-4': variant === 'default',
'pl-1.5': variant === 'alternate',
},
!clientSide && {
'pt-4': variant === 'alternate' && size === 'small',
'pt-5': variant === 'alternate' && size === 'medium',
'pt-8': variant === 'alternate' && size === 'large',
},
!clientSide && {
'py-2.5': variant === 'default' && size === 'small',
'py-3': variant === 'default' && size === 'medium',
'py-5': variant === 'default' && size === 'large',
},
!clientSide && {
'pr-4': variant === 'default' && !indicator,
'pr-1.5': variant === 'alternate' && !indicator,
},
!clientSide && {
'pr-10': indicator && size === 'small',
'pr-12': indicator && size === 'medium',
'pr-16': indicator && size === 'large',
},
!clientSide && {
'min-h-10': size === 'small',
'min-h-12': size === 'medium',
'min-h-16': size === 'large',
},
!clientSide && 'peer',
!clientSide && 'w-full',
clientSide && 'sr-only',
)}
disabled={disabled}
onChange={handleTagsInputChange}
onBlur={handleBlur}
separators={separator.map((s) => TAG_INPUT_SEPARATOR_MAP[s])}
/>
{clientSide && (
<TagsInput
value={tags}
classNames={{
input: 'peer bg-transparent',
tag: 'text-xs p-2',
}}
disabled={disabled}
onChange={handleTagsInputChange}
onBlur={handleBlur}
separators={separator.map((separator) => separator === '\n' ? 'Enter' : separator)}
/>
)}
{
label && (
<label
data-testid="label"
id={labelId}
className={clsx(
'absolute z-[1] w-full top-0.5 left-0 pointer-events-none pl-1 text-xxs font-bold peer-disabled:opacity-50 group-focus-within:text-secondary text-primary leading-none bg-negative',
{
'sr-only': hiddenLabel,
},
{
'pr-1': !indicator,
},
{
'pr-10': indicator && size === 'small',
'pr-12': indicator && size === 'medium',
'pr-16': indicator && size === 'large',
},
)}
>
<span className="block w-full whitespace-nowrap h-[1.1em] overflow-hidden text-ellipsis">
{label}
</span>
</label>
)
}
{hint && (
<div
data-testid="hint"
)}
{
label && (
<label
data-testid="label"
id={labelId}
htmlFor={id}
className={clsx(
'absolute left-0 px-1 pointer-events-none text-xxs peer-disabled:opacity-50 leading-none w-full bg-negative',
{
'bottom-0 pl-4 pb-1': variant === 'default',
'top-0.5': variant === 'alternate',
},
'absolute z-[1] w-full top-0.5 left-0 pointer-events-none pl-1 text-xxs font-bold peer-disabled:opacity-50 group-focus-within:text-secondary text-primary leading-none bg-negative',
{
'pt-2': variant === 'alternate' && size === 'small',
'pt-3': variant === 'alternate' && size !== 'small',
'sr-only': hiddenLabel,
},
{
'pr-4': !indicator && variant === 'default',
'pr-1': !indicator && variant === 'alternate',
'pr-1': !indicator,
},
{
'pr-10': indicator && size === 'small',
@@ -257,38 +246,80 @@ export const TagInput = React.forwardRef<TagInputDerivedElement, TagInputProps>(
},
)}
>
<div
className="opacity-50 whitespace-nowrap w-full h-[1.1em] overflow-hidden text-ellipsis"
>
{hint}
</div>
</div>
)}
{indicator && (
<span className="block w-full whitespace-nowrap h-[1.1em] overflow-hidden text-ellipsis">
{label}
</span>
</label>
)
}
{hint && (
<div
data-testid="hint"
className={clsx(
'absolute left-0 px-1 pointer-events-none text-xxs peer-disabled:opacity-50 leading-none w-full bg-negative',
{
'bottom-0 pl-4 pb-1': variant === 'default',
'top-0.5': variant === 'alternate',
},
{
'pt-2': variant === 'alternate' && size === 'small',
'pt-3': variant === 'alternate' && size !== 'small',
},
{
'pr-4': !indicator && variant === 'default',
'pr-1': !indicator && variant === 'alternate',
},
{
'pr-10': indicator && size === 'small',
'pr-12': indicator && size === 'medium',
'pr-16': indicator && size === 'large',
},
)}
>
<div
className={clsx(
'text-center flex items-center justify-center peer-disabled:opacity-50 aspect-square absolute bottom-0 right-0 pointer-events-none',
{
'w-10': size === 'small',
'w-12': size === 'medium',
'w-16': size === 'large',
},
)}
className="opacity-50 whitespace-nowrap w-full h-[1.1em] overflow-hidden text-ellipsis"
>
{indicator}
{hint}
</div>
)}
{
border && (
<span
data-testid="border"
className="absolute z-[1] peer-disabled:opacity-50 inset-0 rounded-inherit border-2 border-primary pointer-events-none group-focus-within:border-secondary"
/>
)
}
</div>
);
}
);
</div>
)}
{indicator && (
<div
className={clsx(
'text-center flex items-center justify-center peer-disabled:opacity-50 aspect-square absolute bottom-0 right-0 pointer-events-none',
{
'w-10': size === 'small',
'w-12': size === 'medium',
'w-16': size === 'large',
},
)}
>
{indicator}
</div>
)}
{
border && (
<span
data-testid="border"
className="absolute z-[1] peer-disabled:opacity-50 inset-0 rounded-inherit border-2 border-primary pointer-events-none group-focus-within:border-secondary"
/>
)
}
</div>
);
});

TagInput.displayName = 'TagInput';

TagInput.defaultProps = {
label: undefined,
hint: undefined,
indicator: undefined,
size: 'medium',
variant: 'default',
separator: ['newline'],
border: false,
block: false,
hiddenLabel: false,
enhanced: false,
};

+ 32
- 19
categories/multichoice/react/src/components/ToggleButton/index.tsx View File

@@ -10,23 +10,26 @@ export interface ToggleButtonProps extends Omit<React.InputHTMLAttributes<Toggle
size?: Button.Size;
subtext?: React.ReactNode;
badge?: React.ReactNode;
variant: Button.Variant;
variant?: Button.Variant;
indeterminate?: boolean;
}

export const ToggleButton = React.forwardRef<ToggleButtonDerivedElement, ToggleButtonProps>(({
children,
block = false,
compact = false,
size = 'medium',
id: idProp,
className,
subtext,
badge,
variant,
indeterminate = false,
...etcProps
}, forwardedRef) => {
export const ToggleButton = React.forwardRef<ToggleButtonDerivedElement, ToggleButtonProps>((
{
children,
block = false,
compact = false,
size = 'medium' as const,
id: idProp,
className,
subtext,
badge,
variant = 'bare' as const,
indeterminate = false,
...etcProps
},
forwardedRef,
) => {
const defaultRef = React.useRef<ToggleButtonDerivedElement>(null);
const ref = forwardedRef ?? defaultRef;
const defaultId = React.useId();
@@ -69,7 +72,7 @@ export const ToggleButton = React.forwardRef<ToggleButtonDerivedElement, ToggleB
'pl-4 pr-4': !compact,
},
{
'border-2 border-primary peer-disabled:border-primary peer-focus:border-secondary peer-checked:border-tertiary active:border-tertiary' : variant !== 'bare',
'border-2 border-primary peer-disabled:border-primary peer-focus:border-secondary peer-checked:border-tertiary active:border-tertiary': variant !== 'bare',
'bg-primary text-negative peer-disabled:bg-primary peer-focus:bg-secondary peer-checked:bg-tertiary active:bg-tertiary': variant === 'filled',
},
{
@@ -87,7 +90,7 @@ export const ToggleButton = React.forwardRef<ToggleButtonDerivedElement, ToggleB
'border-current': variant !== 'filled',
'border-negative': variant === 'filled',
'-mr-2': compact,
}
},
)}
>
<svg
@@ -96,7 +99,7 @@ export const ToggleButton = React.forwardRef<ToggleButtonDerivedElement, ToggleB
{
'stroke-negative': variant === 'filled',
'stroke-current': variant !== 'filled',
}
},
)}
viewBox="0 0 24 24"
role="presentation"
@@ -111,7 +114,7 @@ export const ToggleButton = React.forwardRef<ToggleButtonDerivedElement, ToggleB
{
'stroke-negative': variant === 'filled',
'stroke-current': variant !== 'filled',
}
},
)}
viewBox="0 0 24 24"
role="presentation"
@@ -170,7 +173,17 @@ export const ToggleButton = React.forwardRef<ToggleButtonDerivedElement, ToggleB
</span>
</label>
</>
)
);
});

ToggleButton.displayName = 'ToggleButton';

ToggleButton.defaultProps = {
block: false,
compact: false,
size: 'medium',
subtext: undefined,
badge: undefined,
indeterminate: false,
variant: 'bare',
};

+ 23
- 14
categories/multichoice/react/src/components/ToggleSwitch/index.tsx View File

@@ -6,24 +6,25 @@ export type ToggleSwitchDerivedElement = HTMLInputElement;
export interface ToggleSwitchProps extends Omit<React.InputHTMLAttributes<ToggleSwitchDerivedElement>, 'type' | 'size'> {
block?: boolean;
subtext?: React.ReactNode;
badge?: React.ReactNode;
indeterminate?: boolean;
checkedLabel?: React.ReactNode;
uncheckedLabel?: React.ReactNode;
}

export const ToggleSwitch = React.forwardRef<ToggleSwitchDerivedElement, ToggleSwitchProps>(({
uncheckedLabel,
checkedLabel,
block = false,
id: idProp,
className,
subtext,
badge,
style,
indeterminate = false,
...etcProps
}, forwardedRef) => {
export const ToggleSwitch = React.forwardRef<ToggleSwitchDerivedElement, ToggleSwitchProps>((
{
uncheckedLabel,
checkedLabel,
block = false,
id: idProp,
className,
subtext,
style,
indeterminate = false,
...etcProps
},
forwardedRef,
) => {
const defaultRef = React.useRef<ToggleSwitchDerivedElement>(null);
const ref = forwardedRef ?? defaultRef;
const defaultId = React.useId();
@@ -112,7 +113,15 @@ export const ToggleSwitch = React.forwardRef<ToggleSwitchDerivedElement, ToggleS
</div>
)}
</div>
)
);
});

ToggleSwitch.displayName = 'ToggleSwitch';

ToggleSwitch.defaultProps = {
block: false,
subtext: undefined,
indeterminate: false,
checkedLabel: undefined,
uncheckedLabel: undefined,
};

+ 20
- 13
categories/multichoice/react/src/components/ToggleTickBox/index.tsx View File

@@ -6,21 +6,22 @@ export type ToggleTickBoxDerivedElement = HTMLInputElement;
export interface ToggleTickBoxProps extends Omit<React.InputHTMLAttributes<ToggleTickBoxDerivedElement>, 'type' | 'size'> {
block?: boolean;
subtext?: React.ReactNode;
badge?: React.ReactNode;
indeterminate?: boolean;
}

export const ToggleTickBox = React.forwardRef<ToggleTickBoxDerivedElement, ToggleTickBoxProps>(({
children,
block = false,
id: idProp,
className,
subtext,
badge,
style,
indeterminate = false,
...etcProps
}, forwardedRef) => {
export const ToggleTickBox = React.forwardRef<ToggleTickBoxDerivedElement, ToggleTickBoxProps>((
{
children,
block = false,
id: idProp,
className,
subtext,
style,
indeterminate = false,
...etcProps
},
forwardedRef,
) => {
const defaultRef = React.useRef<ToggleTickBoxDerivedElement>(null);
const ref = forwardedRef ?? defaultRef;
const defaultId = React.useId();
@@ -113,7 +114,13 @@ export const ToggleTickBox = React.forwardRef<ToggleTickBoxDerivedElement, Toggl
</div>
)}
</div>
)
);
});

ToggleTickBox.displayName = 'ToggleTickBox';

ToggleTickBox.defaultProps = {
block: false,
indeterminate: false,
subtext: undefined,
};

+ 3
- 0
categories/navigation/react/.eslintrc View File

@@ -1,5 +1,8 @@
{
"root": true,
"rules": {
"react/jsx-props-no-spreading": "off"
},
"extends": [
"lxsmnsyc/typescript/react"
],


+ 32
- 18
categories/navigation/react/src/components/LinkButton/index.tsx View File

@@ -4,30 +4,33 @@ import { Button } from '@tesseract-design/web-base';

export type LinkButtonDerivedElement = HTMLAnchorElement;

export interface LinkButtonProps extends Omit<React.HTMLProps<LinkButtonDerivedElement>, 'size'> {
export interface LinkButtonProps<T = any> extends Omit<React.HTMLProps<LinkButtonDerivedElement>, 'size'> {
block?: boolean;
variant: Button.Variant;
variant?: Button.Variant;
subtext?: React.ReactNode;
badge?: React.ReactNode;
menuItem?: boolean;
size?: Button.Size;
compact?: boolean;
component?: React.ElementType;
component?: React.ElementType<T>;
}

export const LinkButton = React.forwardRef<LinkButtonDerivedElement, LinkButtonProps>(({
variant,
subtext,
badge,
menuItem = false,
children,
size = 'medium' as const,
compact = false,
className,
block = false,
component: Component = 'a',
...etcProps
}, forwardedRef) => (
export const LinkButton = React.forwardRef<LinkButtonDerivedElement, LinkButtonProps>((
{
variant = 'bare' as const,
subtext,
badge,
menuItem = false,
children,
size = 'medium' as const,
compact = false,
className,
block = false,
component: Component = 'a',
...etcProps
},
forwardedRef,
) => (
<Component
{...etcProps}
ref={forwardedRef}
@@ -46,7 +49,7 @@ export const LinkButton = React.forwardRef<LinkButtonDerivedElement, LinkButtonP
'pr-2': compact || menuItem,
},
{
'border-2 border-primary focus:border-secondary active:border-tertiary' : variant !== 'bare',
'border-2 border-primary focus:border-secondary active:border-tertiary': variant !== 'bare',
'bg-negative text-primary focus:text-secondary active:text-tertiary': variant !== 'filled',
'bg-primary text-negative focus:bg-secondary active:bg-tertiary': variant === 'filled',
},
@@ -108,7 +111,7 @@ export const LinkButton = React.forwardRef<LinkButtonDerivedElement, LinkButtonP
viewBox="0 0 24 24"
role="presentation"
>
<polyline points="9 18 15 12 9 6"/>
<polyline points="9 18 15 12 9 6" />
</svg>
</span>
)}
@@ -116,3 +119,14 @@ export const LinkButton = React.forwardRef<LinkButtonDerivedElement, LinkButtonP
));

LinkButton.displayName = 'LinkButton';

LinkButton.defaultProps = {
variant: 'bare',
size: 'medium',
compact: false,
menuItem: false,
component: 'a',
badge: undefined,
subtext: undefined,
block: false,
};

+ 4
- 0
categories/number/react/.eslintrc View File

@@ -1,5 +1,9 @@
{
"root": true,
"rules": {
"quote-props": "off",
"react/jsx-props-no-spreading": "off"
},
"extends": [
"lxsmnsyc/typescript/react"
],


+ 107
- 60
categories/number/react/src/components/Slider/index.tsx View File

@@ -29,8 +29,9 @@ const filterOptions = (children: React.ReactNode): React.ReactNode => {
/*
* Caveat for slider:
*
* Since sliders are not as customizable as other components, especially with orientations, prefer using sliders where
* using horizontal sliders would be as acceptable as vertical sliders and vv.
* Since sliders are not as customizable as other components, especially with orientations,
* prefer using sliders where using horizontal sliders would be as acceptable as vertical
* sliders and vv.
*/

export type SliderOrientation = 'horizontal' | 'vertical';
@@ -43,16 +44,19 @@ export interface SliderProps extends Omit<React.HTMLProps<HTMLInputElement>, 'ty
length?: React.CSSProperties['width'];
}

export const Slider = React.forwardRef<SliderDerivedElement, SliderProps>(({
className,
style,
children,
orient = 'horizontal',
length,
min = 0,
max = 100,
...etcProps
}, forwardedRef) => {
export const Slider = React.forwardRef<SliderDerivedElement, SliderProps>((
{
className,
style,
children,
orient = 'horizontal',
length,
min = 0,
max = 100,
...etcProps
},
forwardedRef,
) => {
const [browser, setBrowser] = React.useState<string>();
React.useEffect(() => {
const isFirefox = typeof (window as unknown as Record<string, unknown>).InstallTrigger !== 'undefined';
@@ -69,27 +73,43 @@ export const Slider = React.forwardRef<SliderDerivedElement, SliderProps>(({
const tickMarkId = React.useId();

React.useEffect(() => {
if (!(typeof ref === 'object' && ref)) {
return;
}
const {current: slider} = ref;
if (!slider) {
return;
}
const setupGecko = (theOrient: string, theChildren: unknown, theBrowser?: string) => {
if (!(typeof ref === 'object' && ref)) {
return;
}
const { current: slider } = ref;
if (!slider) {
return;
}

const isFirefox = browser === 'firefox';
const wrapper = slider?.parentElement as HTMLElement;
const parent = wrapper?.parentElement as HTMLElement;
const grandParent = parent?.parentElement as HTMLElement;
if (isFirefox) {
slider.setAttribute('orient', orient);
wrapper.dataset[browser] = orient;
wrapper.removeAttribute('data-orient');
grandParent.style.width = children ? '2.5em' : '1em';
}
const isFirefox = theBrowser === 'firefox';
const wrapper = slider?.parentElement as HTMLElement;
const parent = wrapper?.parentElement as HTMLElement;
const grandParent = parent?.parentElement as HTMLElement;
if (isFirefox) {
slider.setAttribute('orient', theOrient);
wrapper.dataset[theBrowser] = theOrient;
wrapper.removeAttribute('data-orient');
grandParent.style.width = theChildren ? '2.5em' : '1em';
}
};

setupGecko(orient, children, browser);

return () => {
if (slider && isFirefox) {
if (!(typeof ref === 'object' && ref)) {
return;
}
const { current: slider } = ref;
if (!slider) {
return;
}

const isFirefox = browser === 'firefox';
const wrapper = slider?.parentElement as HTMLElement;
const parent = wrapper?.parentElement as HTMLElement;
const grandParent = parent?.parentElement as HTMLElement;
if (slider && isFirefox && parent && grandParent && wrapper) {
grandParent.style.width = 'auto';
wrapper.removeAttribute(`data-${browser}`);
wrapper.dataset.orient = slider.getAttribute(orient) ?? undefined;
@@ -99,42 +119,63 @@ export const Slider = React.forwardRef<SliderDerivedElement, SliderProps>(({
}, [ref, orient, children, browser]);

React.useEffect(() => {
if (!(typeof ref === 'object' && ref)) {
return;
}
const {current: slider} = ref;
if (!slider) {
return;
}
let shouldEffectExecute: boolean;
const setupNonGecko = (theOrient: string, theBrowser?: string) => {
if (!(typeof ref === 'object' && ref)) {
return;
}
const { current: slider } = ref;
if (!slider) {
return;
}

const wrapper = slider?.parentElement as HTMLElement;
const parent = wrapper?.parentElement as HTMLElement;
const grandParent = parent?.parentElement as HTMLElement;
if (!theBrowser) {
return;
}

const isNotFirefox = typeof browser === 'string' && browser !== 'firefox';
if (isNotFirefox) {
wrapper.dataset[browser] = orient;
}
const wrapper = slider?.parentElement as HTMLElement;
const parent = wrapper?.parentElement as HTMLElement;
const grandParent = parent?.parentElement as HTMLElement;

const shouldEffectExecute = isNotFirefox && orient === 'vertical' && slider && parent && grandParent;
if (shouldEffectExecute) {
const trueHeight = parent.clientWidth;
const trueWidth = parent.clientHeight;
const isNotFirefox = theBrowser !== 'firefox';
if (isNotFirefox) {
wrapper.dataset[theBrowser] = theOrient;
}

grandParent.dataset.height = grandParent.clientHeight.toString();
grandParent.dataset.width = grandParent.clientWidth.toString();
grandParent.style.height = trueHeight + 'px';
grandParent.style.width = trueWidth + 'px';
}
shouldEffectExecute = isNotFirefox && theOrient === 'vertical' && Boolean(slider) && Boolean(parent) && Boolean(grandParent);
if (shouldEffectExecute) {
const trueHeight = parent.clientWidth;
const trueWidth = parent.clientHeight;

grandParent.dataset.height = grandParent.clientHeight.toString();
grandParent.dataset.width = grandParent.clientWidth.toString();
grandParent.style.height = `${trueHeight}px`;
grandParent.style.width = `${trueWidth}px`;
}
};

setupNonGecko(orient, browser);

return () => {
if (!(typeof ref === 'object' && ref)) {
return;
}
const { current: slider } = ref;
if (!slider) {
return;
}

const wrapper = slider?.parentElement as HTMLElement;
const parent = wrapper?.parentElement as HTMLElement;
const grandParent = parent?.parentElement as HTMLElement;

if (shouldEffectExecute) {
grandParent.style.height = grandParent.dataset.height + 'px';
grandParent.style.width = grandParent.dataset.width + 'px';
grandParent.style.height = `${grandParent.dataset.height ?? 0}px`;
grandParent.style.width = `${grandParent.dataset.width ?? 0}px`;
grandParent.removeAttribute('data-height');
grandParent.removeAttribute('data-width');
}
wrapper.removeAttribute(`data-${browser}`);
wrapper.removeAttribute(`data-${browser ?? 'unknown'}`);
};
}, [ref, orient, browser]);

@@ -142,7 +183,7 @@ export const Slider = React.forwardRef<SliderDerivedElement, SliderProps>(({
if (!(typeof ref === 'object' && ref)) {
return;
}
const {current: slider} = ref;
const { current: slider } = ref;
if (!slider) {
return;
}
@@ -266,7 +307,7 @@ export const Slider = React.forwardRef<SliderDerivedElement, SliderProps>(({
<div
key={c.props.value}
className="absolute w-full h-full box-border"
data-offset={`${(value - minValue) / (maxValue - minValue) * 100}%`}
data-offset={`${((value - minValue) / (maxValue - minValue)) * 100}%`}
>
<div
className="flex h-full text-xs items-center justify-between"
@@ -281,7 +322,7 @@ export const Slider = React.forwardRef<SliderDerivedElement, SliderProps>(({
</div>
</div>
</div>
)
);
})
}
</div>
@@ -291,7 +332,13 @@ export const Slider = React.forwardRef<SliderDerivedElement, SliderProps>(({
</div>
</div>
</>
)
);
});

Slider.displayName = 'Slider';

Slider.defaultProps = {
orient: 'horizontal',
length: undefined,
children: undefined,
};

+ 144
- 130
categories/number/react/src/components/Spinner/index.tsx View File

@@ -42,156 +42,170 @@ export interface SpinnerProps extends Omit<React.HTMLProps<SpinnerDerivedElement
/**
* Component for inputting numeric values.
*/
export const Spinner = React.forwardRef<SpinnerDerivedElement, SpinnerProps>(
(
{
label,
hint,
indicator,
size = 'medium' as const,
border = false,
block = false,
variant = 'default' as const,
hiddenLabel = false,
className,
...etcProps
}: SpinnerProps,
ref,
) => {
const labelId = React.useId();
export const Spinner = React.forwardRef<SpinnerDerivedElement, SpinnerProps>((
{
label,
hint,
indicator,
size = 'medium' as const,
border = false,
block = false,
variant = 'default' as const,
hiddenLabel = false,
className,
id: idProp,
...etcProps
}: SpinnerProps,
forwardedRef,
) => {
const labelId = React.useId();
const defaultId = React.useId();
const id = idProp ?? defaultId;

return (
<div
return (
<div
className={clsx(
'relative rounded ring-secondary/50',
'focus-within:ring-4',
{
'block': block,
'inline-block align-middle': !block,
},
className,
)}
>
<input
{...etcProps}
ref={forwardedRef}
id={id}
aria-labelledby={labelId}
type="number"
data-testid="input"
className={clsx(
'relative rounded ring-secondary/50',
'focus-within:ring-4',
'bg-negative rounded-inherit w-full peer block tabular-nums',
'focus:outline-0',
'disabled:opacity-50 disabled:cursor-not-allowed',
'tesseract-design-spinner',
{
'text-xxs': size === 'small',
'text-xs': size === 'medium',
},
{
'pl-4': variant === 'default',
'pl-1.5': variant === 'alternate',
},
{
'pt-4': variant === 'alternate',
},
{
'block': block,
'inline-block align-middle': !block,
'pr-4': variant === 'default' && !indicator,
'pr-1.5': variant === 'alternate' && !indicator,
},
{
'pr-10': indicator && size === 'small',
'pr-12': indicator && size === 'medium',
'pr-16': indicator && size === 'large',
},
{
'h-10': size === 'small',
'h-12': size === 'medium',
'h-16': size === 'large',
},
className,
)}
>
<input
{...etcProps}
ref={ref}
aria-labelledby={labelId}
type="number"
data-testid="input"
/>
{
label && (
<label
data-testid="label"
id={labelId}
htmlFor={id}
className={clsx(
'absolute z-[1] w-full top-0.5 left-0 pointer-events-none pl-1 text-xxs font-bold peer-disabled:opacity-50 peer-focus:text-secondary text-primary leading-none bg-negative select-none',
{
'sr-only': hiddenLabel,
},
{
'pr-1': !indicator,
},
{
'pr-10': indicator && size === 'small',
'pr-12': indicator && size === 'medium',
'pr-16': indicator && size === 'large',
},
)}
>
<span className="block w-full whitespace-nowrap h-[1.1em] overflow-hidden text-ellipsis">
{label}
</span>
</label>
)
}
{hint && (
<div
data-testid="hint"
className={clsx(
'bg-negative rounded-inherit w-full peer block tabular-nums',
'focus:outline-0',
'disabled:opacity-50 disabled:cursor-not-allowed',
'tesseract-design-spinner',
{
'text-xxs': size === 'small',
'text-xs': size === 'medium',
},
'absolute left-0 px-1 pointer-events-none text-xxs peer-disabled:opacity-50 leading-none w-full bg-negative select-none text-primary',
{
'pl-4': variant === 'default',
'pl-1.5': variant === 'alternate',
'bottom-0 pl-4 pb-1': variant === 'default',
'top-0.5': variant === 'alternate',
},
{
'pt-4': variant === 'alternate',
'pt-2': variant === 'alternate' && size === 'small',
'pt-3': variant === 'alternate' && size !== 'small',
},
{
'pr-4': variant === 'default' && !indicator,
'pr-1.5': variant === 'alternate' && !indicator,
'pr-4': !indicator && variant === 'default',
'pr-1': !indicator && variant === 'alternate',
},
{
'pr-10': indicator && size === 'small',
'pr-12': indicator && size === 'medium',
'pr-16': indicator && size === 'large',
},
{
'h-10': size === 'small',
'h-12': size === 'medium',
'h-16': size === 'large',
},
)}
/>
{
label && (
<label
data-testid="label"
id={labelId}
className={clsx(
'absolute z-[1] w-full top-0.5 left-0 pointer-events-none pl-1 text-xxs font-bold peer-disabled:opacity-50 peer-focus:text-secondary text-primary leading-none bg-negative',
{
'sr-only': hiddenLabel,
},
{
'pr-1': !indicator,
},
{
'pr-10': indicator && size === 'small',
'pr-12': indicator && size === 'medium',
'pr-16': indicator && size === 'large',
},
)}
>
<span className="block w-full whitespace-nowrap h-[1.1em] overflow-hidden text-ellipsis">
{label}
</span>
</label>
)
}
{hint && (
>
<div
data-testid="hint"
className={clsx(
'absolute left-0 px-1 pointer-events-none text-xxs peer-disabled:opacity-50 leading-none w-full bg-negative',
{
'bottom-0 pl-4 pb-1': variant === 'default',
'top-0.5': variant === 'alternate',
},
{
'pt-2': variant === 'alternate' && size === 'small',
'pt-3': variant === 'alternate' && size !== 'small',
},
{
'pr-4': !indicator && variant === 'default',
'pr-1': !indicator && variant === 'alternate',
},
{
'pr-10': indicator && size === 'small',
'pr-12': indicator && size === 'medium',
'pr-16': indicator && size === 'large',
},
)}
>
<div
className="opacity-50 whitespace-nowrap w-full h-[1.1em] overflow-hidden text-ellipsis"
>
{hint}
</div>
</div>
)}
{indicator && (
<div
className={clsx(
'text-center flex items-center justify-center peer-disabled:opacity-50 aspect-square absolute bottom-0 right-0 pointer-events-none',
{
'w-10': size === 'small',
'w-12': size === 'medium',
'w-16': size === 'large',
},
)}
className="opacity-50 whitespace-nowrap w-full h-[1.1em] overflow-hidden text-ellipsis"
>
{indicator}
{hint}
</div>
)}
{
border && (
<span
data-testid="border"
className="absolute z-[1] peer-disabled:opacity-50 inset-0 rounded-inherit border-2 border-primary pointer-events-none peer-focus:border-secondary"
/>
)
}
</div>
);
},
);
</div>
)}
{indicator && (
<div
className={clsx(
'text-center flex items-center justify-center peer-disabled:opacity-50 aspect-square absolute bottom-0 right-0 pointer-events-none select-none',
{
'w-10': size === 'small',
'w-12': size === 'medium',
'w-16': size === 'large',
},
)}
>
{indicator}
</div>
)}
{
border && (
<span
data-testid="border"
className="absolute z-[1] peer-disabled:opacity-50 inset-0 rounded-inherit border-2 border-primary pointer-events-none peer-focus:border-secondary"
/>
)
}
</div>
);
});

Spinner.displayName = 'Spinner';

Spinner.defaultProps = {
label: undefined,
hint: undefined,
indicator: undefined,
border: false,
block: false,
hiddenLabel: false,
size: 'medium',
variant: 'default',
};

Loading…
Cancel
Save