@homecode/ui 4.19.8 → 4.20.0-beta-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/dist/esm/index.js +1 -0
- package/dist/esm/src/components/Autocomplete/Autocomplete.js +3 -1
- package/dist/esm/src/components/Draggable/Draggable.js +1 -1
- package/dist/esm/src/components/Draggable/Draggable.styl.js +2 -2
- package/dist/esm/src/components/Input/Input.js +150 -200
- package/dist/esm/src/components/Input/Input.styl.js +2 -2
- package/dist/esm/src/components/InputFile/InputFile.js +2 -1
- package/dist/esm/src/components/Notifications/Notifications.js +7 -7
- package/dist/esm/src/components/Popup/Popup.js +28 -31
- package/dist/esm/src/components/Router/Link/Link.js +4 -3
- package/dist/esm/src/components/Router/Route.js +3 -1
- package/dist/esm/src/components/Router/Router.js +4 -2
- package/dist/esm/src/components/Select/Select.js +4 -4
- package/dist/esm/src/components/Select/Select2.js +305 -0
- package/dist/esm/src/hooks/useThrottle.js +28 -0
- package/dist/esm/src/services/i18n.js +13 -7
- package/dist/esm/types/src/components/Icon/Icon.d.ts +2 -0
- package/dist/esm/types/src/components/Input/Input.d.ts +1 -42
- package/dist/esm/types/src/components/Lazy/Lazy.d.ts +1 -0
- package/dist/esm/types/src/components/Notifications/Notifications.d.ts +5 -1
- package/dist/esm/types/src/components/Popup/Popup.d.ts +8 -1
- package/dist/esm/types/src/components/Router/Link/Link.d.ts +2 -1
- package/dist/esm/types/src/components/Router/Route.d.ts +4 -4
- package/dist/esm/types/src/components/Select/Select.d.ts +1 -2
- package/dist/esm/types/src/components/Select/Select.helpers.d.ts +1 -1
- package/dist/esm/types/src/components/Select/Select.types.d.ts +5 -3
- package/dist/esm/types/src/components/Select/Select2.d.ts +2 -0
- package/dist/esm/types/src/components/index.d.ts +1 -0
- package/dist/esm/types/src/hooks/useThrottle.d.ts +5 -0
- package/dist/esm/types/src/services/i18n.d.ts +14 -4
- package/package.json +2 -2
|
@@ -7,7 +7,6 @@ import { Portal } from '../Portal/Portal.js';
|
|
|
7
7
|
import S from './Popup.styl.js';
|
|
8
8
|
import Time from 'timen';
|
|
9
9
|
import cn from 'classnames';
|
|
10
|
-
import { createStore } from 'justorm/react';
|
|
11
10
|
import debounce from '../../tools/debounce.js';
|
|
12
11
|
import { getCoords } from '../../tools/dom.js';
|
|
13
12
|
import { isBrowser } from '../../tools/env.js';
|
|
@@ -37,7 +36,6 @@ class Popup extends Component {
|
|
|
37
36
|
needDropOffset = false;
|
|
38
37
|
id;
|
|
39
38
|
parentPopupContent;
|
|
40
|
-
store;
|
|
41
39
|
timers = Time.create();
|
|
42
40
|
scrollParent;
|
|
43
41
|
offset = { ...INITIAL_OFFSET };
|
|
@@ -46,18 +44,17 @@ class Popup extends Component {
|
|
|
46
44
|
direction: '',
|
|
47
45
|
animated: true,
|
|
48
46
|
};
|
|
47
|
+
state = {
|
|
48
|
+
rootPopupId: null,
|
|
49
|
+
isOpen: Boolean(this.props.isOpen),
|
|
50
|
+
isContentVisible: Boolean(this.props.isOpen),
|
|
51
|
+
animating: false,
|
|
52
|
+
direction: this.props.direction,
|
|
53
|
+
triggerBounds: null,
|
|
54
|
+
};
|
|
49
55
|
constructor(props) {
|
|
50
56
|
super(props);
|
|
51
|
-
const isOpen = Boolean(props.isOpen);
|
|
52
57
|
this.id = getId();
|
|
53
|
-
this.store = createStore(this, {
|
|
54
|
-
rootPopupId: null,
|
|
55
|
-
isOpen,
|
|
56
|
-
isContentVisible: isOpen,
|
|
57
|
-
animating: false,
|
|
58
|
-
direction: props.direction,
|
|
59
|
-
triggerBounds: null,
|
|
60
|
-
});
|
|
61
58
|
}
|
|
62
59
|
componentDidMount() {
|
|
63
60
|
const { hoverControl, focusControl } = this.props;
|
|
@@ -65,9 +62,9 @@ class Popup extends Component {
|
|
|
65
62
|
document.addEventListener('pointerdown', this.onDocPointerDown, true);
|
|
66
63
|
document.addEventListener('pointerup', this.onDocPointerUp, true);
|
|
67
64
|
if (parentPopupContent) {
|
|
68
|
-
|
|
69
|
-
getPopupId(parentPopupContent, 'data-
|
|
70
|
-
|
|
65
|
+
const rootPopupId = getPopupId(parentPopupContent, 'data-root-popup-id') ||
|
|
66
|
+
getPopupId(parentPopupContent, 'data-popup-id');
|
|
67
|
+
this.setState({ rootPopupId });
|
|
71
68
|
}
|
|
72
69
|
if (focusControl) {
|
|
73
70
|
document.addEventListener('keydown', this.onDocKeyDown, true);
|
|
@@ -80,7 +77,7 @@ class Popup extends Component {
|
|
|
80
77
|
componentDidUpdate(prevProps) {
|
|
81
78
|
const { isOpen, disabled, hoverControl } = this.props;
|
|
82
79
|
if (disabled) {
|
|
83
|
-
this.
|
|
80
|
+
this.setState({ isOpen: false }); // close when receive disabled=true
|
|
84
81
|
return;
|
|
85
82
|
}
|
|
86
83
|
if (!prevProps.hoverControl && hoverControl)
|
|
@@ -136,7 +133,7 @@ class Popup extends Component {
|
|
|
136
133
|
document.removeEventListener('pointerup', this.checkHover);
|
|
137
134
|
}
|
|
138
135
|
updateBounds() {
|
|
139
|
-
if (this.
|
|
136
|
+
if (this.state.animating || !this.containerElem)
|
|
140
137
|
return;
|
|
141
138
|
if (!this.triggerElem.current)
|
|
142
139
|
return;
|
|
@@ -153,7 +150,7 @@ class Popup extends Component {
|
|
|
153
150
|
this.triggerElem.current.style[key] = value;
|
|
154
151
|
});
|
|
155
152
|
this.updateOffset();
|
|
156
|
-
this.
|
|
153
|
+
this.setState({ triggerBounds: bounds });
|
|
157
154
|
}, 200, { trailing: true });
|
|
158
155
|
prevContentBounds = { width: 0, height: 0 };
|
|
159
156
|
updateOffset = () => {
|
|
@@ -192,7 +189,7 @@ class Popup extends Component {
|
|
|
192
189
|
checkHover = debounce((e) => {
|
|
193
190
|
if (this.isPointerPressedInside)
|
|
194
191
|
return;
|
|
195
|
-
const { isOpen, rootPopupId } = this.
|
|
192
|
+
const { isOpen, rootPopupId } = this.state;
|
|
196
193
|
const overTrigger = this.isPointerOver(e.target, S.trigger);
|
|
197
194
|
const overContent = this.isPointerOver(e.target, S.content);
|
|
198
195
|
if (!isOpen) {
|
|
@@ -233,7 +230,7 @@ class Popup extends Component {
|
|
|
233
230
|
return target.closest(`.${elem}[data-popup-id="${this.id}"]`);
|
|
234
231
|
}
|
|
235
232
|
onScroll = throttle(e => {
|
|
236
|
-
if (!this.
|
|
233
|
+
if (!this.state.isOpen) {
|
|
237
234
|
const { top, left } = this.offset;
|
|
238
235
|
if (left || top) {
|
|
239
236
|
this.offset = { ...INITIAL_OFFSET };
|
|
@@ -252,7 +249,7 @@ class Popup extends Component {
|
|
|
252
249
|
this.pointerDownTarget = null;
|
|
253
250
|
};
|
|
254
251
|
onDocKeyUp = (e) => {
|
|
255
|
-
if (this.
|
|
252
|
+
if (this.state.isOpen && e.key === 'Escape') {
|
|
256
253
|
e.stopPropagation();
|
|
257
254
|
this.close();
|
|
258
255
|
return;
|
|
@@ -293,19 +290,19 @@ class Popup extends Component {
|
|
|
293
290
|
this.updateBounds();
|
|
294
291
|
};
|
|
295
292
|
open = throttle(() => {
|
|
296
|
-
const { rootPopupId } = this.
|
|
297
|
-
if (this.
|
|
293
|
+
const { rootPopupId } = this.state;
|
|
294
|
+
if (this.state.isOpen)
|
|
298
295
|
return;
|
|
299
296
|
this.updateBounds();
|
|
300
297
|
this.subscribeSizeChange();
|
|
301
298
|
this.subscribeScroll();
|
|
302
|
-
this.
|
|
299
|
+
this.setState({ isContentVisible: true });
|
|
303
300
|
this.changeState(true, this.afterOpen);
|
|
304
301
|
if (rootPopupId)
|
|
305
302
|
setChild(rootPopupId, this.id);
|
|
306
303
|
}, 100);
|
|
307
304
|
close = () => {
|
|
308
|
-
if (!this.
|
|
305
|
+
if (!this.state.isOpen)
|
|
309
306
|
return;
|
|
310
307
|
this.unsubscribeSizeChange();
|
|
311
308
|
this.changeState(false, this.afterClose);
|
|
@@ -313,12 +310,12 @@ class Popup extends Component {
|
|
|
313
310
|
changeState(isOpen, callback) {
|
|
314
311
|
const { animated, onOpen, onClose } = this.props;
|
|
315
312
|
this.timers.clear();
|
|
316
|
-
this.
|
|
313
|
+
this.setState({ isOpen });
|
|
317
314
|
isOpen ? onOpen?.() : onClose?.();
|
|
318
315
|
if (animated) {
|
|
319
|
-
this.
|
|
316
|
+
this.setState({ animating: true });
|
|
320
317
|
this.timers.after(ANIMATION_DURATION + 500, () => {
|
|
321
|
-
this.
|
|
318
|
+
this.setState({ animating: false });
|
|
322
319
|
callback();
|
|
323
320
|
});
|
|
324
321
|
}
|
|
@@ -330,7 +327,7 @@ class Popup extends Component {
|
|
|
330
327
|
this.props.onAfterOpen?.();
|
|
331
328
|
};
|
|
332
329
|
afterClose = () => {
|
|
333
|
-
this.
|
|
330
|
+
this.setState({ isContentVisible: false });
|
|
334
331
|
this.dropOffset();
|
|
335
332
|
this.props.onAfterClose?.();
|
|
336
333
|
};
|
|
@@ -341,12 +338,12 @@ class Popup extends Component {
|
|
|
341
338
|
this.applyOffset();
|
|
342
339
|
}
|
|
343
340
|
toggle = throttle(() => {
|
|
344
|
-
this.
|
|
341
|
+
this.state.isOpen ? this.close() : this.open();
|
|
345
342
|
}, 100);
|
|
346
343
|
renderTrigger() {
|
|
347
344
|
const { trigger, content, disabled, focusControl, hoverControl, ...rest } = this.props;
|
|
348
345
|
const triggerProps = { ...rest.triggerProps };
|
|
349
|
-
const { isOpen } = this.
|
|
346
|
+
const { isOpen } = this.state;
|
|
350
347
|
if (!trigger)
|
|
351
348
|
return null;
|
|
352
349
|
const disableTrigger = disabled || !trigger;
|
|
@@ -370,7 +367,7 @@ class Popup extends Component {
|
|
|
370
367
|
}
|
|
371
368
|
renderContent() {
|
|
372
369
|
const { content, contentProps = {}, wrapperProps = {}, size, disabled, inline, outlined, animated, paranja, blur, elevation, } = this.props;
|
|
373
|
-
const { isOpen, isContentVisible, animating, direction, triggerBounds, rootPopupId, } = this.
|
|
370
|
+
const { isOpen, isContentVisible, animating, direction, triggerBounds, rootPopupId, } = this.state;
|
|
374
371
|
if (disabled)
|
|
375
372
|
return null;
|
|
376
373
|
const wrapperClasses = cn(S.contentWrapper, blur && S.blur, inline && S.inline, isOpen && S.isOpen, wrapperProps.className);
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
2
2
|
import { useRef, useContext, useMemo, useCallback } from 'react';
|
|
3
|
-
import {
|
|
3
|
+
import { useStore } from 'justorm/react';
|
|
4
4
|
import cn from 'classnames';
|
|
5
5
|
import { Icon } from '../../Icon/Icon.js';
|
|
6
6
|
import Context from '../context.js';
|
|
7
7
|
import S from './Link.styl.js';
|
|
8
8
|
|
|
9
9
|
const isStartFromDoubleSlash = href => /^\/\//.test(href);
|
|
10
|
-
const Link =
|
|
10
|
+
const Link = ({ className, exactClassName, children, href: hrefProp, isClear = false, isDisabled = false, disableExternalIcon = false, inline = false, isPartialExact = false, onClick, ...rest }) => {
|
|
11
|
+
const { router } = useStore({ router: ['path'] });
|
|
11
12
|
const { path } = router;
|
|
12
13
|
const isFromRoot = isStartFromDoubleSlash(hrefProp);
|
|
13
14
|
const domElem = useRef(null);
|
|
@@ -56,6 +57,6 @@ const Link = withStore({ router: ['path'] })(({ className, exactClassName, child
|
|
|
56
57
|
props.href = `http://${href}`;
|
|
57
58
|
}
|
|
58
59
|
return (jsxs("a", { className: classes, ...props, onClick: handleClick, ref: domElem, children: [children, isExternal && !disableExternalIcon && (jsx(Icon, { type: "externalLink", className: S.externalIcon }))] }));
|
|
59
|
-
}
|
|
60
|
+
};
|
|
60
61
|
|
|
61
62
|
export { Link };
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { jsx, Fragment } from 'react/jsx-runtime';
|
|
2
2
|
import { useContext, useEffect, useMemo } from 'react';
|
|
3
|
-
import { useStore } from 'justorm/
|
|
3
|
+
import { useStore } from 'justorm/react';
|
|
4
4
|
import STORE from './store.js';
|
|
5
5
|
import Context from './context.js';
|
|
6
6
|
import { parsePath, replaceParamsInPath } from './Router.helpers.js';
|
|
7
7
|
export { Route } from './Route.js';
|
|
8
8
|
export { Redirect } from './Redirect.js';
|
|
9
|
-
|
|
9
|
+
import 'classnames';
|
|
10
|
+
import '../Icon/Icon.js';
|
|
11
|
+
import './Link/Link.styl.js';
|
|
10
12
|
|
|
11
13
|
const Router = (props) => {
|
|
12
14
|
const { router } = useStore({ router: ['path'] });
|
|
@@ -203,7 +203,7 @@ class Select extends Component {
|
|
|
203
203
|
};
|
|
204
204
|
onSearchChange = (e, value) => {
|
|
205
205
|
this.setSearchVal(value);
|
|
206
|
-
this.props.
|
|
206
|
+
this.props.onSearchChange?.(value);
|
|
207
207
|
};
|
|
208
208
|
onExpandClick(e, id) {
|
|
209
209
|
const { expanded } = this.store;
|
|
@@ -405,7 +405,7 @@ class Select extends Component {
|
|
|
405
405
|
return label;
|
|
406
406
|
}
|
|
407
407
|
getFieldLabel(label) {
|
|
408
|
-
const { showSelectedCount, disableLable, value } = this.props;
|
|
408
|
+
const { showSelectedCount, disableLabel: disableLable, value } = this.props;
|
|
409
409
|
if (disableLable)
|
|
410
410
|
return null;
|
|
411
411
|
// @ts-ignore
|
|
@@ -524,8 +524,8 @@ class Select extends Component {
|
|
|
524
524
|
onPointerUp: () => this.onItemToggle(id),
|
|
525
525
|
onPointerEnter: () => this.onOptionHover(id),
|
|
526
526
|
};
|
|
527
|
-
|
|
528
|
-
|
|
527
|
+
if (isFocused || (isSelected && !this.onFocusedElemRef))
|
|
528
|
+
// @ts-ignore
|
|
529
529
|
props.ref = this.onFocusedElemRef;
|
|
530
530
|
if (isIndeterminate || (isSelected && !this.isFirstSelectedMeet)) {
|
|
531
531
|
this.isFirstSelectedMeet = true;
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
2
|
+
import { mapById, isMultiple, renderLabel } from './Select.helpers.js';
|
|
3
|
+
import { useRef, useState, useMemo, useEffect, useCallback } from 'react';
|
|
4
|
+
import { INTERACTION_MODE, getInteractionMode } from '../../tools/dom.js';
|
|
5
|
+
import cn from 'classnames';
|
|
6
|
+
import omit from 'lodash.omit';
|
|
7
|
+
import { AssistiveText } from '../AssistiveText/AssistiveText.js';
|
|
8
|
+
import { Button } from '../Button/Button.js';
|
|
9
|
+
import { Input } from '../Input/Input.js';
|
|
10
|
+
import { Label } from '../Label/Label.js';
|
|
11
|
+
import { Popup } from '../Popup/Popup.js';
|
|
12
|
+
import { RequiredStar } from '../RequiredStar/RequiredStar.js';
|
|
13
|
+
import S from './Select.styl.js';
|
|
14
|
+
import { Scroll } from '../Scroll/Scroll.js';
|
|
15
|
+
import { useThrottle } from '../../hooks/useThrottle.js';
|
|
16
|
+
import useEvent from '../../hooks/useEvent.js';
|
|
17
|
+
|
|
18
|
+
function Select2(props) {
|
|
19
|
+
const { className, value, onChange, onSearchChange, inputProps, popupProps, size = 'm', optionClassName, additionalOptions = [], options, variant, label, error, blur, disabled, trigger, required, hideRequiredStar, isSearchable, presets = [], selectAllButton, clearButton, showSelectedCount, disableLabel, } = props;
|
|
20
|
+
const isMultiple$1 = isMultiple(value);
|
|
21
|
+
const closeOnSelect = props.closeOnSelect ?? !isMultiple$1;
|
|
22
|
+
const contentRef = useRef(null);
|
|
23
|
+
const scrollInnerRef = useRef(null);
|
|
24
|
+
const focusedItemId = useRef(null);
|
|
25
|
+
const focusedElem = useRef(null);
|
|
26
|
+
const maxIndex = useRef(-1);
|
|
27
|
+
const isFirstSelectedMeet = useRef(false);
|
|
28
|
+
const searchValLower = useRef('');
|
|
29
|
+
const [searchVal, _setSearchVal] = useState('');
|
|
30
|
+
const setSearchVal = (val) => {
|
|
31
|
+
_setSearchVal(val);
|
|
32
|
+
searchValLower.current = val.toLowerCase();
|
|
33
|
+
};
|
|
34
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
35
|
+
const [isFocused, setIsFocused] = useState(false);
|
|
36
|
+
const [labelClipPath, setLabelClipPath] = useState('');
|
|
37
|
+
const [focusedItemIndex, setFocusedItemIndex] = useState(-1);
|
|
38
|
+
// Add tree-related state from Select.tsx
|
|
39
|
+
const [items, setItems] = useState([]);
|
|
40
|
+
const ids = useMemo(() => mapById(props.options), [props.options]);
|
|
41
|
+
// Add basic selection state
|
|
42
|
+
const [selected, setSelected] = useState(isMultiple$1 ? value : [value]);
|
|
43
|
+
const [optionsUpdated, setOptionsUpdated] = useState(0);
|
|
44
|
+
const isErrorVisible = !isOpen && !!error;
|
|
45
|
+
const triggerProps = useMemo(() => ({
|
|
46
|
+
label,
|
|
47
|
+
size,
|
|
48
|
+
variant,
|
|
49
|
+
...props.triggerProps,
|
|
50
|
+
}), [props.triggerProps, label, size, variant]);
|
|
51
|
+
// Simplified isSelected check
|
|
52
|
+
const isSelected = (id) =>
|
|
53
|
+
// @ts-ignore
|
|
54
|
+
isMultiple$1 ? selected?.includes(id) : selected === id;
|
|
55
|
+
const isClickedInside = elem => elem.closest(`.${S.root}`) || elem.closest(`.${S.options}`);
|
|
56
|
+
const setNewItems = (newItems) => {
|
|
57
|
+
maxIndex.current = newItems.length - 1;
|
|
58
|
+
setItems(newItems);
|
|
59
|
+
setOptionsUpdated(optionsUpdated + 1);
|
|
60
|
+
if (focusedItemIndex > maxIndex.current && maxIndex.current >= 0) {
|
|
61
|
+
setFocusedItemIndex(maxIndex.current);
|
|
62
|
+
focusedItemId.current = newItems[maxIndex.current]?.id;
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
const setItemFocus = index => {
|
|
66
|
+
console.log('setItemFocus::', index);
|
|
67
|
+
focusedItemId.current = items[index]?.id;
|
|
68
|
+
setFocusedItemIndex(index);
|
|
69
|
+
};
|
|
70
|
+
const handleSearchChange = (e, value) => {
|
|
71
|
+
setSearchVal(value);
|
|
72
|
+
onSearchChange?.(value);
|
|
73
|
+
setNewItems(options.filter(filterOption));
|
|
74
|
+
};
|
|
75
|
+
const selectAll = () => {
|
|
76
|
+
const newValue = options.map(({ id }) => id);
|
|
77
|
+
setSelected(newValue);
|
|
78
|
+
onChange?.(newValue);
|
|
79
|
+
};
|
|
80
|
+
const dropSelected = () => {
|
|
81
|
+
setSelected([]);
|
|
82
|
+
onChange?.([]);
|
|
83
|
+
};
|
|
84
|
+
const onFocusedElemRef = elem => {
|
|
85
|
+
focusedElem.current = elem;
|
|
86
|
+
if (elem) {
|
|
87
|
+
const content = contentRef.current;
|
|
88
|
+
if (!content)
|
|
89
|
+
return;
|
|
90
|
+
const { top, bottom } = elem.getBoundingClientRect();
|
|
91
|
+
const rect = contentRef.current.getBoundingClientRect();
|
|
92
|
+
const list = scrollInnerRef.current;
|
|
93
|
+
if (top < rect.top) {
|
|
94
|
+
list.scrollTop -= rect.top - top;
|
|
95
|
+
}
|
|
96
|
+
else if (bottom > rect.bottom) {
|
|
97
|
+
list.scrollTop += bottom - rect.bottom;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
const onPopupOpen = () => {
|
|
102
|
+
setIsOpen(true);
|
|
103
|
+
if (focusedItemIndex === -1) {
|
|
104
|
+
setItemFocus(0);
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
const onPopupClose = () => {
|
|
108
|
+
setIsOpen(false);
|
|
109
|
+
setItemFocus(0);
|
|
110
|
+
};
|
|
111
|
+
const onFocus = () => {
|
|
112
|
+
setIsFocused(true);
|
|
113
|
+
};
|
|
114
|
+
const onBlur = () => {
|
|
115
|
+
setIsFocused(false);
|
|
116
|
+
};
|
|
117
|
+
const onOptionHover = useThrottle(id => {
|
|
118
|
+
const mode = getInteractionMode();
|
|
119
|
+
if (mode !== INTERACTION_MODE.POINTER)
|
|
120
|
+
return;
|
|
121
|
+
const index = items.findIndex(item => item.id === id);
|
|
122
|
+
setItemFocus(index);
|
|
123
|
+
}, 100, { trailing: true });
|
|
124
|
+
const onItemToggle = (id) => {
|
|
125
|
+
if (isMultiple$1) {
|
|
126
|
+
const newValue = isSelected(id)
|
|
127
|
+
? selected.filter(i => i !== id)
|
|
128
|
+
: [...selected, id];
|
|
129
|
+
setSelected(newValue);
|
|
130
|
+
onChange?.(newValue);
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
// mono select
|
|
134
|
+
const newValue = isSelected(id) ? null : id;
|
|
135
|
+
const newSelected = isSelected(id) ? [] : [id];
|
|
136
|
+
setSelected(newSelected);
|
|
137
|
+
onChange?.(newValue);
|
|
138
|
+
}
|
|
139
|
+
setSearchVal('');
|
|
140
|
+
if (closeOnSelect)
|
|
141
|
+
setIsOpen(false);
|
|
142
|
+
};
|
|
143
|
+
const getLabel = (id) => ids.items[id]?.label || '';
|
|
144
|
+
const getFieldLabel = (label) => {
|
|
145
|
+
if (disableLabel)
|
|
146
|
+
return null;
|
|
147
|
+
// @ts-ignore
|
|
148
|
+
const length = value?.length;
|
|
149
|
+
if (isMultiple$1 && length && showSelectedCount)
|
|
150
|
+
return `${label} (${length})`;
|
|
151
|
+
return label;
|
|
152
|
+
};
|
|
153
|
+
const filterOption = ({ label }) => {
|
|
154
|
+
return label.toLowerCase().includes(searchValLower.current);
|
|
155
|
+
};
|
|
156
|
+
const selectedLabel = useMemo(() => {
|
|
157
|
+
if (!isMultiple$1)
|
|
158
|
+
return getLabel(value);
|
|
159
|
+
if (!value)
|
|
160
|
+
return '';
|
|
161
|
+
return (value
|
|
162
|
+
// @ts-ignore
|
|
163
|
+
.reduce((acc, id) => {
|
|
164
|
+
const label = getLabel(id);
|
|
165
|
+
return label ? [...acc, label] : acc;
|
|
166
|
+
}, [])
|
|
167
|
+
.join(', '));
|
|
168
|
+
}, [isMultiple$1, value]);
|
|
169
|
+
// console.log('selectedLabel::', selectedLabel);
|
|
170
|
+
const renderTriggerArrow = () => {
|
|
171
|
+
return null;
|
|
172
|
+
};
|
|
173
|
+
const renderTriggerInput = () => {
|
|
174
|
+
return (jsx(Input, { ...triggerProps, ...inputProps,
|
|
175
|
+
// TODO: autoComplete
|
|
176
|
+
addonRight: renderTriggerArrow(), error: isErrorVisible, value: isFocused ? searchVal : selectedLabel, onChange: handleSearchChange, label: getFieldLabel(label) }));
|
|
177
|
+
};
|
|
178
|
+
const renderAdditionalLabel = () => {
|
|
179
|
+
return null;
|
|
180
|
+
};
|
|
181
|
+
const renderTriggerButton = () => {
|
|
182
|
+
const { label, className, ...rest } = triggerProps;
|
|
183
|
+
const props = omit(rest, ['name', 'inputProps']);
|
|
184
|
+
const fullSelectedLabel = [selectedLabel, renderAdditionalLabel()].filter(Boolean);
|
|
185
|
+
const hasSelected = fullSelectedLabel.length > 0;
|
|
186
|
+
const displayLabel = hasSelected ? fullSelectedLabel : label;
|
|
187
|
+
const title = hasSelected ? fullSelectedLabel : null;
|
|
188
|
+
const triggerArrow = renderTriggerArrow();
|
|
189
|
+
const isError = isErrorVisible;
|
|
190
|
+
const classes = cn(S.triggerButton, isError && S.isError, triggerArrow , className);
|
|
191
|
+
return (jsxs("div", { children: [jsxs(Button, { className: classes, variant: "default", ...props, style: { clipPath: labelClipPath }, title: title?.join?.(', '), children: [jsx("div", { className: cn(S.triggerButtonLabel, hasSelected && S.hasSelected), children: displayLabel }), triggerArrow] }), jsx(Label, { size: size, isOnTop: hasSelected, isError: isError, onClipPathChange: setLabelClipPath, children: getFieldLabel(label) })] }));
|
|
192
|
+
};
|
|
193
|
+
const renderTrigger = () => {
|
|
194
|
+
if (trigger)
|
|
195
|
+
return trigger;
|
|
196
|
+
const triggerElem = isSearchable
|
|
197
|
+
? renderTriggerInput()
|
|
198
|
+
: renderTriggerButton();
|
|
199
|
+
return (jsxs("div", { className: S.trigger, children: [triggerElem, required && !hideRequiredStar && jsx(RequiredStar, { size: size })] }));
|
|
200
|
+
};
|
|
201
|
+
const renderPresets = () => {
|
|
202
|
+
const items = presets.map(({ label, ids }) => ({
|
|
203
|
+
children: label,
|
|
204
|
+
onClick: () => setSelected(ids),
|
|
205
|
+
key: label,
|
|
206
|
+
}));
|
|
207
|
+
if (selectAllButton) {
|
|
208
|
+
items.push({
|
|
209
|
+
children: 'Select all',
|
|
210
|
+
onClick: selectAll,
|
|
211
|
+
key: 'select-all-button',
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
if (clearButton) {
|
|
215
|
+
items.push({
|
|
216
|
+
children: 'Clear',
|
|
217
|
+
onClick: dropSelected,
|
|
218
|
+
key: 'clear-button',
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
if (items.length === 0)
|
|
222
|
+
return null;
|
|
223
|
+
return (jsx("div", { className: S.presetPanel, children: items.map(props => (jsx(Button, { className: S.presetButton, variant: "clear", ...props }))) }, "preset-panel"));
|
|
224
|
+
};
|
|
225
|
+
const renderOption = (item, level = 0) => {
|
|
226
|
+
const { id, isGroupHeader } = item;
|
|
227
|
+
const isFocused = id === focusedItemId.current;
|
|
228
|
+
const isSelected = selected.includes(id);
|
|
229
|
+
const items = [];
|
|
230
|
+
const className = cn(S.option, isGroupHeader && S.isGroup, isFocused && S.isFocused, isSelected && S.isSelected, S[`level-${level}`], optionClassName);
|
|
231
|
+
const optionProps = {
|
|
232
|
+
className,
|
|
233
|
+
key: id,
|
|
234
|
+
onPointerUp: () => onItemToggle(id),
|
|
235
|
+
onPointerEnter: () => onOptionHover(id),
|
|
236
|
+
};
|
|
237
|
+
// @ts-ignore
|
|
238
|
+
if (isFocused)
|
|
239
|
+
optionProps.ref = onFocusedElemRef;
|
|
240
|
+
if (isSelected && !isFirstSelectedMeet.current) {
|
|
241
|
+
isFirstSelectedMeet.current = true;
|
|
242
|
+
}
|
|
243
|
+
if (filterOption(item)) {
|
|
244
|
+
items.unshift(jsx("div", { ...optionProps, children: renderLabel(item) }));
|
|
245
|
+
}
|
|
246
|
+
return items;
|
|
247
|
+
};
|
|
248
|
+
const renderOptions = () => {
|
|
249
|
+
isFirstSelectedMeet.current = false;
|
|
250
|
+
return [...additionalOptions, ...items]
|
|
251
|
+
.map(item => renderOption(item))
|
|
252
|
+
.flat();
|
|
253
|
+
};
|
|
254
|
+
useEffect(() => {
|
|
255
|
+
const items = additionalOptions?.length
|
|
256
|
+
? [...additionalOptions, ...options]
|
|
257
|
+
: options;
|
|
258
|
+
setNewItems(items);
|
|
259
|
+
}, [options]);
|
|
260
|
+
useEffect(() => {
|
|
261
|
+
const searchVal = props.searchValue;
|
|
262
|
+
if (searchVal)
|
|
263
|
+
setSearchVal(searchVal);
|
|
264
|
+
}, [props.searchValue]);
|
|
265
|
+
const onKeyDown = useCallback((e) => {
|
|
266
|
+
const currIndex = focusedItemIndex;
|
|
267
|
+
if (e.key === 'ArrowUp') {
|
|
268
|
+
if (currIndex > 0)
|
|
269
|
+
setItemFocus(currIndex - 1);
|
|
270
|
+
}
|
|
271
|
+
if (e.key === 'ArrowDown') {
|
|
272
|
+
if (currIndex < maxIndex.current)
|
|
273
|
+
setItemFocus(currIndex + 1);
|
|
274
|
+
}
|
|
275
|
+
if (currIndex === -1 || !isOpen)
|
|
276
|
+
return;
|
|
277
|
+
if (e.key === 'Enter') {
|
|
278
|
+
e.preventDefault();
|
|
279
|
+
e.stopPropagation();
|
|
280
|
+
onItemToggle(items[currIndex].id);
|
|
281
|
+
}
|
|
282
|
+
}, [items, focusedItemIndex, isOpen]);
|
|
283
|
+
useEvent({
|
|
284
|
+
event: 'keydown',
|
|
285
|
+
isActive: isOpen,
|
|
286
|
+
callback: onKeyDown,
|
|
287
|
+
});
|
|
288
|
+
useEvent({
|
|
289
|
+
event: 'click',
|
|
290
|
+
// isActive: isMultiple,
|
|
291
|
+
callback: e => {
|
|
292
|
+
if (!isClickedInside(e.target)) {
|
|
293
|
+
setIsOpen(false);
|
|
294
|
+
}
|
|
295
|
+
},
|
|
296
|
+
});
|
|
297
|
+
const optionsList = useMemo(() => (jsxs("div", { ref: contentRef, children: [renderPresets(), jsx(Scroll, { y: true, offset: { y: { before: 10, after: 10 } }, className: cn(S.options, S[`size-${size}`]), onInnerRef: elem => (scrollInnerRef.current = elem), children: renderOptions() }, "items-scroll")] })), [items, searchVal, focusedItemIndex]);
|
|
298
|
+
const classes = cn(S.root, className, S[`size-${size}`]);
|
|
299
|
+
return (jsxs(Fragment, { children: [jsx(Popup, { className: classes, direction: "bottom", size: size, focusControl: true, hoverControl: isFocused, blur: blur, isOpen: isOpen, disabled: disabled, ...popupProps, onOpen: onPopupOpen, onClose: onPopupClose, trigger: renderTrigger(), triggerProps: {
|
|
300
|
+
onFocus: onFocus,
|
|
301
|
+
onBlur: onBlur,
|
|
302
|
+
}, content: optionsList }), isErrorVisible && (jsx(AssistiveText, { variant: "danger", size: size, children: error }))] }));
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export { Select2 };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { useRef, useEffect, useCallback } from 'react';
|
|
2
|
+
|
|
3
|
+
function useThrottle(cb, limit, options = { trailing: false }, deps = []) {
|
|
4
|
+
const lastRun = useRef(0);
|
|
5
|
+
const timeoutId = useRef(null);
|
|
6
|
+
useEffect(() => {
|
|
7
|
+
return () => {
|
|
8
|
+
clearTimeout(timeoutId.current);
|
|
9
|
+
};
|
|
10
|
+
}, deps);
|
|
11
|
+
return useCallback((...args) => {
|
|
12
|
+
const now = Date.now();
|
|
13
|
+
const execute = () => {
|
|
14
|
+
cb(...args);
|
|
15
|
+
lastRun.current = now;
|
|
16
|
+
};
|
|
17
|
+
const remaining = limit - (now - lastRun.current);
|
|
18
|
+
if (remaining <= 0) {
|
|
19
|
+
clearTimeout(timeoutId.current);
|
|
20
|
+
execute();
|
|
21
|
+
}
|
|
22
|
+
else if (options.trailing && !timeoutId.current) {
|
|
23
|
+
timeoutId.current = setTimeout(execute, remaining);
|
|
24
|
+
}
|
|
25
|
+
}, [limit, options.trailing, ...deps]);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export { useThrottle };
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { jsx, Fragment } from 'react/jsx-runtime';
|
|
2
2
|
import { memo } from 'react';
|
|
3
3
|
import { nanoid } from 'nanoid';
|
|
4
|
-
import {
|
|
4
|
+
import { useStore } from 'justorm/react';
|
|
5
|
+
import { createStore } from 'justorm';
|
|
5
6
|
import _i18n from 'roddeh-i18n';
|
|
6
7
|
import LS from '../tools/localStorage.js';
|
|
7
8
|
import { queryParams } from '../tools/queryParams.js';
|
|
@@ -53,15 +54,20 @@ function init(config) {
|
|
|
53
54
|
storeName,
|
|
54
55
|
componentStore,
|
|
55
56
|
// hook (update when componentStore._updated changed)
|
|
56
|
-
withI18N: Component =>
|
|
57
|
+
withI18N: Component => props => {
|
|
58
|
+
useStore({ [storeName]: true });
|
|
59
|
+
return jsx(Component, { ...props });
|
|
60
|
+
},
|
|
57
61
|
i18n: (key, props) => getTrans(key, props),
|
|
58
|
-
I18N: memo(
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
+
I18N: memo(function I18N({ id, children, props }) {
|
|
63
|
+
useStore({
|
|
64
|
+
i18n: ['lang'],
|
|
65
|
+
[storeName]: true,
|
|
66
|
+
});
|
|
62
67
|
const key = id ?? children;
|
|
68
|
+
// @ts-ignore
|
|
63
69
|
return jsx(Fragment, { children: getTrans(key, props) });
|
|
64
|
-
})
|
|
70
|
+
}),
|
|
65
71
|
};
|
|
66
72
|
}
|
|
67
73
|
|