polpo 0.1.13 → 0.1.16
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/.turbo/cache/eslint/.cache_16wwsjl +1 -0
- package/.turbo/cache/eslint/.cache_rpznjo +1 -0
- package/.turbo/turbo-build$colon$code.log +49 -0
- package/.turbo/turbo-build$colon$css.log +2 -0
- package/.turbo/turbo-build.log +43 -0
- package/.turbo/turbo-lint.log +2 -0
- package/dist/components.cjs +1 -3655
- package/dist/components.css +2 -2173
- package/dist/components.js +1 -3576
- package/dist/helpers.cjs +1 -311
- package/dist/helpers.d.cts +4 -1
- package/dist/helpers.d.ts +4 -1
- package/dist/helpers.js +1 -276
- package/dist/hooks.cjs +1 -1065
- package/dist/hooks.js +1 -998
- package/dist/layouts.cjs +1 -146
- package/dist/layouts.css +2 -5
- package/dist/layouts.js +1 -119
- package/dist/types.cjs +1 -19
- package/dist/types.js +0 -1
- package/eslint.config.js +4 -0
- package/package.json +16 -11
- package/postcss.config.js +3 -0
- package/src/components/accordion/accordion-item.tsx +123 -0
- package/src/components/accordion/accordion.component.tsx +80 -0
- package/src/components/accordion/accordion.styles.css +41 -0
- package/src/components/accordion/index.ts +2 -0
- package/src/components/button/button.component.tsx +50 -0
- package/src/components/button/button.styles.css +317 -0
- package/src/components/button/index.ts +1 -0
- package/src/components/cards/flip-card/flip-card.component.tsx +66 -0
- package/src/components/cards/flip-card/flip-card.styles.css +50 -0
- package/src/components/cards/flip-card/index.ts +1 -0
- package/src/components/cards/hover-card/hover-card.component.tsx +76 -0
- package/src/components/cards/hover-card/hover-card.styles.css +13 -0
- package/src/components/cards/hover-card/index.ts +1 -0
- package/src/components/cards/index.ts +3 -0
- package/src/components/cards/slide-card/index.ts +1 -0
- package/src/components/cards/slide-card/slide-card.component.tsx +27 -0
- package/src/components/component.types.ts +31 -0
- package/src/components/cursor/cursor.component.tsx +49 -0
- package/src/components/cursor/cursor.styles.css +73 -0
- package/src/components/cursor/index.ts +1 -0
- package/src/components/form/checkbox/checkbox.component.tsx +95 -0
- package/src/components/form/checkbox/checkbox.styles.css +162 -0
- package/src/components/form/checkbox/index.ts +1 -0
- package/src/components/form/controller/controller.component.tsx +42 -0
- package/src/components/form/controller/index.ts +1 -0
- package/src/components/form/date-picker/date-picker.component.tsx +66 -0
- package/src/components/form/date-picker/index.ts +1 -0
- package/src/components/form/field/field.component.tsx +84 -0
- package/src/components/form/field/field.styles.css +98 -0
- package/src/components/form/field/field.types.ts +34 -0
- package/src/components/form/field/index.ts +2 -0
- package/src/components/form/form.types.ts +37 -0
- package/src/components/form/index.ts +14 -0
- package/src/components/form/input/index.ts +1 -0
- package/src/components/form/input/input.component.tsx +75 -0
- package/src/components/form/input-color/index.ts +1 -0
- package/src/components/form/input-color/input-color.component.tsx +170 -0
- package/src/components/form/input-color/input-color.styles.css +92 -0
- package/src/components/form/input-file/index.ts +1 -0
- package/src/components/form/input-file/input-file.component.tsx +221 -0
- package/src/components/form/input-file/input-file.styles.css +143 -0
- package/src/components/form/input-number/index.ts +1 -0
- package/src/components/form/input-number/input-number.component.tsx +144 -0
- package/src/components/form/input-password/index.ts +1 -0
- package/src/components/form/input-password/input-password.component.tsx +77 -0
- package/src/components/form/radio/index.ts +1 -0
- package/src/components/form/radio/radio.component.tsx +92 -0
- package/src/components/form/radio/radio.styles.css +117 -0
- package/src/components/form/select/index.ts +1 -0
- package/src/components/form/select/option.tsx +101 -0
- package/src/components/form/select/options.tsx +165 -0
- package/src/components/form/select/select.component.tsx +317 -0
- package/src/components/form/select/select.styles.css +115 -0
- package/src/components/form/select/select.types.ts +97 -0
- package/src/components/form/slider/index.ts +1 -0
- package/src/components/form/slider/slider.component.tsx +117 -0
- package/src/components/form/slider/slider.styles.css +94 -0
- package/src/components/form/switch/index.ts +1 -0
- package/src/components/form/switch/switch.component.tsx +166 -0
- package/src/components/form/switch/switch.styles.css +165 -0
- package/src/components/form/textarea/index.ts +1 -0
- package/src/components/form/textarea/textarea.component.tsx +80 -0
- package/src/components/form/textarea/textarea.styles.css +7 -0
- package/src/components/image/image.tsx +7 -0
- package/src/components/image/index.ts +1 -0
- package/src/components/index.ts +15 -0
- package/src/components/infinity-scroll/index.ts +1 -0
- package/src/components/infinity-scroll/infinity-scroll.component.tsx +51 -0
- package/src/components/infinity-scroll/infinity-scroll.styles.css +25 -0
- package/src/components/line/index.ts +1 -0
- package/src/components/line/line.component.tsx +77 -0
- package/src/components/line/line.styles.css +61 -0
- package/src/components/modals/action-modal/action-modal.component.tsx +150 -0
- package/src/components/modals/action-modal/action-modal.styles.css +115 -0
- package/src/components/modals/action-modal/index.ts +1 -0
- package/src/components/modals/aside-modal/aside-modal.component.tsx +63 -0
- package/src/components/modals/aside-modal/aside-modal.styles.css +86 -0
- package/src/components/modals/aside-modal/index.ts +1 -0
- package/src/components/modals/confirmation-modal/confirmation-modal.component.tsx +43 -0
- package/src/components/modals/confirmation-modal/confirmation-modal.styles.css +17 -0
- package/src/components/modals/confirmation-modal/index.ts +1 -0
- package/src/components/modals/index.ts +6 -0
- package/src/components/modals/menu/index.ts +1 -0
- package/src/components/modals/menu/menu.component.tsx +194 -0
- package/src/components/modals/menu/menu.styles.css +101 -0
- package/src/components/modals/modal/index.ts +2 -0
- package/src/components/modals/modal/modal.backdrop.tsx +67 -0
- package/src/components/modals/modal/modal.component.tsx +104 -0
- package/src/components/modals/modal/modal.styles.css +70 -0
- package/src/components/modals/portal/index.ts +1 -0
- package/src/components/modals/portal/portal.component.tsx +19 -0
- package/src/components/ripple/index.ts +1 -0
- package/src/components/ripple/ripple.component.tsx +70 -0
- package/src/components/ripple/ripple.styles.css +33 -0
- package/src/components/smart-table/index.ts +2 -0
- package/src/components/smart-table/smart-table.column.tsx +62 -0
- package/src/components/smart-table/smart-table.component.tsx +116 -0
- package/src/components/smart-table/smart-table.helpers.tsx +58 -0
- package/src/components/smart-table/smart-table.hooks.ts +27 -0
- package/src/components/smart-table/smart-table.row.tsx +28 -0
- package/src/components/smart-table/smart-table.styles.css +102 -0
- package/src/components/smart-table/smart-table.types.ts +42 -0
- package/src/components/tabs/index.ts +1 -0
- package/src/components/tabs/tabs-list.tsx +120 -0
- package/src/components/tabs/tabs.styles.css +164 -0
- package/src/components/tabs/tabs.tsx +119 -0
- package/src/components/tag/index.ts +1 -0
- package/src/components/tag/tag.component.tsx +34 -0
- package/src/components/tag/tag.styles.css +50 -0
- package/src/components/tooltips/click-to-copy/click-to-copy.component.tsx +41 -0
- package/src/components/tooltips/click-to-copy/index.ts +1 -0
- package/src/components/tooltips/index.ts +2 -0
- package/src/components/tooltips/tooltip/index.ts +1 -0
- package/src/components/tooltips/tooltip/tooltip.component.tsx +64 -0
- package/src/components/tooltips/tooltip/tooltip.styles.css +91 -0
- package/src/components/typography/index.ts +1 -0
- package/src/components/typography/typography.component.tsx +81 -0
- package/src/components/typography/typography.constants.ts +53 -0
- package/src/components/typography/typography.styles.css +122 -0
- package/src/helpers/cn.ts +6 -0
- package/src/helpers/format-bytes.ts +11 -0
- package/src/helpers/format-dates.ts +47 -0
- package/src/helpers/get-modal-position-relative-to-screen.ts +86 -0
- package/src/helpers/get-modal-position.ts +211 -0
- package/src/helpers/index.ts +6 -0
- package/src/helpers/text/index.ts +1 -0
- package/src/helpers/text/to-capitalize.ts +17 -0
- package/src/hooks/index.ts +30 -0
- package/src/hooks/use-async.ts +88 -0
- package/src/hooks/use-classnames.ts +13 -0
- package/src/hooks/use-click-outside.ts +32 -0
- package/src/hooks/use-constant.ts +3 -0
- package/src/hooks/use-cookie.ts +124 -0
- package/src/hooks/use-debounce-state.ts +13 -0
- package/src/hooks/use-debounce.ts +15 -0
- package/src/hooks/use-dimensions.ts +19 -0
- package/src/hooks/use-dom-container.ts +35 -0
- package/src/hooks/use-event-listener.ts +71 -0
- package/src/hooks/use-file-reader.ts +69 -0
- package/src/hooks/use-geolocation.ts +63 -0
- package/src/hooks/use-hover.ts +17 -0
- package/src/hooks/use-in-view.ts +18 -0
- package/src/hooks/use-input-handlers.ts +52 -0
- package/src/hooks/use-intersection-observer.ts +19 -0
- package/src/hooks/use-media-query.ts +25 -0
- package/src/hooks/use-modal-in-container.ts +85 -0
- package/src/hooks/use-modal-transition.ts +64 -0
- package/src/hooks/use-modal.ts +21 -0
- package/src/hooks/use-mouse-position.ts +64 -0
- package/src/hooks/use-online-status.ts +12 -0
- package/src/hooks/use-render-count.ts +11 -0
- package/src/hooks/use-resize-observer.ts +18 -0
- package/src/hooks/use-safe-dispatch.ts +22 -0
- package/src/hooks/use-scroll.ts +31 -0
- package/src/hooks/use-state-history.ts +22 -0
- package/src/hooks/use-toggle-values.ts +14 -0
- package/src/hooks/use-toggle.ts +11 -0
- package/src/hooks/use-viewport.ts +38 -0
- package/src/index.ts +5 -0
- package/src/layouts/flex/flex.component.tsx +75 -0
- package/src/layouts/flex/index.ts +1 -0
- package/src/layouts/grid/grid.component.tsx +89 -0
- package/src/layouts/grid/grid.styles.css +5 -0
- package/src/layouts/grid/index.ts +1 -0
- package/src/layouts/index.ts +2 -0
- package/src/types/generics.ts +68 -0
- package/src/types/index.ts +1 -0
- package/tsconfig.json +10 -0
- package/tsup.config.cjs +36 -0
- package/dist/components.cjs.map +0 -1
- package/dist/components.css.map +0 -1
- package/dist/components.js.map +0 -1
- package/dist/helpers.cjs.map +0 -1
- package/dist/helpers.js.map +0 -1
- package/dist/hooks.cjs.map +0 -1
- package/dist/hooks.js.map +0 -1
- package/dist/layouts.cjs.map +0 -1
- package/dist/layouts.css.map +0 -1
- package/dist/layouts.js.map +0 -1
- package/dist/types.cjs.map +0 -1
- package/dist/types.js.map +0 -1
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import React, { RefObject, useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
import { useClassNames, useEventListener, useMediaQuery, useResizeObserver } from '../../../hooks';
|
|
4
|
+
|
|
5
|
+
import { OptionsProps } from './select.types';
|
|
6
|
+
|
|
7
|
+
type UseDynamicHeight = {
|
|
8
|
+
height: number;
|
|
9
|
+
minHeight: number;
|
|
10
|
+
containerRef: RefObject<HTMLElement | null>;
|
|
11
|
+
offset: number;
|
|
12
|
+
windowOffset: number;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const useDynamicHeight = ({
|
|
16
|
+
height,
|
|
17
|
+
minHeight: defaultMinHeight,
|
|
18
|
+
containerRef,
|
|
19
|
+
offset,
|
|
20
|
+
windowOffset,
|
|
21
|
+
}: UseDynamicHeight) => {
|
|
22
|
+
const [h, setH] = useState<string | undefined>(undefined);
|
|
23
|
+
|
|
24
|
+
const getMaxHeight = useCallback(() => {
|
|
25
|
+
const containerBottom = containerRef.current?.getBoundingClientRect().bottom ?? 0;
|
|
26
|
+
|
|
27
|
+
const heightBottom = window.innerHeight - containerBottom - windowOffset - offset - 5;
|
|
28
|
+
const heightTop = window.innerHeight - windowOffset * 3;
|
|
29
|
+
const heightCalculated = heightBottom < defaultMinHeight ? heightTop : heightBottom;
|
|
30
|
+
const maxHeight = Math.min(heightCalculated, height);
|
|
31
|
+
const minHeight = Math.max(maxHeight, defaultMinHeight);
|
|
32
|
+
|
|
33
|
+
setH(`${Math.round(minHeight)}px`);
|
|
34
|
+
}, [containerRef, windowOffset, offset, defaultMinHeight, height]);
|
|
35
|
+
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
getMaxHeight();
|
|
38
|
+
}, [getMaxHeight]);
|
|
39
|
+
|
|
40
|
+
useResizeObserver(containerRef, getMaxHeight);
|
|
41
|
+
useEventListener('resize', getMaxHeight);
|
|
42
|
+
|
|
43
|
+
return { h };
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const Options = ({
|
|
47
|
+
children,
|
|
48
|
+
searchQueryValue,
|
|
49
|
+
searchQueryPlaceholder = 'Search option',
|
|
50
|
+
searchQueryClassName = '',
|
|
51
|
+
searchQueryStyle = {},
|
|
52
|
+
onSearchQuery,
|
|
53
|
+
containerRef,
|
|
54
|
+
height = 300,
|
|
55
|
+
}: OptionsProps) => {
|
|
56
|
+
const optionsGroupContainerRef = useRef<HTMLDivElement>(null);
|
|
57
|
+
const optionsGroupRef = useRef<HTMLUListElement>(null);
|
|
58
|
+
const [internalSearchQuery, setInternalSearchQuery] = useState('');
|
|
59
|
+
const searchInputRef = useRef<HTMLInputElement>(null);
|
|
60
|
+
const isMobile = useMediaQuery('(max-width: 480px)');
|
|
61
|
+
|
|
62
|
+
useEventListener('keydown', e => {
|
|
63
|
+
if (['ArrowDown', 'ArrowUp'].includes(e.code)) {
|
|
64
|
+
e.preventDefault();
|
|
65
|
+
const focusedItem = document.activeElement;
|
|
66
|
+
const isListItem = focusedItem?.tagName === 'LI';
|
|
67
|
+
|
|
68
|
+
if (isListItem) {
|
|
69
|
+
switch (e.code) {
|
|
70
|
+
case 'ArrowDown':
|
|
71
|
+
(focusedItem?.nextSibling as HTMLElement)?.focus();
|
|
72
|
+
|
|
73
|
+
break;
|
|
74
|
+
case 'ArrowUp':
|
|
75
|
+
(focusedItem?.previousElementSibling as HTMLElement)?.focus();
|
|
76
|
+
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const handleSearchQuery = useCallback(
|
|
84
|
+
(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
85
|
+
const { value } = e.target;
|
|
86
|
+
|
|
87
|
+
if (onSearchQuery) onSearchQuery(value);
|
|
88
|
+
|
|
89
|
+
setInternalSearchQuery(value);
|
|
90
|
+
},
|
|
91
|
+
[onSearchQuery],
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
if (searchInputRef.current) {
|
|
96
|
+
searchInputRef.current.focus();
|
|
97
|
+
}
|
|
98
|
+
}, []);
|
|
99
|
+
|
|
100
|
+
const { h } = useDynamicHeight({
|
|
101
|
+
height,
|
|
102
|
+
minHeight: 150,
|
|
103
|
+
containerRef,
|
|
104
|
+
offset: 5,
|
|
105
|
+
windowOffset: 10,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const [shouldUseAuto, setShouldUseAuto] = useState(false);
|
|
109
|
+
|
|
110
|
+
const calculateHeight = useCallback(() => {
|
|
111
|
+
const optionsContainerHeight =
|
|
112
|
+
Math.round(optionsGroupContainerRef.current?.getBoundingClientRect().height ?? 0) + 2;
|
|
113
|
+
const optionsHeight = Math.round(optionsGroupRef.current?.scrollHeight ?? 0);
|
|
114
|
+
|
|
115
|
+
setShouldUseAuto(optionsContainerHeight >= optionsHeight);
|
|
116
|
+
}, []);
|
|
117
|
+
|
|
118
|
+
useEffect(calculateHeight, [calculateHeight]);
|
|
119
|
+
useResizeObserver(optionsGroupContainerRef, calculateHeight);
|
|
120
|
+
useResizeObserver(optionsGroupRef, calculateHeight);
|
|
121
|
+
useEventListener('resize', calculateHeight);
|
|
122
|
+
|
|
123
|
+
const selectOptionsHeader = useClassNames({
|
|
124
|
+
'select-options-header': true,
|
|
125
|
+
[searchQueryClassName]: Boolean(searchQueryClassName),
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
return (
|
|
129
|
+
<section
|
|
130
|
+
className={`select-options ${onSearchQuery ? 'with-search-query' : ''}`}
|
|
131
|
+
style={
|
|
132
|
+
isMobile
|
|
133
|
+
? {
|
|
134
|
+
maxHeight: window.innerHeight - 100,
|
|
135
|
+
width: window.innerWidth - 100,
|
|
136
|
+
}
|
|
137
|
+
: {
|
|
138
|
+
height: shouldUseAuto ? 'auto' : h,
|
|
139
|
+
maxHeight: height,
|
|
140
|
+
width: containerRef.current?.offsetWidth ?? 'auto',
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
>
|
|
144
|
+
{onSearchQuery && (
|
|
145
|
+
<section className={selectOptionsHeader} style={searchQueryStyle}>
|
|
146
|
+
<input
|
|
147
|
+
name='query'
|
|
148
|
+
className='input-search'
|
|
149
|
+
value={searchQueryValue ?? internalSearchQuery}
|
|
150
|
+
onChange={handleSearchQuery}
|
|
151
|
+
placeholder={searchQueryPlaceholder}
|
|
152
|
+
onClick={e => e.stopPropagation()}
|
|
153
|
+
ref={searchInputRef}
|
|
154
|
+
autoFocus
|
|
155
|
+
/>
|
|
156
|
+
</section>
|
|
157
|
+
)}
|
|
158
|
+
<section className='select-options-list-container' ref={optionsGroupContainerRef}>
|
|
159
|
+
<ul className='select-options-list' ref={optionsGroupRef}>
|
|
160
|
+
{children}
|
|
161
|
+
</ul>
|
|
162
|
+
</section>
|
|
163
|
+
</section>
|
|
164
|
+
);
|
|
165
|
+
};
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import React, { createContext, useCallback, useContext, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import { ImCross } from 'react-icons/im';
|
|
3
|
+
import { PiCaretDown } from 'react-icons/pi';
|
|
4
|
+
|
|
5
|
+
import { useMediaQuery } from '../../../hooks';
|
|
6
|
+
import { InfinityScroll } from '../../infinity-scroll';
|
|
7
|
+
import { Menu } from '../../modals';
|
|
8
|
+
import { Typography } from '../../typography';
|
|
9
|
+
import { Controller } from '../controller';
|
|
10
|
+
import { Field } from '../field';
|
|
11
|
+
import { ControllerGeneratorProps } from '../form.types';
|
|
12
|
+
|
|
13
|
+
import { Option } from './option';
|
|
14
|
+
import { Options } from './options';
|
|
15
|
+
import './select.styles.css';
|
|
16
|
+
import {
|
|
17
|
+
ControllerGeneratorSelectProps,
|
|
18
|
+
MultiSelectProps,
|
|
19
|
+
MultiValue,
|
|
20
|
+
OptionComponentProps,
|
|
21
|
+
SelectItem,
|
|
22
|
+
SingleSelectProps,
|
|
23
|
+
SingleValue,
|
|
24
|
+
UnControlledSelectProps,
|
|
25
|
+
SelectContextValue,
|
|
26
|
+
ValueComponentProps,
|
|
27
|
+
} from './select.types';
|
|
28
|
+
|
|
29
|
+
const SelectContext = createContext<SelectContextValue<unknown> | null>(null);
|
|
30
|
+
|
|
31
|
+
export const useSelectContext = <T extends SelectItem>(): SelectContextValue<T> => {
|
|
32
|
+
const context = useContext(SelectContext as React.Context<SelectContextValue<T> | null>);
|
|
33
|
+
|
|
34
|
+
if (!context) {
|
|
35
|
+
throw new Error('useSelectContext must be used within a Select component');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return context;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
type OptionLabelProps = {
|
|
42
|
+
children: React.ReactNode;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const OptionLabel = ({ children }: OptionLabelProps) => {
|
|
46
|
+
const labelComponent = useMemo(() => {
|
|
47
|
+
if (typeof children === 'string') {
|
|
48
|
+
return (
|
|
49
|
+
<Typography noPadding variant='label' nowrap>
|
|
50
|
+
{children}
|
|
51
|
+
</Typography>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return children;
|
|
56
|
+
}, [children]);
|
|
57
|
+
|
|
58
|
+
return <Menu.GroupLabel>{labelComponent}</Menu.GroupLabel>;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const DefaultOption = <T extends SelectItem>({ value }: OptionComponentProps<T>) => {
|
|
62
|
+
return (
|
|
63
|
+
<Typography variant='label' nowrap>
|
|
64
|
+
{typeof value === 'string' || typeof value === 'number' ? value : JSON.stringify(value)}
|
|
65
|
+
</Typography>
|
|
66
|
+
);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const DefaultValue = <T extends SelectItem>({ value, multiselect }: ValueComponentProps<T>) => {
|
|
70
|
+
if (multiselect) {
|
|
71
|
+
return (
|
|
72
|
+
<Typography noPadding nowrap variant='label'>
|
|
73
|
+
{`${value.length} item${value.length === 1 ? '' : 's'} selected`}
|
|
74
|
+
</Typography>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (value === null) {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<Typography noPadding variant='label' nowrap>
|
|
84
|
+
{typeof value === 'string' || typeof value === 'number' ? value : JSON.stringify(value)}
|
|
85
|
+
</Typography>
|
|
86
|
+
);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export const Select = <T extends SelectItem>({
|
|
90
|
+
// Select props
|
|
91
|
+
options,
|
|
92
|
+
isEqualComparator,
|
|
93
|
+
searchQueryValue,
|
|
94
|
+
searchQueryPlaceholder,
|
|
95
|
+
onSearchQuery,
|
|
96
|
+
loadMore = () => null,
|
|
97
|
+
isLoading = false,
|
|
98
|
+
hasNextPage = false,
|
|
99
|
+
emptyMessage = 'No options to select',
|
|
100
|
+
multiselect,
|
|
101
|
+
optionComponent: OptionComponent = DefaultOption,
|
|
102
|
+
valueComponent: ValueComponent = DefaultValue,
|
|
103
|
+
maxOptions,
|
|
104
|
+
children,
|
|
105
|
+
// Shared props
|
|
106
|
+
name,
|
|
107
|
+
value,
|
|
108
|
+
setValue,
|
|
109
|
+
onBlur,
|
|
110
|
+
onFocus,
|
|
111
|
+
className = '',
|
|
112
|
+
style = {},
|
|
113
|
+
showClearOption = false,
|
|
114
|
+
height,
|
|
115
|
+
searchQueryStyle,
|
|
116
|
+
searchQueryClassName,
|
|
117
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
118
|
+
autoFocus = false,
|
|
119
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
120
|
+
readOnly = false,
|
|
121
|
+
disabled = false,
|
|
122
|
+
placeholder = '',
|
|
123
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
124
|
+
autoComplete = 'off',
|
|
125
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
126
|
+
isDirty = false,
|
|
127
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
128
|
+
isTouched = false,
|
|
129
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
130
|
+
invalid = false,
|
|
131
|
+
error,
|
|
132
|
+
// Field props
|
|
133
|
+
...fieldProps
|
|
134
|
+
}: UnControlledSelectProps<T>) => {
|
|
135
|
+
const modalRef = useRef<HTMLElement>(null);
|
|
136
|
+
const isMobile = useMediaQuery('(max-width: 480px)');
|
|
137
|
+
const id = useMemo(() => crypto.randomUUID(), []);
|
|
138
|
+
const containerRef = useRef<HTMLElement>(null);
|
|
139
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
140
|
+
|
|
141
|
+
const openSelect = useCallback(
|
|
142
|
+
(open: boolean) => {
|
|
143
|
+
setIsOpen(open && !disabled);
|
|
144
|
+
},
|
|
145
|
+
[disabled],
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
const clearOption = useCallback(
|
|
149
|
+
(e: React.MouseEvent) => {
|
|
150
|
+
e.stopPropagation();
|
|
151
|
+
|
|
152
|
+
if (multiselect) {
|
|
153
|
+
setValue([]);
|
|
154
|
+
} else {
|
|
155
|
+
setValue(null);
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
[multiselect, setValue],
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
const valueNonEmpty = useMemo(() => {
|
|
162
|
+
if (multiselect) {
|
|
163
|
+
return value.length > 0;
|
|
164
|
+
} else {
|
|
165
|
+
return Boolean(value);
|
|
166
|
+
}
|
|
167
|
+
}, [multiselect, value]);
|
|
168
|
+
|
|
169
|
+
const renderedOptions = useMemo<React.ReactNode>(() => {
|
|
170
|
+
if (!children)
|
|
171
|
+
return options.map((value, key) => (
|
|
172
|
+
<Select.Option value={value} key={key}>
|
|
173
|
+
<OptionComponent value={value} />
|
|
174
|
+
</Select.Option>
|
|
175
|
+
));
|
|
176
|
+
|
|
177
|
+
return children;
|
|
178
|
+
}, [OptionComponent, children, options]);
|
|
179
|
+
|
|
180
|
+
return (
|
|
181
|
+
<SelectContext.Provider
|
|
182
|
+
value={
|
|
183
|
+
{
|
|
184
|
+
selectedValue: value,
|
|
185
|
+
setValue,
|
|
186
|
+
multiselect: multiselect ?? false,
|
|
187
|
+
isEqualComparator,
|
|
188
|
+
maxOptions: maxOptions ?? null,
|
|
189
|
+
setIsOpen,
|
|
190
|
+
} as SelectContextValue<unknown>
|
|
191
|
+
}
|
|
192
|
+
>
|
|
193
|
+
<Field id={id} error={error} isFocus={isOpen} ref={containerRef} {...fieldProps}>
|
|
194
|
+
<section
|
|
195
|
+
id={name}
|
|
196
|
+
style={style}
|
|
197
|
+
onBlur={onBlur}
|
|
198
|
+
className={`select ${disabled ? 'disabled' : ''} ${className}`}
|
|
199
|
+
>
|
|
200
|
+
<section
|
|
201
|
+
id={id}
|
|
202
|
+
className={`select-container ${valueNonEmpty && showClearOption ? 'three-columns' : ''}`}
|
|
203
|
+
onClick={() => openSelect(true)}
|
|
204
|
+
>
|
|
205
|
+
<button
|
|
206
|
+
type='button'
|
|
207
|
+
className={`input-button ${(Array.isArray(value) ? value.length > 0 : value) ? '' : 'placeholder'}`}
|
|
208
|
+
aria-haspopup='listbox'
|
|
209
|
+
aria-expanded={isOpen}
|
|
210
|
+
onFocus={e => {
|
|
211
|
+
openSelect(true);
|
|
212
|
+
|
|
213
|
+
if (onFocus) onFocus(e);
|
|
214
|
+
}}
|
|
215
|
+
>
|
|
216
|
+
{valueNonEmpty ? (
|
|
217
|
+
<ValueComponent {...(multiselect ? { value, multiselect } : { value: value })} />
|
|
218
|
+
) : (
|
|
219
|
+
<Typography variant='label' noPadding nowrap>
|
|
220
|
+
{placeholder}
|
|
221
|
+
</Typography>
|
|
222
|
+
)}
|
|
223
|
+
</button>
|
|
224
|
+
{valueNonEmpty && showClearOption && (
|
|
225
|
+
<section className='icon-close' onClick={clearOption}>
|
|
226
|
+
<ImCross />
|
|
227
|
+
</section>
|
|
228
|
+
)}
|
|
229
|
+
<PiCaretDown className={`select-caret-icon ${isOpen && 'is-select-open'}`} />
|
|
230
|
+
</section>
|
|
231
|
+
<Menu
|
|
232
|
+
id='form-select'
|
|
233
|
+
isOpen={isOpen}
|
|
234
|
+
onClose={() => openSelect(false)}
|
|
235
|
+
backdrop={isMobile ? 'blur' : 'transparent'}
|
|
236
|
+
opacity={isMobile ? 0.8 : 0.4}
|
|
237
|
+
position={isMobile ? 'center' : 'bottom'}
|
|
238
|
+
offset={5}
|
|
239
|
+
modalRef={modalRef}
|
|
240
|
+
windowOffset={10}
|
|
241
|
+
transitionDuration={200}
|
|
242
|
+
containerRef={isMobile ? undefined : containerRef}
|
|
243
|
+
contentClassName='select-menu-content'
|
|
244
|
+
className='select-options-menu'
|
|
245
|
+
>
|
|
246
|
+
<Options
|
|
247
|
+
containerRef={containerRef}
|
|
248
|
+
onSearchQuery={onSearchQuery}
|
|
249
|
+
searchQueryValue={searchQueryValue}
|
|
250
|
+
searchQueryPlaceholder={searchQueryPlaceholder}
|
|
251
|
+
searchQueryClassName={searchQueryClassName}
|
|
252
|
+
searchQueryStyle={searchQueryStyle}
|
|
253
|
+
height={height}
|
|
254
|
+
>
|
|
255
|
+
<InfinityScroll
|
|
256
|
+
isLoading={isLoading}
|
|
257
|
+
hasNextPage={hasNextPage}
|
|
258
|
+
loadMore={loadMore}
|
|
259
|
+
emptyMessage={emptyMessage}
|
|
260
|
+
>
|
|
261
|
+
{renderedOptions}
|
|
262
|
+
</InfinityScroll>
|
|
263
|
+
</Options>
|
|
264
|
+
</Menu>
|
|
265
|
+
</section>
|
|
266
|
+
</Field>
|
|
267
|
+
</SelectContext.Provider>
|
|
268
|
+
);
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
const MultiSelectController = <T extends SelectItem>({
|
|
272
|
+
rules,
|
|
273
|
+
...props
|
|
274
|
+
}: ControllerGeneratorProps<MultiSelectProps<T>, MultiValue<T>>) => {
|
|
275
|
+
return (
|
|
276
|
+
<Controller<MultiSelectProps<T>, MultiValue<T>>
|
|
277
|
+
Component={Select}
|
|
278
|
+
defaultValue={[]}
|
|
279
|
+
inputProps={{
|
|
280
|
+
...props,
|
|
281
|
+
multiselect: true,
|
|
282
|
+
}}
|
|
283
|
+
rules={rules}
|
|
284
|
+
/>
|
|
285
|
+
);
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
const SingleSelectController = <T extends SelectItem>({
|
|
289
|
+
rules,
|
|
290
|
+
...props
|
|
291
|
+
}: ControllerGeneratorProps<SingleSelectProps<T>, SingleValue<T>>) => {
|
|
292
|
+
return (
|
|
293
|
+
<Controller<SingleSelectProps<T>, SingleValue<T>>
|
|
294
|
+
Component={Select}
|
|
295
|
+
defaultValue={null}
|
|
296
|
+
inputProps={{
|
|
297
|
+
...props,
|
|
298
|
+
multiselect: false,
|
|
299
|
+
}}
|
|
300
|
+
rules={rules}
|
|
301
|
+
/>
|
|
302
|
+
);
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
const SelectController = <T extends SelectItem>(props: ControllerGeneratorSelectProps<T>) => {
|
|
306
|
+
const { multiselect } = props;
|
|
307
|
+
|
|
308
|
+
if (multiselect) {
|
|
309
|
+
return <MultiSelectController<T> {...props} multiselect={true} />;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return <SingleSelectController<T> {...props} multiselect={false} />;
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
Select.Controller = SelectController;
|
|
316
|
+
Select.Option = Option;
|
|
317
|
+
Select.OptionLabel = OptionLabel;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
@reference "polpo-tailwind-config/styles";
|
|
2
|
+
|
|
3
|
+
.select {
|
|
4
|
+
cursor: pointer;
|
|
5
|
+
|
|
6
|
+
.icon-close {
|
|
7
|
+
display: grid;
|
|
8
|
+
place-content: center;
|
|
9
|
+
align-self: center;
|
|
10
|
+
border-radius: 100%;
|
|
11
|
+
cursor: pointer;
|
|
12
|
+
font-size: 0.6em;
|
|
13
|
+
padding: 0.5em;
|
|
14
|
+
|
|
15
|
+
&:hover {
|
|
16
|
+
background: hsl(from var(--color-foreground) h s l / 33%);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.select-container {
|
|
21
|
+
display: grid;
|
|
22
|
+
grid-template-columns: 1fr auto;
|
|
23
|
+
gap: 0.5em;
|
|
24
|
+
align-items: center;
|
|
25
|
+
grid-auto-rows: auto;
|
|
26
|
+
position: relative;
|
|
27
|
+
cursor: pointer;
|
|
28
|
+
user-select: none;
|
|
29
|
+
|
|
30
|
+
&.three-columns {
|
|
31
|
+
grid-template-columns: 1fr auto auto;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.input-button {
|
|
36
|
+
appearance: none;
|
|
37
|
+
-webkit-appearance: none;
|
|
38
|
+
-moz-appearance: none;
|
|
39
|
+
white-space: nowrap;
|
|
40
|
+
text-overflow: ellipsis;
|
|
41
|
+
overflow: hidden;
|
|
42
|
+
display: grid;
|
|
43
|
+
min-width: 0;
|
|
44
|
+
width: 100%;
|
|
45
|
+
border: 0;
|
|
46
|
+
margin: 0;
|
|
47
|
+
border-radius: 0;
|
|
48
|
+
background: transparent;
|
|
49
|
+
color: currentColor;
|
|
50
|
+
text-align: left;
|
|
51
|
+
-webkit-tap-highlight-color: transparent;
|
|
52
|
+
padding: 0;
|
|
53
|
+
grid-template-columns: 1fr auto;
|
|
54
|
+
|
|
55
|
+
&.placeholder {
|
|
56
|
+
color: #919191;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.select-caret-icon {
|
|
61
|
+
transition: transform 200ms ease;
|
|
62
|
+
|
|
63
|
+
&.is-select-open {
|
|
64
|
+
transform: rotate(180deg);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.select-options-menu {
|
|
70
|
+
.select-menu-content {
|
|
71
|
+
gap: 0;
|
|
72
|
+
display: block;
|
|
73
|
+
height: auto;
|
|
74
|
+
overflow: initial;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.select-options {
|
|
79
|
+
gap: 0;
|
|
80
|
+
display: grid;
|
|
81
|
+
grid-template-rows: 1fr;
|
|
82
|
+
height: 100%;
|
|
83
|
+
overflow: auto;
|
|
84
|
+
|
|
85
|
+
&.with-search-query {
|
|
86
|
+
grid-template-rows: auto 1fr;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.select-options-list-container {
|
|
90
|
+
height: 100%;
|
|
91
|
+
overflow: auto;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.select-options-list {
|
|
95
|
+
display: grid;
|
|
96
|
+
gap: 0.2em;
|
|
97
|
+
padding: 0;
|
|
98
|
+
margin: 0;
|
|
99
|
+
list-style: none;
|
|
100
|
+
align-content: start;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.select-options-header {
|
|
105
|
+
background: var(--color-background);
|
|
106
|
+
padding: 2px 5px;
|
|
107
|
+
border-bottom: 1px solid var(--color-border);
|
|
108
|
+
|
|
109
|
+
.input-search {
|
|
110
|
+
outline: none;
|
|
111
|
+
padding: 0.8em 5px;
|
|
112
|
+
@apply text-label;
|
|
113
|
+
width: 100%;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
import { InputFieldProps } from '../field';
|
|
4
|
+
import { ControllerGeneratorProps, UnControlledComponentProps } from '../form.types';
|
|
5
|
+
|
|
6
|
+
export type SelectItem = string | number | Record<string, unknown> | unknown;
|
|
7
|
+
|
|
8
|
+
export type MultiValue<T extends SelectItem> = Array<T>;
|
|
9
|
+
|
|
10
|
+
export type SingleValue<T extends SelectItem> = T | null;
|
|
11
|
+
|
|
12
|
+
// SELECT CONTEXT
|
|
13
|
+
|
|
14
|
+
export type SharedSelectContextValue<T extends SelectItem> = {
|
|
15
|
+
isEqualComparator?: (a: T, b: T) => boolean;
|
|
16
|
+
maxOptions: number | null;
|
|
17
|
+
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type MultiSelectContextValue<T extends SelectItem> = SharedSelectContextValue<T> & {
|
|
21
|
+
selectedValue: MultiValue<T>;
|
|
22
|
+
setValue: (value: MultiValue<T>) => void;
|
|
23
|
+
multiselect: true;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type SingleSelectContextValue<T extends SelectItem> = SharedSelectContextValue<T> & {
|
|
27
|
+
selectedValue: SingleValue<T>;
|
|
28
|
+
setValue: (value: SingleValue<T>) => void;
|
|
29
|
+
multiselect: false;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export type SelectContextValue<T extends SelectItem> = SingleSelectContextValue<T> | MultiSelectContextValue<T>;
|
|
33
|
+
|
|
34
|
+
// SELECT
|
|
35
|
+
|
|
36
|
+
export type OptionComponentProps<T extends SelectItem> = {
|
|
37
|
+
value: T;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export type ValueComponentProps<T extends SelectItem> =
|
|
41
|
+
| {
|
|
42
|
+
value: MultiValue<T>;
|
|
43
|
+
multiselect: true;
|
|
44
|
+
}
|
|
45
|
+
| {
|
|
46
|
+
value: SingleValue<T>;
|
|
47
|
+
multiselect?: false;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export type SharedSelectProps<T extends SelectItem> = InputFieldProps<{
|
|
51
|
+
options: Array<T>;
|
|
52
|
+
isEqualComparator?: (a: T, b: T) => boolean;
|
|
53
|
+
searchQueryValue?: string;
|
|
54
|
+
searchQueryPlaceholder?: string;
|
|
55
|
+
onSearchQuery?: (value: string) => void;
|
|
56
|
+
loadMore?: () => void;
|
|
57
|
+
isLoading?: boolean;
|
|
58
|
+
hasNextPage?: boolean;
|
|
59
|
+
emptyMessage?: string;
|
|
60
|
+
maxOptions?: number;
|
|
61
|
+
optionComponent?: React.FC<OptionComponentProps<T>>;
|
|
62
|
+
valueComponent?: React.FC<ValueComponentProps<T>>;
|
|
63
|
+
showClearOption?: boolean;
|
|
64
|
+
height?: number;
|
|
65
|
+
searchQueryClassName?: string;
|
|
66
|
+
searchQueryStyle?: React.CSSProperties;
|
|
67
|
+
children?: React.ReactNode;
|
|
68
|
+
}>;
|
|
69
|
+
|
|
70
|
+
export type MultiSelectProps<T extends SelectItem> = SharedSelectProps<T> & {
|
|
71
|
+
multiselect: true;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export type SingleSelectProps<T extends SelectItem> = SharedSelectProps<T> & {
|
|
75
|
+
multiselect?: false;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export type UnControlledSelectProps<T extends SelectItem> =
|
|
79
|
+
| UnControlledComponentProps<MultiSelectProps<T>, MultiValue<T>>
|
|
80
|
+
| UnControlledComponentProps<SingleSelectProps<T>, SingleValue<T>>;
|
|
81
|
+
|
|
82
|
+
export type ControllerGeneratorSelectProps<T extends SelectItem> =
|
|
83
|
+
| ControllerGeneratorProps<MultiSelectProps<T>, MultiValue<T>>
|
|
84
|
+
| ControllerGeneratorProps<SingleSelectProps<T>, SingleValue<T>>;
|
|
85
|
+
|
|
86
|
+
// SELECT OPTIONS
|
|
87
|
+
|
|
88
|
+
export type OptionsProps = {
|
|
89
|
+
onSearchQuery?: (value: string) => void;
|
|
90
|
+
searchQueryValue?: string;
|
|
91
|
+
searchQueryPlaceholder?: string;
|
|
92
|
+
searchQueryClassName?: string;
|
|
93
|
+
searchQueryStyle?: React.CSSProperties;
|
|
94
|
+
containerRef: React.RefObject<HTMLElement | null>;
|
|
95
|
+
height?: number;
|
|
96
|
+
children?: React.ReactNode;
|
|
97
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './slider.component';
|