@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.
Files changed (30) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/dist/components/Input/index.d.ts +2 -0
  3. package/dist/components/Input/index.js +1 -0
  4. package/dist/components/Input/index.module.js +25 -0
  5. package/dist/components/Input/index_module.css +312 -0
  6. package/dist/components/Input/input.d.ts +29 -0
  7. package/dist/components/Input/input.js +179 -0
  8. package/dist/components/Input/types.d.ts +115 -0
  9. package/dist/components/Input/types.js +0 -0
  10. package/dist/components/Input/vars.css +32 -0
  11. package/dist/components/Modal/components/content/index.js +3 -2
  12. package/dist/components/Modal/components/controls/index.js +3 -2
  13. package/dist/components/Modal/components/footer/index.js +3 -2
  14. package/dist/components/Modal/components/header/index.js +3 -2
  15. package/dist/components/Textarea/hooks/use-autosize.d.ts +2 -0
  16. package/dist/components/Textarea/hooks/use-autosize.js +106 -0
  17. package/dist/components/Textarea/index.d.ts +2 -0
  18. package/dist/components/Textarea/index.js +1 -0
  19. package/dist/components/Textarea/index.module.js +14 -0
  20. package/dist/components/Textarea/index_module.css +280 -0
  21. package/dist/components/Textarea/textarea.d.ts +27 -0
  22. package/dist/components/Textarea/textarea.js +173 -0
  23. package/dist/components/Textarea/types.d.ts +108 -0
  24. package/dist/components/Textarea/types.js +0 -0
  25. package/dist/components/Textarea/vars.css +9 -0
  26. package/dist/hooks/use-data-test-id.d.ts +5 -0
  27. package/dist/hooks/use-data-test-id.js +7 -0
  28. package/dist/index.d.ts +4 -0
  29. package/dist/index.js +2 -0
  30. 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
@@ -0,0 +1,9 @@
1
+ @import "../Input/vars.css";
2
+
3
+ :root {
4
+ --textarea-width: var(--input-width);
5
+ --textarea-padding: var(--input-padding-40);
6
+ --textarea-font-size: var(--input-font-size-40);
7
+ --textarea-line-height: 17px;
8
+ }
9
+
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Создаёт идентификаторы частей компонента с суффиксом.
3
+ * Без базового dataTestId или при пустой строке возвращает undefined.
4
+ */
5
+ export declare function useDataTestId(dataTestId?: string): (suffix: string) => string | undefined;
@@ -0,0 +1,7 @@
1
+ import { useCallback } from "react";
2
+ function useDataTestId(dataTestId) {
3
+ return useCallback((suffix)=>dataTestId ? `${dataTestId}-${suffix}` : void 0, [
4
+ dataTestId
5
+ ]);
6
+ }
7
+ export { useDataTestId };
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.4.0",
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.1",
83
+ "@koslibs/icons": "1.2.0",
84
84
  "@popperjs/core": "2.11.8",
85
85
  "classnames": "2.5.1"
86
86
  }