@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 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,2 @@
1
+ export { Select } from './select.js';
2
+ export type { SelectProps, SelectOption, SelectChangeEvent, SelectChangePayload } from './types.js';
@@ -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;