@os-design/core 1.0.198 → 1.0.200
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/InputSearch/index.js +2 -2
- package/dist/cjs/InputSearch/index.js.map +1 -1
- package/dist/esm/InputSearch/index.js +2 -2
- package/dist/esm/InputSearch/index.js.map +1 -1
- package/dist/types/InputSearch/index.d.ts.map +1 -1
- package/package.json +21 -13
- package/src/@types/emotion.d.ts +7 -0
- package/src/Alert/index.tsx +112 -0
- package/src/Avatar/index.tsx +173 -0
- package/src/Avatar/utils/nameToInitials.ts +12 -0
- package/src/Avatar/utils/strToHue.ts +13 -0
- package/src/AvatarSkeleton/index.tsx +29 -0
- package/src/Breadcrumb/index.tsx +93 -0
- package/src/BreadcrumbItem/index.tsx +83 -0
- package/src/Button/ButtonContent.tsx +91 -0
- package/src/Button/index.tsx +225 -0
- package/src/Button/utils/useButtonColors.ts +84 -0
- package/src/Checkbox/index.tsx +225 -0
- package/src/CheckboxSkeleton/index.tsx +50 -0
- package/src/DatePicker/DatePickerCalendar.tsx +220 -0
- package/src/DatePicker/index.tsx +568 -0
- package/src/Drawer/index.tsx +212 -0
- package/src/Form/FormConfigContext.ts +16 -0
- package/src/Form/index.tsx +49 -0
- package/src/FormDivider/index.tsx +74 -0
- package/src/FormItem/index.tsx +118 -0
- package/src/Gallery/Status.tsx +62 -0
- package/src/Gallery/index.tsx +290 -0
- package/src/GlobalStyles/index.tsx +17 -0
- package/src/GlobalStyles/resetStyles.ts +17 -0
- package/src/GlobalStyles/typographyStyles.ts +78 -0
- package/src/HeaderSkeleton/index.tsx +64 -0
- package/src/Image/index.tsx +104 -0
- package/src/ImageSkeleton/index.tsx +22 -0
- package/src/Input/index.tsx +330 -0
- package/src/Input/utils/getFocusableElements.ts +8 -0
- package/src/InputNumber/index.tsx +208 -0
- package/src/InputNumber/utils/defaultLocale.ts +9 -0
- package/src/InputPassword/index.tsx +201 -0
- package/src/InputPassword/utils/defaultLocale.ts +11 -0
- package/src/InputSearch/index.tsx +111 -0
- package/src/InputSearch/utils/defaultLocale.ts +9 -0
- package/src/InputSkeleton/index.tsx +28 -0
- package/src/Layout/LayoutContext.ts +21 -0
- package/src/Layout/index.tsx +44 -0
- package/src/Link/index.tsx +129 -0
- package/src/LinkButton/index.tsx +100 -0
- package/src/List/WindowScroller.tsx +53 -0
- package/src/List/index.tsx +255 -0
- package/src/List/utils/bodyPointerEvents.ts +24 -0
- package/src/List/utils/frameTimeout.ts +36 -0
- package/src/List/utils/useRWLoadNext.ts +38 -0
- package/src/ListItem/index.tsx +92 -0
- package/src/ListItemActions/index.tsx +207 -0
- package/src/ListItemLink/index.tsx +63 -0
- package/src/ListSkeleton/index.tsx +115 -0
- package/src/LogoLink/index.tsx +93 -0
- package/src/LogoLink/logo.example.svg +18 -0
- package/src/Menu/index.tsx +128 -0
- package/src/Menu/utils/useFocusWithArrows.ts +50 -0
- package/src/MenuDivider/index.tsx +22 -0
- package/src/MenuGroup/index.tsx +190 -0
- package/src/MenuItem/index.tsx +108 -0
- package/src/Modal/index.tsx +411 -0
- package/src/Modal/utils/defaultLocale.ts +9 -0
- package/src/Navigation/index.tsx +214 -0
- package/src/Navigation/utils/useScrollFlags.ts +39 -0
- package/src/NavigationItem/index.tsx +136 -0
- package/src/PageContent/index.tsx +99 -0
- package/src/PageHeader/index.tsx +246 -0
- package/src/PageHeader/utils/defaultLocale.ts +9 -0
- package/src/PageHeaderInputSearch/index.tsx +145 -0
- package/src/PageHeaderInputSearch/utils/defaultLocale.ts +16 -0
- package/src/PageHeaderSkeleton/index.tsx +33 -0
- package/src/ParagraphSkeleton/index.tsx +65 -0
- package/src/Popover/index.tsx +243 -0
- package/src/Popover/utils/usePopoverPosition.ts +216 -0
- package/src/Progress/index.tsx +100 -0
- package/src/RadioGroup/index.tsx +165 -0
- package/src/RadioGroupSkeleton/index.tsx +36 -0
- package/src/Result/index.tsx +109 -0
- package/src/ScrollButton/index.tsx +159 -0
- package/src/ScrollButton/utils/useContainerPosition.ts +41 -0
- package/src/ScrollButton/utils/useVisibility.ts +56 -0
- package/src/Select/index.tsx +970 -0
- package/src/Select/utils/defaultLocale.ts +11 -0
- package/src/Skeleton/index.tsx +52 -0
- package/src/Switch/index.tsx +217 -0
- package/src/SwitchSkeleton/index.tsx +30 -0
- package/src/Tag/index.tsx +75 -0
- package/src/TagLink/index.tsx +53 -0
- package/src/TagList/index.tsx +95 -0
- package/src/TagListSkeleton/index.tsx +38 -0
- package/src/TagSkeleton/index.tsx +40 -0
- package/src/TextArea/index.tsx +231 -0
- package/src/TextAreaSkeleton/index.tsx +20 -0
- package/src/ThemeSwitcher/index.tsx +39 -0
- package/src/TimePicker/index.tsx +142 -0
- package/src/Video/index.tsx +41 -0
- package/src/index.ts +125 -0
- package/src/message/AlertIcon.tsx +50 -0
- package/src/message/Message.tsx +108 -0
- package/src/message/index.tsx +64 -0
- package/src/message/styles.ts +25 -0
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
import { css } from '@emotion/react';
|
|
2
|
+
import styled from '@emotion/styled';
|
|
3
|
+
import { Loading } from '@os-design/icons';
|
|
4
|
+
import {
|
|
5
|
+
resetFocusStyles,
|
|
6
|
+
sizeStyles,
|
|
7
|
+
transitionStyles,
|
|
8
|
+
WithSize,
|
|
9
|
+
} from '@os-design/styles';
|
|
10
|
+
import { clr, ThemeOverrider } from '@os-design/theming';
|
|
11
|
+
import { omitEmotionProps, useForwardedRef } from '@os-design/utils';
|
|
12
|
+
|
|
13
|
+
import React, {
|
|
14
|
+
ChangeEvent,
|
|
15
|
+
FocusEventHandler,
|
|
16
|
+
ForwardedRef,
|
|
17
|
+
forwardRef,
|
|
18
|
+
KeyboardEventHandler,
|
|
19
|
+
useCallback,
|
|
20
|
+
useMemo,
|
|
21
|
+
} from 'react';
|
|
22
|
+
import getFocusableElements from './utils/getFocusableElements';
|
|
23
|
+
|
|
24
|
+
type JsxInputProps = Omit<
|
|
25
|
+
JSX.IntrinsicElements['input'],
|
|
26
|
+
'value' | 'onChange' | 'size' | 'ref'
|
|
27
|
+
>;
|
|
28
|
+
export interface InputProps extends JsxInputProps, WithSize {
|
|
29
|
+
/**
|
|
30
|
+
* Type of the input.
|
|
31
|
+
* @default text
|
|
32
|
+
*/
|
|
33
|
+
type?: JsxInputProps['type'];
|
|
34
|
+
/**
|
|
35
|
+
* The component located on the left side.
|
|
36
|
+
* @default undefined
|
|
37
|
+
*/
|
|
38
|
+
left?: React.ReactNode;
|
|
39
|
+
/**
|
|
40
|
+
* Adds padding to the left component.
|
|
41
|
+
* It can be useful when passing an icon or text in the left component.
|
|
42
|
+
* @default false
|
|
43
|
+
*/
|
|
44
|
+
leftHasPadding?: boolean;
|
|
45
|
+
/**
|
|
46
|
+
* The component located on the right side.
|
|
47
|
+
* @default undefined
|
|
48
|
+
*/
|
|
49
|
+
right?: React.ReactNode;
|
|
50
|
+
/**
|
|
51
|
+
* Adds padding to the right component.
|
|
52
|
+
* It can be useful when passing an icon or text in the right component.
|
|
53
|
+
* @default false
|
|
54
|
+
*/
|
|
55
|
+
rightHasPadding?: boolean;
|
|
56
|
+
/**
|
|
57
|
+
* Whether the input is disabled.
|
|
58
|
+
* @default false
|
|
59
|
+
*/
|
|
60
|
+
disabled?: boolean;
|
|
61
|
+
/**
|
|
62
|
+
* Shows the loading status.
|
|
63
|
+
* @default false
|
|
64
|
+
*/
|
|
65
|
+
loading?: boolean;
|
|
66
|
+
/**
|
|
67
|
+
* The ref of the input container.
|
|
68
|
+
* @default undefined
|
|
69
|
+
*/
|
|
70
|
+
containerRef?: ForwardedRef<HTMLDivElement>;
|
|
71
|
+
/**
|
|
72
|
+
* The props of the input container.
|
|
73
|
+
* @default undefined
|
|
74
|
+
*/
|
|
75
|
+
containerProps?: JSX.IntrinsicElements['div'];
|
|
76
|
+
/**
|
|
77
|
+
* The input value.
|
|
78
|
+
* @default undefined
|
|
79
|
+
*/
|
|
80
|
+
value?: string;
|
|
81
|
+
/**
|
|
82
|
+
* The change event handler.
|
|
83
|
+
* @default undefined
|
|
84
|
+
*/
|
|
85
|
+
onChange?: (value: string, e: ChangeEvent<HTMLInputElement>) => void;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const hoverStyles = (p) =>
|
|
89
|
+
!p.disabled &&
|
|
90
|
+
css`
|
|
91
|
+
@media (hover: hover) {
|
|
92
|
+
&:hover {
|
|
93
|
+
border-color: ${clr(p.theme.inputHoverColorBorder)};
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
`;
|
|
97
|
+
|
|
98
|
+
const focusStyles = (p) =>
|
|
99
|
+
!p.disabled &&
|
|
100
|
+
css`
|
|
101
|
+
&:focus-within {
|
|
102
|
+
border-color: ${clr(p.theme.inputFocusColorBorder)};
|
|
103
|
+
box-shadow: 0 0 0 0.15em ${clr(p.theme.inputFocusColorShadow)};
|
|
104
|
+
}
|
|
105
|
+
`;
|
|
106
|
+
|
|
107
|
+
const disabledStyles = (p) =>
|
|
108
|
+
p.disabled &&
|
|
109
|
+
css`
|
|
110
|
+
cursor: not-allowed;
|
|
111
|
+
color: ${clr(p.theme.inputDisabledColorText)};
|
|
112
|
+
background-color: ${clr(p.theme.inputDisabledColorBg)};
|
|
113
|
+
border-color: ${clr(p.theme.inputDisabledColorBorder)};
|
|
114
|
+
|
|
115
|
+
input,
|
|
116
|
+
textarea {
|
|
117
|
+
cursor: not-allowed;
|
|
118
|
+
&::placeholder {
|
|
119
|
+
color: ${clr(p.theme.inputDisabledColorPlaceholder)};
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
`;
|
|
123
|
+
|
|
124
|
+
type InputContainerProps = Pick<InputProps, 'disabled' | 'size'>;
|
|
125
|
+
export const InputContainer = styled(
|
|
126
|
+
'div',
|
|
127
|
+
omitEmotionProps('disabled', 'size')
|
|
128
|
+
)<InputContainerProps>`
|
|
129
|
+
${resetFocusStyles};
|
|
130
|
+
|
|
131
|
+
display: inline-flex;
|
|
132
|
+
width: 100%;
|
|
133
|
+
height: ${(p) => p.theme.baseHeight}em;
|
|
134
|
+
box-sizing: border-box;
|
|
135
|
+
background-color: ${(p) => clr(p.theme.inputColorBg)};
|
|
136
|
+
|
|
137
|
+
border: ${(p) => p.theme.inputBorderWidth}px solid
|
|
138
|
+
${(p) => clr(p.theme.inputColorBorder)};
|
|
139
|
+
border-radius: ${(p) => p.theme.borderRadius}em;
|
|
140
|
+
|
|
141
|
+
${hoverStyles};
|
|
142
|
+
${focusStyles};
|
|
143
|
+
${disabledStyles};
|
|
144
|
+
${sizeStyles};
|
|
145
|
+
${transitionStyles('border-color', 'box-shadow')};
|
|
146
|
+
`;
|
|
147
|
+
|
|
148
|
+
const notHasLeftStyles = (p) =>
|
|
149
|
+
!p.hasLeft &&
|
|
150
|
+
css`
|
|
151
|
+
padding-left: ${p.theme.inputPaddingHorizontal}em;
|
|
152
|
+
`;
|
|
153
|
+
|
|
154
|
+
const notHasRightStyles = (p) =>
|
|
155
|
+
!p.hasRight &&
|
|
156
|
+
css`
|
|
157
|
+
padding-right: ${p.theme.inputPaddingHorizontal}em;
|
|
158
|
+
`;
|
|
159
|
+
|
|
160
|
+
interface StyledInputProps {
|
|
161
|
+
hasLeft?: boolean;
|
|
162
|
+
hasRight?: boolean;
|
|
163
|
+
}
|
|
164
|
+
export const StyledInput = styled(
|
|
165
|
+
'input',
|
|
166
|
+
omitEmotionProps('hasLeft', 'hasRight')
|
|
167
|
+
)<StyledInputProps>`
|
|
168
|
+
${resetFocusStyles};
|
|
169
|
+
appearance: none;
|
|
170
|
+
border: none;
|
|
171
|
+
font-size: 1em;
|
|
172
|
+
flex: 1;
|
|
173
|
+
overflow: hidden;
|
|
174
|
+
|
|
175
|
+
color: ${(p) => clr(p.theme.inputColorText)};
|
|
176
|
+
background-color: transparent;
|
|
177
|
+
|
|
178
|
+
&::placeholder {
|
|
179
|
+
color: ${(p) => clr(p.theme.inputColorPlaceholder)};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
${notHasLeftStyles};
|
|
183
|
+
${notHasRightStyles};
|
|
184
|
+
`;
|
|
185
|
+
|
|
186
|
+
interface AddonProps {
|
|
187
|
+
hasPadding: boolean;
|
|
188
|
+
}
|
|
189
|
+
const Addon = styled('span', omitEmotionProps('hasPadding'))<AddonProps>`
|
|
190
|
+
display: flex;
|
|
191
|
+
align-items: center;
|
|
192
|
+
user-select: none;
|
|
193
|
+
color: ${(p) => clr(p.theme.inputColorPlaceholder)};
|
|
194
|
+
|
|
195
|
+
svg {
|
|
196
|
+
transform: scale(1.2);
|
|
197
|
+
}
|
|
198
|
+
`;
|
|
199
|
+
|
|
200
|
+
const LeftAddon = styled(Addon)`
|
|
201
|
+
padding-right: ${(p) => p.theme.inputAddonPaddingHorizontal}em;
|
|
202
|
+
${(p) =>
|
|
203
|
+
p.hasPadding &&
|
|
204
|
+
css`
|
|
205
|
+
padding-left: ${p.theme.inputPaddingHorizontal}em;
|
|
206
|
+
`}
|
|
207
|
+
`;
|
|
208
|
+
|
|
209
|
+
const RightAddon = styled(Addon)`
|
|
210
|
+
padding-left: ${(p) => p.theme.inputAddonPaddingHorizontal}em;
|
|
211
|
+
${(p) =>
|
|
212
|
+
p.hasPadding &&
|
|
213
|
+
css`
|
|
214
|
+
padding-right: ${p.theme.inputPaddingHorizontal}em;
|
|
215
|
+
`}
|
|
216
|
+
`;
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* The basic input component.
|
|
220
|
+
*/
|
|
221
|
+
const Input = forwardRef<HTMLInputElement, InputProps>(
|
|
222
|
+
(
|
|
223
|
+
{
|
|
224
|
+
type = 'text',
|
|
225
|
+
left,
|
|
226
|
+
leftHasPadding = false,
|
|
227
|
+
right,
|
|
228
|
+
rightHasPadding = false,
|
|
229
|
+
disabled = false,
|
|
230
|
+
loading = false,
|
|
231
|
+
containerRef,
|
|
232
|
+
containerProps = {},
|
|
233
|
+
size,
|
|
234
|
+
value,
|
|
235
|
+
onChange = () => {},
|
|
236
|
+
...rest
|
|
237
|
+
},
|
|
238
|
+
ref
|
|
239
|
+
) => {
|
|
240
|
+
const [innerContainerRef, mergedContainerRef] =
|
|
241
|
+
useForwardedRef(containerRef);
|
|
242
|
+
|
|
243
|
+
const rightValue = useMemo(
|
|
244
|
+
() => (loading ? <Loading /> : right),
|
|
245
|
+
[loading, right]
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
const rightHasPaddingValue = useMemo(
|
|
249
|
+
() => (loading ? true : rightHasPadding),
|
|
250
|
+
[loading, rightHasPadding]
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
const onFocus = useCallback<FocusEventHandler>(
|
|
254
|
+
(e) => {
|
|
255
|
+
// Focus the next element if the container element was focused.
|
|
256
|
+
// The next element will be the input or button in the addon.
|
|
257
|
+
if (disabled || e.target !== innerContainerRef.current) return;
|
|
258
|
+
const focusableElements = getFocusableElements(
|
|
259
|
+
innerContainerRef.current
|
|
260
|
+
);
|
|
261
|
+
focusableElements[0].focus();
|
|
262
|
+
},
|
|
263
|
+
[disabled, innerContainerRef]
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
const onKeyDown = useCallback<KeyboardEventHandler>(
|
|
267
|
+
(e) => {
|
|
268
|
+
// Focus the previous element if the first element in the input
|
|
269
|
+
// container is focused and the Shift + Tab combination is pressed.
|
|
270
|
+
const focusableElements = getFocusableElements(document);
|
|
271
|
+
const inputFocusableElements = innerContainerRef.current
|
|
272
|
+
? getFocusableElements(innerContainerRef.current)
|
|
273
|
+
: [];
|
|
274
|
+
const firstInputElementIsFocused =
|
|
275
|
+
inputFocusableElements[0] === document.activeElement;
|
|
276
|
+
if (firstInputElementIsFocused && e.key === 'Tab' && e.shiftKey) {
|
|
277
|
+
const inputContainerIndex = focusableElements.findIndex(
|
|
278
|
+
(el) => el === innerContainerRef.current
|
|
279
|
+
);
|
|
280
|
+
if (inputContainerIndex === 0) return;
|
|
281
|
+
const elementBeforeInputContainer =
|
|
282
|
+
focusableElements[inputContainerIndex - 1];
|
|
283
|
+
elementBeforeInputContainer.focus();
|
|
284
|
+
}
|
|
285
|
+
},
|
|
286
|
+
[innerContainerRef]
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
return (
|
|
290
|
+
<InputContainer
|
|
291
|
+
disabled={disabled}
|
|
292
|
+
size={size}
|
|
293
|
+
tabIndex={!disabled ? 0 : -1}
|
|
294
|
+
onFocus={onFocus}
|
|
295
|
+
onKeyDown={onKeyDown}
|
|
296
|
+
ref={mergedContainerRef}
|
|
297
|
+
{...containerProps}
|
|
298
|
+
>
|
|
299
|
+
{left && (
|
|
300
|
+
<ThemeOverrider overrides={{ buttonPaddingHorizontal: 0.8 }}>
|
|
301
|
+
<LeftAddon hasPadding={leftHasPadding}>{left}</LeftAddon>
|
|
302
|
+
</ThemeOverrider>
|
|
303
|
+
)}
|
|
304
|
+
|
|
305
|
+
<StyledInput
|
|
306
|
+
type={type}
|
|
307
|
+
disabled={disabled}
|
|
308
|
+
hasLeft={!!left}
|
|
309
|
+
hasRight={!!right}
|
|
310
|
+
value={value || ''}
|
|
311
|
+
onChange={(e) => onChange(e.target.value, e)}
|
|
312
|
+
{...rest}
|
|
313
|
+
ref={ref}
|
|
314
|
+
/>
|
|
315
|
+
|
|
316
|
+
{rightValue && (
|
|
317
|
+
<ThemeOverrider overrides={{ buttonPaddingHorizontal: 0.8 }}>
|
|
318
|
+
<RightAddon hasPadding={rightHasPaddingValue}>
|
|
319
|
+
{rightValue}
|
|
320
|
+
</RightAddon>
|
|
321
|
+
</ThemeOverrider>
|
|
322
|
+
)}
|
|
323
|
+
</InputContainer>
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
Input.displayName = 'Input';
|
|
329
|
+
|
|
330
|
+
export default Input;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
const getFocusableElements = (element: ParentNode): HTMLElement[] =>
|
|
2
|
+
[
|
|
3
|
+
...element.querySelectorAll<HTMLElement>(
|
|
4
|
+
'a, button, input, textarea, select, details,[tabindex]:not([tabindex="-1"])'
|
|
5
|
+
),
|
|
6
|
+
].filter((el) => !el.hasAttribute('disabled'));
|
|
7
|
+
|
|
8
|
+
export default getFocusableElements;
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getNextCaret,
|
|
3
|
+
getNextState,
|
|
4
|
+
numberToFormattedString,
|
|
5
|
+
useGetCaretWithinValue,
|
|
6
|
+
useValidate,
|
|
7
|
+
} from '@os-design/input-number-utils';
|
|
8
|
+
import { useForwardedRef } from '@os-design/utils';
|
|
9
|
+
import React, { forwardRef, useEffect, useMemo, useState } from 'react';
|
|
10
|
+
import Input, { InputProps } from '../Input';
|
|
11
|
+
import defaultLocale, { InputNumberLocale } from './utils/defaultLocale';
|
|
12
|
+
|
|
13
|
+
export interface InputNumberProps
|
|
14
|
+
extends Omit<InputProps, 'type' | 'value' | 'onChange'> {
|
|
15
|
+
/**
|
|
16
|
+
* The minimum value.
|
|
17
|
+
* @default 0
|
|
18
|
+
*/
|
|
19
|
+
min?: number;
|
|
20
|
+
/**
|
|
21
|
+
* The maximum value.
|
|
22
|
+
* @default 2147483647
|
|
23
|
+
*/
|
|
24
|
+
max?: number;
|
|
25
|
+
/**
|
|
26
|
+
* The number of digits after the decimal point.
|
|
27
|
+
* @default 0
|
|
28
|
+
*/
|
|
29
|
+
precision?: number;
|
|
30
|
+
/**
|
|
31
|
+
* The string before the number.
|
|
32
|
+
* @default undefined
|
|
33
|
+
*/
|
|
34
|
+
prefix?: string;
|
|
35
|
+
/**
|
|
36
|
+
* The string after the number.
|
|
37
|
+
* @default undefined
|
|
38
|
+
*/
|
|
39
|
+
suffix?: string;
|
|
40
|
+
/**
|
|
41
|
+
* The decimal separator.
|
|
42
|
+
* @default .
|
|
43
|
+
*/
|
|
44
|
+
decimalSeparator?: '.' | ',';
|
|
45
|
+
/**
|
|
46
|
+
* The thousands separator.
|
|
47
|
+
* @default ' '
|
|
48
|
+
*/
|
|
49
|
+
thousandsSeparator?: ' ' | '.' | ',' | null;
|
|
50
|
+
/**
|
|
51
|
+
* The locale.
|
|
52
|
+
* @default undefined
|
|
53
|
+
*/
|
|
54
|
+
locale?: InputNumberLocale;
|
|
55
|
+
/**
|
|
56
|
+
* The input value.
|
|
57
|
+
* @default undefined
|
|
58
|
+
*/
|
|
59
|
+
value?: number | null;
|
|
60
|
+
/**
|
|
61
|
+
* The change event handler.
|
|
62
|
+
* Returns null when the input value is empty.
|
|
63
|
+
* @default undefined
|
|
64
|
+
*/
|
|
65
|
+
onChange?: (value: number | null) => void;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
interface Selection {
|
|
69
|
+
start: number;
|
|
70
|
+
end: number;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The input for entering a number.
|
|
75
|
+
*/
|
|
76
|
+
const InputNumber = forwardRef<HTMLInputElement, InputNumberProps>(
|
|
77
|
+
(
|
|
78
|
+
{
|
|
79
|
+
min = 0,
|
|
80
|
+
max = 2147483647,
|
|
81
|
+
precision = 0,
|
|
82
|
+
prefix = '',
|
|
83
|
+
suffix = '',
|
|
84
|
+
decimalSeparator = '.',
|
|
85
|
+
thousandsSeparator = ' ',
|
|
86
|
+
locale = defaultLocale,
|
|
87
|
+
value = null,
|
|
88
|
+
onChange = () => {},
|
|
89
|
+
onSelect = () => {},
|
|
90
|
+
onFocus = () => {},
|
|
91
|
+
...rest
|
|
92
|
+
},
|
|
93
|
+
ref
|
|
94
|
+
) => {
|
|
95
|
+
useValidate({
|
|
96
|
+
min,
|
|
97
|
+
max,
|
|
98
|
+
precision,
|
|
99
|
+
prefix,
|
|
100
|
+
suffix,
|
|
101
|
+
decimalSeparator,
|
|
102
|
+
thousandsSeparator,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const options = useMemo(
|
|
106
|
+
() => ({
|
|
107
|
+
min,
|
|
108
|
+
max,
|
|
109
|
+
precision,
|
|
110
|
+
prefix,
|
|
111
|
+
suffix,
|
|
112
|
+
decimalSeparator,
|
|
113
|
+
thousandsSeparator,
|
|
114
|
+
}),
|
|
115
|
+
[
|
|
116
|
+
min,
|
|
117
|
+
max,
|
|
118
|
+
precision,
|
|
119
|
+
prefix,
|
|
120
|
+
suffix,
|
|
121
|
+
decimalSeparator,
|
|
122
|
+
thousandsSeparator,
|
|
123
|
+
]
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
const [inputRef, mergedInputRef] = useForwardedRef(ref);
|
|
127
|
+
const [valueString, setValueString] = useState<string>(
|
|
128
|
+
numberToFormattedString(value, options)
|
|
129
|
+
);
|
|
130
|
+
const [selection, setSelection] = useState<Selection>({ start: 0, end: 0 });
|
|
131
|
+
const getCaretWithinValue = useGetCaretWithinValue(prefix, suffix);
|
|
132
|
+
|
|
133
|
+
// Update the value
|
|
134
|
+
useEffect(() => {
|
|
135
|
+
setValueString(numberToFormattedString(value, options));
|
|
136
|
+
}, [value, options]);
|
|
137
|
+
|
|
138
|
+
// Update the selection
|
|
139
|
+
useEffect(() => {
|
|
140
|
+
if (!inputRef.current) return;
|
|
141
|
+
inputRef.current.setSelectionRange(selection.start, selection.end);
|
|
142
|
+
}, [inputRef, selection]);
|
|
143
|
+
|
|
144
|
+
return (
|
|
145
|
+
<Input
|
|
146
|
+
onSelect={(e) => {
|
|
147
|
+
// Update the selection state.
|
|
148
|
+
// Don't use `getCaretWithinValue` here to allow a user to copy
|
|
149
|
+
// the input value with a prefix and suffix.
|
|
150
|
+
const { selectionStart, selectionEnd } = e.currentTarget;
|
|
151
|
+
setSelection({ start: selectionStart || 0, end: selectionEnd || 0 });
|
|
152
|
+
onSelect(e);
|
|
153
|
+
}}
|
|
154
|
+
onFocus={(e) => {
|
|
155
|
+
// Move the caret to the end of the input value and before the suffix
|
|
156
|
+
setSelection({
|
|
157
|
+
start: getCaretWithinValue(valueString.length, valueString),
|
|
158
|
+
end: getCaretWithinValue(valueString.length, valueString),
|
|
159
|
+
});
|
|
160
|
+
onFocus(e);
|
|
161
|
+
}}
|
|
162
|
+
value={valueString}
|
|
163
|
+
onChange={(v, e) => {
|
|
164
|
+
// Get a new value as a string and number
|
|
165
|
+
const nextState = getNextState(v, options);
|
|
166
|
+
|
|
167
|
+
// Set the new string value in the input field
|
|
168
|
+
setValueString(nextState.valueString);
|
|
169
|
+
|
|
170
|
+
// Send the new numeric value in the onChange callback
|
|
171
|
+
onChange(nextState.valueNumber);
|
|
172
|
+
|
|
173
|
+
// Update the caret position
|
|
174
|
+
const nextCaret = getCaretWithinValue(
|
|
175
|
+
getNextCaret({
|
|
176
|
+
value: v,
|
|
177
|
+
prevValueString: valueString,
|
|
178
|
+
nextValueString: nextState.valueString,
|
|
179
|
+
prevCaret: selection.end,
|
|
180
|
+
deleteKeyPressed:
|
|
181
|
+
(e.nativeEvent as InputEvent).inputType ===
|
|
182
|
+
'deleteContentBackward',
|
|
183
|
+
options,
|
|
184
|
+
}),
|
|
185
|
+
nextState.valueString
|
|
186
|
+
);
|
|
187
|
+
setSelection({ start: nextCaret, end: nextCaret });
|
|
188
|
+
}}
|
|
189
|
+
role='spinbutton'
|
|
190
|
+
aria-valuenow={
|
|
191
|
+
value !== undefined && value !== null ? value : undefined
|
|
192
|
+
}
|
|
193
|
+
aria-valuemin={min}
|
|
194
|
+
aria-valuemax={max}
|
|
195
|
+
aria-valuetext={valueString === '' ? locale.emptyLabel : undefined}
|
|
196
|
+
inputMode='decimal'
|
|
197
|
+
autoComplete='off'
|
|
198
|
+
autoCorrect='off'
|
|
199
|
+
{...rest}
|
|
200
|
+
ref={mergedInputRef}
|
|
201
|
+
/>
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
InputNumber.displayName = 'InputNumber';
|
|
207
|
+
|
|
208
|
+
export default InputNumber;
|