Browse Source

Update spinner behavior, document types

Add TSDoc to component props. Spinners now display activated up/down buttons as well as maskedtextinput toggle buttons.
master
TheoryOfNekomata 1 year ago
parent
commit
5625f3e83b
22 changed files with 516 additions and 104 deletions
  1. +3
    -0
      categories/blob/react/src/components/FileSelectBox/index.tsx
  2. +4
    -0
      categories/color/react/src/components/ColorPicker/index.tsx
  3. +22
    -9
      categories/formatted/react/src/components/EmailInput/index.tsx
  4. +6
    -0
      categories/formatted/react/src/components/PatternTextInput/index.tsx
  5. +7
    -1
      categories/formatted/react/src/components/PhoneNumberInput/index.tsx
  6. +6
    -0
      categories/formatted/react/src/components/UrlInput/index.tsx
  7. +79
    -27
      categories/freeform/react/src/components/MaskedTextInput/index.tsx
  8. +6
    -0
      categories/freeform/react/src/components/MultilineTextInput/index.tsx
  9. +6
    -0
      categories/freeform/react/src/components/TextInput/index.tsx
  10. +14
    -0
      categories/information/react/src/components/Badge/index.tsx
  11. +34
    -3
      categories/information/react/src/components/KeyValueTable/index.tsx
  12. +9
    -1
      categories/multichoice/react/src/components/MenuMultiSelect/index.tsx
  13. +12
    -0
      categories/multichoice/react/src/components/TagInput/index.tsx
  14. +32
    -0
      categories/multichoice/react/src/components/ToggleButton/index.tsx
  15. +24
    -0
      categories/multichoice/react/src/components/ToggleSwitch/index.tsx
  16. +18
    -0
      categories/multichoice/react/src/components/ToggleTickBox/index.tsx
  17. +35
    -0
      categories/navigation/react/src/components/LinkButton/index.tsx
  18. +153
    -52
      categories/number/react/src/components/NumberSpinner/index.tsx
  19. +30
    -9
      categories/number/react/src/components/Slider/index.tsx
  20. +7
    -1
      categories/temporal/react/src/components/DateDropdown/index.tsx
  21. +7
    -1
      categories/temporal/react/src/components/TimeSpinner/index.tsx
  22. +2
    -0
      showcases/web-kitchensink-reactnext/src/pages/examples/registration-form/index.tsx

+ 3
- 0
categories/blob/react/src/components/FileSelectBox/index.tsx View File

@@ -31,6 +31,9 @@ export interface FileSelectBoxProps<
* Short textual description as guidelines for valid input values.
*/
hint?: React.ReactNode,
/**
* Should the component be enhanced?
*/
enhanced?: boolean,
/**
* Is the label hidden?


+ 4
- 0
categories/color/react/src/components/ColorPicker/index.tsx View File

@@ -18,6 +18,9 @@ export const colorPickerPlugin = plugin(({ addComponents }) => {
'&::-webkit-color-swatch': {
'border': '2px solid black',
},
'&::-moz-color-swatch': {
'border': '2px solid black',
},
},
});
});
@@ -63,6 +66,7 @@ export const ColorPicker = React.forwardRef<
},
)}
>
{/* todo add chevron down to picker */}
<input
{...etcProps}
className={clsx(


+ 22
- 9
categories/formatted/react/src/components/EmailInput/index.tsx View File

@@ -3,9 +3,15 @@ import { TextControl } from '@tesseract-design/web-base';
import clsx from 'clsx';
import { useFallbackId } from '@modal-sh/react-utils';

/**
* Derived HTML element of the {@link EmailInput} component.
*/
export type EmailInputDerivedElement = HTMLInputElement;

export interface EmailInputProps extends Omit<React.HTMLProps<EmailInputDerivedElement>, 'size' | 'type' | 'label' | 'inputMode' | 'pattern'> {
/**
* Props of the {@link EmailInput} component.
*/
export interface EmailInputProps extends Omit<React.HTMLProps<EmailInputDerivedElement>, 'size' | 'type' | 'label' | 'inputMode' | 'pattern' | 'autoComplete'> {
/**
* Short textual description indicating the nature of the component's value.
*/
@@ -42,6 +48,10 @@ export interface EmailInputProps extends Omit<React.HTMLProps<EmailInputDerivedE
* Visual length of the input.
*/
length?: number,
/**
* Should the component display a list of suggested values?
*/
autoComplete?: boolean,
}

/**
@@ -53,15 +63,16 @@ export const EmailInput = React.forwardRef<EmailInputDerivedElement, EmailInputP
label,
hint,
size = 'medium' as const,
border = false,
block = false,
border = false as const,
block = false as const,
variant = 'default' as const,
hiddenLabel = false,
hiddenLabel = false as const,
className,
id: idProp,
style,
domains = [],
length,
autoComplete = false as const,
...etcProps
}: EmailInputProps,
forwardedRef,
@@ -121,6 +132,7 @@ export const EmailInput = React.forwardRef<EmailInputDerivedElement, EmailInputP
aria-labelledby={labelId}
type="email"
data-testid="input"
autoComplete={autoComplete ? 'email' : undefined}
pattern={pattern}
className={clsx(
'bg-negative rounded-inherit w-full peer block font-inherit tabular-nums',
@@ -200,11 +212,12 @@ EmailInput.displayName = 'EmailInput';
EmailInput.defaultProps = {
label: undefined,
hint: undefined,
size: 'medium',
border: false,
block: false,
variant: 'default',
hiddenLabel: false,
size: 'medium' as const,
border: false as const,
block: false as const,
variant: 'default' as const,
hiddenLabel: false as const,
domains: [],
length: undefined,
autoComplete: false as const,
};

+ 6
- 0
categories/formatted/react/src/components/PatternTextInput/index.tsx View File

@@ -3,8 +3,14 @@ import { TextControl } from '@tesseract-design/web-base';
import clsx from 'clsx';
import { useFallbackId } from '@modal-sh/react-utils';

/**
* Derived HTML element of the {@link PatternTextInput} component.
*/
export type PatternTextInputDerivedElement = HTMLInputElement;

/**
* Props of the {@link PatternTextInput} component.
*/
export interface PatternTextInputProps extends Omit<React.HTMLProps<PatternTextInputDerivedElement>, 'size' | 'type' | 'label' | 'list' | 'inputMode'> {
/**
* Short textual description indicating the nature of the component's value.


+ 7
- 1
categories/formatted/react/src/components/PhoneNumberInput/index.tsx View File

@@ -4,9 +4,15 @@ import { useClientSide, useFallbackId, useProxyInput } from '@modal-sh/react-uti
import PhoneInput, { Country, Value } from 'react-phone-number-input/input';
import clsx from 'clsx';

/**
* Derived HTML element of the {@link PhoneNumberInput} component.
*/
export type PhoneNumberInputDerivedElement = HTMLInputElement;

export interface PhoneNumberInputProps extends Omit<React.HTMLProps<PhoneNumberInputDerivedElement>, 'size' | 'type' | 'label' | 'inputMode'> {
/**
* Props of the {@link PhoneNumberInput} component.
*/
export interface PhoneNumberInputProps extends Omit<React.HTMLProps<PhoneNumberInputDerivedElement>, 'autoComplete' | 'size' | 'type' | 'label' | 'inputMode'> {
/**
* Short textual description indicating the nature of the component's value.
*/


+ 6
- 0
categories/formatted/react/src/components/UrlInput/index.tsx View File

@@ -3,8 +3,14 @@ import { TextControl } from '@tesseract-design/web-base';
import clsx from 'clsx';
import { useFallbackId } from '@modal-sh/react-utils';

/**
* Derived HTML element of the {@link UrlInput} component.
*/
export type UrlInputDerivedElement = HTMLInputElement;

/**
* Props of the {@link UrlInput} component.
*/
export interface UrlInputProps extends Omit<React.HTMLProps<UrlInputDerivedElement>, 'size' | 'type' | 'label' | 'inputMode'> {
/**
* Short textual description indicating the nature of the component's value.


+ 79
- 27
categories/freeform/react/src/components/MaskedTextInput/index.tsx View File

@@ -3,9 +3,15 @@ import { TextControl } from '@tesseract-design/web-base';
import clsx from 'clsx';
import { useClientSide, useFallbackId } from '@modal-sh/react-utils';

/**
* Derived HTML element of the {@link MaskedTextInput} component.
*/
export type MaskedTextInputDerivedElement = HTMLInputElement;

export interface MaskedTextInputProps extends Omit<React.HTMLProps<MaskedTextInputDerivedElement>, 'size' | 'type' | 'label' | 'pattern'> {
/**
* Props of the {@link MaskedTextInput} component.
*/
export interface MaskedTextInputProps extends Omit<React.HTMLProps<MaskedTextInputDerivedElement>, 'autoComplete' | 'size' | 'type' | 'label' | 'pattern'> {
/**
* Short textual description indicating the nature of the component's value.
*/
@@ -38,6 +44,14 @@ export interface MaskedTextInputProps extends Omit<React.HTMLProps<MaskedTextInp
* Visual length of the input.
*/
length?: number,
/**
* Should the component be enhanced?
*/
enhanced?: boolean,
/**
* Autocomplete value type of the component.
*/
autoComplete?: 'current-password' | 'new-password',
}

/**
@@ -51,31 +65,45 @@ export const MaskedTextInput = React.forwardRef<
label,
hint,
size = 'medium' as const,
border = false,
block = false,
border = false as const,
block = false as const,
variant = 'default' as const,
hiddenLabel = false,
hiddenLabel = false as const,
className,
id: idProp,
style,
length,
disabled,
onKeyDown,
onKeyUp,
autoComplete,
enhanced: enhancedProp = false as const,
...etcProps
}: MaskedTextInputProps,
forwardedRef,
) => {
const { clientSide: indicator } = useClientSide({ clientSide: true, initial: false });
const { clientSide: enhanced } = useClientSide({ clientSide: enhancedProp });
const labelId = React.useId();
const id = useFallbackId(idProp);
const [visible, setVisible] = React.useState(false);
const [visibleViaKey, setVisibleViaKey] = React.useState(false);
const defaultRef = React.useRef<MaskedTextInputDerivedElement>(null);
const ref = forwardedRef ?? defaultRef;

const handleKeyDown: React.KeyboardEventHandler<
MaskedTextInputDerivedElement
> = React.useCallback((e) => {
if (e.ctrlKey && e.code === 'Space') {
setVisibleViaKey(true);
}
onKeyDown?.(e);
}, [onKeyDown]);

const handleKeyUp: React.KeyboardEventHandler<
MaskedTextInputDerivedElement
> = React.useCallback((e) => {
if (e.ctrlKey && e.code === 'Space') {
setVisibleViaKey(false);
setVisible((prev) => !prev);
}
onKeyUp?.(e);
@@ -98,6 +126,22 @@ export const MaskedTextInput = React.forwardRef<
});
}, [ref, defaultRef]);

// const preserveFocus: React.MouseEventHandler<HTMLButtonElement> = React.useCallback(() => {
// const { current } = typeof ref === 'object' ? ref : defaultRef;
// setVisibleViaKey(true);
// setTimeout(() => {
// current?.focus();
// });
// }, [ref, defaultRef]);

// const handleToggleMouseUp: React.MouseEventHandler<HTMLButtonElement> = React.useCallback(() => {
// const { current } = typeof ref === 'object' ? ref : defaultRef;
// setVisibleViaKey(false);
// setTimeout(() => {
// current?.focus();
// });
// }, [ref, defaultRef]);

React.useEffect(() => {
if (typeof ref === 'function') {
const defaultElement = defaultRef.current as MaskedTextInputDerivedElement;
@@ -131,12 +175,12 @@ export const MaskedTextInput = React.forwardRef<
'sr-only': hiddenLabel,
},
{
'pr-1': !indicator,
'pr-1': !enhanced,
},
{
'pr-10': indicator && size === 'small',
'pr-12': indicator && size === 'medium',
'pr-16': indicator && size === 'large',
'pr-10': enhanced && size === 'small',
'pr-12': enhanced && size === 'medium',
'pr-16': enhanced && size === 'large',
},
)}
>
@@ -156,7 +200,9 @@ export const MaskedTextInput = React.forwardRef<
aria-labelledby={labelId}
type={!visible ? 'password' : 'text'}
data-testid="input"
onKeyUp={handleKeyUp}
autoComplete={autoComplete ?? 'off'}
onKeyUp={enhanced ? handleKeyUp : undefined}
onKeyDown={enhanced ? handleKeyDown : undefined}
className={clsx(
'bg-negative rounded-inherit w-full peer block font-inherit',
'focus:outline-0',
@@ -173,13 +219,13 @@ export const MaskedTextInput = React.forwardRef<
'pt-4': variant === 'alternate',
},
{
'pr-4': variant === 'default' && !indicator,
'pr-1.5': variant === 'alternate' && !indicator,
'pr-4': variant === 'default' && !enhanced,
'pr-1.5': variant === 'alternate' && !enhanced,
},
{
'pr-10': indicator && size === 'small',
'pr-12': indicator && size === 'medium',
'pr-16': indicator && size === 'large',
'pr-10': enhanced && size === 'small',
'pr-12': enhanced && size === 'medium',
'pr-16': enhanced && size === 'large',
},
{
'h-10': size === 'small',
@@ -202,13 +248,13 @@ export const MaskedTextInput = React.forwardRef<
'pt-3': variant === 'alternate' && size !== 'small',
},
{
'pr-4': !indicator && variant === 'default',
'pr-1': !indicator && variant === 'alternate',
'pr-4': !enhanced && variant === 'default',
'pr-1': !enhanced && variant === 'alternate',
},
{
'pr-10': indicator && size === 'small',
'pr-12': indicator && size === 'medium',
'pr-16': indicator && size === 'large',
'pr-10': enhanced && size === 'small',
'pr-12': enhanced && size === 'medium',
'pr-16': enhanced && size === 'large',
},
)}
>
@@ -219,14 +265,18 @@ export const MaskedTextInput = React.forwardRef<
</div>
</div>
)}
{indicator && (
{enhanced && (
<button
disabled={disabled}
type="button"
data-testid="indicator"
tabIndex={-1}
className={clsx(
'text-center z-[1] focus:outline-0 flex items-center justify-center aspect-square absolute bottom-0 right-0 select-none text-primary group-focus-within:text-secondary',
'text-center z-[1] focus:outline-0 flex items-center justify-center aspect-square absolute bottom-0 right-0 select-none',
{
'text-primary group-focus-within:text-secondary group-focus-within:active:text-tertiary': !visibleViaKey,
'text-tertiary': visibleViaKey,
},
{
'w-10': size === 'small',
'w-12': size === 'medium',
@@ -270,10 +320,12 @@ MaskedTextInput.displayName = 'MaskedTextInput';
MaskedTextInput.defaultProps = {
label: undefined,
hint: undefined,
size: 'medium',
border: false,
block: false,
variant: 'default',
hiddenLabel: false,
size: 'medium' as const,
border: false as const,
block: false as const,
variant: 'default' as const,
hiddenLabel: false as const,
length: undefined,
enhanced: false as const,
autoComplete: undefined,
};

+ 6
- 0
categories/freeform/react/src/components/MultilineTextInput/index.tsx View File

@@ -3,8 +3,14 @@ import { TextControl } from '@tesseract-design/web-base';
import clsx from 'clsx';
import { useFallbackId } from '@modal-sh/react-utils';

/**
* Derived HTML element of the {@link MultilineTextInput} component.
*/
export type MultilineTextInputDerivedElement = HTMLTextAreaElement;

/**
* Props of the {@link MultilineTextInput} component.
*/
export interface MultilineTextInputProps extends Omit<React.HTMLProps<MultilineTextInputDerivedElement>, 'size' | 'label' | 'inputMode' | 'pattern'> {
/**
* Short textual description indicating the nature of the component's value.


+ 6
- 0
categories/freeform/react/src/components/TextInput/index.tsx View File

@@ -3,8 +3,14 @@ import { TextControl } from '@tesseract-design/web-base';
import clsx from 'clsx';
import { useFallbackId } from '@modal-sh/react-utils';

/**
* Derived HTML element of the {@link TextInput} component.
*/
export type TextInputDerivedElement = HTMLInputElement;

/**
* Props of the {@link TextInput} component.
*/
export interface TextInputProps extends Omit<React.HTMLProps<TextInputDerivedElement>, 'size' | 'type' | 'label' | 'list' | 'inputMode' | 'pattern'> {
/**
* Short textual description indicating the nature of the component's value.


+ 14
- 0
categories/information/react/src/components/Badge/index.tsx View File

@@ -1,17 +1,30 @@
import * as React from 'react';
import clsx from 'clsx';

/**
* Derived HTML element of the {@link Badge} component.
*/
export type BadgeDerivedElement = HTMLSpanElement;

/**
* Props of the {@link Badge} component.
*/
export interface BadgeProps extends React.HTMLProps<BadgeDerivedElement> {
/**
* Is the component rounded?
*/
rounded?: boolean;
}

/**
* Component for displaying textual information in a small container.
*/
export const Badge = React.forwardRef<BadgeDerivedElement, BadgeProps>((
{
children,
rounded = false,
className,
style,
...etcProps
},
forwardedRef,
@@ -28,6 +41,7 @@ export const Badge = React.forwardRef<BadgeDerivedElement, BadgeProps>((
},
className,
)}
style={style}
data-testid="badge"
>
<span className="relative w-full">


+ 34
- 3
categories/information/react/src/components/KeyValueTable/index.tsx View File

@@ -1,23 +1,52 @@
import * as React from 'react';
import clsx from 'clsx';

/**
* Derived HTML element of the {@link KeyValueTable} component.
*/
export type KeyValueTableDerivedElement = HTMLDListElement;

interface KeyValueProperty {
/**
* Individual property of the {@link KeyValueTable} component.
*/
export interface KeyValueTableProperty {
/**
* Key of the property.
*/
key: string;
/**
* Class name of the property.
*/
className?: string;
/**
* Value of the property.
*/
valueProps?: React.HTMLProps<HTMLElement>;
}

/**
* Props of the {@link KeyValueTable} component.
*/
export interface KeyValueTableProps extends Omit<React.HTMLProps<KeyValueTableDerivedElement>, 'children'> {
/**
* Should the keys be hidden?
*/
hiddenKeys?: boolean;
properties?: (KeyValueProperty | boolean)[];
/**
* Properties displayed on the component.
*/
properties?: (KeyValueTableProperty | boolean | null | undefined)[];
}

/**
* Component for displaying key-value pairs.
*/
export const KeyValueTable = React.forwardRef<KeyValueTableDerivedElement, KeyValueTableProps>((
{
hiddenKeys = false,
properties = [],
className,
style,
...etcProps
},
forwardedRef,
@@ -26,10 +55,12 @@ export const KeyValueTable = React.forwardRef<KeyValueTableDerivedElement, KeyVa
{...etcProps}
className={clsx(
'grid gap-y-1 grid-cols-3',
className,
)}
ref={forwardedRef}
style={style}
>
{properties.map((property) => typeof property === 'object' && (
{properties.map((property) => typeof property === 'object' && property && (
<div
key={property.key}
className={clsx('contents', property.className)}


+ 9
- 1
categories/multichoice/react/src/components/MenuMultiSelect/index.tsx View File

@@ -4,9 +4,15 @@ import clsx from 'clsx';
import plugin from 'tailwindcss/plugin';
import { useFallbackId } from '@modal-sh/react-utils';

/**
* Derived HTML element of the {@link MenuMultiSelect} component.
*/
export type MenuMultiSelectDerivedElement = HTMLSelectElement;

export interface MenuMultiSelectProps extends Omit<React.HTMLProps<MenuMultiSelectDerivedElement>, 'size' | 'style' | 'label' | 'multiple'> {
/**
* Props of the {@link MenuMultiSelect} component.
*/
export interface MenuMultiSelectProps extends Omit<React.HTMLProps<MenuMultiSelectDerivedElement>, 'size' | 'label' | 'multiple'> {
/**
* Short textual description indicating the nature of the component's value.
*/
@@ -88,6 +94,7 @@ export const MenuMultiSelect = React.forwardRef<
className,
startingHeight = '15rem',
id: idProp,
style,
...etcProps
},
forwardedRef,
@@ -107,6 +114,7 @@ export const MenuMultiSelect = React.forwardRef<
className,
)}
data-testid="base"
style={style}
>
{label && (
<>


+ 12
- 0
categories/multichoice/react/src/components/TagInput/index.tsx View File

@@ -17,12 +17,24 @@ const TAG_INPUT_VALUE_SEPARATOR_MAP = {
'semicolon': ';',
} as const;

/**
* Separator for splitting the input value into multiple tags.
*/
export type TagInputSeparator = keyof typeof TAG_INPUT_SEPARATOR_MAP

/**
* Derived HTML element of the {@link TagInput} component.
*/
export type TagInputDerivedElement = HTMLTextAreaElement | HTMLInputElement;

/**
* Proxied HTML element of the {@link TagInput} component.
*/
export type TagInputProxiedElement = HTMLTextAreaElement & HTMLInputElement;

/**
* Props of the {@link TagsInput} component.
*/
export interface TagInputProps extends Omit<React.HTMLProps<TagInputDerivedElement>, 'size' | 'type' | 'style' | 'label' | 'list'> {
/**
* Short textual description indicating the nature of the component's value.


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

@@ -4,15 +4,42 @@ import { Button } from '@tesseract-design/web-base';
import plugin from 'tailwindcss/plugin';
import { useFallbackId } from '@modal-sh/react-utils';

/**
* Derived HTML element of the {@link ToggleButton} component.
*/
export type ToggleButtonDerivedElement = HTMLInputElement;

/**
* Props of the {@link ToggleButton} component.
*/
export interface ToggleButtonProps extends Omit<React.InputHTMLAttributes<ToggleButtonDerivedElement>, 'type' | 'size'> {
/**
* Should the component occupy the whole width of its parent?
*/
block?: boolean;
/**
* Should the component's content use minimal space?
*/
compact?: boolean;
/**
* Size of the component.
*/
size?: Button.Size;
/**
* Complementary content of the component.
*/
subtext?: React.ReactNode;
/**
* Short complementary content displayed at the edge of the component.
*/
badge?: React.ReactNode;
/**
* Variant of the component.
*/
variant?: Button.Variant;
/**
* Is the component in an indeterminate state?
*/
indeterminate?: boolean;
}

@@ -35,6 +62,9 @@ export const toggleButtonPlugin = plugin(({ addComponents, }) => {
});
});

/**
* Component for toggling a Boolean value.
*/
export const ToggleButton = React.forwardRef<ToggleButtonDerivedElement, ToggleButtonProps>((
{
children,
@@ -47,6 +77,7 @@ export const ToggleButton = React.forwardRef<ToggleButtonDerivedElement, ToggleB
badge,
variant = 'bare' as const,
indeterminate = false,
style,
...etcProps
},
forwardedRef,
@@ -104,6 +135,7 @@ export const ToggleButton = React.forwardRef<ToggleButtonDerivedElement, ToggleB
},
className,
)}
style={style}
>
<span
className={clsx(


+ 24
- 0
categories/multichoice/react/src/components/ToggleSwitch/index.tsx View File

@@ -3,13 +3,34 @@ import clsx from 'clsx';
import plugin from 'tailwindcss/plugin';
import { useFallbackId } from '@modal-sh/react-utils';

/**
* Derived HTML element of the {@link ToggleSwitch} component.
*/
export type ToggleSwitchDerivedElement = HTMLInputElement;

/**
* Props of the {@link ToggleSwitch} component.
*/
export interface ToggleSwitchProps extends Omit<React.InputHTMLAttributes<ToggleSwitchDerivedElement>, 'type' | 'size'> {
/**
* Should the component occupy the whole width of its parent?
*/
block?: boolean;
/**
* Complementary content of the component.
*/
subtext?: React.ReactNode;
/**
* Is the component in an indeterminate state?
*/
indeterminate?: boolean;
/**
* Label to display when the component is checked.
*/
checkedLabel?: React.ReactNode;
/**
* Label to display when the component is unchecked.
*/
uncheckedLabel?: React.ReactNode;
}

@@ -134,6 +155,9 @@ export const toggleSwitchPlugin = plugin(({ addComponents }) => {
});
});

/**
* Component for toggling a Boolean value in an appearance of a toggle switch.
*/
export const ToggleSwitch = React.forwardRef<ToggleSwitchDerivedElement, ToggleSwitchProps>((
{
uncheckedLabel,


+ 18
- 0
categories/multichoice/react/src/components/ToggleTickBox/index.tsx View File

@@ -3,11 +3,26 @@ import clsx from 'clsx';
import plugin from 'tailwindcss/plugin';
import { useFallbackId } from '@modal-sh/react-utils';

/**
* Derived HTML element of the {@link ToggleTickBox} component.
*/
export type ToggleTickBoxDerivedElement = HTMLInputElement;

/**
* Props of the {@link ToggleTickBox} component.
*/
export interface ToggleTickBoxProps extends Omit<React.InputHTMLAttributes<ToggleTickBoxDerivedElement>, 'type' | 'size'> {
/**
* Should the component occupy the whole width of its parent?
*/
block?: boolean;
/**
* Complementary content of the component.
*/
subtext?: React.ReactNode;
/**
* Is the component in an indeterminate state?
*/
indeterminate?: boolean;
}

@@ -30,6 +45,9 @@ export const toggleTickBoxPlugin = plugin(({ addComponents, }) => {
});
});

/**
* Component for toggling a Boolean value with the appearance of a tick box.
*/
export const ToggleTickBox = React.forwardRef<ToggleTickBoxDerivedElement, ToggleTickBoxProps>((
{
children,


+ 35
- 0
categories/navigation/react/src/components/LinkButton/index.tsx View File

@@ -2,17 +2,50 @@ import * as React from 'react';
import clsx from 'clsx';
import { Button } from '@tesseract-design/web-base';

/**
* Derived HTML element of the {@link LinkButton} component.
*/
export type LinkButtonDerivedElement = HTMLAnchorElement;

/**
* Props of the {@link LinkButton} component.
*/
export interface LinkButtonProps<T = any> extends Omit<React.HTMLProps<LinkButtonDerivedElement>, 'size'> {
/**
* Should the component occupy the whole width of its parent?
*/
block?: boolean;
/**
* Variant of the component.
*/
variant?: Button.Variant;
/**
* Complementary content of the component.
*/
subtext?: React.ReactNode;
/**
* Short complementary content displayed at the edge of the component.
*/
badge?: React.ReactNode;
/**
* Is this component part of a menu?
*/
menuItem?: boolean;
/**
* Size of the component.
*/
size?: Button.Size;
/**
* Should the component's content use minimal space?
*/
compact?: boolean;
/**
* Component to use in rendering.
*/
component?: React.ElementType<T>;
/**
* Is the component unable to receive activation?
*/
disabled?: boolean;
}

@@ -30,6 +63,7 @@ export const LinkButton = React.forwardRef<LinkButtonDerivedElement, LinkButtonP
component: EnabledComponent = 'a',
disabled = false,
href,
style,
...etcProps
},
forwardedRef,
@@ -70,6 +104,7 @@ export const LinkButton = React.forwardRef<LinkButtonDerivedElement, LinkButtonP
className,
)}
data-testid="link"
style={style}
>
<span
className={clsx(


+ 153
- 52
categories/number/react/src/components/NumberSpinner/index.tsx View File

@@ -4,8 +4,14 @@ import clsx from 'clsx';
import plugin from 'tailwindcss/plugin';
import { useBrowser, useClientSide, useFallbackId } from '@modal-sh/react-utils';

/**
* Derived HTML element of the {@link NumberSpinner} component.
*/
export type NumberSpinnerDerivedElement = HTMLInputElement;

/**
* Props of the {@link NumberSpinner} component.
*/
export interface NumberSpinnerProps extends Omit<React.HTMLProps<NumberSpinnerDerivedElement>, 'size' | 'type' | 'label'> {
/**
* Short textual description indicating the nature of the component's value.
@@ -39,8 +45,17 @@ export interface NumberSpinnerProps extends Omit<React.HTMLProps<NumberSpinnerDe
* Visual length of the input.
*/
length?: number,
/**
* Should the component be enhanced?
*/
enhanced?: boolean,
/**
* Interval between steps in milliseconds.
*/
stepInterval?: number,
/**
* Delay before the first step in milliseconds.
*/
initialStepDelay?: number,
}

@@ -68,7 +83,7 @@ export const numberSpinnerPlugin = plugin(({ addComponents, }) => {
});

/**
* Component for inputting numeric values.
* Component for inputting discrete numeric values.
*/
export const NumberSpinner = React.forwardRef<NumberSpinnerDerivedElement, NumberSpinnerProps>((
{
@@ -96,63 +111,131 @@ export const NumberSpinner = React.forwardRef<NumberSpinnerDerivedElement, Numbe
const browser = useBrowser();
const defaultRef = React.useRef<NumberSpinnerDerivedElement>(null);
const ref = forwardedRef ?? defaultRef;
const intervalRef = React.useRef<
number | undefined
>() as React.MutableRefObject<number | undefined>;
const intervalRef = React.useRef<number | undefined>();
const clickYRef = React.useRef<number>();
const spinnerYRef = React.useRef<number>();
const spinEventSource = React.useRef<'mouse' | 'keyboard'>();
const [displayStep, setDisplayStep] = React.useState<boolean>();

const handleStepUp = React.useCallback(() => {
const { current } = typeof ref === 'object' ? ref : defaultRef;
if (current) {
const theStep = current.step ?? 'any';
const performStepMouse = (
input: NumberSpinnerDerivedElement,
theStepUpMode?: boolean,
) => {
if (typeof theStepUpMode !== 'boolean') {
return;
}
const current = input;
const theStep = current.step ?? 'any';
current.step = theStep === 'any' ? '1' : theStep;
if (theStepUpMode) {
current.stepUp();
} else {
current.stepDown();
}
current.step = theStep;
current.focus();
};

const windowMouseMove = (e: MouseEvent) => {
if (spinEventSource.current !== 'mouse') {
return;
}
clickYRef.current = e.pageY;
};

const doStepUp = () => {
current.step = theStep === 'any' ? '1' : theStep;
current.stepUp();
current.step = theStep;
current.focus();
};
const checkMouseStepUpMode = () => {
if (typeof clickYRef.current !== 'number' || typeof spinnerYRef.current !== 'number') {
return undefined;
}
return clickYRef.current < spinnerYRef.current;
};

const doStepMouse: React.MouseEventHandler<HTMLButtonElement> = (e) => {
if (spinEventSource.current === 'keyboard') {
return;
}
const { current } = typeof ref === 'object' ? ref : defaultRef;
if (!current) {
return;
}
spinEventSource.current = 'mouse';
const { top, bottom } = e.currentTarget.getBoundingClientRect();
const { pageY } = e;
spinnerYRef.current = top + ((bottom - top) / 2);
clickYRef.current = pageY;
window.addEventListener('mousemove', windowMouseMove);
setTimeout(() => {
clearInterval(intervalRef.current);
doStepUp();
const stepUpMode = checkMouseStepUpMode();
setDisplayStep(stepUpMode);
performStepMouse(current, stepUpMode);
intervalRef.current = window.setTimeout(() => {
doStepUp();
const stepUpMode = checkMouseStepUpMode();
setDisplayStep(stepUpMode);
performStepMouse(current, stepUpMode);
intervalRef.current = window.setInterval(() => {
doStepUp();
const stepUpMode = checkMouseStepUpMode();
setDisplayStep(stepUpMode);
performStepMouse(current, stepUpMode);
}, stepInterval);
}, initialStepDelay);
}
}, [ref, defaultRef, intervalRef, stepInterval, initialStepDelay]);

const handleStepDown = React.useCallback(() => {
const { current } = typeof ref === 'object' ? ref : defaultRef;
if (current) {
const theStep = current.step ?? 'any';
const doStepDown = () => {
current.step = theStep === 'any' ? '1' : theStep;
current.stepDown();
current.step = theStep;
current.focus();
};
});
};

const doStepKeyboard: React.KeyboardEventHandler<NumberSpinnerDerivedElement> = (e) => {
if (spinEventSource.current === 'mouse') {
return;
}
if (!(e.code === 'ArrowUp' || e.code === 'ArrowDown')) {
return;
}
e.preventDefault();
spinEventSource.current = 'keyboard';
const current = e.currentTarget;
const theStepUpMode = e.code === 'ArrowUp';
setDisplayStep(theStepUpMode);
setTimeout(() => {
clearInterval(intervalRef.current);
doStepDown();
performStepMouse(current, theStepUpMode);
intervalRef.current = window.setTimeout(() => {
doStepDown();
performStepMouse(current, theStepUpMode);
intervalRef.current = window.setInterval(() => {
doStepDown();
performStepMouse(current, theStepUpMode);
}, stepInterval);
}, initialStepDelay);
}
}, [ref, defaultRef, intervalRef, stepInterval, initialStepDelay]);
});
};

React.useEffect(() => {
const stopStep = () => {
const stopStepMouse = () => {
if (spinEventSource.current === 'keyboard') {
return;
}
clearInterval(intervalRef.current);
clickYRef.current = undefined;
spinnerYRef.current = undefined;
window.removeEventListener('mousemove', windowMouseMove);
spinEventSource.current = undefined;
setDisplayStep(undefined);
};

window.addEventListener('mouseup', stopStep, { capture: true });
const stopStepKeyboard = (e: KeyboardEvent) => {
if (spinEventSource.current === 'mouse') {
return;
}
if (!(e.code === 'ArrowUp' || e.code === 'ArrowDown')) {
return;
}
clearInterval(intervalRef.current);
spinEventSource.current = undefined;
setDisplayStep(undefined);
};

window.addEventListener('mouseup', stopStepMouse, { capture: true });
window.addEventListener('keyup', stopStepKeyboard, { capture: true });
return () => {
window.removeEventListener('mouseup', stopStep, { capture: true });
window.removeEventListener('mouseup', stopStepMouse, { capture: true });
window.addEventListener('keyup', stopStepKeyboard, { capture: true });
};
}, []);

@@ -209,6 +292,7 @@ export const NumberSpinner = React.forwardRef<NumberSpinnerDerivedElement, Numbe
aria-labelledby={labelId}
type="number"
data-testid="input"
onKeyDown={doStepKeyboard}
className={clsx(
'bg-negative rounded-inherit w-full peer block tabular-nums font-inherit relative',
'focus:outline-0',
@@ -272,8 +356,9 @@ export const NumberSpinner = React.forwardRef<NumberSpinnerDerivedElement, Numbe
</div>
)}
{indicator && (
<div
<button
data-testid="indicator"
type="button"
className={clsx(
'text-center z-[1] flex flex-col items-stretch justify-center aspect-square absolute bottom-0 right-0 select-none text-primary group-focus-within:text-secondary',
{
@@ -282,12 +367,16 @@ export const NumberSpinner = React.forwardRef<NumberSpinnerDerivedElement, Numbe
'w-16': size === 'large',
},
)}
onMouseDown={doStepMouse}
>
<button
aria-label="Step Up"
type="button"
className="h-0 flex-auto flex justify-center items-start text-inherit overflow-hidden"
onMouseDown={handleStepUp}
<span
className={clsx(
'h-0 flex-auto flex justify-center items-end overflow-hidden',
{
'text-tertiary': displayStep === true,
'text-inherit': displayStep !== true,
},
)}
>
<svg
className="w-6 h-6 fill-none stroke-current stroke-2 linejoin-round linecap-round"
@@ -296,12 +385,21 @@ export const NumberSpinner = React.forwardRef<NumberSpinnerDerivedElement, Numbe
>
<polyline points="18 15 12 9 6 15" />
</svg>
</button>
<button
aria-label="Step Down"
type="button"
className="h-0 flex-auto flex justify-center items-end text-inherit overflow-hidden"
onMouseDown={handleStepDown}
<span className="sr-only">
Step Up
</span>
</span>
<span className="sr-only">
/
</span>
<span
className={clsx(
'h-0 flex-auto flex justify-center items-end overflow-hidden',
{
'text-tertiary': displayStep === false,
'text-inherit': displayStep !== false,
},
)}
>
<svg
className="w-6 h-6 fill-none stroke-current stroke-2 linejoin-round linecap-round"
@@ -310,8 +408,11 @@ export const NumberSpinner = React.forwardRef<NumberSpinnerDerivedElement, Numbe
>
<polyline points="6 9 12 15 18 9" />
</svg>
</button>
</div>
<span className="sr-only">
Step Down
</span>
</span>
</button>
)}
{border && (
<span


+ 30
- 9
categories/number/react/src/components/Slider/index.tsx View File

@@ -36,13 +36,31 @@ const filterOptions = (children: React.ReactNode): React.ReactNode => {
* sliders and vv.
*/

/**
* Orientation of the {@link Slider} component.
*/
export type SliderOrientation = 'horizontal' | 'vertical';

/**
* Derived HTML element of the {@link Slider} component.
*/
export type SliderDerivedElement = HTMLInputElement;

/**
* Props of the {@link Slider} component.
*/
export interface SliderProps extends Omit<React.HTMLProps<HTMLInputElement>, 'type'> {
/**
* Orientation of the component.
*/
orient?: SliderOrientation;
/**
* Options of the component.
*/
children?: React.ReactNode;
/**
* Length of the component.
*/
length?: React.CSSProperties['width'];
}

@@ -93,7 +111,7 @@ export const sliderPlugin = plugin(({ addComponents }) => {
'aspect-ratio': '1 / 1',
'z-index': '1',
position: 'relative',
'box-shadow': '-100000.5em 0 0 100000em rgb(var(--color-primary) / 50%)',
'box-shadow': 'calc(-200001em / 2) 0 0 100000em rgb(var(--color-primary) / 50%)',
},

'& > input:focus::-webkit-slider-container': {
@@ -105,11 +123,11 @@ export const sliderPlugin = plugin(({ addComponents }) => {
},

'& > input:focus::-webkit-slider-thumb': {
'box-shadow': '-100000.5em 0 0 100000em rgb(var(--color-secondary) / 50%)',
'box-shadow': 'calc(-200001em / 2) 0 0 100000em rgb(var(--color-secondary) / 50%)',
},

'& > input:active::-webkit-slider-thumb': {
'box-shadow': '-100000.5em 0 0 100000em rgb(var(--color-tertiary) / 50%)',
'box-shadow': 'calc(-200001em / 2) 0 0 100000em rgb(var(--color-tertiary) / 50%)',
},

'&[data-orient="horizontal"]': {
@@ -155,15 +173,15 @@ export const sliderPlugin = plugin(({ addComponents }) => {
'aspect-ratio': '1 / 1',
'z-index': '1',
position: 'relative',
'box-shadow': '-100000.5em 0 0 100000em rgb(var(--color-primary) / 50%)',
'box-shadow': 'calc(-200001em / 2) 0 0 100000em rgb(var(--color-primary) / 50%)',
},

'& > input:focus::-moz-range-thumb': {
'box-shadow': '-100000.5em 0 0 100000em rgb(var(--color-secondary) / 50%)',
'box-shadow': 'calc(-200001em / 2) 0 0 100000em rgb(var(--color-secondary) / 50%)',
},

'& > input:active::-moz-range-thumb': {
'box-shadow': '-100000.5em 0 0 100000em rgb(var(--color-tertiary) / 50%)',
'box-shadow': 'calc(-200001em / 2) 0 0 100000em rgb(var(--color-tertiary) / 50%)',
},

'& > input[orient="vertical"]': {
@@ -179,15 +197,15 @@ export const sliderPlugin = plugin(({ addComponents }) => {
'& > input[orient="vertical"]::-moz-range-thumb': {
width: '100%',
height: '1em',
'box-shadow': '0 100000.5em 0 100000em rgb(var(--color-primary) / 50%)',
'box-shadow': '0 calc(200001em / 2) 0 100000em rgb(var(--color-primary) / 50%)',
},

'& > input[orient="vertical"]:focus::-moz-range-thumb': {
'box-shadow': '0 100000.5em 0 100000em rgb(var(--color-secondary) / 50%)',
'box-shadow': '0 calc(200001em / 2) 0 100000em rgb(var(--color-secondary) / 50%)',
},

'& > input[orient="vertical"]:active::-moz-range-thumb': {
'box-shadow': '0 100000.5em 0 100000em rgb(var(--color-tertiary) / 50%)',
'box-shadow': '0 calc(200001em / 2) 0 100000em rgb(var(--color-tertiary) / 50%)',
},

'&[data-chrome] > input + *': {
@@ -208,6 +226,9 @@ export const sliderPlugin = plugin(({ addComponents }) => {
});
});

/**
* Component for inputting continuous numeric values.
*/
export const Slider = React.forwardRef<SliderDerivedElement, SliderProps>((
{
className,


+ 7
- 1
categories/temporal/react/src/components/DateDropdown/index.tsx View File

@@ -4,8 +4,14 @@ import clsx from 'clsx';
import plugin from 'tailwindcss/plugin';
import { useFallbackId } from '@modal-sh/react-utils';

/**
* Derived HTML element of the {@link DateDropdown} component.
*/
export type DateDropdownDerivedElement = HTMLInputElement;

/**
* Props of the {@link DateDropdown} component.
*/
export interface DateDropdownProps extends Omit<React.HTMLProps<DateDropdownDerivedElement>, 'size' | 'type' | 'label' | 'pattern'> {
/**
* Short textual description indicating the nature of the component's value.
@@ -68,7 +74,7 @@ export const dateDropdownPlugin = plugin(({ addComponents }) => {
});

/**
* Component for inputting textual values.
* Component for inputting date values.
*/
export const DateDropdown = React.forwardRef<
DateDropdownDerivedElement,


+ 7
- 1
categories/temporal/react/src/components/TimeSpinner/index.tsx View File

@@ -4,6 +4,9 @@ import clsx from 'clsx';
import plugin from 'tailwindcss/plugin';
import { useFallbackId } from '@modal-sh/react-utils';

/**
* Derived HTML element of the {@link TimeSpinner} component.
*/
export type TimeSpinnerDerivedElement = HTMLInputElement;

type Digit = (0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9);
@@ -16,6 +19,9 @@ type StepHhMmSs = `${StepHhMm}:${Segment}`;

type Step = StepHhMm | StepHhMmSs;

/**
* Props of the {@link TimeSpinner} component.
*/
export interface TimeSpinnerProps extends Omit<React.HTMLProps<TimeSpinnerDerivedElement>, 'size' | 'type' | 'label' | 'step' | 'pattern'> {
/**
* Short textual description indicating the nature of the component's value.
@@ -82,7 +88,7 @@ export const timeSpinnerPlugin = plugin(({ addComponents }) => {
});

/**
* Component for inputting textual values.
* Component for inputting time values.
*/
export const TimeSpinner = React.forwardRef<
TimeSpinnerDerivedElement,


+ 2
- 0
showcases/web-kitchensink-reactnext/src/pages/examples/registration-form/index.tsx View File

@@ -102,6 +102,7 @@ const RegistrationFormPage: NextPage = () => {
label="Password"
name="password"
onChange={handleChange}
enhanced
/>
</div>
<div className="sm:col-span-2">
@@ -111,6 +112,7 @@ const RegistrationFormPage: NextPage = () => {
label="Confirm Password"
name="confirmPassword"
onChange={handleChange}
enhanced
/>
</div>
<div className="sm:col-span-6 text-center">


Loading…
Cancel
Save