From 09cfe2301c5bbc6450f36441796db9dc82337a85 Mon Sep 17 00:00:00 2001 From: TheoryOfNekomata Date: Sat, 22 Jul 2023 21:44:44 +0800 Subject: [PATCH] Improve numberspinner UI Add consistent behavior for numberspinner. --- .../react/src/components/EmailInput/index.tsx | 2 +- .../src/components/PatternTextInput/index.tsx | 2 +- .../src/components/PhoneNumberInput/index.tsx | 10 +- .../react/src/components/UrlInput/index.tsx | 2 +- .../components/MultilineTextInput/index.tsx | 2 +- .../react/src/components/TextInput/index.tsx | 2 +- .../src/components/MenuMultiSelect/index.tsx | 2 +- .../react/src/components/TagInput/index.tsx | 2 +- .../src/components/ToggleButton/index.tsx | 2 +- .../src/components/ToggleSwitch/index.tsx | 2 +- .../src/components/ToggleTickBox/index.tsx | 2 +- .../src/components/NumberSpinner/index.tsx | 145 +++++++++++++++--- .../react/src/components/Slider/index.tsx | 13 +- .../src/components/DateDropdown/index.tsx | 2 +- .../src/components/TimeSpinner/index.tsx | 2 +- packages/react-utils/src/hooks/browser.ts | 16 ++ packages/react-utils/src/index.ts | 1 + .../src/pages/categories/number/index.tsx | 19 +++ 18 files changed, 179 insertions(+), 49 deletions(-) create mode 100644 packages/react-utils/src/hooks/browser.ts diff --git a/categories/formatted/react/src/components/EmailInput/index.tsx b/categories/formatted/react/src/components/EmailInput/index.tsx index f399464..21ad7f8 100644 --- a/categories/formatted/react/src/components/EmailInput/index.tsx +++ b/categories/formatted/react/src/components/EmailInput/index.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { TextControl } from '@tesseract-design/web-base'; import clsx from 'clsx'; -import {useFallbackId} from '@modal-sh/react-utils'; +import { useFallbackId } from '@modal-sh/react-utils'; export type EmailInputDerivedElement = HTMLInputElement; diff --git a/categories/formatted/react/src/components/PatternTextInput/index.tsx b/categories/formatted/react/src/components/PatternTextInput/index.tsx index 43e0702..91c009c 100644 --- a/categories/formatted/react/src/components/PatternTextInput/index.tsx +++ b/categories/formatted/react/src/components/PatternTextInput/index.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { TextControl } from '@tesseract-design/web-base'; import clsx from 'clsx'; -import {useFallbackId} from '@modal-sh/react-utils'; +import { useFallbackId } from '@modal-sh/react-utils'; export type PatternTextInputDerivedElement = HTMLInputElement; diff --git a/categories/formatted/react/src/components/PhoneNumberInput/index.tsx b/categories/formatted/react/src/components/PhoneNumberInput/index.tsx index 4d5fe5a..ac53c86 100644 --- a/categories/formatted/react/src/components/PhoneNumberInput/index.tsx +++ b/categories/formatted/react/src/components/PhoneNumberInput/index.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; 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 clsx from 'clsx'; @@ -60,14 +60,14 @@ export const PhoneNumberInput = 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, - enhanced = true, + enhanced = false as const, country = 'PH' as const, value, onChange, diff --git a/categories/formatted/react/src/components/UrlInput/index.tsx b/categories/formatted/react/src/components/UrlInput/index.tsx index 4627e66..1fb913d 100644 --- a/categories/formatted/react/src/components/UrlInput/index.tsx +++ b/categories/formatted/react/src/components/UrlInput/index.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { TextControl } from '@tesseract-design/web-base'; import clsx from 'clsx'; -import {useFallbackId} from '@modal-sh/react-utils'; +import { useFallbackId } from '@modal-sh/react-utils'; export type UrlInputDerivedElement = HTMLInputElement; diff --git a/categories/freeform/react/src/components/MultilineTextInput/index.tsx b/categories/freeform/react/src/components/MultilineTextInput/index.tsx index 5cf724e..f449e38 100644 --- a/categories/freeform/react/src/components/MultilineTextInput/index.tsx +++ b/categories/freeform/react/src/components/MultilineTextInput/index.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { TextControl } from '@tesseract-design/web-base'; import clsx from 'clsx'; -import {useFallbackId} from '@modal-sh/react-utils'; +import { useFallbackId } from '@modal-sh/react-utils'; export type MultilineTextInputDerivedElement = HTMLTextAreaElement; diff --git a/categories/freeform/react/src/components/TextInput/index.tsx b/categories/freeform/react/src/components/TextInput/index.tsx index becf03e..c77c675 100644 --- a/categories/freeform/react/src/components/TextInput/index.tsx +++ b/categories/freeform/react/src/components/TextInput/index.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { TextControl } from '@tesseract-design/web-base'; import clsx from 'clsx'; -import {useFallbackId} from '@modal-sh/react-utils'; +import { useFallbackId } from '@modal-sh/react-utils'; export type TextInputDerivedElement = HTMLInputElement; diff --git a/categories/multichoice/react/src/components/MenuMultiSelect/index.tsx b/categories/multichoice/react/src/components/MenuMultiSelect/index.tsx index 4ce694b..d7f868c 100644 --- a/categories/multichoice/react/src/components/MenuMultiSelect/index.tsx +++ b/categories/multichoice/react/src/components/MenuMultiSelect/index.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import { TextControl } from '@tesseract-design/web-base'; import clsx from 'clsx'; import plugin from 'tailwindcss/plugin'; -import {useFallbackId} from '@modal-sh/react-utils'; +import { useFallbackId } from '@modal-sh/react-utils'; export type MenuMultiSelectDerivedElement = HTMLSelectElement; diff --git a/categories/multichoice/react/src/components/TagInput/index.tsx b/categories/multichoice/react/src/components/TagInput/index.tsx index 5ab670d..748984a 100644 --- a/categories/multichoice/react/src/components/TagInput/index.tsx +++ b/categories/multichoice/react/src/components/TagInput/index.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { TagsInput } from 'react-tag-input-component'; 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 plugin from 'tailwindcss/plugin'; diff --git a/categories/multichoice/react/src/components/ToggleButton/index.tsx b/categories/multichoice/react/src/components/ToggleButton/index.tsx index b96b71a..ed8b42c 100644 --- a/categories/multichoice/react/src/components/ToggleButton/index.tsx +++ b/categories/multichoice/react/src/components/ToggleButton/index.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import clsx from 'clsx'; import { Button } from '@tesseract-design/web-base'; import plugin from 'tailwindcss/plugin'; -import {useFallbackId} from '@modal-sh/react-utils'; +import { useFallbackId } from '@modal-sh/react-utils'; export type ToggleButtonDerivedElement = HTMLInputElement; diff --git a/categories/multichoice/react/src/components/ToggleSwitch/index.tsx b/categories/multichoice/react/src/components/ToggleSwitch/index.tsx index 1396b30..c944cc8 100644 --- a/categories/multichoice/react/src/components/ToggleSwitch/index.tsx +++ b/categories/multichoice/react/src/components/ToggleSwitch/index.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import clsx from 'clsx'; import plugin from 'tailwindcss/plugin'; -import {useFallbackId} from '@modal-sh/react-utils'; +import { useFallbackId } from '@modal-sh/react-utils'; export type ToggleSwitchDerivedElement = HTMLInputElement; diff --git a/categories/multichoice/react/src/components/ToggleTickBox/index.tsx b/categories/multichoice/react/src/components/ToggleTickBox/index.tsx index 6dfacaa..706f544 100644 --- a/categories/multichoice/react/src/components/ToggleTickBox/index.tsx +++ b/categories/multichoice/react/src/components/ToggleTickBox/index.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import clsx from 'clsx'; import plugin from 'tailwindcss/plugin'; -import {useFallbackId} from '@modal-sh/react-utils'; +import { useFallbackId } from '@modal-sh/react-utils'; export type ToggleTickBoxDerivedElement = HTMLInputElement; diff --git a/categories/number/react/src/components/NumberSpinner/index.tsx b/categories/number/react/src/components/NumberSpinner/index.tsx index 6d529d1..52638cc 100644 --- a/categories/number/react/src/components/NumberSpinner/index.tsx +++ b/categories/number/react/src/components/NumberSpinner/index.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import { TextControl } from '@tesseract-design/web-base'; import clsx from 'clsx'; 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; @@ -19,10 +19,6 @@ export interface NumberSpinnerProps extends Omit { addComponents({ '.number-spinner': { - '&::-webkit-inner-spin-button': { + '&[data-browser="chrome"] > input::-webkit-inner-spin-button': { 'position': 'absolute', 'top': '0', 'right': '0', @@ -56,6 +55,14 @@ export const numberSpinnerPlugin = plugin(({ addComponents, }) => { 'width': '1.5rem', '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 { + const { clientSide: indicator } = useClientSide({ clientSide: enhancedProp }); const labelId = React.useId(); const id = useFallbackId(idProp); + const browser = useBrowser(); + const defaultRef = React.useRef(null); + const ref = forwardedRef ?? defaultRef; + const intervalRef = React.useRef< + number | undefined + >() as React.MutableRefObject; + + 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 (
{label && ( <> @@ -129,7 +204,7 @@ export const NumberSpinner = React.forwardRef - {indicator} + +
)} {border && ( )} @@ -227,11 +328,13 @@ NumberSpinner.displayName = 'NumberSpinner'; NumberSpinner.defaultProps = { label: 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, + enhanced: false as const, + stepInterval: 100 as const, + initialStepDelay: 400 as const, }; diff --git a/categories/number/react/src/components/Slider/index.tsx b/categories/number/react/src/components/Slider/index.tsx index 6bfbb4f..58f6031 100644 --- a/categories/number/react/src/components/Slider/index.tsx +++ b/categories/number/react/src/components/Slider/index.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; import clsx from 'clsx'; import plugin from 'tailwindcss/plugin'; +import { useBrowser } from '@modal-sh/react-utils'; const filterOptions = (children: React.ReactNode): React.ReactNode => { const childrenArray = Array.isArray(children) ? children : [children]; @@ -220,17 +221,7 @@ export const Slider = React.forwardRef(( }, forwardedRef, ) => { - const [browser, setBrowser] = React.useState(); - React.useEffect(() => { - const isFirefox = typeof (window as unknown as Record).InstallTrigger !== 'undefined'; - if (isFirefox) { - setBrowser('firefox'); - } else { - // TODO - detect other browsers - setBrowser('chrome'); - } - }, []); - + const browser = useBrowser(); const defaultRef = React.useRef(null); const ref = forwardedRef ?? defaultRef; const tickMarkId = React.useId(); diff --git a/categories/temporal/react/src/components/DateDropdown/index.tsx b/categories/temporal/react/src/components/DateDropdown/index.tsx index 51a4721..06b25a4 100644 --- a/categories/temporal/react/src/components/DateDropdown/index.tsx +++ b/categories/temporal/react/src/components/DateDropdown/index.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import { TextControl } from '@tesseract-design/web-base'; import clsx from 'clsx'; import plugin from 'tailwindcss/plugin'; -import {useFallbackId} from '@modal-sh/react-utils'; +import { useFallbackId } from '@modal-sh/react-utils'; export type DateDropdownDerivedElement = HTMLInputElement; diff --git a/categories/temporal/react/src/components/TimeSpinner/index.tsx b/categories/temporal/react/src/components/TimeSpinner/index.tsx index fc80b5c..bc1bb41 100644 --- a/categories/temporal/react/src/components/TimeSpinner/index.tsx +++ b/categories/temporal/react/src/components/TimeSpinner/index.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import { TextControl } from '@tesseract-design/web-base'; import clsx from 'clsx'; import plugin from 'tailwindcss/plugin'; -import {useFallbackId} from '@modal-sh/react-utils'; +import { useFallbackId } from '@modal-sh/react-utils'; export type TimeSpinnerDerivedElement = HTMLInputElement; diff --git a/packages/react-utils/src/hooks/browser.ts b/packages/react-utils/src/hooks/browser.ts new file mode 100644 index 0000000..b215b4a --- /dev/null +++ b/packages/react-utils/src/hooks/browser.ts @@ -0,0 +1,16 @@ +import * as React from 'react'; + +export const useBrowser = () => { + const [browser, setBrowser] = React.useState(); + React.useEffect(() => { + const isFirefox = typeof (window as unknown as Record).InstallTrigger !== 'undefined'; + if (isFirefox) { + setBrowser('firefox'); + } else { + // TODO - detect other browsers + setBrowser('chrome'); + } + }, []); + + return React.useMemo(() => browser, [browser]); +}; diff --git a/packages/react-utils/src/index.ts b/packages/react-utils/src/index.ts index c9b4fa6..46d8a33 100644 --- a/packages/react-utils/src/index.ts +++ b/packages/react-utils/src/index.ts @@ -1,3 +1,4 @@ +export * from './hooks/browser'; export * from './hooks/client-side'; export * from './hooks/form'; export * from './hooks/id'; diff --git a/showcases/web-kitchensink-reactnext/src/pages/categories/number/index.tsx b/showcases/web-kitchensink-reactnext/src/pages/categories/number/index.tsx index bd719e8..ded534c 100644 --- a/showcases/web-kitchensink-reactnext/src/pages/categories/number/index.tsx +++ b/showcases/web-kitchensink-reactnext/src/pages/categories/number/index.tsx @@ -14,6 +14,25 @@ const NumberPage: NextPage = () => { step="any" label="Step" border + enhanced + size="small" + /> + +