@bitrise/bitkit 10.8.0 → 10.9.2

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.
@@ -0,0 +1,345 @@
1
+ import React, {
2
+ cloneElement,
3
+ forwardRef,
4
+ ReactNode,
5
+ useCallback,
6
+ useEffect,
7
+ useImperativeHandle,
8
+ useMemo,
9
+ useRef,
10
+ useState,
11
+ } from 'react';
12
+ import {
13
+ chakra,
14
+ ChakraProps,
15
+ Input,
16
+ InputGroup,
17
+ InputLeftElement,
18
+ useControllableState,
19
+ useMultiStyleConfig,
20
+ } from '@chakra-ui/react';
21
+ import { FloatingFocusManager } from '@floating-ui/react-dom-interactions';
22
+ import Icon from '../Icon/Icon';
23
+ import { DropdownEventArgs, DropdownProvider, useDropdownContext, useDropdownStyles } from './Dropdown.context';
24
+ import { DropdownOption, DropdownGroup, DropdownDetailedOption, DropdownOptionProps } from './DropdownOption';
25
+ import DropdownButton from './DropdownButton';
26
+ import useFloatingDropdown from './hooks/useFloatingDropdown';
27
+ import { useSimpleSearch, NoResultsFound } from './hooks/useSimpleSearch';
28
+ import { isSearchable } from './isNodeMatch';
29
+
30
+ type DropdownSearchCustomProps = {
31
+ value: string;
32
+ onChange: (newValue: string) => void;
33
+ };
34
+ type DropdownSearchProps = { placeholder?: string; reset?: boolean };
35
+ const DropdownSearch = ({
36
+ placeholder = 'Start typing to filter options',
37
+ reset = true,
38
+ ...rest
39
+ }: DropdownSearchProps | (DropdownSearchProps & DropdownSearchCustomProps)) => {
40
+ const { search } = useDropdownStyles();
41
+ const { searchValue, searchOnSubmit, searchOnChange, searchRef } = useDropdownContext();
42
+
43
+ const { value, onChange } = 'onChange' in rest ? rest : { value: searchValue, onChange: searchOnChange };
44
+
45
+ const onChangeCB = useCallback(
46
+ (ev: React.ChangeEvent<HTMLInputElement>) => {
47
+ onChange(ev.target.value);
48
+ },
49
+ [onChange],
50
+ );
51
+ useEffect(() => {
52
+ return () => {
53
+ if (reset) {
54
+ onChange('');
55
+ }
56
+ };
57
+ }, [reset]);
58
+ const onKeyDown = useCallback(
59
+ (ev: React.KeyboardEvent) => {
60
+ if (ev.key === 'Enter') {
61
+ ev.preventDefault();
62
+ searchOnSubmit();
63
+ }
64
+ },
65
+ [searchOnSubmit],
66
+ );
67
+ return (
68
+ <InputGroup sx={search}>
69
+ <InputLeftElement pointerEvents="none">
70
+ <Icon aria-hidden color="neutral.60" size="24" name="Magnifier" />
71
+ </InputLeftElement>
72
+ <Input
73
+ onKeyDown={onKeyDown}
74
+ ref={searchRef}
75
+ role="search"
76
+ aria-label={placeholder}
77
+ width="100%"
78
+ placeholder={placeholder}
79
+ value={value}
80
+ onChange={onChangeCB}
81
+ />
82
+ </InputGroup>
83
+ );
84
+ };
85
+ export { DropdownOption, DropdownGroup, DropdownSearch, NoResultsFound, DropdownDetailedOption };
86
+
87
+ type DropdownInstance<T> = { value: T; name?: string };
88
+ type DropdownChangeEventHandler<T> = (ev: { target: DropdownInstance<T> }) => void;
89
+ export interface DropdownProps<T> extends ChakraProps {
90
+ name?: string;
91
+ onChange?: DropdownChangeEventHandler<T>;
92
+ onBlur?: DropdownChangeEventHandler<T>;
93
+ value?: T;
94
+ defaultValue?: T;
95
+ size?: 'small' | 'medium';
96
+ dropdownMaxHeight?: ChakraProps['maxH'];
97
+ dropdownMinHeight?: ChakraProps['minH'];
98
+ readOnly?: boolean;
99
+ disabled?: boolean;
100
+ placeholder?: string;
101
+ 'aria-label'?: string;
102
+ search?: ReactNode;
103
+ children?: ReactNode;
104
+ }
105
+
106
+ function useOptionListWithIndexes({ children }: { children: ReactNode }) {
107
+ return useMemo(() => {
108
+ const childList = React.Children.toArray(children);
109
+ let idx = 0;
110
+ const transform = (ch: ReactNode): ReactNode => {
111
+ if (React.isValidElement(ch)) {
112
+ if (ch.type === DropdownOption || ch.type === DropdownDetailedOption) {
113
+ const index = idx;
114
+ idx += 1;
115
+ return cloneElement(ch, {
116
+ ...ch.props,
117
+ index,
118
+ });
119
+ }
120
+ if ('children' in ch.props) {
121
+ return cloneElement(ch, {
122
+ ...ch.props,
123
+ children: React.Children.toArray(ch.props.children).map(transform),
124
+ });
125
+ }
126
+ }
127
+ return ch;
128
+ };
129
+ return childList.map(transform);
130
+ }, [children]);
131
+ }
132
+
133
+ type UseDropdownProps<T> = {
134
+ ref: React.Ref<DropdownInstance<T>>;
135
+ optionsRef: React.RefObject<HTMLDivElement>;
136
+ };
137
+
138
+ function findOption<T>(children: ReactNode, value: T): { label: ReactNode; index: number } | null {
139
+ const list = React.Children.toArray(children);
140
+ for (let i = 0; i < list.length; i++) {
141
+ const elem = list[i];
142
+ if (React.isValidElement(elem)) {
143
+ const optValue = typeof elem.props.value === 'undefined' ? null : elem.props.value;
144
+ if (elem.type === DropdownOption && optValue === value) {
145
+ return { label: elem.props.children, index: elem.props.index };
146
+ }
147
+ const ch =
148
+ findOption(elem.props.children, value) || (isSearchable(elem.type) && findOption(elem.type(elem.props), value));
149
+ if (ch) {
150
+ return ch;
151
+ }
152
+ }
153
+ }
154
+ return null;
155
+ }
156
+
157
+ function useDropdown<T>({
158
+ name,
159
+ onChange,
160
+ value,
161
+ optionsRef,
162
+ defaultValue,
163
+ ref,
164
+ children,
165
+ readOnly,
166
+ ...rest
167
+ }: DropdownProps<T> & UseDropdownProps<T>) {
168
+ const searchRef = useRef(null);
169
+ const {
170
+ close,
171
+ isOpen,
172
+ getReferenceProps,
173
+ floatingProps,
174
+ context: floatingContext,
175
+ activeIndex,
176
+ setSelectedIndex,
177
+ setActiveIndex,
178
+ getItemProps,
179
+ listRef,
180
+ } = useFloatingDropdown({ enabled: !readOnly, optionsRef });
181
+ const [formValue, setFormValue] = useControllableState<T>({
182
+ onChange: (newValue) => onChange?.({ target: { value: newValue, name } }),
183
+ defaultValue,
184
+ value,
185
+ });
186
+
187
+ const [formLabel, setFormLabel] = useState<ReactNode>();
188
+ useImperativeHandle(ref, () => ({ value: formValue, name }), [formValue, name]);
189
+ const refdChildren = useOptionListWithIndexes({ children });
190
+
191
+ const searchOnSubmit = useCallback(() => {
192
+ if (activeIndex !== null) {
193
+ listRef.current[activeIndex]?.click();
194
+ }
195
+ }, [activeIndex]);
196
+ const referenceKeyDown = useCallback(
197
+ (ev: React.KeyboardEvent) => {
198
+ if (ev.key === 'Enter') {
199
+ ev.preventDefault();
200
+ searchOnSubmit();
201
+ }
202
+ },
203
+ [searchOnSubmit],
204
+ );
205
+ const { searchValue, searchOnChange, ...searchResults } = useSimpleSearch({
206
+ children: refdChildren,
207
+ onSearch: () => setActiveIndex(null),
208
+ });
209
+
210
+ const onOptionSelected = useCallback(
211
+ (args: DropdownEventArgs<T>) => {
212
+ setFormValue(args.value);
213
+ close();
214
+ },
215
+ [close, setFormValue],
216
+ );
217
+ const context = useMemo(
218
+ () => ({
219
+ formValue,
220
+ onOptionSelected,
221
+ getItemProps,
222
+ activeIndex,
223
+ searchOnChange,
224
+ searchValue,
225
+ searchRef,
226
+ searchOnSubmit,
227
+ listRef,
228
+ }),
229
+ [
230
+ listRef,
231
+ formValue,
232
+ onOptionSelected,
233
+ activeIndex,
234
+ getItemProps,
235
+ searchOnChange,
236
+ searchValue,
237
+ searchRef,
238
+ searchOnSubmit,
239
+ ],
240
+ );
241
+
242
+ useEffect(() => {
243
+ const currentOption = findOption(refdChildren, formValue);
244
+ if (currentOption) {
245
+ setFormLabel(currentOption.label);
246
+ setSelectedIndex(currentOption.index);
247
+ }
248
+ }, [refdChildren, formValue]);
249
+ return {
250
+ isOpen,
251
+ referenceProps: getReferenceProps({
252
+ onKeyDown: referenceKeyDown,
253
+ }),
254
+ floatingProps,
255
+ floatingContext,
256
+ searchOnSubmit,
257
+ searchRef,
258
+ context,
259
+ formLabel,
260
+ formValue,
261
+ ...searchResults,
262
+ ...rest,
263
+ };
264
+ }
265
+
266
+ const Dropdown = forwardRef<DropdownInstance<string | null>, DropdownProps<string | null>>(
267
+ (
268
+ { dropdownMaxHeight, dropdownMinHeight, readOnly, onBlur, placeholder, name, search, size = 'medium', ...props },
269
+ ref,
270
+ ) => {
271
+ const optionsRef = useRef(null);
272
+ const {
273
+ context: dropdownCtx,
274
+ floatingContext,
275
+ isOpen,
276
+ referenceProps,
277
+ floatingProps,
278
+ formValue,
279
+ formLabel,
280
+ searchRef,
281
+ children,
282
+ ...rest
283
+ } = useDropdown({
284
+ name,
285
+ readOnly,
286
+ optionsRef,
287
+ ref,
288
+ ...props,
289
+ });
290
+ const dropdownStyles = useMultiStyleConfig('Dropdown', { disabled: readOnly, size, ...rest });
291
+
292
+ const blurHandler = () => {
293
+ if (!onBlur) {
294
+ return;
295
+ }
296
+ if (!isOpen) {
297
+ onBlur({ target: { value: formValue, name } });
298
+ }
299
+ };
300
+ const listStyles = useMemo(
301
+ () => ({
302
+ ...dropdownStyles.list,
303
+ '--dropdown-floating-max': dropdownMaxHeight,
304
+ '--dropdown-floating-min': dropdownMinHeight,
305
+ }),
306
+ [dropdownMinHeight, dropdownMaxHeight, dropdownStyles.list],
307
+ );
308
+ const searchElement = search === false ? null : search || <DropdownSearch />;
309
+ return (
310
+ <>
311
+ {name && formValue && <input type="hidden" name={name} value={formValue} />}
312
+ <DropdownProvider context={dropdownCtx} styles={dropdownStyles}>
313
+ <DropdownButton
314
+ {...referenceProps}
315
+ {...props}
316
+ placeholder={placeholder}
317
+ size={size}
318
+ formLabel={formLabel}
319
+ blurHandler={blurHandler}
320
+ readOnly={readOnly}
321
+ />
322
+ {isOpen && (
323
+ <FloatingFocusManager initialFocus={searchRef} context={floatingContext}>
324
+ <chakra.div {...floatingProps} sx={listStyles}>
325
+ {searchElement}
326
+ <chakra.div tabIndex={-1} ref={optionsRef} __css={dropdownStyles.options}>
327
+ {children}
328
+ </chakra.div>
329
+ </chakra.div>
330
+ </FloatingFocusManager>
331
+ )}
332
+ </DropdownProvider>
333
+ </>
334
+ );
335
+ },
336
+ );
337
+
338
+ export function typedDropdown<T>() {
339
+ return {
340
+ Dropdown: Dropdown as React.ForwardRefExoticComponent<DropdownProps<T> & React.RefAttributes<DropdownInstance<T>>>,
341
+ DropdownOption: DropdownOption as (p: DropdownOptionProps<T>) => JSX.Element,
342
+ };
343
+ }
344
+
345
+ export default Dropdown;
@@ -0,0 +1,29 @@
1
+ import { forwardRef, ReactNode } from 'react';
2
+ import { chakra, ChakraProps } from '@chakra-ui/react';
3
+ import Icon from '../Icon/Icon';
4
+ import { useDropdownStyles } from './Dropdown.context';
5
+
6
+ interface DropdownButtonProps extends ChakraProps {
7
+ blurHandler: () => void;
8
+ readOnly?: boolean;
9
+ placeholder?: string;
10
+ formLabel?: ReactNode;
11
+ size: 'medium' | 'small';
12
+ children?: ReactNode;
13
+ }
14
+ const DropdownButton = forwardRef<HTMLButtonElement, DropdownButtonProps>(
15
+ ({ size, blurHandler, formLabel, readOnly, placeholder, children, ...rest }, ref) => {
16
+ const { field, icon } = useDropdownStyles();
17
+ const iconSize = size === 'medium' ? '24' : '16';
18
+ return (
19
+ <chakra.button aria-readonly={readOnly} type="button" ref={ref} onBlur={blurHandler} __css={field} {...rest}>
20
+ <chakra.span overflow="hidden" textOverflow="ellipsis" whiteSpace="nowrap" flexGrow={1}>
21
+ {formLabel || placeholder}
22
+ </chakra.span>
23
+ <Icon aria-hidden="true" __css={icon} name="DropdownArrows" fontSize={iconSize} size={iconSize} />
24
+ </chakra.button>
25
+ );
26
+ },
27
+ );
28
+
29
+ export default DropdownButton;
@@ -0,0 +1,89 @@
1
+ import { ReactNode, useId } from 'react';
2
+ import { chakra } from '@chakra-ui/react';
3
+ import { cx } from '@chakra-ui/utils';
4
+ import Text from '../Text/Text';
5
+ import Divider from '../Divider/Divider';
6
+ import { useDropdownContext, useDropdownStyles } from './Dropdown.context';
7
+
8
+ export type DropdownOptionProps<T> = {
9
+ value?: T | null;
10
+ children?: ReactNode;
11
+ 'aria-label'?: string;
12
+ };
13
+ const DropdownOption = <T = string,>({ value = null, children, ...rest }: DropdownOptionProps<T>) => {
14
+ const { item } = useDropdownStyles();
15
+ const ctx = useDropdownContext<T | null>();
16
+ const { index } = rest as { index?: number };
17
+ return (
18
+ <chakra.div
19
+ role="option"
20
+ id={useId()}
21
+ ref={(node) => {
22
+ if (ctx.listRef.current) {
23
+ ctx.listRef.current[index!] = node;
24
+ }
25
+ }}
26
+ {...rest}
27
+ className={cx(
28
+ value === ctx.formValue && 'bitkit-select__option-active',
29
+ index === ctx.activeIndex && 'bitkit-select__option-hover',
30
+ )}
31
+ __css={item}
32
+ {...ctx.getItemProps({
33
+ onClick() {
34
+ ctx.onOptionSelected({ value, index, label: children });
35
+ },
36
+ })}
37
+ >
38
+ {children}
39
+ </chakra.div>
40
+ );
41
+ };
42
+
43
+ const DropdownGroup = ({ children, label }: { children: ReactNode; label: string }) => {
44
+ const { group } = useDropdownStyles();
45
+ const textId = useId();
46
+ return (
47
+ <div role="group" aria-labelledby={textId}>
48
+ <chakra.div __css={group}>
49
+ <Text size="1" id={textId} fontWeight="bold" color="neutral.60">
50
+ {label}
51
+ </Text>
52
+ <Divider color="neutral.93" />
53
+ </chakra.div>
54
+ {children}
55
+ </div>
56
+ );
57
+ };
58
+
59
+ const DropdownDetailedOption = ({
60
+ icon,
61
+ title,
62
+ subtitle,
63
+ value,
64
+ ...rest
65
+ }: {
66
+ icon: ReactNode;
67
+ title: string;
68
+ subtitle: string;
69
+ value?: string;
70
+ }) => {
71
+ return (
72
+ <DropdownOption value={value} {...rest} aria-label={title}>
73
+ <chakra.div alignItems="center" gap="12" display="flex" flexDir="row">
74
+ {icon}
75
+ <chakra.div>
76
+ <Text size="3" color="purple.10">
77
+ {title}
78
+ </Text>
79
+ <Text size="2" color="neutral.60">
80
+ {subtitle}
81
+ </Text>
82
+ </chakra.div>
83
+ </chakra.div>
84
+ </DropdownOption>
85
+ );
86
+ };
87
+ DropdownDetailedOption.searchable = true;
88
+
89
+ export { DropdownOption, DropdownGroup, DropdownDetailedOption };
@@ -0,0 +1,61 @@
1
+ import { useLayoutEffect, useRef } from 'react';
2
+
3
+ const useAutoScroll = ({
4
+ optionsRef,
5
+ listRef,
6
+ activeIndex,
7
+ selectedIndex,
8
+ keyboardControlled,
9
+ }: {
10
+ optionsRef: React.RefObject<HTMLDivElement>;
11
+ listRef: React.MutableRefObject<HTMLElement[]>;
12
+ activeIndex: number | null;
13
+ selectedIndex: number | null;
14
+ keyboardControlled: boolean;
15
+ }) => {
16
+ const prevActiveIndexRef = useRef<number | null>();
17
+ useLayoutEffect(() => {
18
+ prevActiveIndexRef.current = activeIndex;
19
+ }, [activeIndex]);
20
+ const prevActiveIndex = prevActiveIndexRef.current;
21
+
22
+ useLayoutEffect(() => {
23
+ const options = optionsRef.current;
24
+
25
+ if (options && keyboardControlled) {
26
+ let activeItem = null;
27
+ if (activeIndex != null) {
28
+ activeItem = listRef.current[activeIndex];
29
+ } else if (selectedIndex != null) {
30
+ activeItem = listRef.current[selectedIndex];
31
+ }
32
+ if (activeItem && prevActiveIndex != null) {
33
+ const activeItemHeight = listRef.current[prevActiveIndex]?.offsetHeight ?? 0;
34
+
35
+ const floatingHeight = options.offsetHeight;
36
+ const top = activeItem.offsetTop;
37
+ const bottom = top + activeItemHeight;
38
+
39
+ if (top < options.scrollTop) {
40
+ options.scrollTop -= options.scrollTop - top + 5;
41
+ } else if (bottom > floatingHeight + options.scrollTop) {
42
+ options.scrollTop += bottom - floatingHeight - options.scrollTop + 5;
43
+ }
44
+ }
45
+ }
46
+ }, [prevActiveIndex, activeIndex, optionsRef, selectedIndex, keyboardControlled]);
47
+
48
+ useLayoutEffect(() => {
49
+ requestAnimationFrame(() => {
50
+ const options = optionsRef.current;
51
+ if (options && options.clientHeight < options.scrollHeight) {
52
+ const selectedItem = selectedIndex && listRef.current[selectedIndex];
53
+ if (selectedItem) {
54
+ options.scrollTop = selectedItem.offsetTop - options.offsetHeight / 2 + selectedItem.offsetHeight / 2;
55
+ }
56
+ }
57
+ });
58
+ }, [selectedIndex, optionsRef]);
59
+ };
60
+
61
+ export default useAutoScroll;
@@ -0,0 +1,99 @@
1
+ import { RefObject, useEffect, useRef, useState } from 'react';
2
+ import {
3
+ autoUpdate,
4
+ useFloating,
5
+ useInteractions,
6
+ size,
7
+ useClick,
8
+ useListNavigation,
9
+ useRole,
10
+ useDismiss,
11
+ flip,
12
+ } from '@floating-ui/react-dom-interactions';
13
+ import useAutoScroll from './useAutoScroll';
14
+
15
+ const useFloatingDropdown = ({ enabled, optionsRef }: { enabled: boolean; optionsRef: RefObject<HTMLDivElement> }) => {
16
+ const listRef = useRef<HTMLElement[]>([]);
17
+ const [isOpen, setOpen] = useState(false);
18
+ const [activeIndex, setActiveIndex] = useState<number | null>(null);
19
+ const [selectedIndex, setSelectedIndex] = useState<number | null>(null);
20
+ const [keyboardControlled, setKeyboardControlled] = useState(true);
21
+ useEffect(() => {
22
+ setKeyboardControlled(true);
23
+ }, [isOpen]);
24
+ const keyboardControlledHandlers = {
25
+ onPointerEnter() {
26
+ setKeyboardControlled(false);
27
+ },
28
+ onPointerMove() {
29
+ setKeyboardControlled(false);
30
+ },
31
+ onKeyDown() {
32
+ setKeyboardControlled(true);
33
+ },
34
+ };
35
+
36
+ const { context, reference, floating, strategy, x, y } = useFloating({
37
+ open: enabled && isOpen,
38
+ onOpenChange: setOpen,
39
+ whileElementsMounted(aRef, aFloat, update) {
40
+ return autoUpdate(aRef, aFloat, update, { elementResize: false });
41
+ },
42
+ middleware: [
43
+ size({
44
+ padding: 5,
45
+ apply({ elements, availableHeight, rects }) {
46
+ Object.assign(elements.floating.style, {
47
+ width: `${rects.reference.width}px`,
48
+ });
49
+ elements.floating.style.setProperty('--floating-available-height', `${availableHeight}px`);
50
+ },
51
+ }),
52
+ flip(),
53
+ ],
54
+ });
55
+ const { getReferenceProps, getFloatingProps, getItemProps } = useInteractions([
56
+ useClick(context, { enabled }),
57
+ useRole(context, { role: 'listbox' }),
58
+ useDismiss(context),
59
+ useListNavigation(context, {
60
+ enabled,
61
+ listRef,
62
+ activeIndex,
63
+ selectedIndex,
64
+ virtual: true,
65
+ loop: true,
66
+ onNavigate: setActiveIndex,
67
+ }),
68
+ {
69
+ floating: keyboardControlledHandlers,
70
+ },
71
+ ]);
72
+ useAutoScroll({
73
+ listRef,
74
+ optionsRef,
75
+ activeIndex,
76
+ selectedIndex,
77
+ keyboardControlled,
78
+ });
79
+
80
+ return {
81
+ listRef,
82
+ close: () => setOpen(false),
83
+ isOpen,
84
+ getItemProps,
85
+ activeIndex,
86
+ setActiveIndex,
87
+ setSelectedIndex,
88
+ getReferenceProps(props: React.HTMLProps<Element>) {
89
+ return getReferenceProps({ ...props, ref: reference });
90
+ },
91
+ floatingProps: getFloatingProps({
92
+ ref: floating,
93
+ style: { position: strategy, top: y ?? 0, left: x ?? 0 },
94
+ }),
95
+ context,
96
+ };
97
+ };
98
+
99
+ export default useFloatingDropdown;
@@ -0,0 +1,43 @@
1
+ import { Children, cloneElement, isValidElement, ReactNode, useMemo, useState } from 'react';
2
+ import { chakra } from '@chakra-ui/react';
3
+ import { useDropdownStyles } from '../Dropdown.context';
4
+ import isNodeMatch from '../isNodeMatch';
5
+ import { DropdownGroup } from '../DropdownOption';
6
+
7
+ const NoResultsFound = ({ children }: { children: ReactNode }) => {
8
+ const { item } = useDropdownStyles();
9
+ return <chakra.div __css={item}>{children}</chakra.div>;
10
+ };
11
+ function useSimpleSearch({ children, onSearch }: { children?: ReactNode; onSearch?: () => void }) {
12
+ const [searchValue, setSearchValue] = useState('');
13
+ const searchOnChange = (newValue: string) => {
14
+ setSearchValue(newValue);
15
+ onSearch?.();
16
+ };
17
+ const options = useMemo(() => {
18
+ if (!searchValue) {
19
+ return children;
20
+ }
21
+
22
+ const transform = (node: ReactNode): ReactNode => {
23
+ if (isValidElement(node) && node.type === DropdownGroup) {
24
+ const groupChildren = Children.toArray(node.props.children).map(transform).filter(Boolean);
25
+ if (groupChildren.length === 0) {
26
+ return null;
27
+ }
28
+ return cloneElement(node, {
29
+ ...node.props,
30
+ children: groupChildren,
31
+ });
32
+ }
33
+ return isNodeMatch(node, searchValue) ? node : null;
34
+ };
35
+
36
+ const results = Children.toArray(children).map(transform).filter(Boolean);
37
+
38
+ return results.length ? results : <NoResultsFound>No results found for `{searchValue}`</NoResultsFound>;
39
+ }, [children, searchValue]);
40
+
41
+ return { children: options, searchValue, searchOnChange };
42
+ }
43
+ export { useSimpleSearch, NoResultsFound };
@@ -0,0 +1,39 @@
1
+ import React, { JSXElementConstructor, ReactElement, ReactNode } from 'react';
2
+
3
+ type SearchableElement = JSXElementConstructor<any> & { searchable: true };
4
+ export function isSearchable(
5
+ node: string | JSXElementConstructor<any> | SearchableElement,
6
+ ): node is (x: any) => ReactElement {
7
+ return typeof node === 'function' && 'searchable' in node && node.searchable;
8
+ }
9
+
10
+ function isNodeMatch(node: ReactNode, filter: string): boolean {
11
+ if (typeof node === 'number' || typeof node === 'boolean' || typeof node === 'undefined' || node === null) {
12
+ return false;
13
+ }
14
+ if (typeof node === 'string') {
15
+ return node.toLowerCase().includes(filter.toLowerCase());
16
+ }
17
+ if (Array.isArray(node)) {
18
+ return node.some((child) => isNodeMatch(child, filter));
19
+ }
20
+ if ('children' in node) {
21
+ return isNodeMatch(node.children, filter);
22
+ }
23
+ if (React.isValidElement(node)) {
24
+ if (node.type === 'svg') {
25
+ return false;
26
+ }
27
+ const ctor = node.type;
28
+ if (isSearchable(ctor)) {
29
+ return isNodeMatch(ctor(node.props), filter);
30
+ }
31
+ return isNodeMatch(node.props.children, filter);
32
+ }
33
+ if (Symbol.iterator in node) {
34
+ return isNodeMatch(Array.from(node), filter);
35
+ }
36
+ return false;
37
+ }
38
+
39
+ export default isNodeMatch;