Browse Source

Improve numberspinner UI

Add consistent behavior for numberspinner.
master
TheoryOfNekomata 11 months ago
parent
commit
09cfe2301c
18 changed files with 179 additions and 49 deletions
  1. +1
    -1
      categories/formatted/react/src/components/EmailInput/index.tsx
  2. +1
    -1
      categories/formatted/react/src/components/PatternTextInput/index.tsx
  3. +5
    -5
      categories/formatted/react/src/components/PhoneNumberInput/index.tsx
  4. +1
    -1
      categories/formatted/react/src/components/UrlInput/index.tsx
  5. +1
    -1
      categories/freeform/react/src/components/MultilineTextInput/index.tsx
  6. +1
    -1
      categories/freeform/react/src/components/TextInput/index.tsx
  7. +1
    -1
      categories/multichoice/react/src/components/MenuMultiSelect/index.tsx
  8. +1
    -1
      categories/multichoice/react/src/components/TagInput/index.tsx
  9. +1
    -1
      categories/multichoice/react/src/components/ToggleButton/index.tsx
  10. +1
    -1
      categories/multichoice/react/src/components/ToggleSwitch/index.tsx
  11. +1
    -1
      categories/multichoice/react/src/components/ToggleTickBox/index.tsx
  12. +124
    -21
      categories/number/react/src/components/NumberSpinner/index.tsx
  13. +2
    -11
      categories/number/react/src/components/Slider/index.tsx
  14. +1
    -1
      categories/temporal/react/src/components/DateDropdown/index.tsx
  15. +1
    -1
      categories/temporal/react/src/components/TimeSpinner/index.tsx
  16. +16
    -0
      packages/react-utils/src/hooks/browser.ts
  17. +1
    -0
      packages/react-utils/src/index.ts
  18. +19
    -0
      showcases/web-kitchensink-reactnext/src/pages/categories/number/index.tsx

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

@@ -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;



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

@@ -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;



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

@@ -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,


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

@@ -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;



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

@@ -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;



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

@@ -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;



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

@@ -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;



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

@@ -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';



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

@@ -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;



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

@@ -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;



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

@@ -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;



+ 124
- 21
categories/number/react/src/components/NumberSpinner/index.tsx View File

@@ -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<React.HTMLProps<NumberSpinnerDe
* Size of the component.
*/
size?: TextControl.Size,
/**
* Additional description, usually graphical, indicating the nature of the component's value.
*/
indicator?: React.ReactNode,
/**
* Should the component display a border?
*/
@@ -43,12 +39,15 @@ export interface NumberSpinnerProps extends Omit<React.HTMLProps<NumberSpinnerDe
* Visual length of the input.
*/
length?: number,
enhanced?: boolean,
stepInterval?: number,
initialStepDelay?: number,
}

export const numberSpinnerPlugin = plugin(({ addComponents, }) => {
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<NumberSpinnerDerivedElement, Numbe
{
label,
hint,
indicator,
size = 'medium' as const,
border = false,
block = false,
border = false as const,
block = false as const,
variant = 'default' as const,
hiddenLabel = false,
hiddenLabel = false as const,
className,
id: idProp,
style,
length,
enhanced: enhancedProp = false as const,
stepInterval = 100 as const,
initialStepDelay = 400 as const,
...etcProps
}: NumberSpinnerProps,
forwardedRef,
) => {
const { clientSide: indicator } = useClientSide({ clientSide: enhancedProp });
const labelId = React.useId();
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 (
<div
className={clsx(
'relative rounded ring-secondary/50 group has-[:disabled]:opacity-50',
'focus-within:ring-4',
'number-spinner',
{
'block': block,
'inline-block align-middle': !block,
@@ -97,6 +170,8 @@ export const NumberSpinner = React.forwardRef<NumberSpinnerDerivedElement, Numbe
)}
style={style}
data-testid="base"
data-browser={browser}
data-enhanced={indicator}
>
{label && (
<>
@@ -129,7 +204,7 @@ export const NumberSpinner = React.forwardRef<NumberSpinnerDerivedElement, Numbe
<input
{...etcProps}
size={length}
ref={forwardedRef}
ref={typeof ref === 'function' ? defaultRef : ref}
id={id}
aria-labelledby={labelId}
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',
'focus:outline-0',
'disabled:opacity-50 disabled:cursor-not-allowed',
'number-spinner',
{
'text-xxs': size === 'small',
'text-xs': size === 'medium',
@@ -201,7 +275,7 @@ export const NumberSpinner = React.forwardRef<NumberSpinnerDerivedElement, Numbe
<div
data-testid="indicator"
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-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>
)}
{border && (
<span
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>
@@ -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,
};

+ 2
- 11
categories/number/react/src/components/Slider/index.tsx View File

@@ -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<SliderDerivedElement, SliderProps>((
},
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 ref = forwardedRef ?? defaultRef;
const tickMarkId = React.useId();


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

@@ -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;



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

@@ -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;



+ 16
- 0
packages/react-utils/src/hooks/browser.ts View File

@@ -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
- 0
packages/react-utils/src/index.ts View File

@@ -1,3 +1,4 @@
export * from './hooks/browser';
export * from './hooks/client-side';
export * from './hooks/form';
export * from './hooks/id';

+ 19
- 0
showcases/web-kitchensink-reactnext/src/pages/categories/number/index.tsx View File

@@ -14,6 +14,25 @@ const NumberPage: NextPage = () => {
step="any"
label="Step"
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>
</Section>


Loading…
Cancel
Save