Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions src/components/ui/NumberField/NumberField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import NumberFieldRoot from './fragments/NumberFieldRoot';
import NumberFieldInput from './fragments/NumberFieldInput';
import NumberFieldIncrement from './fragments/NumberFieldIncrement';
import NumberFieldDecrement from './fragments/NumberFieldDecrement';

const NumberField = () => {
console.warn('Direct usage of NumberField is not supported. Please use NumberField.Root, NumberField.Item instead.');
return null;
};
Comment on lines +6 to +9
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix incorrect component name in warning message.

The warning message mentions "NumberField.Item" but should reference "NumberField.Input" based on the actual subcomponents exposed.

-    console.warn('Direct usage of NumberField is not supported. Please use NumberField.Root, NumberField.Item instead.');
+    console.warn('Direct usage of NumberField is not supported. Please use NumberField.Root, NumberField.Input, NumberField.Increment, NumberField.Decrement instead.');
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const NumberField = () => {
console.warn('Direct usage of NumberField is not supported. Please use NumberField.Root, NumberField.Item instead.');
return null;
};
const NumberField = () => {
console.warn(
'Direct usage of NumberField is not supported. Please use NumberField.Root, NumberField.Input, NumberField.Increment, NumberField.Decrement instead.'
);
return null;
};
🤖 Prompt for AI Agents
In src/components/ui/NumberField/NumberField.tsx between lines 6 and 9, the
warning message incorrectly references "NumberField.Item" instead of
"NumberField.Input". Update the warning string to mention "NumberField.Input" to
accurately reflect the actual subcomponents users should use.


NumberField.Root = NumberFieldRoot;
NumberField.Input = NumberFieldInput;
NumberField.Increment = NumberFieldIncrement;
NumberField.Decrement = NumberFieldDecrement;

export default NumberField;
17 changes: 17 additions & 0 deletions src/components/ui/NumberField/contexts/NumberFieldContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import React from 'react';

export type NumberFieldContextType = {
inputValue: number|'';
handleOnChange: (input: number|'') => void;
handleStep: (opts: { direction: 'increment' | 'decrement'; type: 'small' | 'large' }) => void;
id?: string;
name?: string;
disabled?: boolean;
readOnly?: boolean;
required?: boolean;
rootClass?: string;
};

const NumberFieldContext = React.createContext<NumberFieldContextType | null>(null);

export default NumberFieldContext;
29 changes: 29 additions & 0 deletions src/components/ui/NumberField/fragments/NumberFieldDecrement.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import React, { useContext } from 'react';
import NumberFieldContext from '../contexts/NumberFieldContext';
import clsx from 'clsx';

export type NumberFieldDecrementProps = {
className?: string;
children?: React.ReactNode
}

const NumberFieldDecrement = ({ children, className, ...props }: NumberFieldDecrementProps) => {
const context = useContext(NumberFieldContext);
if (!context) {
console.error('NumberFieldDecrement must be used within a NumberField');
return null;
}
const { handleStep, rootClass, disabled, readOnly } = context;
return (
<button
onClick={() => handleStep({ direction: 'decrement', type: 'small' })}
className={clsx(`${rootClass}-decrement`, className)}
disabled={disabled || readOnly}
type="button"
{...props}>
{children}
</button>
);
};

export default NumberFieldDecrement;
30 changes: 30 additions & 0 deletions src/components/ui/NumberField/fragments/NumberFieldIncrement.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import React, { useContext } from 'react';
import NumberFieldContext from '../contexts/NumberFieldContext';
import clsx from 'clsx';

export type NumberFieldIncrementProps = {
className?: string
children?: React.ReactNode
}

const NumberFieldIncrement = ({ children, className, ...props }: NumberFieldIncrementProps) => {
const context = useContext(NumberFieldContext);
if (!context) {
console.error('NumberFieldIncrement must be used within a NumberField');
return null;
}
const { handleStep, rootClass, disabled, readOnly } = context;

return (
<button
onClick={() => handleStep({ direction: 'increment', type: 'small' })}
className={clsx(`${rootClass}-increment`, className)}
disabled={disabled || readOnly}
type="button"
{...props}>
{children}
</button>
);
};

export default NumberFieldIncrement;
60 changes: 60 additions & 0 deletions src/components/ui/NumberField/fragments/NumberFieldInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import React, { useContext } from 'react';
import NumberFieldContext from '../contexts/NumberFieldContext';
import clsx from 'clsx';

export type NumberFieldInputProps = {
className?: string
}
const NumberFieldInput = ({ className, ...props }: NumberFieldInputProps) => {
const context = useContext(NumberFieldContext);
if (!context) {
console.error('NumberFieldInput must be used within a NumberField');
return null;
}
const {
inputValue,
handleOnChange,
handleStep,
id,
name,
disabled,
readOnly,
required,
rootClass
} = context;

const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === 'ArrowUp' && !event.shiftKey) {
event.preventDefault();
handleStep({ direction: 'increment', type: 'small' });
}
if (event.key === 'ArrowDown' && !event.shiftKey) {
event.preventDefault();
handleStep({ direction: 'decrement', type: 'small' });
}
if (event.key === 'ArrowUp' && event.shiftKey) {
event.preventDefault();
handleStep({ direction: 'increment', type: 'large' });
}
if (event.key === 'ArrowDown' && event.shiftKey) {
event.preventDefault();
handleStep({ direction: 'decrement', type: 'large' });
}
};
return (
<input
type="number"
onKeyDown={handleKeyDown}
value={inputValue === '' ? '' : inputValue}
onChange={(e) => { const val = e.target.value; handleOnChange(val === '' ? '' : Number(val)); }}
id={id}
name={name}
disabled={disabled}
readOnly={readOnly}
required={required}
className={clsx(`${rootClass}-input`, className)}
{...props}/>
);
};

export default NumberFieldInput;
116 changes: 116 additions & 0 deletions src/components/ui/NumberField/fragments/NumberFieldRoot.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import React from 'react';
import { useControllableState } from '~/core/hooks/useControllableState';
import NumberFieldContext from '../contexts/NumberFieldContext';
import { customClassSwitcher } from '~/core';
import clsx from 'clsx';

const COMPONENT_NAME = 'NumberField';

export type NumberFieldRootProps = {
name?: string
defaultValue?: number | ''
value?: number | ''
onValueChange?: (value: number | '') => void
step?: number
largeStep?: number
min?: number
max?: number
disabled?: boolean
readOnly?: boolean
required?: boolean
id?: string
className?: string
children?: React.ReactNode
};

const NumberFieldRoot = ({ children, name, defaultValue = '', value, onValueChange, largeStep, step, min, max, disabled, readOnly, required, id, className, ...props }: NumberFieldRootProps) => {
const rootClass = customClassSwitcher(className, COMPONENT_NAME);
const [inputValue, setInputValue] = useControllableState<number | ''>(
value,
defaultValue,
onValueChange);

const handleOnChange = (input: number| '') => {
if (input === '') {
setInputValue('');
return;
}
if (max !== undefined && input > max) {
setInputValue(max);
return;
}

if (min !== undefined && input < min) {
setInputValue(min);
return;
}

setInputValue(input);
};
const applyStep = (amount: number) => {
setInputValue((prev) => {
let temp = prev;
if (temp === '') {
if (min !== undefined) {
temp = min;
} else {
temp = -1;
}
}
const nextValue = temp + amount;

if (max !== undefined && nextValue > max) {
return max;
}

if (min !== undefined && nextValue < min) {
return min;
}

return nextValue;
});
};

const handleStep = ({ type, direction } : {type: 'small'| 'large', direction: 'increment' | 'decrement' }) => {
let amount = 0;

switch (type) {
case 'small':
if (!step) return;
amount = step;
break;
case 'large':
if (!largeStep) return;
amount = largeStep;
break;
}

if (direction === 'decrement') {
amount *= -1;
}

applyStep(amount);
};

const contextValues = {
inputValue,
handleOnChange,
handleStep,
id,
name,
disabled,
readOnly,
required,
rootClass
};

return (
<div className={clsx(`${rootClass}-root`, className)} {...props}>
<NumberFieldContext.Provider value={contextValues}>
{children}
</NumberFieldContext.Provider>
</div>
);
};

export default NumberFieldRoot;
58 changes: 58 additions & 0 deletions src/components/ui/NumberField/stories/NumberField.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import React from 'react';
import NumberField from '../NumberField';
import SandboxEditor from '~/components/tools/SandboxEditor/SandboxEditor';

export default {
title: 'WIP/NumberField',
component: NumberField
};

export const Basic = () => (
<SandboxEditor>
<NumberField.Root defaultValue={5} step={1} min={-10} max={110} largeStep={5}>
<NumberField.Decrement>-</NumberField.Decrement>
<NumberField.Input />
<NumberField.Increment>+</NumberField.Increment>
</NumberField.Root>
</SandboxEditor>
);

export const Controlled = () => {
const [value, setValue] = React.useState<number | ''>(3);
return (
<SandboxEditor>
<NumberField.Root value={value} onValueChange={setValue} defaultValue={3} step={1} min={0} max={10} largeStep={5}>
<NumberField.Decrement>-</NumberField.Decrement>
<NumberField.Input />
<NumberField.Increment>+</NumberField.Increment>
</NumberField.Root>
<div style={{ marginTop: 8 }}>Current value: {value}</div>
</SandboxEditor>
);
};

export const FormExample = () => {
const [fieldValue, setFieldValue] = React.useState<number | ''>(2);
const [submitted, setSubmitted] = React.useState<number | null>(null);
return (
<SandboxEditor>
<form
onSubmit={e => {
e.preventDefault();
setSubmitted(fieldValue === '' ? null : fieldValue);
}}
>
<NumberField.Root name="quantity" value={fieldValue} onValueChange={setFieldValue} defaultValue={2} step={1} min={0} max={10} largeStep={5}>
<NumberField.Decrement>-</NumberField.Decrement>
<NumberField.Input />
<NumberField.Increment>+</NumberField.Increment>

</NumberField.Root>
<button type="submit" style={{ marginTop: 8 }}>Submit</button>
</form>
{submitted !== null && (
<div style={{ marginTop: 8 }}>Submitted value: {submitted}</div>
)}
</SandboxEditor>
);
};
Loading
Loading