@@ -1,7 +1,7 @@ | |||||
import * as React from 'react'; | import * as React from 'react'; | ||||
import { TextControl } from '@tesseract-design/web-base'; | 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'; | |||||
export type EmailInputDerivedElement = HTMLInputElement; | export type EmailInputDerivedElement = HTMLInputElement; | ||||
@@ -1,7 +1,7 @@ | |||||
import * as React from 'react'; | import * as React from 'react'; | ||||
import { TextControl } from '@tesseract-design/web-base'; | 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'; | |||||
export type PatternTextInputDerivedElement = HTMLInputElement; | export type PatternTextInputDerivedElement = HTMLInputElement; | ||||
@@ -1,6 +1,6 @@ | |||||
import * as React from 'react'; | import * as React from 'react'; | ||||
import { TextControl } from '@tesseract-design/web-base'; | import { TextControl } from '@tesseract-design/web-base'; | ||||
import {useClientSide, useFallbackId, useProxyInput} from '@modal-sh/react-utils'; | |||||
import { useClientSide, useFallbackId, useProxyInput } from '@modal-sh/react-utils'; | |||||
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'; | ||||
@@ -60,14 +60,14 @@ export const PhoneNumberInput = 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, | ||||
enhanced = true, | |||||
enhanced = false as const, | |||||
country = 'PH' as const, | country = 'PH' as const, | ||||
value, | value, | ||||
onChange, | onChange, | ||||
@@ -1,7 +1,7 @@ | |||||
import * as React from 'react'; | import * as React from 'react'; | ||||
import { TextControl } from '@tesseract-design/web-base'; | 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'; | |||||
export type UrlInputDerivedElement = HTMLInputElement; | export type UrlInputDerivedElement = HTMLInputElement; | ||||
@@ -1,7 +1,7 @@ | |||||
import * as React from 'react'; | import * as React from 'react'; | ||||
import { TextControl } from '@tesseract-design/web-base'; | 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'; | |||||
export type MultilineTextInputDerivedElement = HTMLTextAreaElement; | export type MultilineTextInputDerivedElement = HTMLTextAreaElement; | ||||
@@ -1,7 +1,7 @@ | |||||
import * as React from 'react'; | import * as React from 'react'; | ||||
import { TextControl } from '@tesseract-design/web-base'; | 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'; | |||||
export type TextInputDerivedElement = HTMLInputElement; | export type TextInputDerivedElement = HTMLInputElement; | ||||
@@ -2,7 +2,7 @@ import * as React from 'react'; | |||||
import { TextControl } from '@tesseract-design/web-base'; | import { TextControl } from '@tesseract-design/web-base'; | ||||
import clsx from 'clsx'; | 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'; | |||||
export type MenuMultiSelectDerivedElement = HTMLSelectElement; | export type MenuMultiSelectDerivedElement = HTMLSelectElement; | ||||
@@ -1,7 +1,7 @@ | |||||
import * as React from 'react'; | import * as React from 'react'; | ||||
import { TagsInput } from 'react-tag-input-component'; | import { TagsInput } from 'react-tag-input-component'; | ||||
import clsx from 'clsx'; | import clsx from 'clsx'; | ||||
import {useClientSide, useFallbackId, useProxyInput} from '@modal-sh/react-utils'; | |||||
import { useClientSide, useFallbackId, useProxyInput } from '@modal-sh/react-utils'; | |||||
import { TextControl } from '@tesseract-design/web-base'; | import { TextControl } from '@tesseract-design/web-base'; | ||||
import plugin from 'tailwindcss/plugin'; | import plugin from 'tailwindcss/plugin'; | ||||
@@ -2,7 +2,7 @@ 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'; | ||||
import plugin from 'tailwindcss/plugin'; | import plugin from 'tailwindcss/plugin'; | ||||
import {useFallbackId} from '@modal-sh/react-utils'; | |||||
import { useFallbackId } from '@modal-sh/react-utils'; | |||||
export type ToggleButtonDerivedElement = HTMLInputElement; | export type ToggleButtonDerivedElement = HTMLInputElement; | ||||
@@ -1,7 +1,7 @@ | |||||
import * as React from 'react'; | import * as React from 'react'; | ||||
import clsx from 'clsx'; | 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'; | |||||
export type ToggleSwitchDerivedElement = HTMLInputElement; | export type ToggleSwitchDerivedElement = HTMLInputElement; | ||||
@@ -1,7 +1,7 @@ | |||||
import * as React from 'react'; | import * as React from 'react'; | ||||
import clsx from 'clsx'; | 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'; | |||||
export type ToggleTickBoxDerivedElement = HTMLInputElement; | export type ToggleTickBoxDerivedElement = HTMLInputElement; | ||||
@@ -2,7 +2,7 @@ import * as React from 'react'; | |||||
import { TextControl } from '@tesseract-design/web-base'; | import { TextControl } from '@tesseract-design/web-base'; | ||||
import clsx from 'clsx'; | import clsx from 'clsx'; | ||||
import plugin from 'tailwindcss/plugin'; | import plugin from 'tailwindcss/plugin'; | ||||
import {useFallbackId} from '@modal-sh/react-utils'; | |||||
import { useBrowser, useClientSide, useFallbackId } from '@modal-sh/react-utils'; | |||||
export type NumberSpinnerDerivedElement = HTMLInputElement; | export type NumberSpinnerDerivedElement = HTMLInputElement; | ||||
@@ -19,10 +19,6 @@ export interface NumberSpinnerProps extends Omit<React.HTMLProps<NumberSpinnerDe | |||||
* Size of the component. | * Size of the component. | ||||
*/ | */ | ||||
size?: TextControl.Size, | size?: TextControl.Size, | ||||
/** | |||||
* Additional description, usually graphical, indicating the nature of the component's value. | |||||
*/ | |||||
indicator?: React.ReactNode, | |||||
/** | /** | ||||
* Should the component display a border? | * Should the component display a border? | ||||
*/ | */ | ||||
@@ -43,12 +39,15 @@ export interface NumberSpinnerProps extends Omit<React.HTMLProps<NumberSpinnerDe | |||||
* Visual length of the input. | * Visual length of the input. | ||||
*/ | */ | ||||
length?: number, | length?: number, | ||||
enhanced?: boolean, | |||||
stepInterval?: number, | |||||
initialStepDelay?: number, | |||||
} | } | ||||
export const numberSpinnerPlugin = plugin(({ addComponents, }) => { | export const numberSpinnerPlugin = plugin(({ addComponents, }) => { | ||||
addComponents({ | addComponents({ | ||||
'.number-spinner': { | '.number-spinner': { | ||||
'&::-webkit-inner-spin-button': { | |||||
'&[data-browser="chrome"] > input::-webkit-inner-spin-button': { | |||||
'position': 'absolute', | 'position': 'absolute', | ||||
'top': '0', | 'top': '0', | ||||
'right': '0', | 'right': '0', | ||||
@@ -56,6 +55,14 @@ export const numberSpinnerPlugin = plugin(({ addComponents, }) => { | |||||
'width': '1.5rem', | 'width': '1.5rem', | ||||
'z-index': '2', | 'z-index': '2', | ||||
}, | }, | ||||
'&[data-browser="chrome"][data-enhanced] > input::-webkit-inner-spin-button': { | |||||
'display': 'none', | |||||
}, | |||||
'&[data-browser="firefox"][data-enhanced] > input[type="number"]': { | |||||
'appearance': 'textfield', | |||||
}, | |||||
}, | }, | ||||
}); | }); | ||||
}); | }); | ||||
@@ -67,28 +74,94 @@ export const NumberSpinner = React.forwardRef<NumberSpinnerDerivedElement, Numbe | |||||
{ | { | ||||
label, | label, | ||||
hint, | hint, | ||||
indicator, | |||||
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, | ||||
enhanced: enhancedProp = false as const, | |||||
stepInterval = 100 as const, | |||||
initialStepDelay = 400 as const, | |||||
...etcProps | ...etcProps | ||||
}: NumberSpinnerProps, | }: NumberSpinnerProps, | ||||
forwardedRef, | forwardedRef, | ||||
) => { | ) => { | ||||
const { clientSide: indicator } = useClientSide({ clientSide: enhancedProp }); | |||||
const labelId = React.useId(); | const labelId = React.useId(); | ||||
const id = useFallbackId(idProp); | const id = useFallbackId(idProp); | ||||
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 handleStepUp = React.useCallback(() => { | |||||
const { current } = typeof ref === 'object' ? ref : defaultRef; | |||||
if (current) { | |||||
const theStep = current.step ?? 'any'; | |||||
const doStepUp = () => { | |||||
current.step = theStep === 'any' ? '1' : theStep; | |||||
current.stepUp(); | |||||
current.step = theStep; | |||||
current.focus(); | |||||
}; | |||||
clearInterval(intervalRef.current); | |||||
doStepUp(); | |||||
intervalRef.current = window.setTimeout(() => { | |||||
doStepUp(); | |||||
intervalRef.current = window.setInterval(() => { | |||||
doStepUp(); | |||||
}, 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(); | |||||
}; | |||||
clearInterval(intervalRef.current); | |||||
doStepDown(); | |||||
intervalRef.current = window.setTimeout(() => { | |||||
doStepDown(); | |||||
intervalRef.current = window.setInterval(() => { | |||||
doStepDown(); | |||||
}, stepInterval); | |||||
}, initialStepDelay); | |||||
} | |||||
}, [ref, defaultRef, intervalRef, stepInterval, initialStepDelay]); | |||||
React.useEffect(() => { | |||||
const stopStep = () => { | |||||
clearInterval(intervalRef.current); | |||||
}; | |||||
window.addEventListener('mouseup', stopStep, { capture: true }); | |||||
return () => { | |||||
window.removeEventListener('mouseup', stopStep, { capture: true }); | |||||
}; | |||||
}, []); | |||||
return ( | return ( | ||||
<div | <div | ||||
className={clsx( | className={clsx( | ||||
'relative rounded ring-secondary/50 group has-[:disabled]:opacity-50', | 'relative rounded ring-secondary/50 group has-[:disabled]:opacity-50', | ||||
'focus-within:ring-4', | 'focus-within:ring-4', | ||||
'number-spinner', | |||||
{ | { | ||||
'block': block, | 'block': block, | ||||
'inline-block align-middle': !block, | 'inline-block align-middle': !block, | ||||
@@ -97,6 +170,8 @@ export const NumberSpinner = React.forwardRef<NumberSpinnerDerivedElement, Numbe | |||||
)} | )} | ||||
style={style} | style={style} | ||||
data-testid="base" | data-testid="base" | ||||
data-browser={browser} | |||||
data-enhanced={indicator} | |||||
> | > | ||||
{label && ( | {label && ( | ||||
<> | <> | ||||
@@ -129,7 +204,7 @@ export const NumberSpinner = React.forwardRef<NumberSpinnerDerivedElement, Numbe | |||||
<input | <input | ||||
{...etcProps} | {...etcProps} | ||||
size={length} | size={length} | ||||
ref={forwardedRef} | |||||
ref={typeof ref === 'function' ? defaultRef : ref} | |||||
id={id} | id={id} | ||||
aria-labelledby={labelId} | aria-labelledby={labelId} | ||||
type="number" | type="number" | ||||
@@ -138,7 +213,6 @@ export const NumberSpinner = React.forwardRef<NumberSpinnerDerivedElement, Numbe | |||||
'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', | ||||
'disabled:opacity-50 disabled:cursor-not-allowed', | 'disabled:opacity-50 disabled:cursor-not-allowed', | ||||
'number-spinner', | |||||
{ | { | ||||
'text-xxs': size === 'small', | 'text-xxs': size === 'small', | ||||
'text-xs': size === 'medium', | 'text-xs': size === 'medium', | ||||
@@ -201,7 +275,7 @@ export const NumberSpinner = React.forwardRef<NumberSpinnerDerivedElement, Numbe | |||||
<div | <div | ||||
data-testid="indicator" | data-testid="indicator" | ||||
className={clsx( | className={clsx( | ||||
'text-center flex items-center justify-center aspect-square absolute bottom-0 right-0 pointer-events-none select-none', | |||||
'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', | |||||
{ | { | ||||
'w-10': size === 'small', | 'w-10': size === 'small', | ||||
'w-12': size === 'medium', | 'w-12': size === 'medium', | ||||
@@ -209,13 +283,40 @@ export const NumberSpinner = React.forwardRef<NumberSpinnerDerivedElement, Numbe | |||||
}, | }, | ||||
)} | )} | ||||
> | > | ||||
{indicator} | |||||
<button | |||||
aria-label="Step Up" | |||||
type="button" | |||||
className="h-0 flex-auto flex justify-center items-start text-inherit overflow-hidden" | |||||
onMouseDown={handleStepUp} | |||||
> | |||||
<svg | |||||
className="w-6 h-6 fill-none stroke-current stroke-2 linejoin-round linecap-round" | |||||
viewBox="0 0 24 24" | |||||
role="presentation" | |||||
> | |||||
<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} | |||||
> | |||||
<svg | |||||
className="w-6 h-6 fill-none stroke-current stroke-2 linejoin-round linecap-round" | |||||
viewBox="0 0 24 24" | |||||
role="presentation" | |||||
> | |||||
<polyline points="6 9 12 15 18 9" /> | |||||
</svg> | |||||
</button> | |||||
</div> | </div> | ||||
)} | )} | ||||
{border && ( | {border && ( | ||||
<span | <span | ||||
data-testid="border" | data-testid="border" | ||||
className="absolute z-[1] inset-0 rounded-inherit border-2 border-primary pointer-events-none peer-focus:border-secondary" | |||||
className="absolute z-[1] inset-0 rounded-inherit border-2 border-primary pointer-events-none group-focus-within:border-secondary" | |||||
/> | /> | ||||
)} | )} | ||||
</div> | </div> | ||||
@@ -227,11 +328,13 @@ NumberSpinner.displayName = 'NumberSpinner'; | |||||
NumberSpinner.defaultProps = { | NumberSpinner.defaultProps = { | ||||
label: undefined, | label: undefined, | ||||
hint: undefined, | hint: undefined, | ||||
indicator: undefined, | |||||
border: false, | |||||
block: false, | |||||
hiddenLabel: false, | |||||
size: 'medium', | |||||
variant: 'default', | |||||
border: false as const, | |||||
block: false as const, | |||||
hiddenLabel: false as const, | |||||
size: 'medium' as const, | |||||
variant: 'default' as const, | |||||
length: undefined, | length: undefined, | ||||
enhanced: false as const, | |||||
stepInterval: 100 as const, | |||||
initialStepDelay: 400 as const, | |||||
}; | }; |
@@ -1,6 +1,7 @@ | |||||
import * as React from 'react'; | import * as React from 'react'; | ||||
import clsx from 'clsx'; | import clsx from 'clsx'; | ||||
import plugin from 'tailwindcss/plugin'; | import plugin from 'tailwindcss/plugin'; | ||||
import { useBrowser } from '@modal-sh/react-utils'; | |||||
const filterOptions = (children: React.ReactNode): React.ReactNode => { | const filterOptions = (children: React.ReactNode): React.ReactNode => { | ||||
const childrenArray = Array.isArray(children) ? children : [children]; | const childrenArray = Array.isArray(children) ? children : [children]; | ||||
@@ -220,17 +221,7 @@ export const Slider = React.forwardRef<SliderDerivedElement, SliderProps>(( | |||||
}, | }, | ||||
forwardedRef, | forwardedRef, | ||||
) => { | ) => { | ||||
const [browser, setBrowser] = React.useState<string>(); | |||||
React.useEffect(() => { | |||||
const isFirefox = typeof (window as unknown as Record<string, unknown>).InstallTrigger !== 'undefined'; | |||||
if (isFirefox) { | |||||
setBrowser('firefox'); | |||||
} else { | |||||
// TODO - detect other browsers | |||||
setBrowser('chrome'); | |||||
} | |||||
}, []); | |||||
const browser = useBrowser(); | |||||
const defaultRef = React.useRef<HTMLInputElement>(null); | const defaultRef = React.useRef<HTMLInputElement>(null); | ||||
const ref = forwardedRef ?? defaultRef; | const ref = forwardedRef ?? defaultRef; | ||||
const tickMarkId = React.useId(); | const tickMarkId = React.useId(); | ||||
@@ -2,7 +2,7 @@ import * as React from 'react'; | |||||
import { TextControl } from '@tesseract-design/web-base'; | import { TextControl } from '@tesseract-design/web-base'; | ||||
import clsx from 'clsx'; | 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'; | |||||
export type DateDropdownDerivedElement = HTMLInputElement; | export type DateDropdownDerivedElement = HTMLInputElement; | ||||
@@ -2,7 +2,7 @@ import * as React from 'react'; | |||||
import { TextControl } from '@tesseract-design/web-base'; | import { TextControl } from '@tesseract-design/web-base'; | ||||
import clsx from 'clsx'; | 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'; | |||||
export type TimeSpinnerDerivedElement = HTMLInputElement; | export type TimeSpinnerDerivedElement = HTMLInputElement; | ||||
@@ -0,0 +1,16 @@ | |||||
import * as React from 'react'; | |||||
export const useBrowser = () => { | |||||
const [browser, setBrowser] = React.useState<string>(); | |||||
React.useEffect(() => { | |||||
const isFirefox = typeof (window as unknown as Record<string, unknown>).InstallTrigger !== 'undefined'; | |||||
if (isFirefox) { | |||||
setBrowser('firefox'); | |||||
} else { | |||||
// TODO - detect other browsers | |||||
setBrowser('chrome'); | |||||
} | |||||
}, []); | |||||
return React.useMemo(() => browser, [browser]); | |||||
}; |
@@ -1,3 +1,4 @@ | |||||
export * from './hooks/browser'; | |||||
export * from './hooks/client-side'; | export * from './hooks/client-side'; | ||||
export * from './hooks/form'; | export * from './hooks/form'; | ||||
export * from './hooks/id'; | export * from './hooks/id'; |
@@ -14,6 +14,25 @@ const NumberPage: NextPage = () => { | |||||
step="any" | step="any" | ||||
label="Step" | label="Step" | ||||
border | border | ||||
enhanced | |||||
size="small" | |||||
/> | |||||
<TesseractNumber.NumberSpinner | |||||
min={-100} | |||||
max={100} | |||||
step="any" | |||||
label="Step" | |||||
border | |||||
enhanced | |||||
/> | |||||
<TesseractNumber.NumberSpinner | |||||
min={-100} | |||||
max={100} | |||||
step="any" | |||||
label="Step" | |||||
border | |||||
enhanced | |||||
size="large" | |||||
/> | /> | ||||
</Subsection> | </Subsection> | ||||
</Section> | </Section> | ||||