Add TSDoc to component props. Spinners now display activated up/down buttons as well as maskedtextinput toggle buttons.master
@@ -31,6 +31,9 @@ export interface FileSelectBoxProps< | |||||
* Short textual description as guidelines for valid input values. | * Short textual description as guidelines for valid input values. | ||||
*/ | */ | ||||
hint?: React.ReactNode, | hint?: React.ReactNode, | ||||
/** | |||||
* Should the component be enhanced? | |||||
*/ | |||||
enhanced?: boolean, | enhanced?: boolean, | ||||
/** | /** | ||||
* Is the label hidden? | * Is the label hidden? | ||||
@@ -18,6 +18,9 @@ export const colorPickerPlugin = plugin(({ addComponents }) => { | |||||
'&::-webkit-color-swatch': { | '&::-webkit-color-swatch': { | ||||
'border': '2px solid black', | '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 | <input | ||||
{...etcProps} | {...etcProps} | ||||
className={clsx( | className={clsx( | ||||
@@ -3,9 +3,15 @@ import { TextControl } from '@tesseract-design/web-base'; | |||||
import clsx from 'clsx'; | import clsx from 'clsx'; | ||||
import { useFallbackId } from '@modal-sh/react-utils'; | import { useFallbackId } from '@modal-sh/react-utils'; | ||||
/** | |||||
* Derived HTML element of the {@link EmailInput} component. | |||||
*/ | |||||
export type EmailInputDerivedElement = HTMLInputElement; | 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. | * 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. | * Visual length of the input. | ||||
*/ | */ | ||||
length?: number, | 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, | label, | ||||
hint, | hint, | ||||
size = 'medium' as const, | size = 'medium' as const, | ||||
border = false, | |||||
block = false, | |||||
border = false as const, | |||||
block = false as const, | |||||
variant = 'default' as const, | variant = 'default' as const, | ||||
hiddenLabel = false, | |||||
hiddenLabel = false as const, | |||||
className, | className, | ||||
id: idProp, | id: idProp, | ||||
style, | style, | ||||
domains = [], | domains = [], | ||||
length, | length, | ||||
autoComplete = false as const, | |||||
...etcProps | ...etcProps | ||||
}: EmailInputProps, | }: EmailInputProps, | ||||
forwardedRef, | forwardedRef, | ||||
@@ -121,6 +132,7 @@ export const EmailInput = React.forwardRef<EmailInputDerivedElement, EmailInputP | |||||
aria-labelledby={labelId} | aria-labelledby={labelId} | ||||
type="email" | type="email" | ||||
data-testid="input" | data-testid="input" | ||||
autoComplete={autoComplete ? 'email' : undefined} | |||||
pattern={pattern} | pattern={pattern} | ||||
className={clsx( | className={clsx( | ||||
'bg-negative rounded-inherit w-full peer block font-inherit tabular-nums', | 'bg-negative rounded-inherit w-full peer block font-inherit tabular-nums', | ||||
@@ -200,11 +212,12 @@ EmailInput.displayName = 'EmailInput'; | |||||
EmailInput.defaultProps = { | EmailInput.defaultProps = { | ||||
label: undefined, | label: undefined, | ||||
hint: 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: [], | domains: [], | ||||
length: undefined, | length: undefined, | ||||
autoComplete: false as const, | |||||
}; | }; |
@@ -3,8 +3,14 @@ import { TextControl } from '@tesseract-design/web-base'; | |||||
import clsx from 'clsx'; | import clsx from 'clsx'; | ||||
import { useFallbackId } from '@modal-sh/react-utils'; | import { useFallbackId } from '@modal-sh/react-utils'; | ||||
/** | |||||
* Derived HTML element of the {@link PatternTextInput} component. | |||||
*/ | |||||
export type PatternTextInputDerivedElement = HTMLInputElement; | export type PatternTextInputDerivedElement = HTMLInputElement; | ||||
/** | |||||
* Props of the {@link PatternTextInput} component. | |||||
*/ | |||||
export interface PatternTextInputProps extends Omit<React.HTMLProps<PatternTextInputDerivedElement>, 'size' | 'type' | 'label' | 'list' | 'inputMode'> { | export interface PatternTextInputProps extends Omit<React.HTMLProps<PatternTextInputDerivedElement>, 'size' | 'type' | 'label' | 'list' | 'inputMode'> { | ||||
/** | /** | ||||
* Short textual description indicating the nature of the component's value. | * Short textual description indicating the nature of the component's value. | ||||
@@ -4,9 +4,15 @@ import { useClientSide, useFallbackId, useProxyInput } from '@modal-sh/react-uti | |||||
import PhoneInput, { Country, Value } from 'react-phone-number-input/input'; | import PhoneInput, { Country, Value } from 'react-phone-number-input/input'; | ||||
import clsx from 'clsx'; | import clsx from 'clsx'; | ||||
/** | |||||
* Derived HTML element of the {@link PhoneNumberInput} component. | |||||
*/ | |||||
export type PhoneNumberInputDerivedElement = HTMLInputElement; | 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. | * Short textual description indicating the nature of the component's value. | ||||
*/ | */ | ||||
@@ -3,8 +3,14 @@ import { TextControl } from '@tesseract-design/web-base'; | |||||
import clsx from 'clsx'; | import clsx from 'clsx'; | ||||
import { useFallbackId } from '@modal-sh/react-utils'; | import { useFallbackId } from '@modal-sh/react-utils'; | ||||
/** | |||||
* Derived HTML element of the {@link UrlInput} component. | |||||
*/ | |||||
export type UrlInputDerivedElement = HTMLInputElement; | export type UrlInputDerivedElement = HTMLInputElement; | ||||
/** | |||||
* Props of the {@link UrlInput} component. | |||||
*/ | |||||
export interface UrlInputProps extends Omit<React.HTMLProps<UrlInputDerivedElement>, 'size' | 'type' | 'label' | 'inputMode'> { | export interface UrlInputProps extends Omit<React.HTMLProps<UrlInputDerivedElement>, 'size' | 'type' | 'label' | 'inputMode'> { | ||||
/** | /** | ||||
* Short textual description indicating the nature of the component's value. | * Short textual description indicating the nature of the component's value. | ||||
@@ -3,9 +3,15 @@ import { TextControl } from '@tesseract-design/web-base'; | |||||
import clsx from 'clsx'; | import clsx from 'clsx'; | ||||
import { useClientSide, useFallbackId } from '@modal-sh/react-utils'; | import { useClientSide, useFallbackId } from '@modal-sh/react-utils'; | ||||
/** | |||||
* Derived HTML element of the {@link MaskedTextInput} component. | |||||
*/ | |||||
export type MaskedTextInputDerivedElement = HTMLInputElement; | 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. | * 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. | * Visual length of the input. | ||||
*/ | */ | ||||
length?: number, | 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, | label, | ||||
hint, | hint, | ||||
size = 'medium' as const, | size = 'medium' as const, | ||||
border = false, | |||||
block = false, | |||||
border = false as const, | |||||
block = false as const, | |||||
variant = 'default' as const, | variant = 'default' as const, | ||||
hiddenLabel = false, | |||||
hiddenLabel = false as const, | |||||
className, | className, | ||||
id: idProp, | id: idProp, | ||||
style, | style, | ||||
length, | length, | ||||
disabled, | disabled, | ||||
onKeyDown, | |||||
onKeyUp, | onKeyUp, | ||||
autoComplete, | |||||
enhanced: enhancedProp = false as const, | |||||
...etcProps | ...etcProps | ||||
}: MaskedTextInputProps, | }: MaskedTextInputProps, | ||||
forwardedRef, | forwardedRef, | ||||
) => { | ) => { | ||||
const { clientSide: indicator } = useClientSide({ clientSide: true, initial: false }); | |||||
const { clientSide: enhanced } = useClientSide({ clientSide: enhancedProp }); | |||||
const labelId = React.useId(); | const labelId = React.useId(); | ||||
const id = useFallbackId(idProp); | const id = useFallbackId(idProp); | ||||
const [visible, setVisible] = React.useState(false); | const [visible, setVisible] = React.useState(false); | ||||
const [visibleViaKey, setVisibleViaKey] = React.useState(false); | |||||
const defaultRef = React.useRef<MaskedTextInputDerivedElement>(null); | const defaultRef = React.useRef<MaskedTextInputDerivedElement>(null); | ||||
const ref = forwardedRef ?? defaultRef; | 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< | const handleKeyUp: React.KeyboardEventHandler< | ||||
MaskedTextInputDerivedElement | MaskedTextInputDerivedElement | ||||
> = React.useCallback((e) => { | > = React.useCallback((e) => { | ||||
if (e.ctrlKey && e.code === 'Space') { | if (e.ctrlKey && e.code === 'Space') { | ||||
setVisibleViaKey(false); | |||||
setVisible((prev) => !prev); | setVisible((prev) => !prev); | ||||
} | } | ||||
onKeyUp?.(e); | onKeyUp?.(e); | ||||
@@ -98,6 +126,22 @@ export const MaskedTextInput = React.forwardRef< | |||||
}); | }); | ||||
}, [ref, defaultRef]); | }, [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(() => { | React.useEffect(() => { | ||||
if (typeof ref === 'function') { | if (typeof ref === 'function') { | ||||
const defaultElement = defaultRef.current as MaskedTextInputDerivedElement; | const defaultElement = defaultRef.current as MaskedTextInputDerivedElement; | ||||
@@ -131,12 +175,12 @@ export const MaskedTextInput = React.forwardRef< | |||||
'sr-only': hiddenLabel, | '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} | aria-labelledby={labelId} | ||||
type={!visible ? 'password' : 'text'} | type={!visible ? 'password' : 'text'} | ||||
data-testid="input" | data-testid="input" | ||||
onKeyUp={handleKeyUp} | |||||
autoComplete={autoComplete ?? 'off'} | |||||
onKeyUp={enhanced ? handleKeyUp : undefined} | |||||
onKeyDown={enhanced ? handleKeyDown : undefined} | |||||
className={clsx( | className={clsx( | ||||
'bg-negative rounded-inherit w-full peer block font-inherit', | 'bg-negative rounded-inherit w-full peer block font-inherit', | ||||
'focus:outline-0', | 'focus:outline-0', | ||||
@@ -173,13 +219,13 @@ export const MaskedTextInput = React.forwardRef< | |||||
'pt-4': variant === 'alternate', | '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', | 'h-10': size === 'small', | ||||
@@ -202,13 +248,13 @@ export const MaskedTextInput = React.forwardRef< | |||||
'pt-3': variant === 'alternate' && size !== 'small', | '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> | ||||
</div> | </div> | ||||
)} | )} | ||||
{indicator && ( | |||||
{enhanced && ( | |||||
<button | <button | ||||
disabled={disabled} | disabled={disabled} | ||||
type="button" | type="button" | ||||
data-testid="indicator" | data-testid="indicator" | ||||
tabIndex={-1} | tabIndex={-1} | ||||
className={clsx( | 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-10': size === 'small', | ||||
'w-12': size === 'medium', | 'w-12': size === 'medium', | ||||
@@ -270,10 +320,12 @@ MaskedTextInput.displayName = 'MaskedTextInput'; | |||||
MaskedTextInput.defaultProps = { | MaskedTextInput.defaultProps = { | ||||
label: undefined, | label: undefined, | ||||
hint: 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, | length: undefined, | ||||
enhanced: false as const, | |||||
autoComplete: undefined, | |||||
}; | }; |
@@ -3,8 +3,14 @@ import { TextControl } from '@tesseract-design/web-base'; | |||||
import clsx from 'clsx'; | import clsx from 'clsx'; | ||||
import { useFallbackId } from '@modal-sh/react-utils'; | import { useFallbackId } from '@modal-sh/react-utils'; | ||||
/** | |||||
* Derived HTML element of the {@link MultilineTextInput} component. | |||||
*/ | |||||
export type MultilineTextInputDerivedElement = HTMLTextAreaElement; | export type MultilineTextInputDerivedElement = HTMLTextAreaElement; | ||||
/** | |||||
* Props of the {@link MultilineTextInput} component. | |||||
*/ | |||||
export interface MultilineTextInputProps extends Omit<React.HTMLProps<MultilineTextInputDerivedElement>, 'size' | 'label' | 'inputMode' | 'pattern'> { | export interface MultilineTextInputProps extends Omit<React.HTMLProps<MultilineTextInputDerivedElement>, 'size' | 'label' | 'inputMode' | 'pattern'> { | ||||
/** | /** | ||||
* Short textual description indicating the nature of the component's value. | * Short textual description indicating the nature of the component's value. | ||||
@@ -3,8 +3,14 @@ import { TextControl } from '@tesseract-design/web-base'; | |||||
import clsx from 'clsx'; | import clsx from 'clsx'; | ||||
import { useFallbackId } from '@modal-sh/react-utils'; | import { useFallbackId } from '@modal-sh/react-utils'; | ||||
/** | |||||
* Derived HTML element of the {@link TextInput} component. | |||||
*/ | |||||
export type TextInputDerivedElement = HTMLInputElement; | export type TextInputDerivedElement = HTMLInputElement; | ||||
/** | |||||
* Props of the {@link TextInput} component. | |||||
*/ | |||||
export interface TextInputProps extends Omit<React.HTMLProps<TextInputDerivedElement>, 'size' | 'type' | 'label' | 'list' | 'inputMode' | 'pattern'> { | 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. | * Short textual description indicating the nature of the component's value. | ||||
@@ -1,17 +1,30 @@ | |||||
import * as React from 'react'; | import * as React from 'react'; | ||||
import clsx from 'clsx'; | import clsx from 'clsx'; | ||||
/** | |||||
* Derived HTML element of the {@link Badge} component. | |||||
*/ | |||||
export type BadgeDerivedElement = HTMLSpanElement; | export type BadgeDerivedElement = HTMLSpanElement; | ||||
/** | |||||
* Props of the {@link Badge} component. | |||||
*/ | |||||
export interface BadgeProps extends React.HTMLProps<BadgeDerivedElement> { | export interface BadgeProps extends React.HTMLProps<BadgeDerivedElement> { | ||||
/** | |||||
* Is the component rounded? | |||||
*/ | |||||
rounded?: boolean; | rounded?: boolean; | ||||
} | } | ||||
/** | |||||
* Component for displaying textual information in a small container. | |||||
*/ | |||||
export const Badge = React.forwardRef<BadgeDerivedElement, BadgeProps>(( | export const Badge = React.forwardRef<BadgeDerivedElement, BadgeProps>(( | ||||
{ | { | ||||
children, | children, | ||||
rounded = false, | rounded = false, | ||||
className, | className, | ||||
style, | |||||
...etcProps | ...etcProps | ||||
}, | }, | ||||
forwardedRef, | forwardedRef, | ||||
@@ -28,6 +41,7 @@ export const Badge = React.forwardRef<BadgeDerivedElement, BadgeProps>(( | |||||
}, | }, | ||||
className, | className, | ||||
)} | )} | ||||
style={style} | |||||
data-testid="badge" | data-testid="badge" | ||||
> | > | ||||
<span className="relative w-full"> | <span className="relative w-full"> | ||||
@@ -1,23 +1,52 @@ | |||||
import * as React from 'react'; | import * as React from 'react'; | ||||
import clsx from 'clsx'; | import clsx from 'clsx'; | ||||
/** | |||||
* Derived HTML element of the {@link KeyValueTable} component. | |||||
*/ | |||||
export type KeyValueTableDerivedElement = HTMLDListElement; | export type KeyValueTableDerivedElement = HTMLDListElement; | ||||
interface KeyValueProperty { | |||||
/** | |||||
* Individual property of the {@link KeyValueTable} component. | |||||
*/ | |||||
export interface KeyValueTableProperty { | |||||
/** | |||||
* Key of the property. | |||||
*/ | |||||
key: string; | key: string; | ||||
/** | |||||
* Class name of the property. | |||||
*/ | |||||
className?: string; | className?: string; | ||||
/** | |||||
* Value of the property. | |||||
*/ | |||||
valueProps?: React.HTMLProps<HTMLElement>; | valueProps?: React.HTMLProps<HTMLElement>; | ||||
} | } | ||||
/** | |||||
* Props of the {@link KeyValueTable} component. | |||||
*/ | |||||
export interface KeyValueTableProps extends Omit<React.HTMLProps<KeyValueTableDerivedElement>, 'children'> { | export interface KeyValueTableProps extends Omit<React.HTMLProps<KeyValueTableDerivedElement>, 'children'> { | ||||
/** | |||||
* Should the keys be hidden? | |||||
*/ | |||||
hiddenKeys?: boolean; | 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>(( | export const KeyValueTable = React.forwardRef<KeyValueTableDerivedElement, KeyValueTableProps>(( | ||||
{ | { | ||||
hiddenKeys = false, | hiddenKeys = false, | ||||
properties = [], | properties = [], | ||||
className, | |||||
style, | |||||
...etcProps | ...etcProps | ||||
}, | }, | ||||
forwardedRef, | forwardedRef, | ||||
@@ -26,10 +55,12 @@ export const KeyValueTable = React.forwardRef<KeyValueTableDerivedElement, KeyVa | |||||
{...etcProps} | {...etcProps} | ||||
className={clsx( | className={clsx( | ||||
'grid gap-y-1 grid-cols-3', | 'grid gap-y-1 grid-cols-3', | ||||
className, | |||||
)} | )} | ||||
ref={forwardedRef} | ref={forwardedRef} | ||||
style={style} | |||||
> | > | ||||
{properties.map((property) => typeof property === 'object' && ( | |||||
{properties.map((property) => typeof property === 'object' && property && ( | |||||
<div | <div | ||||
key={property.key} | key={property.key} | ||||
className={clsx('contents', property.className)} | className={clsx('contents', property.className)} | ||||
@@ -4,9 +4,15 @@ import clsx from 'clsx'; | |||||
import plugin from 'tailwindcss/plugin'; | import plugin from 'tailwindcss/plugin'; | ||||
import { useFallbackId } from '@modal-sh/react-utils'; | import { useFallbackId } from '@modal-sh/react-utils'; | ||||
/** | |||||
* Derived HTML element of the {@link MenuMultiSelect} component. | |||||
*/ | |||||
export type MenuMultiSelectDerivedElement = HTMLSelectElement; | 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. | * Short textual description indicating the nature of the component's value. | ||||
*/ | */ | ||||
@@ -88,6 +94,7 @@ export const MenuMultiSelect = React.forwardRef< | |||||
className, | className, | ||||
startingHeight = '15rem', | startingHeight = '15rem', | ||||
id: idProp, | id: idProp, | ||||
style, | |||||
...etcProps | ...etcProps | ||||
}, | }, | ||||
forwardedRef, | forwardedRef, | ||||
@@ -107,6 +114,7 @@ export const MenuMultiSelect = React.forwardRef< | |||||
className, | className, | ||||
)} | )} | ||||
data-testid="base" | data-testid="base" | ||||
style={style} | |||||
> | > | ||||
{label && ( | {label && ( | ||||
<> | <> | ||||
@@ -17,12 +17,24 @@ const TAG_INPUT_VALUE_SEPARATOR_MAP = { | |||||
'semicolon': ';', | 'semicolon': ';', | ||||
} as const; | } as const; | ||||
/** | |||||
* Separator for splitting the input value into multiple tags. | |||||
*/ | |||||
export type TagInputSeparator = keyof typeof TAG_INPUT_SEPARATOR_MAP | export type TagInputSeparator = keyof typeof TAG_INPUT_SEPARATOR_MAP | ||||
/** | |||||
* Derived HTML element of the {@link TagInput} component. | |||||
*/ | |||||
export type TagInputDerivedElement = HTMLTextAreaElement | HTMLInputElement; | export type TagInputDerivedElement = HTMLTextAreaElement | HTMLInputElement; | ||||
/** | |||||
* Proxied HTML element of the {@link TagInput} component. | |||||
*/ | |||||
export type TagInputProxiedElement = HTMLTextAreaElement & HTMLInputElement; | export type TagInputProxiedElement = HTMLTextAreaElement & HTMLInputElement; | ||||
/** | |||||
* Props of the {@link TagsInput} component. | |||||
*/ | |||||
export interface TagInputProps extends Omit<React.HTMLProps<TagInputDerivedElement>, 'size' | 'type' | 'style' | 'label' | 'list'> { | export interface TagInputProps extends Omit<React.HTMLProps<TagInputDerivedElement>, 'size' | 'type' | 'style' | 'label' | 'list'> { | ||||
/** | /** | ||||
* Short textual description indicating the nature of the component's value. | * Short textual description indicating the nature of the component's value. | ||||
@@ -4,15 +4,42 @@ import { Button } from '@tesseract-design/web-base'; | |||||
import plugin from 'tailwindcss/plugin'; | import plugin from 'tailwindcss/plugin'; | ||||
import { useFallbackId } from '@modal-sh/react-utils'; | import { useFallbackId } from '@modal-sh/react-utils'; | ||||
/** | |||||
* Derived HTML element of the {@link ToggleButton} component. | |||||
*/ | |||||
export type ToggleButtonDerivedElement = HTMLInputElement; | export type ToggleButtonDerivedElement = HTMLInputElement; | ||||
/** | |||||
* Props of the {@link ToggleButton} component. | |||||
*/ | |||||
export interface ToggleButtonProps extends Omit<React.InputHTMLAttributes<ToggleButtonDerivedElement>, 'type' | 'size'> { | export interface ToggleButtonProps extends Omit<React.InputHTMLAttributes<ToggleButtonDerivedElement>, 'type' | 'size'> { | ||||
/** | |||||
* Should the component occupy the whole width of its parent? | |||||
*/ | |||||
block?: boolean; | block?: boolean; | ||||
/** | |||||
* Should the component's content use minimal space? | |||||
*/ | |||||
compact?: boolean; | compact?: boolean; | ||||
/** | |||||
* Size of the component. | |||||
*/ | |||||
size?: Button.Size; | size?: Button.Size; | ||||
/** | |||||
* Complementary content of the component. | |||||
*/ | |||||
subtext?: React.ReactNode; | subtext?: React.ReactNode; | ||||
/** | |||||
* Short complementary content displayed at the edge of the component. | |||||
*/ | |||||
badge?: React.ReactNode; | badge?: React.ReactNode; | ||||
/** | |||||
* Variant of the component. | |||||
*/ | |||||
variant?: Button.Variant; | variant?: Button.Variant; | ||||
/** | |||||
* Is the component in an indeterminate state? | |||||
*/ | |||||
indeterminate?: boolean; | indeterminate?: boolean; | ||||
} | } | ||||
@@ -35,6 +62,9 @@ export const toggleButtonPlugin = plugin(({ addComponents, }) => { | |||||
}); | }); | ||||
}); | }); | ||||
/** | |||||
* Component for toggling a Boolean value. | |||||
*/ | |||||
export const ToggleButton = React.forwardRef<ToggleButtonDerivedElement, ToggleButtonProps>(( | export const ToggleButton = React.forwardRef<ToggleButtonDerivedElement, ToggleButtonProps>(( | ||||
{ | { | ||||
children, | children, | ||||
@@ -47,6 +77,7 @@ export const ToggleButton = React.forwardRef<ToggleButtonDerivedElement, ToggleB | |||||
badge, | badge, | ||||
variant = 'bare' as const, | variant = 'bare' as const, | ||||
indeterminate = false, | indeterminate = false, | ||||
style, | |||||
...etcProps | ...etcProps | ||||
}, | }, | ||||
forwardedRef, | forwardedRef, | ||||
@@ -104,6 +135,7 @@ export const ToggleButton = React.forwardRef<ToggleButtonDerivedElement, ToggleB | |||||
}, | }, | ||||
className, | className, | ||||
)} | )} | ||||
style={style} | |||||
> | > | ||||
<span | <span | ||||
className={clsx( | className={clsx( | ||||
@@ -3,13 +3,34 @@ import clsx from 'clsx'; | |||||
import plugin from 'tailwindcss/plugin'; | import plugin from 'tailwindcss/plugin'; | ||||
import { useFallbackId } from '@modal-sh/react-utils'; | import { useFallbackId } from '@modal-sh/react-utils'; | ||||
/** | |||||
* Derived HTML element of the {@link ToggleSwitch} component. | |||||
*/ | |||||
export type ToggleSwitchDerivedElement = HTMLInputElement; | export type ToggleSwitchDerivedElement = HTMLInputElement; | ||||
/** | |||||
* Props of the {@link ToggleSwitch} component. | |||||
*/ | |||||
export interface ToggleSwitchProps extends Omit<React.InputHTMLAttributes<ToggleSwitchDerivedElement>, 'type' | 'size'> { | export interface ToggleSwitchProps extends Omit<React.InputHTMLAttributes<ToggleSwitchDerivedElement>, 'type' | 'size'> { | ||||
/** | |||||
* Should the component occupy the whole width of its parent? | |||||
*/ | |||||
block?: boolean; | block?: boolean; | ||||
/** | |||||
* Complementary content of the component. | |||||
*/ | |||||
subtext?: React.ReactNode; | subtext?: React.ReactNode; | ||||
/** | |||||
* Is the component in an indeterminate state? | |||||
*/ | |||||
indeterminate?: boolean; | indeterminate?: boolean; | ||||
/** | |||||
* Label to display when the component is checked. | |||||
*/ | |||||
checkedLabel?: React.ReactNode; | checkedLabel?: React.ReactNode; | ||||
/** | |||||
* Label to display when the component is unchecked. | |||||
*/ | |||||
uncheckedLabel?: React.ReactNode; | 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>(( | export const ToggleSwitch = React.forwardRef<ToggleSwitchDerivedElement, ToggleSwitchProps>(( | ||||
{ | { | ||||
uncheckedLabel, | uncheckedLabel, | ||||
@@ -3,11 +3,26 @@ import clsx from 'clsx'; | |||||
import plugin from 'tailwindcss/plugin'; | import plugin from 'tailwindcss/plugin'; | ||||
import { useFallbackId } from '@modal-sh/react-utils'; | import { useFallbackId } from '@modal-sh/react-utils'; | ||||
/** | |||||
* Derived HTML element of the {@link ToggleTickBox} component. | |||||
*/ | |||||
export type ToggleTickBoxDerivedElement = HTMLInputElement; | export type ToggleTickBoxDerivedElement = HTMLInputElement; | ||||
/** | |||||
* Props of the {@link ToggleTickBox} component. | |||||
*/ | |||||
export interface ToggleTickBoxProps extends Omit<React.InputHTMLAttributes<ToggleTickBoxDerivedElement>, 'type' | 'size'> { | export interface ToggleTickBoxProps extends Omit<React.InputHTMLAttributes<ToggleTickBoxDerivedElement>, 'type' | 'size'> { | ||||
/** | |||||
* Should the component occupy the whole width of its parent? | |||||
*/ | |||||
block?: boolean; | block?: boolean; | ||||
/** | |||||
* Complementary content of the component. | |||||
*/ | |||||
subtext?: React.ReactNode; | subtext?: React.ReactNode; | ||||
/** | |||||
* Is the component in an indeterminate state? | |||||
*/ | |||||
indeterminate?: boolean; | 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>(( | export const ToggleTickBox = React.forwardRef<ToggleTickBoxDerivedElement, ToggleTickBoxProps>(( | ||||
{ | { | ||||
children, | children, | ||||
@@ -2,17 +2,50 @@ import * as React from 'react'; | |||||
import clsx from 'clsx'; | import clsx from 'clsx'; | ||||
import { Button } from '@tesseract-design/web-base'; | import { Button } from '@tesseract-design/web-base'; | ||||
/** | |||||
* Derived HTML element of the {@link LinkButton} component. | |||||
*/ | |||||
export type LinkButtonDerivedElement = HTMLAnchorElement; | export type LinkButtonDerivedElement = HTMLAnchorElement; | ||||
/** | |||||
* Props of the {@link LinkButton} component. | |||||
*/ | |||||
export interface LinkButtonProps<T = any> extends Omit<React.HTMLProps<LinkButtonDerivedElement>, 'size'> { | export interface LinkButtonProps<T = any> extends Omit<React.HTMLProps<LinkButtonDerivedElement>, 'size'> { | ||||
/** | |||||
* Should the component occupy the whole width of its parent? | |||||
*/ | |||||
block?: boolean; | block?: boolean; | ||||
/** | |||||
* Variant of the component. | |||||
*/ | |||||
variant?: Button.Variant; | variant?: Button.Variant; | ||||
/** | |||||
* Complementary content of the component. | |||||
*/ | |||||
subtext?: React.ReactNode; | subtext?: React.ReactNode; | ||||
/** | |||||
* Short complementary content displayed at the edge of the component. | |||||
*/ | |||||
badge?: React.ReactNode; | badge?: React.ReactNode; | ||||
/** | |||||
* Is this component part of a menu? | |||||
*/ | |||||
menuItem?: boolean; | menuItem?: boolean; | ||||
/** | |||||
* Size of the component. | |||||
*/ | |||||
size?: Button.Size; | size?: Button.Size; | ||||
/** | |||||
* Should the component's content use minimal space? | |||||
*/ | |||||
compact?: boolean; | compact?: boolean; | ||||
/** | |||||
* Component to use in rendering. | |||||
*/ | |||||
component?: React.ElementType<T>; | component?: React.ElementType<T>; | ||||
/** | |||||
* Is the component unable to receive activation? | |||||
*/ | |||||
disabled?: boolean; | disabled?: boolean; | ||||
} | } | ||||
@@ -30,6 +63,7 @@ export const LinkButton = React.forwardRef<LinkButtonDerivedElement, LinkButtonP | |||||
component: EnabledComponent = 'a', | component: EnabledComponent = 'a', | ||||
disabled = false, | disabled = false, | ||||
href, | href, | ||||
style, | |||||
...etcProps | ...etcProps | ||||
}, | }, | ||||
forwardedRef, | forwardedRef, | ||||
@@ -70,6 +104,7 @@ export const LinkButton = React.forwardRef<LinkButtonDerivedElement, LinkButtonP | |||||
className, | className, | ||||
)} | )} | ||||
data-testid="link" | data-testid="link" | ||||
style={style} | |||||
> | > | ||||
<span | <span | ||||
className={clsx( | className={clsx( | ||||
@@ -4,8 +4,14 @@ import clsx from 'clsx'; | |||||
import plugin from 'tailwindcss/plugin'; | import plugin from 'tailwindcss/plugin'; | ||||
import { useBrowser, useClientSide, useFallbackId } from '@modal-sh/react-utils'; | import { useBrowser, useClientSide, useFallbackId } from '@modal-sh/react-utils'; | ||||
/** | |||||
* Derived HTML element of the {@link NumberSpinner} component. | |||||
*/ | |||||
export type NumberSpinnerDerivedElement = HTMLInputElement; | export type NumberSpinnerDerivedElement = HTMLInputElement; | ||||
/** | |||||
* Props of the {@link NumberSpinner} component. | |||||
*/ | |||||
export interface NumberSpinnerProps extends Omit<React.HTMLProps<NumberSpinnerDerivedElement>, 'size' | 'type' | 'label'> { | export interface NumberSpinnerProps extends Omit<React.HTMLProps<NumberSpinnerDerivedElement>, 'size' | 'type' | 'label'> { | ||||
/** | /** | ||||
* Short textual description indicating the nature of the component's value. | * 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. | * Visual length of the input. | ||||
*/ | */ | ||||
length?: number, | length?: number, | ||||
/** | |||||
* Should the component be enhanced? | |||||
*/ | |||||
enhanced?: boolean, | enhanced?: boolean, | ||||
/** | |||||
* Interval between steps in milliseconds. | |||||
*/ | |||||
stepInterval?: number, | stepInterval?: number, | ||||
/** | |||||
* Delay before the first step in milliseconds. | |||||
*/ | |||||
initialStepDelay?: number, | 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>(( | export const NumberSpinner = React.forwardRef<NumberSpinnerDerivedElement, NumberSpinnerProps>(( | ||||
{ | { | ||||
@@ -96,63 +111,131 @@ export const NumberSpinner = React.forwardRef<NumberSpinnerDerivedElement, Numbe | |||||
const browser = useBrowser(); | const browser = useBrowser(); | ||||
const defaultRef = React.useRef<NumberSpinnerDerivedElement>(null); | const defaultRef = React.useRef<NumberSpinnerDerivedElement>(null); | ||||
const ref = forwardedRef ?? defaultRef; | 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); | clearInterval(intervalRef.current); | ||||
doStepUp(); | |||||
const stepUpMode = checkMouseStepUpMode(); | |||||
setDisplayStep(stepUpMode); | |||||
performStepMouse(current, stepUpMode); | |||||
intervalRef.current = window.setTimeout(() => { | intervalRef.current = window.setTimeout(() => { | ||||
doStepUp(); | |||||
const stepUpMode = checkMouseStepUpMode(); | |||||
setDisplayStep(stepUpMode); | |||||
performStepMouse(current, stepUpMode); | |||||
intervalRef.current = window.setInterval(() => { | intervalRef.current = window.setInterval(() => { | ||||
doStepUp(); | |||||
const stepUpMode = checkMouseStepUpMode(); | |||||
setDisplayStep(stepUpMode); | |||||
performStepMouse(current, stepUpMode); | |||||
}, stepInterval); | }, stepInterval); | ||||
}, initialStepDelay); | }, 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); | clearInterval(intervalRef.current); | ||||
doStepDown(); | |||||
performStepMouse(current, theStepUpMode); | |||||
intervalRef.current = window.setTimeout(() => { | intervalRef.current = window.setTimeout(() => { | ||||
doStepDown(); | |||||
performStepMouse(current, theStepUpMode); | |||||
intervalRef.current = window.setInterval(() => { | intervalRef.current = window.setInterval(() => { | ||||
doStepDown(); | |||||
performStepMouse(current, theStepUpMode); | |||||
}, stepInterval); | }, stepInterval); | ||||
}, initialStepDelay); | }, initialStepDelay); | ||||
} | |||||
}, [ref, defaultRef, intervalRef, stepInterval, initialStepDelay]); | |||||
}); | |||||
}; | |||||
React.useEffect(() => { | React.useEffect(() => { | ||||
const stopStep = () => { | |||||
const stopStepMouse = () => { | |||||
if (spinEventSource.current === 'keyboard') { | |||||
return; | |||||
} | |||||
clearInterval(intervalRef.current); | 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 () => { | 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} | aria-labelledby={labelId} | ||||
type="number" | type="number" | ||||
data-testid="input" | data-testid="input" | ||||
onKeyDown={doStepKeyboard} | |||||
className={clsx( | className={clsx( | ||||
'bg-negative rounded-inherit w-full peer block tabular-nums font-inherit relative', | 'bg-negative rounded-inherit w-full peer block tabular-nums font-inherit relative', | ||||
'focus:outline-0', | 'focus:outline-0', | ||||
@@ -272,8 +356,9 @@ export const NumberSpinner = React.forwardRef<NumberSpinnerDerivedElement, Numbe | |||||
</div> | </div> | ||||
)} | )} | ||||
{indicator && ( | {indicator && ( | ||||
<div | |||||
<button | |||||
data-testid="indicator" | data-testid="indicator" | ||||
type="button" | |||||
className={clsx( | 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', | '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', | '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 | <svg | ||||
className="w-6 h-6 fill-none stroke-current stroke-2 linejoin-round linecap-round" | 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" /> | <polyline points="18 15 12 9 6 15" /> | ||||
</svg> | </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 | <svg | ||||
className="w-6 h-6 fill-none stroke-current stroke-2 linejoin-round linecap-round" | 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" /> | <polyline points="6 9 12 15 18 9" /> | ||||
</svg> | </svg> | ||||
</button> | |||||
</div> | |||||
<span className="sr-only"> | |||||
Step Down | |||||
</span> | |||||
</span> | |||||
</button> | |||||
)} | )} | ||||
{border && ( | {border && ( | ||||
<span | <span | ||||
@@ -36,13 +36,31 @@ const filterOptions = (children: React.ReactNode): React.ReactNode => { | |||||
* sliders and vv. | * sliders and vv. | ||||
*/ | */ | ||||
/** | |||||
* Orientation of the {@link Slider} component. | |||||
*/ | |||||
export type SliderOrientation = 'horizontal' | 'vertical'; | export type SliderOrientation = 'horizontal' | 'vertical'; | ||||
/** | |||||
* Derived HTML element of the {@link Slider} component. | |||||
*/ | |||||
export type SliderDerivedElement = HTMLInputElement; | export type SliderDerivedElement = HTMLInputElement; | ||||
/** | |||||
* Props of the {@link Slider} component. | |||||
*/ | |||||
export interface SliderProps extends Omit<React.HTMLProps<HTMLInputElement>, 'type'> { | export interface SliderProps extends Omit<React.HTMLProps<HTMLInputElement>, 'type'> { | ||||
/** | |||||
* Orientation of the component. | |||||
*/ | |||||
orient?: SliderOrientation; | orient?: SliderOrientation; | ||||
/** | |||||
* Options of the component. | |||||
*/ | |||||
children?: React.ReactNode; | children?: React.ReactNode; | ||||
/** | |||||
* Length of the component. | |||||
*/ | |||||
length?: React.CSSProperties['width']; | length?: React.CSSProperties['width']; | ||||
} | } | ||||
@@ -93,7 +111,7 @@ export const sliderPlugin = plugin(({ addComponents }) => { | |||||
'aspect-ratio': '1 / 1', | 'aspect-ratio': '1 / 1', | ||||
'z-index': '1', | 'z-index': '1', | ||||
position: 'relative', | 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': { | '& > input:focus::-webkit-slider-container': { | ||||
@@ -105,11 +123,11 @@ export const sliderPlugin = plugin(({ addComponents }) => { | |||||
}, | }, | ||||
'& > input:focus::-webkit-slider-thumb': { | '& > 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': { | '& > 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"]': { | '&[data-orient="horizontal"]': { | ||||
@@ -155,15 +173,15 @@ export const sliderPlugin = plugin(({ addComponents }) => { | |||||
'aspect-ratio': '1 / 1', | 'aspect-ratio': '1 / 1', | ||||
'z-index': '1', | 'z-index': '1', | ||||
position: 'relative', | 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': { | '& > 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': { | '& > 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"]': { | '& > input[orient="vertical"]': { | ||||
@@ -179,15 +197,15 @@ export const sliderPlugin = plugin(({ addComponents }) => { | |||||
'& > input[orient="vertical"]::-moz-range-thumb': { | '& > input[orient="vertical"]::-moz-range-thumb': { | ||||
width: '100%', | width: '100%', | ||||
height: '1em', | 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': { | '& > 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': { | '& > 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 + *': { | '&[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>(( | export const Slider = React.forwardRef<SliderDerivedElement, SliderProps>(( | ||||
{ | { | ||||
className, | className, | ||||
@@ -4,8 +4,14 @@ import clsx from 'clsx'; | |||||
import plugin from 'tailwindcss/plugin'; | import plugin from 'tailwindcss/plugin'; | ||||
import { useFallbackId } from '@modal-sh/react-utils'; | import { useFallbackId } from '@modal-sh/react-utils'; | ||||
/** | |||||
* Derived HTML element of the {@link DateDropdown} component. | |||||
*/ | |||||
export type DateDropdownDerivedElement = HTMLInputElement; | export type DateDropdownDerivedElement = HTMLInputElement; | ||||
/** | |||||
* Props of the {@link DateDropdown} component. | |||||
*/ | |||||
export interface DateDropdownProps extends Omit<React.HTMLProps<DateDropdownDerivedElement>, 'size' | 'type' | 'label' | 'pattern'> { | export interface DateDropdownProps extends Omit<React.HTMLProps<DateDropdownDerivedElement>, 'size' | 'type' | 'label' | 'pattern'> { | ||||
/** | /** | ||||
* Short textual description indicating the nature of the component's value. | * 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< | export const DateDropdown = React.forwardRef< | ||||
DateDropdownDerivedElement, | DateDropdownDerivedElement, | ||||
@@ -4,6 +4,9 @@ import clsx from 'clsx'; | |||||
import plugin from 'tailwindcss/plugin'; | import plugin from 'tailwindcss/plugin'; | ||||
import { useFallbackId } from '@modal-sh/react-utils'; | import { useFallbackId } from '@modal-sh/react-utils'; | ||||
/** | |||||
* Derived HTML element of the {@link TimeSpinner} component. | |||||
*/ | |||||
export type TimeSpinnerDerivedElement = HTMLInputElement; | export type TimeSpinnerDerivedElement = HTMLInputElement; | ||||
type Digit = (0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9); | type Digit = (0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9); | ||||
@@ -16,6 +19,9 @@ type StepHhMmSs = `${StepHhMm}:${Segment}`; | |||||
type Step = StepHhMm | StepHhMmSs; | type Step = StepHhMm | StepHhMmSs; | ||||
/** | |||||
* Props of the {@link TimeSpinner} component. | |||||
*/ | |||||
export interface TimeSpinnerProps extends Omit<React.HTMLProps<TimeSpinnerDerivedElement>, 'size' | 'type' | 'label' | 'step' | 'pattern'> { | export interface TimeSpinnerProps extends Omit<React.HTMLProps<TimeSpinnerDerivedElement>, 'size' | 'type' | 'label' | 'step' | 'pattern'> { | ||||
/** | /** | ||||
* Short textual description indicating the nature of the component's value. | * 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< | export const TimeSpinner = React.forwardRef< | ||||
TimeSpinnerDerivedElement, | TimeSpinnerDerivedElement, | ||||
@@ -102,6 +102,7 @@ const RegistrationFormPage: NextPage = () => { | |||||
label="Password" | label="Password" | ||||
name="password" | name="password" | ||||
onChange={handleChange} | onChange={handleChange} | ||||
enhanced | |||||
/> | /> | ||||
</div> | </div> | ||||
<div className="sm:col-span-2"> | <div className="sm:col-span-2"> | ||||
@@ -111,6 +112,7 @@ const RegistrationFormPage: NextPage = () => { | |||||
label="Confirm Password" | label="Confirm Password" | ||||
name="confirmPassword" | name="confirmPassword" | ||||
onChange={handleChange} | onChange={handleChange} | ||||
enhanced | |||||
/> | /> | ||||
</div> | </div> | ||||
<div className="sm:col-span-6 text-center"> | <div className="sm:col-span-6 text-center"> | ||||