@koslibs/components 1.4.0 → 1.5.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 +23 -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/hooks/use-data-test-id.d.ts +5 -0
- package/dist/hooks/use-data-test-id.js +7 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# @koslibs/components
|
|
2
2
|
|
|
3
|
+
## 1.5.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#6](https://github.com/koslibs/components/pull/6) [`360f774`](https://github.com/koslibs/components/commit/360f7746facda771d84165a16caeb6fe56abcc4a) Thanks [@holypower777](https://github.com/holypower777)! - Добавлен Input по дизайну Figma с view primary/secondary, размерами 40–72,
|
|
8
|
+
label внутри/снаружи, подсказками и ошибками, аддонами и очисткой.
|
|
9
|
+
Размер по умолчанию — 40.
|
|
10
|
+
Добавлен showValueLength для счётчика текущей длины и maxLength: внутри для inner,
|
|
11
|
+
снаружи рядом с label для outer. Поддержаны ввод, очистка и сброс формы.
|
|
12
|
+
Шрифт счётчика — 11 px; счётчик и label выровнены по базовой линии текста.
|
|
13
|
+
Сообщение ошибки заменяет hint; без hint отображается только красная рамка.
|
|
14
|
+
Поддержаны controlled/uncontrolled ввод, нативный ref и HTML-атрибуты,
|
|
15
|
+
disabled/readOnly и сброс формы. Добавлены Storybook и отдельный импорт.
|
|
16
|
+
Внутренний label поднимается при фокусе или заполнении поля; placeholder появляется
|
|
17
|
+
при фокусе, а после blur пустого поля label возвращается в центр.
|
|
18
|
+
Клики по отступам и декоративным аддонам фокусируют input, в том числе внутри
|
|
19
|
+
контейнеров с tabIndex; интерактивные аддоны сохраняют собственный фокус.
|
|
20
|
+
Формирование dataTestId частей Input и Modal унифицировано через общий внутренний
|
|
21
|
+
хук useDataTestId; существующие идентификаторы и наследование в Modal сохранены.
|
|
22
|
+
Убраны служебные ID сообщений и счётчика, автоматические aria-describedby и aria-invalid.
|
|
23
|
+
Обновлена зависимость @koslibs/icons до 1.2.0. В примерах Input используется
|
|
24
|
+
SearchIcon вместо локального SVG.
|
|
25
|
+
|
|
3
26
|
## 1.4.0
|
|
4
27
|
|
|
5
28
|
### Minor Changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Input } from "./input.js";
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import "./index_module.css";
|
|
2
|
+
const index_module = {
|
|
3
|
+
component: "component-aUdiT5",
|
|
4
|
+
"size-40": "size-40-afh8b8",
|
|
5
|
+
size40: "size-40-afh8b8",
|
|
6
|
+
"size-48": "size-48-MxoUiI",
|
|
7
|
+
size48: "size-48-MxoUiI",
|
|
8
|
+
"size-56": "size-56-p4fx5c",
|
|
9
|
+
size56: "size-56-p4fx5c",
|
|
10
|
+
"size-64": "size-64-p9mwzX",
|
|
11
|
+
size64: "size-64-p9mwzX",
|
|
12
|
+
"size-72": "size-72-csDcgy",
|
|
13
|
+
size72: "size-72-csDcgy",
|
|
14
|
+
field: "field-MFnvYv",
|
|
15
|
+
input: "input-IlmcAi",
|
|
16
|
+
value: "value-ehhHJ8",
|
|
17
|
+
header: "header-bcofZJ",
|
|
18
|
+
innerHeader: "innerHeader-Iga0Hb",
|
|
19
|
+
counter: "counter-C758Bk",
|
|
20
|
+
label: "label-tvCOwp",
|
|
21
|
+
addon: "addon-OXeKuK",
|
|
22
|
+
clear: "clear-A5Krz8",
|
|
23
|
+
message: "message-ArUnJU"
|
|
24
|
+
};
|
|
25
|
+
export default index_module;
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
@import "../../styles/theme.css";
|
|
2
|
+
@import "./vars.css";
|
|
3
|
+
|
|
4
|
+
.component-aUdiT5 {
|
|
5
|
+
--field-background: var(--input-primary-background-color);
|
|
6
|
+
--field-border: var(--input-border-color);
|
|
7
|
+
--field-color: var(--input-color);
|
|
8
|
+
--field-label-color: var(--input-label-color);
|
|
9
|
+
--field-placeholder-color: var(--input-placeholder-color);
|
|
10
|
+
--field-icon-color: var(--input-icon-color);
|
|
11
|
+
gap: var(--input-gap);
|
|
12
|
+
width: var(--input-width);
|
|
13
|
+
vertical-align: top;
|
|
14
|
+
min-width: 0;
|
|
15
|
+
max-width: 100%;
|
|
16
|
+
font-family: var(--font-primary);
|
|
17
|
+
color: var(--field-color);
|
|
18
|
+
flex-direction: column;
|
|
19
|
+
display: inline-flex;
|
|
20
|
+
|
|
21
|
+
&[data-block] {
|
|
22
|
+
width: 100%;
|
|
23
|
+
display: flex;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
&[data-view="secondary"] {
|
|
27
|
+
--field-background: var(--input-secondary-background-color);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
&[data-read-only] {
|
|
31
|
+
--field-border: var(--input-read-only-border-color);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
&[data-error] {
|
|
35
|
+
--field-border: var(--input-error-border-color);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
&[data-disabled] {
|
|
39
|
+
--field-background: var(--input-disabled-background-color);
|
|
40
|
+
--field-border: transparent;
|
|
41
|
+
--field-color: var(--input-disabled-color);
|
|
42
|
+
--field-label-color: var(--input-disabled-color);
|
|
43
|
+
--field-placeholder-color: var(--input-disabled-color);
|
|
44
|
+
--field-icon-color: var(--input-disabled-color);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.size-40-afh8b8 {
|
|
49
|
+
--field-height: 40px;
|
|
50
|
+
--field-padding: var(--input-padding-40);
|
|
51
|
+
--field-font-size: var(--input-font-size-40);
|
|
52
|
+
--field-line-height: 17px;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.size-48-MxoUiI {
|
|
56
|
+
--field-height: 48px;
|
|
57
|
+
--field-padding: var(--input-padding-48);
|
|
58
|
+
--field-font-size: var(--input-font-size-48);
|
|
59
|
+
--field-line-height: 17px;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.size-56-p4fx5c {
|
|
63
|
+
--field-height: 56px;
|
|
64
|
+
--field-padding: var(--input-padding-56);
|
|
65
|
+
--field-font-size: var(--input-font-size-56);
|
|
66
|
+
--field-line-height: 17px;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.size-64-p9mwzX {
|
|
70
|
+
--field-height: 64px;
|
|
71
|
+
--field-padding: var(--input-padding-64);
|
|
72
|
+
--field-font-size: var(--input-font-size-64);
|
|
73
|
+
--field-line-height: 17px;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.size-72-csDcgy {
|
|
77
|
+
--field-height: 72px;
|
|
78
|
+
--field-padding: var(--input-padding-72);
|
|
79
|
+
--field-font-size: var(--input-font-size-72);
|
|
80
|
+
--field-line-height: 20px;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.field-MFnvYv {
|
|
84
|
+
box-sizing: border-box;
|
|
85
|
+
align-items: center;
|
|
86
|
+
gap: var(--input-gap);
|
|
87
|
+
width: 100%;
|
|
88
|
+
height: var(--field-height);
|
|
89
|
+
padding: 0 var(--field-padding);
|
|
90
|
+
border-radius: var(--input-border-radius);
|
|
91
|
+
background: var(--field-background);
|
|
92
|
+
box-shadow: inset 0 0 0 1px var(--field-border);
|
|
93
|
+
cursor: text;
|
|
94
|
+
display: flex;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
@media (hover: hover) {
|
|
98
|
+
.component-aUdiT5:not([data-disabled], [data-read-only], [data-error]) .field-MFnvYv:hover {
|
|
99
|
+
box-shadow: inset 0 0 0 1px var(--input-hover-border-color);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.component-aUdiT5:not([data-disabled]) .field-MFnvYv:focus-within {
|
|
104
|
+
box-shadow: none;
|
|
105
|
+
outline: 2px solid var(--input-focus-border-color);
|
|
106
|
+
outline-offset: 0;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.component-aUdiT5[data-error]:not([data-disabled]) .field-MFnvYv:focus-within {
|
|
110
|
+
outline-color: var(--input-error-border-color);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.component-aUdiT5[data-disabled] .field-MFnvYv, .input-IlmcAi:disabled {
|
|
114
|
+
cursor: not-allowed;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.value-ehhHJ8 {
|
|
118
|
+
flex-direction: column;
|
|
119
|
+
flex: 1;
|
|
120
|
+
min-width: 0;
|
|
121
|
+
display: flex;
|
|
122
|
+
position: relative;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.header-bcofZJ {
|
|
126
|
+
align-items: baseline;
|
|
127
|
+
gap: var(--input-gap);
|
|
128
|
+
min-width: 0;
|
|
129
|
+
display: flex;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.innerHeader-Iga0Hb {
|
|
133
|
+
width: 100%;
|
|
134
|
+
position: absolute;
|
|
135
|
+
top: 0;
|
|
136
|
+
left: 0;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.component-aUdiT5 .counter-C758Bk {
|
|
140
|
+
color: var(--field-label-color);
|
|
141
|
+
white-space: nowrap;
|
|
142
|
+
text-align: right;
|
|
143
|
+
font-variant-numeric: tabular-nums;
|
|
144
|
+
flex-shrink: 0;
|
|
145
|
+
margin-left: auto;
|
|
146
|
+
line-height: 16px;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.innerHeader-Iga0Hb .counter-C758Bk {
|
|
150
|
+
line-height: 15px;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.label-tvCOwp {
|
|
154
|
+
min-width: 0;
|
|
155
|
+
color: var(--field-label-color);
|
|
156
|
+
overflow-wrap: anywhere;
|
|
157
|
+
cursor: inherit;
|
|
158
|
+
flex: 1;
|
|
159
|
+
font-size: 13px;
|
|
160
|
+
line-height: 16px;
|
|
161
|
+
display: block;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.component-aUdiT5[data-inner-label] .label-tvCOwp {
|
|
165
|
+
white-space: nowrap;
|
|
166
|
+
text-overflow: ellipsis;
|
|
167
|
+
font-size: var(--field-font-size);
|
|
168
|
+
line-height: var(--field-line-height);
|
|
169
|
+
transition: transform .18s, font-size .18s, line-height .18s;
|
|
170
|
+
overflow: hidden;
|
|
171
|
+
transform: translateY(7.5px);
|
|
172
|
+
|
|
173
|
+
& > span {
|
|
174
|
+
font-size: inherit;
|
|
175
|
+
line-height: inherit;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.component-aUdiT5[data-inner-label][data-filled] .label-tvCOwp, .component-aUdiT5[data-inner-label]:not([data-disabled], [data-read-only]) .value-ehhHJ8:focus-within .label-tvCOwp {
|
|
180
|
+
font-size: 12px;
|
|
181
|
+
line-height: 15px;
|
|
182
|
+
transform: translateY(0);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.component-aUdiT5[data-inner-label] .value-ehhHJ8:has(.input-IlmcAi:autofill) .label-tvCOwp {
|
|
186
|
+
font-size: 12px;
|
|
187
|
+
line-height: 15px;
|
|
188
|
+
transform: translateY(0);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.component-aUdiT5[data-inner-label] .input-IlmcAi {
|
|
192
|
+
margin-top: 15px;
|
|
193
|
+
|
|
194
|
+
&::placeholder {
|
|
195
|
+
opacity: 0;
|
|
196
|
+
transition: opacity .18s;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
.component-aUdiT5[data-inner-label]:not([data-disabled], [data-read-only]) .input-IlmcAi:focus::placeholder {
|
|
201
|
+
opacity: 1;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
.component-aUdiT5[data-inner-counter]:not([data-inner-label]) .input-IlmcAi {
|
|
205
|
+
margin-top: 15px;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
@media (prefers-reduced-motion: reduce) {
|
|
209
|
+
.component-aUdiT5[data-inner-label] .label-tvCOwp, .component-aUdiT5[data-inner-label] .input-IlmcAi::placeholder {
|
|
210
|
+
transition: none;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.input-IlmcAi {
|
|
215
|
+
box-sizing: border-box;
|
|
216
|
+
width: 100%;
|
|
217
|
+
min-width: 0;
|
|
218
|
+
height: var(--field-line-height);
|
|
219
|
+
color: var(--field-color);
|
|
220
|
+
font-family: var(--font-primary);
|
|
221
|
+
font-size: var(--field-font-size);
|
|
222
|
+
font-weight: 400;
|
|
223
|
+
line-height: var(--field-line-height);
|
|
224
|
+
text-overflow: ellipsis;
|
|
225
|
+
appearance: none;
|
|
226
|
+
background: none;
|
|
227
|
+
border: 0;
|
|
228
|
+
border-radius: 0;
|
|
229
|
+
outline: none;
|
|
230
|
+
margin: 0;
|
|
231
|
+
padding: 0;
|
|
232
|
+
display: block;
|
|
233
|
+
|
|
234
|
+
&:focus {
|
|
235
|
+
text-overflow: clip;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
&::placeholder {
|
|
239
|
+
color: var(--field-placeholder-color);
|
|
240
|
+
opacity: 1;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
&:disabled {
|
|
244
|
+
opacity: 1;
|
|
245
|
+
-webkit-text-fill-color: var(--field-color);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
&:-webkit-autofill {
|
|
249
|
+
-webkit-text-fill-color: var(--field-color);
|
|
250
|
+
box-shadow: inset 0 0 0 1000px var(--field-background);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
&::-webkit-search-cancel-button {
|
|
254
|
+
appearance: none;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
&::-webkit-search-decoration {
|
|
258
|
+
appearance: none;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
.addon-OXeKuK {
|
|
263
|
+
align-items: center;
|
|
264
|
+
gap: var(--input-gap);
|
|
265
|
+
color: var(--field-icon-color);
|
|
266
|
+
font-size: var(--field-font-size);
|
|
267
|
+
line-height: var(--field-line-height);
|
|
268
|
+
flex-shrink: 0;
|
|
269
|
+
display: flex;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
.component-aUdiT5 .clear-A5Krz8 {
|
|
273
|
+
width: 24px;
|
|
274
|
+
min-width: 0;
|
|
275
|
+
height: 24px;
|
|
276
|
+
min-height: 0;
|
|
277
|
+
color: var(--field-icon-color);
|
|
278
|
+
background: none;
|
|
279
|
+
flex: 0 0 24px;
|
|
280
|
+
margin: 0 -4px;
|
|
281
|
+
padding: 0;
|
|
282
|
+
|
|
283
|
+
&:hover, &:active {
|
|
284
|
+
color: var(--field-icon-color);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
&:focus-visible {
|
|
288
|
+
outline: 2px solid var(--input-focus-border-color);
|
|
289
|
+
outline-offset: 2px;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
& svg {
|
|
293
|
+
width: var(--input-icon-size);
|
|
294
|
+
height: var(--input-icon-size);
|
|
295
|
+
display: block;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
.component-aUdiT5 .message-ArUnJU {
|
|
300
|
+
color: var(--input-hint-color);
|
|
301
|
+
overflow-wrap: anywhere;
|
|
302
|
+
line-height: 15px;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
.component-aUdiT5[data-error] .message-ArUnJU {
|
|
306
|
+
color: var(--input-error-message-color);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
.component-aUdiT5[data-disabled] .message-ArUnJU {
|
|
310
|
+
color: var(--input-disabled-color);
|
|
311
|
+
}
|
|
312
|
+
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type ChangeEvent, type MouseEvent } from 'react';
|
|
2
|
+
export declare const Input: import("react").ForwardRefExoticComponent<Omit<import("react").InputHTMLAttributes<HTMLInputElement>, "size" | "value" | "defaultValue" | "children" | "onChange" | "maxLength"> & {
|
|
3
|
+
value?: string;
|
|
4
|
+
defaultValue?: string;
|
|
5
|
+
maxLength?: number;
|
|
6
|
+
showValueLength?: boolean;
|
|
7
|
+
view?: "primary" | "secondary";
|
|
8
|
+
size?: 40 | 48 | 56 | 64 | 72;
|
|
9
|
+
block?: boolean;
|
|
10
|
+
label?: import("react").ReactNode;
|
|
11
|
+
labelView?: "outer" | "inner";
|
|
12
|
+
hint?: import("react").ReactNode;
|
|
13
|
+
error?: import("react").ReactNode | boolean;
|
|
14
|
+
leftAddon?: import("react").ReactNode;
|
|
15
|
+
rightAddon?: import("react").ReactNode;
|
|
16
|
+
clear?: boolean;
|
|
17
|
+
clearLabel?: string;
|
|
18
|
+
onChange?: (event: ChangeEvent<HTMLInputElement>, payload: {
|
|
19
|
+
value: string;
|
|
20
|
+
}) => void;
|
|
21
|
+
onClear?: (event: MouseEvent<HTMLButtonElement>) => void;
|
|
22
|
+
className?: string;
|
|
23
|
+
style?: import("react").CSSProperties;
|
|
24
|
+
fieldClassName?: string;
|
|
25
|
+
inputClassName?: string;
|
|
26
|
+
labelClassName?: string;
|
|
27
|
+
messageClassName?: string;
|
|
28
|
+
dataTestId?: string;
|
|
29
|
+
} & import("react").RefAttributes<HTMLInputElement>>;
|
|
@@ -0,0 +1,179 @@
|
|
|
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 index_module from "./index.module.js";
|
|
9
|
+
const Input = /*#__PURE__*/ forwardRef(({ view = 'primary', size = 40, block = false, label, labelView = 'outer', hint, error, leftAddon, rightAddon, clear = false, clearLabel = 'Очистить поле', value, defaultValue, maxLength, showValueLength = false, onChange, onClear, disabled = false, readOnly = false, type = 'text', id, className, style, fieldClassName, inputClassName, labelClassName, messageClassName, dataTestId, ...restProps }, ref)=>{
|
|
10
|
+
const generatedId = useId();
|
|
11
|
+
const inputId = id ?? generatedId;
|
|
12
|
+
const inputRef = useRef(null);
|
|
13
|
+
const [uncontrolledLength, setUncontrolledLength] = useState(defaultValue?.length ?? 0);
|
|
14
|
+
const controlled = void 0 !== value;
|
|
15
|
+
const valueLength = controlled ? value.length : uncontrolledLength;
|
|
16
|
+
const hasValue = valueLength > 0;
|
|
17
|
+
const showCounter = showValueLength && void 0 !== maxLength && Number.isInteger(maxLength) && maxLength >= 0;
|
|
18
|
+
const hasLabel = null != label && false !== label && '' !== label;
|
|
19
|
+
const hasError = Boolean(error);
|
|
20
|
+
const errorMessage = hint && 'boolean' != typeof error ? error : void 0;
|
|
21
|
+
const message = errorMessage || hint;
|
|
22
|
+
const hasMessage = Boolean(message);
|
|
23
|
+
const showClear = clear && hasValue && !disabled && !readOnly;
|
|
24
|
+
const getDataTestId = useDataTestId(dataTestId);
|
|
25
|
+
useImperativeHandle(ref, ()=>inputRef.current, []);
|
|
26
|
+
useEffect(()=>{
|
|
27
|
+
const input = inputRef.current;
|
|
28
|
+
const form = input?.form;
|
|
29
|
+
let active = true;
|
|
30
|
+
const handleReset = (event)=>{
|
|
31
|
+
queueMicrotask(()=>{
|
|
32
|
+
if (active && !event.defaultPrevented) setUncontrolledLength(input?.value.length ?? 0);
|
|
33
|
+
});
|
|
34
|
+
};
|
|
35
|
+
form?.addEventListener('reset', handleReset);
|
|
36
|
+
return ()=>{
|
|
37
|
+
active = false;
|
|
38
|
+
form?.removeEventListener('reset', handleReset);
|
|
39
|
+
};
|
|
40
|
+
}, [
|
|
41
|
+
restProps.form
|
|
42
|
+
]);
|
|
43
|
+
const handleChange = (event)=>{
|
|
44
|
+
const nextValue = event.target.value;
|
|
45
|
+
if (!controlled) setUncontrolledLength(nextValue.length);
|
|
46
|
+
onChange?.(event, {
|
|
47
|
+
value: nextValue
|
|
48
|
+
});
|
|
49
|
+
};
|
|
50
|
+
const handleClear = (event)=>{
|
|
51
|
+
const input = inputRef.current;
|
|
52
|
+
if (!input || !showClear) return;
|
|
53
|
+
if (!controlled) {
|
|
54
|
+
input.value = '';
|
|
55
|
+
setUncontrolledLength(0);
|
|
56
|
+
}
|
|
57
|
+
onClear?.(event);
|
|
58
|
+
input.focus();
|
|
59
|
+
};
|
|
60
|
+
const handleFieldClick = (event)=>{
|
|
61
|
+
if (disabled || event.defaultPrevented || !(event.target instanceof Element)) return;
|
|
62
|
+
const interactiveTarget = event.target.closest('input, button, a, label, select, textarea, [role="button"], [contenteditable], [tabindex]');
|
|
63
|
+
if (interactiveTarget && event.currentTarget.contains(interactiveTarget)) return;
|
|
64
|
+
inputRef.current?.focus();
|
|
65
|
+
};
|
|
66
|
+
const labelElement = hasLabel && /*#__PURE__*/ jsx("label", {
|
|
67
|
+
htmlFor: inputId,
|
|
68
|
+
className: classnames(index_module.label, labelClassName),
|
|
69
|
+
"data-test-id": getDataTestId('label'),
|
|
70
|
+
children: /*#__PURE__*/ jsx(TypographyText, {
|
|
71
|
+
size: 'inner' === labelView ? 12 : 13,
|
|
72
|
+
weight: 'inner' === labelView ? 'regular' : 'medium',
|
|
73
|
+
children: label
|
|
74
|
+
})
|
|
75
|
+
});
|
|
76
|
+
const counterElement = showCounter && /*#__PURE__*/ jsxs(TypographyText, {
|
|
77
|
+
tag: "span",
|
|
78
|
+
size: 11,
|
|
79
|
+
className: index_module.counter,
|
|
80
|
+
style: {
|
|
81
|
+
minWidth: `${2 * String(maxLength).length + 1}ch`
|
|
82
|
+
},
|
|
83
|
+
dataTestId: getDataTestId('counter'),
|
|
84
|
+
children: [
|
|
85
|
+
valueLength,
|
|
86
|
+
"/",
|
|
87
|
+
maxLength
|
|
88
|
+
]
|
|
89
|
+
});
|
|
90
|
+
return /*#__PURE__*/ jsxs("div", {
|
|
91
|
+
className: classnames(index_module.component, index_module[`size-${size}`], className),
|
|
92
|
+
style: style,
|
|
93
|
+
"data-view": view,
|
|
94
|
+
"data-block": block || void 0,
|
|
95
|
+
"data-disabled": disabled || void 0,
|
|
96
|
+
"data-read-only": readOnly || void 0,
|
|
97
|
+
"data-error": hasError || void 0,
|
|
98
|
+
"data-label-view": labelView,
|
|
99
|
+
"data-inner-label": 'inner' === labelView && hasLabel || void 0,
|
|
100
|
+
"data-inner-counter": 'inner' === labelView && showCounter || void 0,
|
|
101
|
+
"data-filled": hasValue || void 0,
|
|
102
|
+
"data-test-id": getDataTestId('form-control'),
|
|
103
|
+
children: [
|
|
104
|
+
'outer' === labelView && (hasLabel || showCounter) && /*#__PURE__*/ jsxs("div", {
|
|
105
|
+
className: index_module.header,
|
|
106
|
+
children: [
|
|
107
|
+
labelElement,
|
|
108
|
+
counterElement
|
|
109
|
+
]
|
|
110
|
+
}),
|
|
111
|
+
/*#__PURE__*/ jsxs("div", {
|
|
112
|
+
className: classnames(index_module.field, fieldClassName),
|
|
113
|
+
onClick: handleFieldClick,
|
|
114
|
+
"data-test-id": getDataTestId('field'),
|
|
115
|
+
children: [
|
|
116
|
+
leftAddon && /*#__PURE__*/ jsx("div", {
|
|
117
|
+
className: index_module.addon,
|
|
118
|
+
"data-test-id": getDataTestId('left-addon'),
|
|
119
|
+
children: leftAddon
|
|
120
|
+
}),
|
|
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("input", {
|
|
132
|
+
...restProps,
|
|
133
|
+
ref: inputRef,
|
|
134
|
+
id: inputId,
|
|
135
|
+
type: type,
|
|
136
|
+
value: value,
|
|
137
|
+
defaultValue: defaultValue,
|
|
138
|
+
maxLength: maxLength,
|
|
139
|
+
disabled: disabled,
|
|
140
|
+
readOnly: readOnly,
|
|
141
|
+
onChange: handleChange,
|
|
142
|
+
className: classnames(index_module.input, inputClassName),
|
|
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
|
+
"aria-label": clearLabel,
|
|
152
|
+
onMouseDown: (event)=>event.preventDefault(),
|
|
153
|
+
onClick: handleClear,
|
|
154
|
+
dataTestId: getDataTestId('clear'),
|
|
155
|
+
children: /*#__PURE__*/ jsx(CrossHeavyMIcon, {
|
|
156
|
+
width: 16,
|
|
157
|
+
height: 16,
|
|
158
|
+
"aria-hidden": "true"
|
|
159
|
+
})
|
|
160
|
+
}),
|
|
161
|
+
rightAddon && /*#__PURE__*/ jsx("div", {
|
|
162
|
+
className: index_module.addon,
|
|
163
|
+
"data-test-id": getDataTestId('right-addon'),
|
|
164
|
+
children: rightAddon
|
|
165
|
+
})
|
|
166
|
+
]
|
|
167
|
+
}),
|
|
168
|
+
hasMessage && /*#__PURE__*/ jsx(TypographyText, {
|
|
169
|
+
tag: "div",
|
|
170
|
+
size: 12,
|
|
171
|
+
className: classnames(index_module.message, messageClassName),
|
|
172
|
+
dataTestId: getDataTestId(errorMessage ? 'error' : 'hint'),
|
|
173
|
+
children: message
|
|
174
|
+
})
|
|
175
|
+
]
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
Input.displayName = 'Input';
|
|
179
|
+
export { Input };
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import type { ChangeEvent, CSSProperties, InputHTMLAttributes, MouseEvent, ReactNode } from 'react';
|
|
2
|
+
export type InputProps = Omit<InputHTMLAttributes<HTMLInputElement>, 'size' | 'value' | 'defaultValue' | 'maxLength' | 'onChange' | 'children'> & {
|
|
3
|
+
/**
|
|
4
|
+
* Значение управляемого поля.
|
|
5
|
+
*/
|
|
6
|
+
value?: string;
|
|
7
|
+
/**
|
|
8
|
+
* Начальное значение неуправляемого поля.
|
|
9
|
+
*/
|
|
10
|
+
defaultValue?: string;
|
|
11
|
+
/**
|
|
12
|
+
* Максимальная длина ввода. Передаётся в нативный maxLength.
|
|
13
|
+
* Длина считается в кодовых единицах UTF-16, как в HTML input.
|
|
14
|
+
*/
|
|
15
|
+
maxLength?: number;
|
|
16
|
+
/**
|
|
17
|
+
* Показывает текущую длину и maxLength, например 13/64.
|
|
18
|
+
* Работает только при заданном неотрицательном целом maxLength.
|
|
19
|
+
* При inner расположен внутри поля, при outer — рядом с внешним label.
|
|
20
|
+
* @default false
|
|
21
|
+
*/
|
|
22
|
+
showValueLength?: boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Вид поля: белый primary или светлый secondary.
|
|
25
|
+
* @default 'primary'
|
|
26
|
+
*/
|
|
27
|
+
view?: 'primary' | 'secondary';
|
|
28
|
+
/**
|
|
29
|
+
* Высота поля в пикселях без внешнего label и сообщения.
|
|
30
|
+
* @default 40
|
|
31
|
+
*/
|
|
32
|
+
size?: 40 | 48 | 56 | 64 | 72;
|
|
33
|
+
/**
|
|
34
|
+
* Растягивает компонент на ширину контейнера.
|
|
35
|
+
* @default false
|
|
36
|
+
*/
|
|
37
|
+
block?: boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Название поля. Связывается с input через htmlFor и id.
|
|
40
|
+
*/
|
|
41
|
+
label?: ReactNode;
|
|
42
|
+
/**
|
|
43
|
+
* Положение label: снаружи или внутри поля.
|
|
44
|
+
* Внутренний label поднимается при фокусе или заполнении поля.
|
|
45
|
+
* @default 'outer'
|
|
46
|
+
*/
|
|
47
|
+
labelView?: 'outer' | 'inner';
|
|
48
|
+
/**
|
|
49
|
+
* Подсказка под полем. При наличии сообщения об ошибке заменяется им.
|
|
50
|
+
*/
|
|
51
|
+
hint?: ReactNode;
|
|
52
|
+
/**
|
|
53
|
+
* Сообщение об ошибке или true для состояния ошибки без сообщения.
|
|
54
|
+
* Сообщение заменяет hint; без hint отображается только состояние ошибки.
|
|
55
|
+
*/
|
|
56
|
+
error?: ReactNode | boolean;
|
|
57
|
+
/**
|
|
58
|
+
* Аддон слева от значения, например иконка поиска.
|
|
59
|
+
*/
|
|
60
|
+
leftAddon?: ReactNode;
|
|
61
|
+
/**
|
|
62
|
+
* Аддон справа от значения, например единица измерения или кнопка.
|
|
63
|
+
*/
|
|
64
|
+
rightAddon?: ReactNode;
|
|
65
|
+
/**
|
|
66
|
+
* Показывает кнопку очистки заполненного поля. Скрыта в disabled и readOnly.
|
|
67
|
+
* @default false
|
|
68
|
+
*/
|
|
69
|
+
clear?: boolean;
|
|
70
|
+
/**
|
|
71
|
+
* Доступное имя кнопки очистки.
|
|
72
|
+
* @default 'Очистить поле'
|
|
73
|
+
*/
|
|
74
|
+
clearLabel?: string;
|
|
75
|
+
/**
|
|
76
|
+
* Обработчик ввода с нативным событием React и текущим значением.
|
|
77
|
+
*/
|
|
78
|
+
onChange?: (event: ChangeEvent<HTMLInputElement>, payload: {
|
|
79
|
+
value: string;
|
|
80
|
+
}) => void;
|
|
81
|
+
/**
|
|
82
|
+
* Обработчик очистки. Не вызывает onChange.
|
|
83
|
+
* Для управляемого поля обновите value в этом обработчике.
|
|
84
|
+
*/
|
|
85
|
+
onClear?: (event: MouseEvent<HTMLButtonElement>) => void;
|
|
86
|
+
/**
|
|
87
|
+
* Класс обёртки компонента.
|
|
88
|
+
*/
|
|
89
|
+
className?: string;
|
|
90
|
+
/**
|
|
91
|
+
* Стили обёртки компонента, включая ширину и CSS-переменные.
|
|
92
|
+
*/
|
|
93
|
+
style?: CSSProperties;
|
|
94
|
+
/**
|
|
95
|
+
* Класс визуального поля с аддонами.
|
|
96
|
+
*/
|
|
97
|
+
fieldClassName?: string;
|
|
98
|
+
/**
|
|
99
|
+
* Класс нативного input.
|
|
100
|
+
*/
|
|
101
|
+
inputClassName?: string;
|
|
102
|
+
/**
|
|
103
|
+
* Класс label.
|
|
104
|
+
*/
|
|
105
|
+
labelClassName?: string;
|
|
106
|
+
/**
|
|
107
|
+
* Класс подсказки или сообщения об ошибке.
|
|
108
|
+
*/
|
|
109
|
+
messageClassName?: string;
|
|
110
|
+
/**
|
|
111
|
+
* Идентификатор нативного input. Части получают суффиксы -form-control,
|
|
112
|
+
* -field, -label, -hint, -error, -left-addon, -right-addon, -clear и -counter.
|
|
113
|
+
*/
|
|
114
|
+
dataTestId?: string;
|
|
115
|
+
};
|
|
File without changes
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
--input-primary-background-color: #fff;
|
|
3
|
+
--input-secondary-background-color: #f5f3ec;
|
|
4
|
+
--input-color: #1c1a18;
|
|
5
|
+
--input-placeholder-color: #736554;
|
|
6
|
+
--input-label-color: #736554;
|
|
7
|
+
--input-hint-color: #736554;
|
|
8
|
+
--input-icon-color: #736554;
|
|
9
|
+
--input-border-color: transparent;
|
|
10
|
+
--input-hover-border-color: #c8beaa;
|
|
11
|
+
--input-focus-border-color: #d95300;
|
|
12
|
+
--input-error-border-color: #ef5350;
|
|
13
|
+
--input-error-message-color: #b42318;
|
|
14
|
+
--input-disabled-background-color: #cfd1cb;
|
|
15
|
+
--input-disabled-color: #858981;
|
|
16
|
+
--input-read-only-border-color: #c8beaa;
|
|
17
|
+
--input-border-radius: 8px;
|
|
18
|
+
--input-gap: 8px;
|
|
19
|
+
--input-icon-size: 16px;
|
|
20
|
+
--input-width: 320px;
|
|
21
|
+
--input-font-size-40: 13px;
|
|
22
|
+
--input-font-size-48: 13px;
|
|
23
|
+
--input-font-size-56: 14px;
|
|
24
|
+
--input-font-size-64: 14px;
|
|
25
|
+
--input-font-size-72: 16px;
|
|
26
|
+
--input-padding-40: 12px;
|
|
27
|
+
--input-padding-48: 12px;
|
|
28
|
+
--input-padding-56: 16px;
|
|
29
|
+
--input-padding-64: 16px;
|
|
30
|
+
--input-padding-72: 20px;
|
|
31
|
+
}
|
|
32
|
+
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import { jsx } from "react/jsx-runtime";
|
|
2
2
|
import classnames from "classnames";
|
|
3
3
|
import { forwardRef, useContext } from "react";
|
|
4
|
+
import { useDataTestId } from "../../../../hooks/use-data-test-id.js";
|
|
4
5
|
import { ModalContext } from "../../context.js";
|
|
5
6
|
import index_module from "./index.module.js";
|
|
6
7
|
const Content = /*#__PURE__*/ forwardRef(({ children, className, dataTestId, ...restProps }, ref)=>{
|
|
7
8
|
const { dataTestId: modalDataTestId } = useContext(ModalContext);
|
|
8
|
-
const
|
|
9
|
+
const getDataTestId = useDataTestId(dataTestId ?? modalDataTestId);
|
|
9
10
|
return /*#__PURE__*/ jsx("div", {
|
|
10
11
|
...restProps,
|
|
11
12
|
ref: ref,
|
|
12
13
|
className: classnames(index_module.component, className),
|
|
13
|
-
"data-test-id":
|
|
14
|
+
"data-test-id": getDataTestId('content'),
|
|
14
15
|
children: children
|
|
15
16
|
});
|
|
16
17
|
});
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
2
|
import classnames from "classnames";
|
|
3
3
|
import react, { forwardRef, useContext } from "react";
|
|
4
|
+
import { useDataTestId } from "../../../../hooks/use-data-test-id.js";
|
|
4
5
|
import { ModalContext } from "../../context.js";
|
|
5
6
|
import index_module from "./index.module.js";
|
|
6
7
|
const Controls = /*#__PURE__*/ forwardRef(({ primary, secondary, layout = 'end', gap = 12, className, children, dataTestId, ...restProps }, ref)=>{
|
|
7
8
|
const { dataTestId: modalDataTestId } = useContext(ModalContext);
|
|
8
|
-
const
|
|
9
|
+
const getDataTestId = useDataTestId(dataTestId ?? modalDataTestId);
|
|
9
10
|
return /*#__PURE__*/ jsx("div", {
|
|
10
11
|
...restProps,
|
|
11
12
|
ref: ref,
|
|
12
13
|
className: classnames(index_module.component, index_module[layout], index_module[`gap-${gap}`], className),
|
|
13
|
-
"data-test-id":
|
|
14
|
+
"data-test-id": getDataTestId('controls'),
|
|
14
15
|
children: children ?? /*#__PURE__*/ jsxs(react.Fragment, {
|
|
15
16
|
children: [
|
|
16
17
|
secondary,
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import { jsx } from "react/jsx-runtime";
|
|
2
2
|
import classnames from "classnames";
|
|
3
3
|
import { forwardRef, useContext } from "react";
|
|
4
|
+
import { useDataTestId } from "../../../../hooks/use-data-test-id.js";
|
|
4
5
|
import { ModalContext } from "../../context.js";
|
|
5
6
|
import index_module from "./index.module.js";
|
|
6
7
|
const Footer = /*#__PURE__*/ forwardRef(({ children, className, dataTestId, ...restProps }, ref)=>{
|
|
7
8
|
const { dataTestId: modalDataTestId } = useContext(ModalContext);
|
|
8
|
-
const
|
|
9
|
+
const getDataTestId = useDataTestId(dataTestId ?? modalDataTestId);
|
|
9
10
|
return /*#__PURE__*/ jsx("div", {
|
|
10
11
|
...restProps,
|
|
11
12
|
ref: ref,
|
|
12
13
|
className: classnames(index_module.component, className),
|
|
13
|
-
"data-test-id":
|
|
14
|
+
"data-test-id": getDataTestId('footer'),
|
|
14
15
|
children: children
|
|
15
16
|
});
|
|
16
17
|
});
|
|
@@ -2,13 +2,14 @@ import { jsx, jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { CrossHeavyMIcon } from "@koslibs/icons/CrossHeavyMIcon";
|
|
3
3
|
import classnames from "classnames";
|
|
4
4
|
import { forwardRef, useContext } from "react";
|
|
5
|
+
import { useDataTestId } from "../../../../hooks/use-data-test-id.js";
|
|
5
6
|
import { Button } from "../../../Button/index.js";
|
|
6
7
|
import { TypographyText } from "../../../Typography/index.js";
|
|
7
8
|
import { ModalContext } from "../../context.js";
|
|
8
9
|
import index_module from "./index.module.js";
|
|
9
10
|
const Header = /*#__PURE__*/ forwardRef(({ title, subtitle, hasCloser = true, className, dataTestId, ...restProps }, ref)=>{
|
|
10
11
|
const { onClose, dataTestId: modalDataTestId } = useContext(ModalContext);
|
|
11
|
-
const
|
|
12
|
+
const getDataTestId = useDataTestId(dataTestId ?? modalDataTestId);
|
|
12
13
|
const handleClick = ()=>{
|
|
13
14
|
onClose?.();
|
|
14
15
|
};
|
|
@@ -16,7 +17,7 @@ const Header = /*#__PURE__*/ forwardRef(({ title, subtitle, hasCloser = true, cl
|
|
|
16
17
|
...restProps,
|
|
17
18
|
ref: ref,
|
|
18
19
|
className: classnames(index_module.component, className),
|
|
19
|
-
"data-test-id":
|
|
20
|
+
"data-test-id": getDataTestId('header'),
|
|
20
21
|
children: [
|
|
21
22
|
/*#__PURE__*/ jsxs("div", {
|
|
22
23
|
className: index_module.content,
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
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';
|
|
3
5
|
export { Spinner } from './components/Spinner/index.js';
|
|
4
6
|
export type { SpinnerProps } from './components/Spinner/index.js';
|
|
5
7
|
export { Typography, TypographyText, TypographyTitle, Text, Title } from './components/Typography/index.js';
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { Button } from "./components/Button/index.js";
|
|
2
|
+
export { Input } from "./components/Input/index.js";
|
|
2
3
|
export { Spinner } from "./components/Spinner/index.js";
|
|
3
4
|
export { Text, Title, Typography, TypographyText, TypographyTitle } from "./components/Typography/index.js";
|
|
4
5
|
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.5.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
|
}
|