@koslibs/components 1.7.0 → 1.9.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 +14 -0
- package/dist/components/Select/components/select-dropdown.d.ts +7 -0
- package/dist/components/Select/components/select-dropdown.js +96 -0
- package/dist/components/Select/hooks/use-select-state.d.ts +23 -0
- package/dist/components/Select/hooks/use-select-state.js +183 -0
- package/dist/components/Select/index.d.ts +2 -0
- package/dist/components/Select/index.js +1 -0
- package/dist/components/Select/index.module.js +44 -0
- package/dist/components/Select/index_module.css +334 -0
- package/dist/components/Select/select.d.ts +2 -0
- package/dist/components/Select/select.js +161 -0
- package/dist/components/Select/types.d.ts +145 -0
- package/dist/components/Select/types.js +0 -0
- package/dist/components/Select/vars.css +34 -0
- package/dist/components/Status/index.d.ts +2 -0
- package/dist/components/Status/index.js +1 -0
- package/dist/components/Status/index.module.js +25 -0
- package/dist/components/Status/index_module.css +109 -0
- package/dist/components/Status/status.d.ts +85 -0
- package/dist/components/Status/status.js +35 -0
- package/dist/components/Status/vars.css +36 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# @koslibs/components
|
|
2
2
|
|
|
3
|
+
## 1.9.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#10](https://github.com/koslibs/components/pull/10) [`01aa734`](https://github.com/koslibs/components/commit/01aa734718eb2b649cc8ec361368b825fc8432d6) Thanks [@holypower777](https://github.com/holypower777)! - Добавлен Select по документации Figma: одиночный и множественный выбор, локальный поиск с внутренним состоянием запроса и сообщением при отсутствии совпадений, список на Popover, форма и примеры Storybook. Выбранные значения отображаются через запятую с однострочным многоточием. Выбранные строки отличаются от поиска и hover; внутренний label плавно перемещается по аналогии с Input. Очистка использует тонкий крестик 14×14 px с постоянным label «close».
|
|
8
|
+
|
|
9
|
+
## 1.8.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- [#9](https://github.com/koslibs/components/pull/9) [`03ecceb`](https://github.com/koslibs/components/commit/03ecceb7d7f6831f6d4e6e85b80373aeb6ffd743) Thanks [@holypower777](https://github.com/holypower777)! - Добавлен Status: размеры 20/24/32/40, формы rectangular/rounded, восемь цветов,
|
|
14
|
+
декоративные аддоны, подпись message, uppercase (по умолчанию false),
|
|
15
|
+
сокращение подписи при ограничении ширины и Storybook Docs/Playground.
|
|
16
|
+
|
|
3
17
|
## 1.7.0
|
|
4
18
|
|
|
5
19
|
### Minor Changes
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { SelectState } from '../hooks/use-select-state.js';
|
|
2
|
+
import type { SelectProps } from '../types.js';
|
|
3
|
+
type Props = Pick<SelectProps, 'multiple' | 'searchable' | 'searchPlaceholder' | 'noResultsText' | 'dataTestId'> & {
|
|
4
|
+
state: SelectState;
|
|
5
|
+
};
|
|
6
|
+
export declare function SelectDropdown({ state, multiple, searchable, searchPlaceholder, noResultsText, dataTestId, }: Props): import("react").JSX.Element;
|
|
7
|
+
export {};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { SearchIcon } from "@koslibs/icons/SearchIcon";
|
|
3
|
+
import { TickIcon } from "@koslibs/icons/TickIcon";
|
|
4
|
+
import classnames from "classnames";
|
|
5
|
+
import { useEffect, useRef } from "react";
|
|
6
|
+
import { useDataTestId } from "../../../hooks/use-data-test-id.js";
|
|
7
|
+
import { Input } from "../../Input/index.js";
|
|
8
|
+
import { TypographyText } from "../../Typography/text.js";
|
|
9
|
+
import index_module from "../index.module.js";
|
|
10
|
+
function SelectDropdown({ state, multiple, searchable, searchPlaceholder = 'Поиск по списку', noResultsText = 'Ничего не найдено', dataTestId }) {
|
|
11
|
+
const getDataTestId = useDataTestId(dataTestId);
|
|
12
|
+
const { options, selectedIds, search, activeId, setSearch, handleKeyDown, select } = state;
|
|
13
|
+
const activeOptionRef = useRef(null);
|
|
14
|
+
useEffect(()=>{
|
|
15
|
+
activeOptionRef.current?.scrollIntoView?.({
|
|
16
|
+
block: 'nearest'
|
|
17
|
+
});
|
|
18
|
+
}, [
|
|
19
|
+
activeId
|
|
20
|
+
]);
|
|
21
|
+
return /*#__PURE__*/ jsxs(Fragment, {
|
|
22
|
+
children: [
|
|
23
|
+
searchable && /*#__PURE__*/ jsx(Input, {
|
|
24
|
+
block: true,
|
|
25
|
+
autoFocus: true,
|
|
26
|
+
size: 40,
|
|
27
|
+
view: "secondary",
|
|
28
|
+
value: search,
|
|
29
|
+
placeholder: searchPlaceholder,
|
|
30
|
+
leftAddon: /*#__PURE__*/ jsx(SearchIcon, {
|
|
31
|
+
width: 16,
|
|
32
|
+
height: 16,
|
|
33
|
+
"aria-hidden": "true"
|
|
34
|
+
}),
|
|
35
|
+
clear: true,
|
|
36
|
+
clearLabel: "close",
|
|
37
|
+
onChange: (_, { value })=>setSearch(value),
|
|
38
|
+
onClear: ()=>setSearch(''),
|
|
39
|
+
onKeyDown: handleKeyDown,
|
|
40
|
+
className: index_module.search,
|
|
41
|
+
dataTestId: getDataTestId('search')
|
|
42
|
+
}),
|
|
43
|
+
0 === options.length && /*#__PURE__*/ jsxs("div", {
|
|
44
|
+
className: classnames(index_module.listState, index_module.listHint),
|
|
45
|
+
"data-test-id": getDataTestId('empty'),
|
|
46
|
+
children: [
|
|
47
|
+
/*#__PURE__*/ jsx(TypographyText, {
|
|
48
|
+
children: noResultsText
|
|
49
|
+
}),
|
|
50
|
+
search.trim() && /*#__PURE__*/ jsx(TypographyText, {
|
|
51
|
+
size: 12,
|
|
52
|
+
children: "Измените поисковый запрос"
|
|
53
|
+
})
|
|
54
|
+
]
|
|
55
|
+
}),
|
|
56
|
+
options.length > 0 && /*#__PURE__*/ jsx("div", {
|
|
57
|
+
className: index_module.options,
|
|
58
|
+
"data-test-id": getDataTestId('options'),
|
|
59
|
+
children: options.map((option)=>{
|
|
60
|
+
const selected = selectedIds.includes(option.id);
|
|
61
|
+
return /*#__PURE__*/ jsxs("button", {
|
|
62
|
+
type: "button",
|
|
63
|
+
tabIndex: -1,
|
|
64
|
+
ref: option.id !== activeId || option.disabled ? void 0 : activeOptionRef,
|
|
65
|
+
className: classnames(index_module.option, {
|
|
66
|
+
[index_module.selected]: selected,
|
|
67
|
+
[index_module.active]: option.id === activeId && !option.disabled
|
|
68
|
+
}),
|
|
69
|
+
disabled: option.disabled,
|
|
70
|
+
onMouseDown: (event)=>event.preventDefault(),
|
|
71
|
+
onClick: (event)=>select(option, event),
|
|
72
|
+
"data-test-id": getDataTestId(`option-${option.id}`),
|
|
73
|
+
title: option.label,
|
|
74
|
+
children: [
|
|
75
|
+
/*#__PURE__*/ jsx(TypographyText, {
|
|
76
|
+
size: 13,
|
|
77
|
+
rowLimit: 1,
|
|
78
|
+
className: index_module.optionLabel,
|
|
79
|
+
children: option.label
|
|
80
|
+
}),
|
|
81
|
+
(multiple || selected) && /*#__PURE__*/ jsx("span", {
|
|
82
|
+
className: classnames(index_module.marker, multiple && index_module.checkbox),
|
|
83
|
+
children: selected && /*#__PURE__*/ jsx(TickIcon, {
|
|
84
|
+
width: multiple ? 14 : 18,
|
|
85
|
+
height: multiple ? 14 : 18,
|
|
86
|
+
"aria-hidden": "true"
|
|
87
|
+
})
|
|
88
|
+
})
|
|
89
|
+
]
|
|
90
|
+
}, option.id);
|
|
91
|
+
})
|
|
92
|
+
})
|
|
93
|
+
]
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
export { SelectDropdown };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type ButtonHTMLAttributes, type KeyboardEvent } from 'react';
|
|
2
|
+
import type { SelectChangeEvent, SelectOption, SelectProps } from '../types.js';
|
|
3
|
+
export declare function useSelectState(props: SelectProps): {
|
|
4
|
+
options: SelectOption[];
|
|
5
|
+
selectedIds: string[];
|
|
6
|
+
selectedOptions: SelectOption[];
|
|
7
|
+
search: string;
|
|
8
|
+
open: boolean;
|
|
9
|
+
blocked: boolean | undefined;
|
|
10
|
+
activeId: string | null;
|
|
11
|
+
raisedLabel: boolean;
|
|
12
|
+
buttonRef: import("react").RefObject<HTMLButtonElement>;
|
|
13
|
+
popupRef: import("react").RefObject<HTMLDivElement>;
|
|
14
|
+
anchor: HTMLDivElement | null;
|
|
15
|
+
setAnchor: import("react").Dispatch<import("react").SetStateAction<HTMLDivElement | null>>;
|
|
16
|
+
triggerProps: ButtonHTMLAttributes<HTMLButtonElement>;
|
|
17
|
+
close: () => void;
|
|
18
|
+
select: (option: SelectOption, event: SelectChangeEvent) => void;
|
|
19
|
+
handleKeyDown: (event: KeyboardEvent<HTMLElement>) => void;
|
|
20
|
+
setSearch(value: string): void;
|
|
21
|
+
clear(event: Parameters<NonNullable<SelectProps["onClear"]>>[0]): void;
|
|
22
|
+
};
|
|
23
|
+
export type SelectState = ReturnType<typeof useSelectState>;
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from "react";
|
|
2
|
+
function toIds(value) {
|
|
3
|
+
return [
|
|
4
|
+
...new Set('string' == typeof value ? [
|
|
5
|
+
value
|
|
6
|
+
] : value ?? [])
|
|
7
|
+
];
|
|
8
|
+
}
|
|
9
|
+
function useSelectState(props) {
|
|
10
|
+
const [localIds, setLocalIds] = useState(()=>toIds(props.defaultValue));
|
|
11
|
+
const [localOpen, setLocalOpen] = useState(props.defaultOpen ?? false);
|
|
12
|
+
const [search, setSearch] = useState('');
|
|
13
|
+
const [activeId, setActiveId] = useState(null);
|
|
14
|
+
const [focused, setFocused] = useState(false);
|
|
15
|
+
const [anchor, setAnchor] = useState(null);
|
|
16
|
+
const buttonRef = useRef(null);
|
|
17
|
+
const popupRef = useRef(null);
|
|
18
|
+
const retainedOptions = useRef(new Map());
|
|
19
|
+
const ids = void 0 === props.value ? localIds : toIds(props.value);
|
|
20
|
+
const selectedIds = props.multiple ? ids : ids.slice(0, 1);
|
|
21
|
+
const optionMap = new Map(props.options.map((option)=>[
|
|
22
|
+
option.id,
|
|
23
|
+
option
|
|
24
|
+
]));
|
|
25
|
+
const resolve = (id)=>optionMap.get(id) ?? retainedOptions.current.get(id) ?? {
|
|
26
|
+
id,
|
|
27
|
+
label: id
|
|
28
|
+
};
|
|
29
|
+
const selectedOptions = selectedIds.map(resolve);
|
|
30
|
+
const blocked = props.disabled || props.readOnly;
|
|
31
|
+
const open = !blocked && (props.open ?? localOpen);
|
|
32
|
+
const query = props.searchable ? search.trim().toLocaleLowerCase() : '';
|
|
33
|
+
const options = props.options.filter((option)=>!query || option.label.toLocaleLowerCase().includes(query));
|
|
34
|
+
useEffect(()=>{
|
|
35
|
+
retainedOptions.current = new Map(selectedOptions.map((option)=>[
|
|
36
|
+
option.id,
|
|
37
|
+
option
|
|
38
|
+
]));
|
|
39
|
+
});
|
|
40
|
+
useEffect(()=>{
|
|
41
|
+
if (!open) setSearch('');
|
|
42
|
+
}, [
|
|
43
|
+
open
|
|
44
|
+
]);
|
|
45
|
+
const resetRef = useRef();
|
|
46
|
+
resetRef.current = ()=>{
|
|
47
|
+
if (void 0 === props.value) setLocalIds(toIds(props.defaultValue));
|
|
48
|
+
if (void 0 === props.open) setLocalOpen(false);
|
|
49
|
+
setSearch('');
|
|
50
|
+
};
|
|
51
|
+
useEffect(()=>{
|
|
52
|
+
const form = buttonRef.current?.form;
|
|
53
|
+
let active = true;
|
|
54
|
+
const handleReset = (event)=>{
|
|
55
|
+
queueMicrotask(()=>{
|
|
56
|
+
if (active && !event.defaultPrevented) resetRef.current?.();
|
|
57
|
+
});
|
|
58
|
+
};
|
|
59
|
+
form?.addEventListener('reset', handleReset);
|
|
60
|
+
return ()=>{
|
|
61
|
+
active = false;
|
|
62
|
+
form?.removeEventListener('reset', handleReset);
|
|
63
|
+
};
|
|
64
|
+
}, [
|
|
65
|
+
props.form
|
|
66
|
+
]);
|
|
67
|
+
function setOpen(next) {
|
|
68
|
+
if (next && blocked) return;
|
|
69
|
+
if (void 0 === props.open) setLocalOpen(next);
|
|
70
|
+
if (next !== open) props.onOpenChange?.(next);
|
|
71
|
+
}
|
|
72
|
+
function select(option, event) {
|
|
73
|
+
if (blocked || option.disabled) return;
|
|
74
|
+
let nextIds = [
|
|
75
|
+
option.id
|
|
76
|
+
];
|
|
77
|
+
if (props.multiple) nextIds = selectedIds.includes(option.id) ? selectedIds.filter((id)=>id !== option.id) : [
|
|
78
|
+
...selectedIds,
|
|
79
|
+
option.id
|
|
80
|
+
];
|
|
81
|
+
if (void 0 === props.value) setLocalIds(nextIds);
|
|
82
|
+
if (props.multiple || selectedIds[0] !== option.id) {
|
|
83
|
+
const nextOptions = nextIds.map(resolve);
|
|
84
|
+
if (props.multiple) props.onChange?.(event, {
|
|
85
|
+
value: nextIds,
|
|
86
|
+
selectedOptions: nextOptions
|
|
87
|
+
});
|
|
88
|
+
else props.onChange?.(event, {
|
|
89
|
+
value: option.id,
|
|
90
|
+
selectedOptions: nextOptions
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
if (!props.multiple) {
|
|
94
|
+
setOpen(false);
|
|
95
|
+
buttonRef.current?.focus();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function close() {
|
|
99
|
+
const restoreFocus = popupRef.current?.contains(buttonRef.current?.ownerDocument.activeElement ?? null);
|
|
100
|
+
setOpen(false);
|
|
101
|
+
if (restoreFocus) buttonRef.current?.focus();
|
|
102
|
+
}
|
|
103
|
+
function handleKeyDown(event) {
|
|
104
|
+
if (event.defaultPrevented || blocked) return;
|
|
105
|
+
const { key } = event;
|
|
106
|
+
if ('Tab' === key) {
|
|
107
|
+
if (open) setOpen(false);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const available = options.filter((option)=>!option.disabled);
|
|
111
|
+
if ([
|
|
112
|
+
'ArrowDown',
|
|
113
|
+
'ArrowUp',
|
|
114
|
+
'Home',
|
|
115
|
+
'End'
|
|
116
|
+
].includes(key)) {
|
|
117
|
+
if ([
|
|
118
|
+
'Home',
|
|
119
|
+
'End'
|
|
120
|
+
].includes(key) && (!open || 'INPUT' === event.currentTarget.tagName)) return;
|
|
121
|
+
event.preventDefault();
|
|
122
|
+
setOpen(true);
|
|
123
|
+
const current = available.findIndex((option)=>option.id === activeId);
|
|
124
|
+
let next = current <= 0 ? available.length - 1 : current - 1;
|
|
125
|
+
if ('Home' === key) next = 0;
|
|
126
|
+
if ('End' === key) next = available.length - 1;
|
|
127
|
+
if ('ArrowDown' === key) next = (current + 1) % available.length;
|
|
128
|
+
setActiveId(available[next]?.id ?? null);
|
|
129
|
+
} else if ('Enter' === key && open) {
|
|
130
|
+
event.preventDefault();
|
|
131
|
+
const active = available.find((option)=>option.id === activeId);
|
|
132
|
+
if (active) select(active, event);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const triggerProps = {
|
|
136
|
+
onFocus (event) {
|
|
137
|
+
setFocused(true);
|
|
138
|
+
props.onFocus?.(event);
|
|
139
|
+
},
|
|
140
|
+
onBlur (event) {
|
|
141
|
+
setFocused(false);
|
|
142
|
+
props.onBlur?.(event);
|
|
143
|
+
},
|
|
144
|
+
onClick (event) {
|
|
145
|
+
props.onClick?.(event);
|
|
146
|
+
if (event.defaultPrevented || blocked) return;
|
|
147
|
+
setActiveId(null);
|
|
148
|
+
setOpen(!open);
|
|
149
|
+
},
|
|
150
|
+
onKeyDown (event) {
|
|
151
|
+
props.onKeyDown?.(event);
|
|
152
|
+
handleKeyDown(event);
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
return {
|
|
156
|
+
options,
|
|
157
|
+
selectedIds,
|
|
158
|
+
selectedOptions,
|
|
159
|
+
search,
|
|
160
|
+
open,
|
|
161
|
+
blocked,
|
|
162
|
+
activeId: open ? activeId : null,
|
|
163
|
+
raisedLabel: selectedIds.length > 0 || open || focused && !blocked,
|
|
164
|
+
buttonRef,
|
|
165
|
+
popupRef,
|
|
166
|
+
anchor,
|
|
167
|
+
setAnchor,
|
|
168
|
+
triggerProps,
|
|
169
|
+
close,
|
|
170
|
+
select,
|
|
171
|
+
handleKeyDown,
|
|
172
|
+
setSearch (value) {
|
|
173
|
+
setActiveId(null);
|
|
174
|
+
setSearch(value);
|
|
175
|
+
},
|
|
176
|
+
clear (event) {
|
|
177
|
+
if (void 0 === props.value) setLocalIds([]);
|
|
178
|
+
props.onClear?.(event);
|
|
179
|
+
buttonRef.current?.focus();
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
export { useSelectState };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Select } from "./select.js";
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import "./index_module.css";
|
|
2
|
+
const index_module = {
|
|
3
|
+
component: "component-cM8DTg",
|
|
4
|
+
block: "block-EvThms",
|
|
5
|
+
secondary: "secondary-CnWByB",
|
|
6
|
+
readOnly: "readOnly-zwkIeK",
|
|
7
|
+
error: "error-TrKhzv",
|
|
8
|
+
disabled: "disabled-fZIeN_",
|
|
9
|
+
"size-40": "size-40-naTxHA",
|
|
10
|
+
size40: "size-40-naTxHA",
|
|
11
|
+
"size-48": "size-48-Lf4nSh",
|
|
12
|
+
size48: "size-48-Lf4nSh",
|
|
13
|
+
"size-56": "size-56-U3fog3",
|
|
14
|
+
size56: "size-56-U3fog3",
|
|
15
|
+
"size-64": "size-64-ZMZbdh",
|
|
16
|
+
size64: "size-64-ZMZbdh",
|
|
17
|
+
"size-72": "size-72-byhU5x",
|
|
18
|
+
size72: "size-72-byhU5x",
|
|
19
|
+
field: "field-jGXYY5",
|
|
20
|
+
open: "open-CCkiBd",
|
|
21
|
+
trigger: "trigger-nKdmww",
|
|
22
|
+
valueColumn: "valueColumn-IqzQmw",
|
|
23
|
+
value: "value-flrRUI",
|
|
24
|
+
placeholder: "placeholder-SlfxjN",
|
|
25
|
+
label: "label-rrakc7",
|
|
26
|
+
innerLabel: "innerLabel-njfkYe",
|
|
27
|
+
labelRaised: "labelRaised-iztTyX",
|
|
28
|
+
withInnerLabel: "withInnerLabel-a0m9CO",
|
|
29
|
+
chevron: "chevron-hiAxaW",
|
|
30
|
+
clear: "clear-x9k2FN",
|
|
31
|
+
message: "message-nYZGHP",
|
|
32
|
+
dropdown: "dropdown-ufLXzq",
|
|
33
|
+
search: "search-aVnGDP",
|
|
34
|
+
options: "options-ygwOHf",
|
|
35
|
+
option: "option-d9Lc3i",
|
|
36
|
+
selected: "selected-uq_Ki8",
|
|
37
|
+
active: "active-KEr7O6",
|
|
38
|
+
optionLabel: "optionLabel-g0AQIH",
|
|
39
|
+
marker: "marker-i_70bm",
|
|
40
|
+
checkbox: "checkbox-r7078O",
|
|
41
|
+
listState: "listState-_7We4G",
|
|
42
|
+
listHint: "listHint-C_Zt_z"
|
|
43
|
+
};
|
|
44
|
+
export default index_module;
|