mirror of
https://github.com/Stremio/stremio-web.git
synced 2026-03-11 21:27:05 +00:00
feat(NumberInput): added NumberInput common component
This commit is contained in:
parent
27b6942fcd
commit
e8a6e72b13
4 changed files with 166 additions and 0 deletions
84
src/components/NumberInput/NumberInput.tsx
Normal file
84
src/components/NumberInput/NumberInput.tsx
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
// Copyright (C) 2017-2025 Smart code 203358507
|
||||
|
||||
import React, { forwardRef, useCallback, useState } from 'react';
|
||||
import { type KeyboardEvent, type InputHTMLAttributes } from 'react';
|
||||
import classnames from 'classnames';
|
||||
import styles from './styles.less';
|
||||
import Button from '../Button';
|
||||
import Icon from '@stremio/stremio-icons/react';
|
||||
|
||||
type Props = InputHTMLAttributes<HTMLInputElement> & {
|
||||
containerClassName?: string;
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
showButtons?: boolean;
|
||||
defaultValue?: number;
|
||||
label?: string;
|
||||
min?: number;
|
||||
max?: number;
|
||||
onKeyDown?: (event: KeyboardEvent<HTMLInputElement>) => void;
|
||||
onSubmit?: (event: KeyboardEvent<HTMLInputElement>) => void;
|
||||
};
|
||||
|
||||
const NumberInput = forwardRef<HTMLInputElement, Props>(({ defaultValue, ...props }, ref) => {
|
||||
const [value, setValue] = useState(defaultValue || 1);
|
||||
const onKeyDown = useCallback((event: KeyboardEvent<HTMLInputElement>) => {
|
||||
props.onKeyDown && props.onKeyDown(event);
|
||||
|
||||
if (event.key === 'Enter' ) {
|
||||
props.onSubmit && props.onSubmit(event);
|
||||
}
|
||||
}, [props.onKeyDown, props.onSubmit]);
|
||||
|
||||
const handleIncrease = () => {
|
||||
const { max } = props;
|
||||
if (max) {
|
||||
return setValue((prevVal) =>
|
||||
prevVal + 1 > max ? max : prevVal + 1
|
||||
);
|
||||
}
|
||||
setValue((prevVal) => prevVal + 1);
|
||||
};
|
||||
|
||||
const handleDecrease = () => {
|
||||
const { min } = props;
|
||||
if (min) {
|
||||
return setValue((prevVal) =>
|
||||
prevVal - 1 < min ? min : prevVal - 1
|
||||
);
|
||||
}
|
||||
setValue((prevVal) => prevVal - 1);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={classnames(props.containerClassName, styles['number-input'])}>
|
||||
{props.showButtons ? <Button
|
||||
className={styles['btn']}
|
||||
onClick={handleDecrease}
|
||||
disabled={props.disabled || props.min ? value <= props.min : false}>
|
||||
<Icon className={styles['icon']} name={'remove'} />
|
||||
</Button> : null}
|
||||
<div className={classnames(styles['number-display'], props.showButtons ? styles['with-btns'] : '')}>
|
||||
{props.label && <div className={styles['label']}>{props.label}</div>}
|
||||
<input
|
||||
ref={ref}
|
||||
type="number"
|
||||
tabIndex={0}
|
||||
value={value}
|
||||
{...props}
|
||||
className={classnames(props.className, styles['value'], { 'disabled': props.disabled })}
|
||||
onChange={(event) => setValue(event.value)}
|
||||
onKeyDown={onKeyDown}
|
||||
/>
|
||||
</div>
|
||||
{props.showButtons ? <Button
|
||||
className={styles['btn']} onClick={handleIncrease} disabled={props.disabled || props.max ? value >= props.max : false}>
|
||||
<Icon className={styles['icon']} name={'add'} />
|
||||
</Button> : null}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
NumberInput.displayName = 'NumberInput';
|
||||
|
||||
export default NumberInput;
|
||||
5
src/components/NumberInput/index.ts
Normal file
5
src/components/NumberInput/index.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
// Copyright (C) 2017-2025 Smart code 203358507
|
||||
|
||||
import NumberInput from './NumberInput';
|
||||
|
||||
export default NumberInput;
|
||||
75
src/components/NumberInput/styles.less
Normal file
75
src/components/NumberInput/styles.less
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
// Copyright (C) 2017-2025 Smart code 203358507
|
||||
|
||||
.number-input {
|
||||
user-select: text;
|
||||
display: flex;
|
||||
max-width: 12rem;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--primary-foreground-color);
|
||||
background: var(--overlay-color);
|
||||
border-radius: 100rem;
|
||||
|
||||
.btn {
|
||||
width: 2.875rem;
|
||||
height: 2.875rem;
|
||||
background: var(--overlay-color);
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: background 0.3s;
|
||||
z-index: 1;
|
||||
|
||||
svg {
|
||||
width: 1.625rem;
|
||||
}
|
||||
}
|
||||
|
||||
.number-display {
|
||||
height: 2.875rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
padding: 0 1rem;
|
||||
|
||||
&::-moz-focus-inner {
|
||||
border: none;
|
||||
}
|
||||
|
||||
&.with-btns {
|
||||
padding: 0 1.9375rem;
|
||||
margin-left: -1.4375rem;
|
||||
margin-right: -1.4375rem;
|
||||
max-width: 9.125rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Label */
|
||||
.number-display .label {
|
||||
font-size: 0.625rem;
|
||||
font-weight: 400;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* Value */
|
||||
.number-display .value {
|
||||
font-size: 1.3125rem;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
color: white;
|
||||
text-align: center;
|
||||
|
||||
&::-webkit-outer-spin-button,
|
||||
&::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: textfield;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -17,6 +17,7 @@ import ModalDialog from './ModalDialog';
|
|||
import Multiselect from './Multiselect';
|
||||
import MultiselectMenu from './MultiselectMenu';
|
||||
import { HorizontalNavBar, VerticalNavBar } from './NavBar';
|
||||
import NumberInput from './NumberInput';
|
||||
import Popup from './Popup';
|
||||
import RadioButton from './RadioButton';
|
||||
import SearchBar from './SearchBar';
|
||||
|
|
@ -48,6 +49,7 @@ export {
|
|||
MultiselectMenu,
|
||||
HorizontalNavBar,
|
||||
VerticalNavBar,
|
||||
NumberInput,
|
||||
Popup,
|
||||
RadioButton,
|
||||
SearchBar,
|
||||
|
|
|
|||
Loading…
Reference in a new issue