@koslibs/components 1.4.0 → 1.6.0
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/CHANGELOG.md +31 -0
- package/dist/components/Input/index.d.ts +2 -0
- package/dist/components/Input/index.js +1 -0
- package/dist/components/Input/index.module.js +25 -0
- package/dist/components/Input/index_module.css +312 -0
- package/dist/components/Input/input.d.ts +29 -0
- package/dist/components/Input/input.js +179 -0
- package/dist/components/Input/types.d.ts +115 -0
- package/dist/components/Input/types.js +0 -0
- package/dist/components/Input/vars.css +32 -0
- package/dist/components/Modal/components/content/index.js +3 -2
- package/dist/components/Modal/components/controls/index.js +3 -2
- package/dist/components/Modal/components/footer/index.js +3 -2
- package/dist/components/Modal/components/header/index.js +3 -2
- package/dist/components/Textarea/hooks/use-autosize.d.ts +2 -0
- package/dist/components/Textarea/hooks/use-autosize.js +106 -0
- package/dist/components/Textarea/index.d.ts +2 -0
- package/dist/components/Textarea/index.js +1 -0
- package/dist/components/Textarea/index.module.js +14 -0
- package/dist/components/Textarea/index_module.css +280 -0
- package/dist/components/Textarea/textarea.d.ts +27 -0
- package/dist/components/Textarea/textarea.js +173 -0
- package/dist/components/Textarea/types.d.ts +108 -0
- package/dist/components/Textarea/types.js +0 -0
- package/dist/components/Textarea/vars.css +9 -0
- package/dist/hooks/use-data-test-id.d.ts +5 -0
- package/dist/hooks/use-data-test-id.js +7 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2 -0
- package/package.json +2 -2
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { CrossHeavyMIcon } from "@koslibs/icons/CrossHeavyMIcon";
|
|
3
|
+
import classnames from "classnames";
|
|
4
|
+
import { forwardRef, useEffect, useId, useImperativeHandle, useRef, useState } from "react";
|
|
5
|
+
import { useDataTestId } from "../../hooks/use-data-test-id.js";
|
|
6
|
+
import { Button } from "../Button/index.js";
|
|
7
|
+
import { TypographyText } from "../Typography/text.js";
|
|
8
|
+
import { useAutosize } from "./hooks/use-autosize.js";
|
|
9
|
+
import index_module from "./index.module.js";
|
|
10
|
+
const Textarea = /*#__PURE__*/ forwardRef(({ view = 'primary', block = false, label, labelView = 'outer', hint, error, minRows = 3, maxRows, autosize = false, resize = false, clear = false, value, defaultValue, maxLength, showValueLength = false, onChange, onClear, disabled = false, readOnly = false, id, className, style, fieldClassName, textareaClassName, labelClassName, messageClassName, dataTestId, ...restProps }, ref)=>{
|
|
11
|
+
const generatedId = useId();
|
|
12
|
+
const textareaId = id ?? generatedId;
|
|
13
|
+
const textareaRef = useRef(null);
|
|
14
|
+
const [uncontrolledLength, setUncontrolledLength] = useState(defaultValue?.length ?? 0);
|
|
15
|
+
const controlled = void 0 !== value;
|
|
16
|
+
const valueLength = controlled ? value.length : uncontrolledLength;
|
|
17
|
+
const hasValue = valueLength > 0;
|
|
18
|
+
const showCounter = showValueLength && void 0 !== maxLength && Number.isInteger(maxLength) && maxLength >= 0;
|
|
19
|
+
const hasLabel = null != label && false !== label && '' !== label;
|
|
20
|
+
const errorMessage = hint && 'boolean' != typeof error ? error : void 0;
|
|
21
|
+
const message = errorMessage || hint;
|
|
22
|
+
const showClear = clear && hasValue && !disabled && !readOnly;
|
|
23
|
+
const rows = Number.isFinite(minRows) ? Math.max(1, Math.floor(minRows)) : 3;
|
|
24
|
+
const limit = void 0 !== maxRows && Number.isFinite(maxRows) ? Math.max(rows, Math.floor(maxRows)) : void 0;
|
|
25
|
+
const getDataTestId = useDataTestId(dataTestId);
|
|
26
|
+
useImperativeHandle(ref, ()=>textareaRef.current, []);
|
|
27
|
+
useAutosize(textareaRef, autosize, rows, limit, restProps.form);
|
|
28
|
+
useEffect(()=>{
|
|
29
|
+
const textarea = textareaRef.current;
|
|
30
|
+
const form = textarea?.form;
|
|
31
|
+
let active = true;
|
|
32
|
+
const handleReset = (event)=>{
|
|
33
|
+
setTimeout(()=>{
|
|
34
|
+
if (active && !event.defaultPrevented) setUncontrolledLength(textarea?.value.length ?? 0);
|
|
35
|
+
}, 0);
|
|
36
|
+
};
|
|
37
|
+
form?.addEventListener('reset', handleReset);
|
|
38
|
+
return ()=>{
|
|
39
|
+
active = false;
|
|
40
|
+
form?.removeEventListener('reset', handleReset);
|
|
41
|
+
};
|
|
42
|
+
}, [
|
|
43
|
+
restProps.form
|
|
44
|
+
]);
|
|
45
|
+
const handleChange = (event)=>{
|
|
46
|
+
const nextValue = event.target.value;
|
|
47
|
+
if (!controlled) setUncontrolledLength(nextValue.length);
|
|
48
|
+
onChange?.(event, {
|
|
49
|
+
value: nextValue
|
|
50
|
+
});
|
|
51
|
+
};
|
|
52
|
+
const handleClear = (event)=>{
|
|
53
|
+
const textarea = textareaRef.current;
|
|
54
|
+
if (!textarea || !showClear) return;
|
|
55
|
+
if (!controlled) {
|
|
56
|
+
textarea.value = '';
|
|
57
|
+
setUncontrolledLength(0);
|
|
58
|
+
}
|
|
59
|
+
onClear?.(event);
|
|
60
|
+
textarea.focus();
|
|
61
|
+
};
|
|
62
|
+
const handleFieldClick = (event)=>{
|
|
63
|
+
if (disabled || event.defaultPrevented || !(event.target instanceof Element)) return;
|
|
64
|
+
const target = event.target.closest('textarea, button, a, label, input, select, [contenteditable], [tabindex]');
|
|
65
|
+
if (target && event.currentTarget.contains(target)) return;
|
|
66
|
+
textareaRef.current?.focus();
|
|
67
|
+
};
|
|
68
|
+
const labelElement = hasLabel && /*#__PURE__*/ jsx("label", {
|
|
69
|
+
htmlFor: textareaId,
|
|
70
|
+
className: classnames(index_module.label, labelClassName),
|
|
71
|
+
"data-test-id": getDataTestId('label'),
|
|
72
|
+
children: /*#__PURE__*/ jsx(TypographyText, {
|
|
73
|
+
size: 'inner' === labelView ? 12 : 13,
|
|
74
|
+
weight: 'inner' === labelView ? 'regular' : 'medium',
|
|
75
|
+
children: label
|
|
76
|
+
})
|
|
77
|
+
});
|
|
78
|
+
const counterElement = showCounter && /*#__PURE__*/ jsxs(TypographyText, {
|
|
79
|
+
size: 11,
|
|
80
|
+
className: index_module.counter,
|
|
81
|
+
style: {
|
|
82
|
+
minWidth: `${2 * String(maxLength).length + 1}ch`
|
|
83
|
+
},
|
|
84
|
+
dataTestId: getDataTestId('counter'),
|
|
85
|
+
children: [
|
|
86
|
+
valueLength,
|
|
87
|
+
"/",
|
|
88
|
+
maxLength
|
|
89
|
+
]
|
|
90
|
+
});
|
|
91
|
+
return /*#__PURE__*/ jsxs("div", {
|
|
92
|
+
className: classnames(index_module.component, className),
|
|
93
|
+
style: {
|
|
94
|
+
'--textarea-rows': rows,
|
|
95
|
+
...style
|
|
96
|
+
},
|
|
97
|
+
"data-view": view,
|
|
98
|
+
"data-block": block || void 0,
|
|
99
|
+
"data-disabled": disabled || void 0,
|
|
100
|
+
"data-read-only": readOnly || void 0,
|
|
101
|
+
"data-error": Boolean(error) || void 0,
|
|
102
|
+
"data-label-view": labelView,
|
|
103
|
+
"data-inner-label": 'inner' === labelView && hasLabel || void 0,
|
|
104
|
+
"data-inner-header": 'inner' === labelView && (hasLabel || showCounter) || void 0,
|
|
105
|
+
"data-filled": hasValue || void 0,
|
|
106
|
+
"data-resize": resize && !autosize && !disabled && !readOnly || void 0,
|
|
107
|
+
"data-test-id": getDataTestId('form-control'),
|
|
108
|
+
children: [
|
|
109
|
+
'outer' === labelView && (hasLabel || showCounter) && /*#__PURE__*/ jsxs("div", {
|
|
110
|
+
className: index_module.header,
|
|
111
|
+
children: [
|
|
112
|
+
labelElement,
|
|
113
|
+
counterElement
|
|
114
|
+
]
|
|
115
|
+
}),
|
|
116
|
+
/*#__PURE__*/ jsxs("div", {
|
|
117
|
+
className: classnames(index_module.field, fieldClassName),
|
|
118
|
+
onClick: handleFieldClick,
|
|
119
|
+
"data-test-id": getDataTestId('field'),
|
|
120
|
+
children: [
|
|
121
|
+
/*#__PURE__*/ jsxs("div", {
|
|
122
|
+
className: index_module.value,
|
|
123
|
+
children: [
|
|
124
|
+
'inner' === labelView && (hasLabel || showCounter) && /*#__PURE__*/ jsxs("div", {
|
|
125
|
+
className: classnames(index_module.header, index_module.innerHeader),
|
|
126
|
+
children: [
|
|
127
|
+
labelElement,
|
|
128
|
+
counterElement
|
|
129
|
+
]
|
|
130
|
+
}),
|
|
131
|
+
/*#__PURE__*/ jsx("textarea", {
|
|
132
|
+
...restProps,
|
|
133
|
+
ref: textareaRef,
|
|
134
|
+
id: textareaId,
|
|
135
|
+
value: value,
|
|
136
|
+
defaultValue: defaultValue,
|
|
137
|
+
rows: rows,
|
|
138
|
+
maxLength: maxLength,
|
|
139
|
+
disabled: disabled,
|
|
140
|
+
readOnly: readOnly,
|
|
141
|
+
onChange: handleChange,
|
|
142
|
+
className: classnames(index_module.textarea, textareaClassName),
|
|
143
|
+
"data-test-id": dataTestId
|
|
144
|
+
})
|
|
145
|
+
]
|
|
146
|
+
}),
|
|
147
|
+
showClear && /*#__PURE__*/ jsx(Button, {
|
|
148
|
+
view: "text",
|
|
149
|
+
size: 32,
|
|
150
|
+
className: index_module.clear,
|
|
151
|
+
title: "Очистить поле",
|
|
152
|
+
onMouseDown: (event)=>event.preventDefault(),
|
|
153
|
+
onClick: handleClear,
|
|
154
|
+
dataTestId: getDataTestId('clear'),
|
|
155
|
+
children: /*#__PURE__*/ jsx(CrossHeavyMIcon, {
|
|
156
|
+
width: 16,
|
|
157
|
+
height: 16
|
|
158
|
+
})
|
|
159
|
+
})
|
|
160
|
+
]
|
|
161
|
+
}),
|
|
162
|
+
Boolean(message) && /*#__PURE__*/ jsx(TypographyText, {
|
|
163
|
+
tag: "div",
|
|
164
|
+
size: 12,
|
|
165
|
+
className: classnames(index_module.message, messageClassName),
|
|
166
|
+
dataTestId: getDataTestId(errorMessage ? 'error' : 'hint'),
|
|
167
|
+
children: message
|
|
168
|
+
})
|
|
169
|
+
]
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
Textarea.displayName = 'Textarea';
|
|
173
|
+
export { Textarea };
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { ChangeEvent, MouseEvent, ReactNode, TextareaHTMLAttributes } from 'react';
|
|
2
|
+
export type TextareaProps = Omit<TextareaHTMLAttributes<HTMLTextAreaElement>, 'value' | 'defaultValue' | 'maxLength' | 'onChange' | 'children' | 'rows'> & {
|
|
3
|
+
/**
|
|
4
|
+
* Значение управляемого поля.
|
|
5
|
+
*/
|
|
6
|
+
value?: string;
|
|
7
|
+
/**
|
|
8
|
+
* Начальное значение неуправляемого поля.
|
|
9
|
+
*/
|
|
10
|
+
defaultValue?: string;
|
|
11
|
+
/**
|
|
12
|
+
* Максимальная длина нативного ввода в кодовых единицах UTF-16.
|
|
13
|
+
*/
|
|
14
|
+
maxLength?: number;
|
|
15
|
+
/**
|
|
16
|
+
* Показывает текущую длину и maxLength рядом с label.
|
|
17
|
+
* Работает только при заданном неотрицательном целом maxLength.
|
|
18
|
+
* @default false
|
|
19
|
+
*/
|
|
20
|
+
showValueLength?: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Вид поля: белый primary или светлый secondary.
|
|
23
|
+
* @default 'primary'
|
|
24
|
+
*/
|
|
25
|
+
view?: 'primary' | 'secondary';
|
|
26
|
+
/**
|
|
27
|
+
* Растягивает компонент на ширину контейнера.
|
|
28
|
+
* @default false
|
|
29
|
+
*/
|
|
30
|
+
block?: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Название поля. Клик переводит фокус в textarea.
|
|
33
|
+
*/
|
|
34
|
+
label?: ReactNode;
|
|
35
|
+
/**
|
|
36
|
+
* Положение label. Внутренний label поднимается при фокусе или заполнении.
|
|
37
|
+
* @default 'outer'
|
|
38
|
+
*/
|
|
39
|
+
labelView?: 'outer' | 'inner';
|
|
40
|
+
/**
|
|
41
|
+
* Подсказка под полем. Сообщение об ошибке заменяет её.
|
|
42
|
+
*/
|
|
43
|
+
hint?: ReactNode;
|
|
44
|
+
/**
|
|
45
|
+
* Сообщение об ошибке или true для состояния ошибки без сообщения.
|
|
46
|
+
* Без непустого hint отображается только рамка. true сохраняет hint.
|
|
47
|
+
*/
|
|
48
|
+
error?: ReactNode | boolean;
|
|
49
|
+
/**
|
|
50
|
+
* Начальное число видимых строк; при autosize — минимальное число строк.
|
|
51
|
+
* Должно быть положительным целым числом.
|
|
52
|
+
* @default 3
|
|
53
|
+
*/
|
|
54
|
+
minRows?: number;
|
|
55
|
+
/**
|
|
56
|
+
* Максимальное число видимых строк при autosize. Затем текст прокручивается.
|
|
57
|
+
* Значение меньше minRows ограничивается minRows.
|
|
58
|
+
*/
|
|
59
|
+
maxRows?: number;
|
|
60
|
+
/**
|
|
61
|
+
* Подстраивает высоту под содержимое, включая переносы по ширине.
|
|
62
|
+
* @default false
|
|
63
|
+
*/
|
|
64
|
+
autosize?: boolean;
|
|
65
|
+
/**
|
|
66
|
+
* Разрешает менять высоту вручную за нижний правый угол.
|
|
67
|
+
* Не работает вместе с autosize, в disabled и readOnly. Ширина сохраняется.
|
|
68
|
+
* @default false
|
|
69
|
+
*/
|
|
70
|
+
resize?: boolean;
|
|
71
|
+
/**
|
|
72
|
+
* Показывает кнопку очистки заполненного редактируемого поля.
|
|
73
|
+
* @default false
|
|
74
|
+
*/
|
|
75
|
+
clear?: boolean;
|
|
76
|
+
/**
|
|
77
|
+
* Обработчик ввода с нативным событием React и текущим значением.
|
|
78
|
+
*/
|
|
79
|
+
onChange?: (event: ChangeEvent<HTMLTextAreaElement>, payload: {
|
|
80
|
+
value: string;
|
|
81
|
+
}) => void;
|
|
82
|
+
/**
|
|
83
|
+
* Обработчик очистки. Не вызывает onChange.
|
|
84
|
+
* В управляемом режиме обновите value в этом обработчике.
|
|
85
|
+
*/
|
|
86
|
+
onClear?: (event: MouseEvent<HTMLButtonElement>) => void;
|
|
87
|
+
/**
|
|
88
|
+
* Класс визуального поля.
|
|
89
|
+
*/
|
|
90
|
+
fieldClassName?: string;
|
|
91
|
+
/**
|
|
92
|
+
* Класс нативного textarea. className и style относятся к обёртке.
|
|
93
|
+
*/
|
|
94
|
+
textareaClassName?: string;
|
|
95
|
+
/**
|
|
96
|
+
* Класс label.
|
|
97
|
+
*/
|
|
98
|
+
labelClassName?: string;
|
|
99
|
+
/**
|
|
100
|
+
* Класс подсказки или сообщения об ошибке.
|
|
101
|
+
*/
|
|
102
|
+
messageClassName?: string;
|
|
103
|
+
/**
|
|
104
|
+
* Идентификатор нативного textarea. Части получают суффиксы -form-control,
|
|
105
|
+
* -field, -label, -hint, -error, -clear и -counter.
|
|
106
|
+
*/
|
|
107
|
+
dataTestId?: string;
|
|
108
|
+
};
|
|
File without changes
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
export { Button } from './components/Button/index.js';
|
|
2
2
|
export type { ButtonProps } from './components/Button/index.js';
|
|
3
|
+
export { Input } from './components/Input/index.js';
|
|
4
|
+
export type { InputProps } from './components/Input/index.js';
|
|
5
|
+
export { Textarea } from './components/Textarea/index.js';
|
|
6
|
+
export type { TextareaProps } from './components/Textarea/index.js';
|
|
3
7
|
export { Spinner } from './components/Spinner/index.js';
|
|
4
8
|
export type { SpinnerProps } from './components/Spinner/index.js';
|
|
5
9
|
export { Typography, TypographyText, TypographyTitle, Text, Title } from './components/Typography/index.js';
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export { Button } from "./components/Button/index.js";
|
|
2
|
+
export { Input } from "./components/Input/index.js";
|
|
3
|
+
export { Textarea } from "./components/Textarea/index.js";
|
|
2
4
|
export { Spinner } from "./components/Spinner/index.js";
|
|
3
5
|
export { Text, Title, Typography, TypographyText, TypographyTitle } from "./components/Typography/index.js";
|
|
4
6
|
export { Popover } from "./components/Popover/index.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@koslibs/components",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "Shared React components for koslibs projects",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -80,7 +80,7 @@
|
|
|
80
80
|
"react-dom": "^18.3.1 || ^19.0.0"
|
|
81
81
|
},
|
|
82
82
|
"dependencies": {
|
|
83
|
-
"@koslibs/icons": "1.0
|
|
83
|
+
"@koslibs/icons": "1.2.0",
|
|
84
84
|
"@popperjs/core": "2.11.8",
|
|
85
85
|
"classnames": "2.5.1"
|
|
86
86
|
}
|