@ssa-ui-kit/core 1.0.2 → 1.0.4

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.
Files changed (66) hide show
  1. package/dist/components/Button/fixtures.d.ts +8 -0
  2. package/dist/components/Button/types.d.ts +2 -0
  3. package/dist/components/FormHelperText/FormHelperText.d.ts +1 -1
  4. package/dist/components/ImageItem/ImageItem.d.ts +2 -0
  5. package/dist/components/ImageItem/index.d.ts +1 -0
  6. package/dist/components/ImageItem/types.d.ts +8 -0
  7. package/dist/components/Input/types.d.ts +1 -0
  8. package/dist/components/Label/Label.d.ts +1 -1
  9. package/dist/components/Label/LabelBase.d.ts +2 -0
  10. package/dist/components/Label/types.d.ts +1 -0
  11. package/dist/components/Typeahead/Typeahead.context.d.ts +36 -0
  12. package/dist/components/Typeahead/Typeahead.d.ts +11 -0
  13. package/dist/components/Typeahead/components/MultipleTrigger.d.ts +1 -0
  14. package/dist/components/Typeahead/components/NoOptions.d.ts +1 -0
  15. package/dist/components/Typeahead/components/SingleTrigger.d.ts +1 -0
  16. package/dist/components/Typeahead/components/TypeaheadItem.d.ts +8 -0
  17. package/dist/components/Typeahead/components/TypeaheadOption.d.ts +2 -0
  18. package/dist/components/Typeahead/components/TypeaheadOptions.d.ts +2 -0
  19. package/dist/components/Typeahead/components/TypeaheadTrigger.d.ts +1 -0
  20. package/dist/components/Typeahead/components/index.d.ts +7 -0
  21. package/dist/components/Typeahead/index.d.ts +5 -0
  22. package/dist/components/Typeahead/styles.d.ts +47 -0
  23. package/dist/components/Typeahead/types.d.ts +46 -0
  24. package/dist/components/Typeahead/useTypeahead.d.ts +36 -0
  25. package/dist/components/Typeahead/utils.d.ts +1 -0
  26. package/dist/components/index.d.ts +2 -0
  27. package/dist/index.js +1 -1
  28. package/dist/index.js.map +1 -1
  29. package/dist/types/emotion.d.ts +4 -1
  30. package/package.json +3 -3
  31. package/src/components/Badge/Badge.stories.tsx +1 -1
  32. package/src/components/Button/Button.tsx +10 -2
  33. package/src/components/Button/types.ts +2 -0
  34. package/src/components/FormHelperText/FormHelperText.tsx +2 -1
  35. package/src/components/ImageItem/ImageItem.spec.tsx +54 -0
  36. package/src/components/ImageItem/ImageItem.stories.tsx +33 -0
  37. package/src/components/ImageItem/ImageItem.tsx +43 -0
  38. package/src/components/ImageItem/index.ts +1 -0
  39. package/src/components/ImageItem/types.ts +7 -0
  40. package/src/components/Input/Input.spec.tsx +6 -8
  41. package/src/components/Input/Input.tsx +14 -8
  42. package/src/components/Input/types.ts +1 -0
  43. package/src/components/Label/Label.tsx +2 -0
  44. package/src/components/Label/LabelBase.tsx +3 -2
  45. package/src/components/Label/types.ts +1 -0
  46. package/src/components/Typeahead/Typeahead.context.ts +59 -0
  47. package/src/components/Typeahead/Typeahead.spec.tsx +506 -0
  48. package/src/components/Typeahead/Typeahead.stories.tsx +372 -0
  49. package/src/components/Typeahead/Typeahead.tsx +120 -0
  50. package/src/components/Typeahead/components/MultipleTrigger.tsx +116 -0
  51. package/src/components/Typeahead/components/NoOptions.tsx +7 -0
  52. package/src/components/Typeahead/components/SingleTrigger.tsx +71 -0
  53. package/src/components/Typeahead/components/TypeaheadItem.ts +14 -0
  54. package/src/components/Typeahead/components/TypeaheadOption.tsx +12 -0
  55. package/src/components/Typeahead/components/TypeaheadOptions.tsx +25 -0
  56. package/src/components/Typeahead/components/TypeaheadTrigger.tsx +26 -0
  57. package/src/components/Typeahead/components/index.ts +7 -0
  58. package/src/components/Typeahead/index.ts +5 -0
  59. package/src/components/Typeahead/styles.ts +193 -0
  60. package/src/components/Typeahead/types.ts +77 -0
  61. package/src/components/Typeahead/useTypeahead.tsx +321 -0
  62. package/src/components/Typeahead/utils.tsx +22 -0
  63. package/src/components/index.ts +2 -0
  64. package/src/themes/main.ts +3 -0
  65. package/src/types/emotion.ts +3 -0
  66. package/tsbuildcache +1 -1
@@ -0,0 +1,12 @@
1
+ import Icon from '@components/Icon';
2
+ import * as S from '../styles';
3
+ import { TypeaheadItemProps } from '../types';
4
+
5
+ export const TypeaheadOption = ({ children, ...rest }: TypeaheadItemProps) => (
6
+ <S.TypeaheadOption {...rest}>
7
+ {children}{' '}
8
+ {rest.isActive && (
9
+ <Icon name="check" size={10} css={{ marginLeft: 'auto' }} />
10
+ )}
11
+ </S.TypeaheadOption>
12
+ );
@@ -0,0 +1,25 @@
1
+ import React from 'react';
2
+ import { NoOptions } from './NoOptions';
3
+ import { TypeaheadItemsListProps } from '../types';
4
+ import { useTypeaheadContext } from '../Typeahead.context';
5
+ import * as S from '../styles';
6
+
7
+ export const TypeaheadOptions = ({
8
+ noItemsMessage = 'No matches found',
9
+ children,
10
+ }: TypeaheadItemsListProps) => {
11
+ const context = useTypeaheadContext();
12
+ let options = context.options || [];
13
+
14
+ if (React.Children.toArray(children).filter(Boolean).length === 0) {
15
+ options = [
16
+ <NoOptions key={'no-items'} aria-selected={false}>
17
+ {noItemsMessage}
18
+ </NoOptions>,
19
+ ];
20
+ }
21
+
22
+ return (
23
+ <S.TypeaheadOptionsBase role="listbox">{options}</S.TypeaheadOptionsBase>
24
+ );
25
+ };
@@ -0,0 +1,26 @@
1
+ import { MultipleTrigger } from './MultipleTrigger';
2
+ import { SingleTrigger } from './SingleTrigger';
3
+ import { useTypeaheadContext } from '../Typeahead.context';
4
+ import * as S from '../styles';
5
+
6
+ export const TypeaheadTrigger = () => {
7
+ const context = useTypeaheadContext();
8
+ return (
9
+ <S.TypeaheadTrigger
10
+ as="div"
11
+ role="combobox"
12
+ ref={context.triggerRef}
13
+ className={context.className}
14
+ isOpen={context.isOpen}
15
+ isDisabled={context.isDisabled}
16
+ status={context.status}
17
+ aria-labelledby={`typeahead-label-${context.typeaheadId}`}
18
+ aria-controls={`typeahead-popup-${context.typeaheadId}`}
19
+ startIcon={context.startIcon}
20
+ startIconClassName={context.startIconClassName}
21
+ endIcon={context.endIcon}
22
+ endIconClassName={context.endIconClassName}>
23
+ {context.isMultiple ? <MultipleTrigger /> : <SingleTrigger />}
24
+ </S.TypeaheadTrigger>
25
+ );
26
+ };
@@ -0,0 +1,7 @@
1
+ export * from './MultipleTrigger';
2
+ export * from './SingleTrigger';
3
+ export * from './TypeaheadTrigger';
4
+ export * from './TypeaheadItem';
5
+ export * from './TypeaheadOptions';
6
+ export * from './TypeaheadOption';
7
+ export * from './NoOptions';
@@ -0,0 +1,5 @@
1
+ export * from './Typeahead';
2
+ export * from './components/TypeaheadOptions';
3
+ export * from './components/TypeaheadOption';
4
+ export * from './components/TypeaheadItem';
5
+ export * from './utils';
@@ -0,0 +1,193 @@
1
+ import { css } from '@emotion/css';
2
+ import styled from '@emotion/styled';
3
+ import Wrapper from '@components/Wrapper';
4
+ import Button from '@components/Button';
5
+ import { PopoverTrigger } from '@components/Popover';
6
+ import { useTypeahead } from './useTypeahead';
7
+ import { TypeaheadItemProps } from './types';
8
+
9
+ export const TypeaheadOptionsBase = styled.ul`
10
+ padding: 0;
11
+ margin: 0;
12
+ list-style: none;
13
+ background: #fff;
14
+ border-radius: 8px;
15
+ filter: ${({ theme }) =>
16
+ `drop-shadow(-4px 4px 14px ${theme.colors.greyDarker14})`};
17
+ backdrop-filter: ${({ theme }) =>
18
+ `drop-shadow(-4px 4px 14px ${theme.colors.greyDarker14})`};
19
+ `;
20
+
21
+ export const TypeaheadOption = styled.li<TypeaheadItemProps>`
22
+ display: flex;
23
+ align-items: center;
24
+ padding: 8px 16px;
25
+ border: none;
26
+ cursor: pointer;
27
+ font-size: 14px;
28
+ gap: 8px;
29
+ padding: 12px;
30
+ height: 40px;
31
+ background: ${({ isActive, theme }) =>
32
+ isActive ? theme.colors.blueRoyal12 : 'none'};
33
+ &:hover {
34
+ background: rgba(72, 125, 225, 0.06);
35
+ }
36
+ `;
37
+
38
+ export const TypeaheadInput = css`
39
+ &.typeahead-input {
40
+ border: none;
41
+ border-radius: 0;
42
+ height: 32px;
43
+ cursor: pointer;
44
+ padding: 0;
45
+ background: transparent;
46
+ text-indent: 8px;
47
+ &:active,
48
+ &:focus {
49
+ min-width: 100%;
50
+ }
51
+ }
52
+ `;
53
+
54
+ export const TypeaheadInputPlaceholder = css`
55
+ position: absolute;
56
+ top: 0;
57
+ left: -4px;
58
+ font-weight: 400;
59
+ font-size: 0.875rem;
60
+ line-height: 1rem;
61
+ color: rgba(0, 0, 0, 0.54);
62
+ &:disabled:hover {
63
+ cursor: default;
64
+ }
65
+ `;
66
+
67
+ export const TypeaheadInputWrapper = css`
68
+ height: 32px;
69
+ z-index: 5;
70
+ background: transparent;
71
+ margin-left: -8px;
72
+ &:active,
73
+ &:focus {
74
+ min-width: 100%;
75
+ }
76
+ `;
77
+
78
+ export const TypeaheadItem = styled.div<{ isDisabled?: boolean }>`
79
+ display: flex;
80
+ gap: 6px;
81
+ background: ${({ theme, isDisabled }) =>
82
+ isDisabled
83
+ ? theme.colors.greySelectedMenuItem
84
+ : theme.colors.greyLighter40};
85
+ border-radius: 24px;
86
+ border: 1px solid ${({ theme }) => theme.colors.grey};
87
+ color: ${({ theme, isDisabled }) =>
88
+ isDisabled ? theme.colors.grey : theme.colors.greyDarker};
89
+ font-weight: 500;
90
+ font-size: 14px;
91
+ height: 32px;
92
+ align-items: center;
93
+ padding: 6px;
94
+ user-select: none;
95
+ `;
96
+
97
+ export const TypeaheadItemLabel = styled.div<{ isDisabled?: boolean }>`
98
+ color: ${({ theme, isDisabled }) =>
99
+ isDisabled ? theme.colors.grey : theme.colors.greyDarker};
100
+ font-size: 14px;
101
+ font-weight: 500;
102
+ display: flex;
103
+ gap: 6px;
104
+ align-items: center;
105
+ cursor: default;
106
+ `;
107
+
108
+ export const TypeaheadItemCross = styled(Button)`
109
+ background: none;
110
+ padding: 0;
111
+ padding-right: 5;
112
+ &:active,
113
+ &:focus,
114
+ &:hover {
115
+ cursor: ${({ isDisabled }) => (isDisabled ? 'default' : 'pointer')};
116
+ background: none;
117
+ box-shadow: none;
118
+ }
119
+ &:disabled {
120
+ background: none;
121
+ }
122
+ `;
123
+
124
+ export const TypeaheadInputsGroupWrapper = styled(Wrapper)<{
125
+ isOpen: boolean;
126
+ }>`
127
+ position: relative;
128
+ flex: 1 1 0%;
129
+ min-width: ${({ isOpen }) => (isOpen ? '50px' : 'auto')};
130
+ flex-direction: column !important;
131
+ `;
132
+
133
+ export const TypeaheadTrigger = styled(PopoverTrigger)<{
134
+ isOpen: boolean;
135
+ status: ReturnType<typeof useTypeahead>['status'];
136
+ }>`
137
+ position: relative;
138
+ border-radius: 8px;
139
+ border: 1px solid
140
+ ${({ status, theme }) =>
141
+ status === 'basic'
142
+ ? theme.colors.grey
143
+ : status === 'error'
144
+ ? theme.colors.red
145
+ : theme.colors.greenLighter};
146
+ min-height: 44px;
147
+ height: auto;
148
+ background: #fff;
149
+ gap: 8px;
150
+ padding: 5px 28px 5px 8px;
151
+ width: 300px;
152
+ flex-wrap: wrap;
153
+ border-color: ${({ isOpen, theme, status }) =>
154
+ isOpen &&
155
+ (status === 'error'
156
+ ? theme.colors.red
157
+ : status === 'success'
158
+ ? theme.colors.greenLighter
159
+ : theme.colors.blueRoyal)};
160
+ background: ${({ isDisabled, theme }) =>
161
+ isDisabled ? theme.colors.greyLighter : theme.colors.white};
162
+ &:active,
163
+ &:focus,
164
+ &:hover {
165
+ background: ${({ isDisabled, theme }) =>
166
+ isDisabled ? theme.colors.greyLighter : theme.colors.white};
167
+ box-shadow: none;
168
+ }
169
+ &:hover {
170
+ border-color: ${({ isDisabled, theme, status }) =>
171
+ isDisabled
172
+ ? theme.colors.grey
173
+ : status === 'error'
174
+ ? theme.colors.red
175
+ : status === 'success'
176
+ ? theme.colors.greenLighter
177
+ : theme.colors.greyDarker80};
178
+ cursor: ${({ isDisabled }) => (isDisabled ? 'default' : 'pointer')};
179
+ }
180
+ &:focus,
181
+ &:active {
182
+ border-color: ${({ theme, status }) =>
183
+ status === 'error'
184
+ ? theme.colors.red
185
+ : status === 'success'
186
+ ? theme.colors.greenLighter
187
+ : theme.colors.blueRoyal};
188
+ ${({ isDisabled, theme }) =>
189
+ isDisabled && {
190
+ borderColor: theme.colors.grey,
191
+ }}
192
+ }
193
+ `;
@@ -0,0 +1,77 @@
1
+ import { CommonProps } from '@global-types/emotion';
2
+ import {
3
+ FieldError,
4
+ FieldValues,
5
+ UseFormReturn,
6
+ UseFormSetValue,
7
+ } from 'react-hook-form';
8
+
9
+ export type TypeaheadValue = string | number;
10
+
11
+ export type TypeaheadOptionProps = Record<string, TypeaheadValue>;
12
+
13
+ export interface TypeaheadProps {
14
+ initialSelectedItems?: Array<TypeaheadValue>;
15
+ isMultiple?: boolean;
16
+ isDisabled?: boolean;
17
+ children?: React.ReactNode;
18
+ className?: string;
19
+ optionsClassName?: string;
20
+ isOpen?: boolean;
21
+ startIcon?: React.ReactNode;
22
+ endIcon?: React.ReactNode;
23
+ startIconClassName?: string;
24
+ endIconClassName?: string;
25
+ name?: string;
26
+ label?: string;
27
+ helperText?: string;
28
+ errors?: FieldError;
29
+ success?: boolean;
30
+ validationSchema?: Record<string, unknown>;
31
+ placeholder?: string | null;
32
+ setValue?: UseFormSetValue<FieldValues>;
33
+ register?: UseFormReturn['register'];
34
+ onChange?: (selectedItem: TypeaheadValue, isSelected: boolean) => void;
35
+ renderOption?: (data: {
36
+ value: string | number;
37
+ input: string;
38
+ label: string;
39
+ }) => React.ReactNode;
40
+ }
41
+
42
+ export type UseTypeaheadProps = Pick<
43
+ TypeaheadProps,
44
+ | 'initialSelectedItems'
45
+ | 'isDisabled'
46
+ | 'children'
47
+ | 'isMultiple'
48
+ | 'onChange'
49
+ | 'renderOption'
50
+ | 'isOpen'
51
+ | 'className'
52
+ | 'startIcon'
53
+ | 'endIcon'
54
+ | 'startIconClassName'
55
+ | 'endIconClassName'
56
+ | 'name'
57
+ | 'register'
58
+ | 'setValue'
59
+ | 'validationSchema'
60
+ | 'errors'
61
+ | 'success'
62
+ | 'placeholder'
63
+ >;
64
+
65
+ export interface TypeaheadItemsListProps extends CommonProps {
66
+ children?: React.ReactNode;
67
+ noItemsMessage?: string;
68
+ }
69
+
70
+ export interface TypeaheadItemProps extends CommonProps {
71
+ isActive?: boolean;
72
+ isDisabled?: boolean;
73
+ value?: string | number;
74
+ label?: string | number;
75
+ children?: React.ReactNode;
76
+ onClick?: (e: React.MouseEvent<HTMLElement>) => void;
77
+ }
@@ -0,0 +1,321 @@
1
+ import React, {
2
+ BaseSyntheticEvent,
3
+ MouseEventHandler,
4
+ useEffect,
5
+ useId,
6
+ useRef,
7
+ useState,
8
+ } from 'react';
9
+ import { propOr } from '@ssa-ui-kit/utils';
10
+ import { TypeaheadOptionProps, UseTypeaheadProps } from './types';
11
+
12
+ export const useTypeahead = ({
13
+ name = 'typeahead-input',
14
+ isOpen: isInitOpen,
15
+ initialSelectedItems,
16
+ isDisabled,
17
+ isMultiple,
18
+ children,
19
+ className,
20
+ startIcon,
21
+ endIcon,
22
+ startIconClassName,
23
+ endIconClassName,
24
+ validationSchema,
25
+ errors,
26
+ success,
27
+ placeholder,
28
+ register,
29
+ setValue,
30
+ onChange,
31
+ renderOption,
32
+ }: UseTypeaheadProps) => {
33
+ const inputName = `${name}-text`;
34
+ const [isOpen, setIsOpen] = useState(isInitOpen || false);
35
+ const [selected, setSelected] = useState<Array<string | number>>(
36
+ initialSelectedItems || [],
37
+ );
38
+ const [optionsWithKey, setOptionsWithKey] = useState<
39
+ Record<number | string, Record<string, string | number>>
40
+ >({});
41
+ const [items, setItems] = useState<Array<React.ReactElement> | undefined>();
42
+ const [inputValue, setInputValue] = useState<string>('');
43
+ const [status, setStatus] = useState<'basic' | 'success' | 'error'>('basic');
44
+
45
+ const inputRef = useRef<HTMLInputElement>(null);
46
+ const typeaheadId = useId();
47
+ const triggerRef: React.MutableRefObject<HTMLDivElement | null> =
48
+ useRef<HTMLDivElement>(null);
49
+ const [firstSuggestion, setFirstSuggestion] = useState('');
50
+
51
+ useEffect(() => {
52
+ if (!register) {
53
+ console.warn('Typeahead component must be used within a Form component');
54
+ }
55
+ }, []);
56
+
57
+ useEffect(() => {
58
+ if (isMultiple) {
59
+ setValue?.(name, selected);
60
+ setInputValue('');
61
+ setFirstSuggestion('');
62
+ } else {
63
+ setValue?.(name, selected.length ? selected[0] : undefined);
64
+ }
65
+ }, [selected]);
66
+
67
+ useEffect(() => {
68
+ if (isDisabled && isOpen) {
69
+ setIsOpen(false);
70
+ }
71
+ }, [isDisabled]);
72
+
73
+ useEffect(() => {
74
+ const status = success ? 'success' : errors ? 'error' : 'basic';
75
+ setStatus(status);
76
+ }, [errors, success]);
77
+
78
+ useEffect(() => {
79
+ const keyedOptions: Record<
80
+ number | string,
81
+ Record<string, string | number>
82
+ > = {};
83
+ const childItems = (
84
+ React.Children.toArray(children).filter(Boolean) as React.ReactElement[]
85
+ ).map((child, index) => {
86
+ keyedOptions[child.props.value] = {
87
+ ...child.props,
88
+ };
89
+
90
+ return React.cloneElement(child, {
91
+ index,
92
+ ...child.props,
93
+ });
94
+ });
95
+ setOptionsWithKey(keyedOptions);
96
+ setItems(childItems);
97
+ }, [initialSelectedItems, children]);
98
+
99
+ useEffect(() => {
100
+ const childrenArray = React.Children.toArray(children).filter(Boolean);
101
+ const filteredOptions = [...childrenArray] as React.ReactElement[];
102
+ const childItems = filteredOptions.map((child, index) => {
103
+ const { id, value, label, isDisabled } = child.props;
104
+ const isActive = selected.includes(child.props.value);
105
+ return React.cloneElement(child, {
106
+ index,
107
+ ...child.props,
108
+ isActive,
109
+ isDisabled,
110
+ id,
111
+ 'aria-selected': isActive,
112
+ 'aria-labelledby': `typeahead-label-${name}`,
113
+ role: 'option',
114
+ onClick: (event: BaseSyntheticEvent) => {
115
+ event.preventDefault();
116
+ if (!isDisabled) {
117
+ handleChange(child.props.value);
118
+ }
119
+ },
120
+ children: renderOption
121
+ ? renderOption({ value: id || value, input: inputValue || '', label })
122
+ : child.props.children || child.props.label || child.props.value,
123
+ });
124
+ });
125
+ setItems(childItems);
126
+ }, [inputValue, optionsWithKey, selected]);
127
+
128
+ useEffect(() => {
129
+ if (!isMultiple && Object.keys(optionsWithKey).length) {
130
+ const foundItem = Object.values(optionsWithKey).find(
131
+ (item) => item.label === inputValue,
132
+ );
133
+ if (!foundItem && selected.length) {
134
+ setSelected([]);
135
+ }
136
+ if (foundItem && !selected.includes(foundItem?.value)) {
137
+ setSelected([foundItem?.value]);
138
+ }
139
+ }
140
+ }, [optionsWithKey, inputValue]);
141
+
142
+ useEffect(() => {
143
+ if (!isMultiple && selected.length && Object.keys(optionsWithKey).length) {
144
+ const currentOption = optionsWithKey[selected[0]];
145
+ const optionText =
146
+ currentOption &&
147
+ (currentOption.children || currentOption.label || currentOption.value);
148
+ setInputValue(`${optionText}`);
149
+ }
150
+ }, [selected, optionsWithKey]);
151
+
152
+ useEffect(() => {
153
+ if (inputValue) {
154
+ const newFirstSuggestion = Object.values(optionsWithKey)?.find((item) => {
155
+ const label = propOr<TypeaheadOptionProps, string>('', 'label')(item);
156
+ return label.toLowerCase().startsWith(inputValue.toLowerCase());
157
+ });
158
+ const firstSuggestionLabel = propOr<TypeaheadOptionProps, string>(
159
+ '',
160
+ 'label',
161
+ )(newFirstSuggestion as unknown as TypeaheadOptionProps);
162
+ const humanSuggestionLabel = inputValue.concat(
163
+ firstSuggestionLabel.slice(inputValue.length),
164
+ );
165
+ setFirstSuggestion(humanSuggestionLabel);
166
+ } else {
167
+ setFirstSuggestion('');
168
+ if (isMultiple) {
169
+ setInputValue('');
170
+ }
171
+ }
172
+ }, [inputValue, items, selected]);
173
+
174
+ const handleOpenChange = (open: boolean) => {
175
+ if (!isDisabled) {
176
+ setIsOpen(open);
177
+ }
178
+ };
179
+
180
+ const handleChange = (changingValue?: string | number) => {
181
+ if (isDisabled || changingValue === undefined) {
182
+ return;
183
+ }
184
+ const isNewSelected = true;
185
+ const isChangingItemSelected = selected.includes(changingValue);
186
+ if (isMultiple) {
187
+ setSelected((currentSelected) =>
188
+ isChangingItemSelected
189
+ ? currentSelected.filter((current) => current !== changingValue)
190
+ : [...currentSelected, changingValue],
191
+ );
192
+ setInputValue('');
193
+ } else {
194
+ if (selected[0] === changingValue) {
195
+ setSelected([]);
196
+ setInputValue('');
197
+ } else {
198
+ setSelected([changingValue]);
199
+ }
200
+ }
201
+ setIsOpen(false);
202
+ setFirstSuggestion('');
203
+ inputRef.current?.focus();
204
+ onChange && onChange(changingValue, isNewSelected);
205
+ };
206
+
207
+ const handleClearAll = (event: React.MouseEvent<HTMLElement, MouseEvent>) => {
208
+ if (isDisabled) {
209
+ return;
210
+ }
211
+ event.stopPropagation();
212
+ event.preventDefault();
213
+ setSelected([]);
214
+ setInputValue('');
215
+ setIsOpen(false);
216
+ setFirstSuggestion('');
217
+ inputRef.current?.focus();
218
+ };
219
+
220
+ const handleInputClick: React.MouseEventHandler<HTMLInputElement> = (
221
+ event,
222
+ ) => {
223
+ if (!isDisabled) {
224
+ inputRef.current?.focus();
225
+ setIsOpen(true);
226
+ }
227
+ event.stopPropagation();
228
+ event.preventDefault();
229
+ };
230
+
231
+ const handleInputKeyDown: React.KeyboardEventHandler<HTMLInputElement> = (
232
+ event,
233
+ ) => {
234
+ if (['Tab', 'Space'].includes(event.code) && !firstSuggestion) {
235
+ setIsOpen(true);
236
+ inputRef.current?.focus();
237
+ event.stopPropagation();
238
+ event.preventDefault();
239
+ }
240
+ if (['Tab', 'Enter'].includes(event.code) && firstSuggestion) {
241
+ const foundItem = Object.values(optionsWithKey).find(
242
+ (item) =>
243
+ `${item.label}`.toLowerCase() === firstSuggestion.toLowerCase(),
244
+ );
245
+ handleChange(foundItem?.value);
246
+ if (foundItem) {
247
+ setInputValue(`${foundItem?.label}`);
248
+ }
249
+ event.preventDefault();
250
+ return false;
251
+ }
252
+ if (
253
+ isMultiple &&
254
+ event.code === 'Backspace' &&
255
+ selected.length > 0 &&
256
+ !firstSuggestion
257
+ ) {
258
+ handleChange(selected[selected.length - 1]);
259
+ event.preventDefault();
260
+ return false;
261
+ }
262
+ if (!isOpen) {
263
+ setIsOpen(true);
264
+ }
265
+ };
266
+
267
+ const handleInputChange: React.ChangeEventHandler<HTMLInputElement> = (
268
+ event,
269
+ ) => {
270
+ setInputValue(event.target.value);
271
+ };
272
+
273
+ const handleSelectedClick: React.MouseEventHandler<HTMLDivElement> = (
274
+ event,
275
+ ) => {
276
+ event.stopPropagation();
277
+ };
278
+
279
+ const handleRemoveSelectedClick =
280
+ (selectedItem: number | string): MouseEventHandler<HTMLButtonElement> =>
281
+ (event) => {
282
+ event.stopPropagation();
283
+ handleChange(selectedItem);
284
+ };
285
+
286
+ return {
287
+ isOpen,
288
+ isDisabled,
289
+ optionsWithKey,
290
+ selectedItems: selected,
291
+ inputRef,
292
+ firstSuggestion,
293
+ isMultiple,
294
+ typeaheadId,
295
+ triggerRef,
296
+ className,
297
+ startIcon,
298
+ endIcon,
299
+ startIconClassName,
300
+ endIconClassName,
301
+ name,
302
+ inputName,
303
+ inputValue,
304
+ validationSchema,
305
+ status,
306
+ placeholder,
307
+ options: items,
308
+ register,
309
+ setValue,
310
+ handleChange,
311
+ handleClearAll,
312
+ handleOpenChange,
313
+ handleInputChange,
314
+ handleInputClick,
315
+ handleInputKeyDown,
316
+ handleSelectedClick,
317
+ handleRemoveSelectedClick,
318
+ };
319
+ };
320
+
321
+ export type UseTypeaheadResult = ReturnType<typeof useTypeahead>;
@@ -0,0 +1,22 @@
1
+ export const highlightInputMatch = (
2
+ item: string | undefined,
3
+ keyword: string,
4
+ ) => {
5
+ if (!item || !keyword) return item;
6
+ const lowerCasedInputValue = keyword.toLowerCase();
7
+ const hitIndex = item
8
+ .toString()
9
+ .toLocaleLowerCase()
10
+ .indexOf(lowerCasedInputValue);
11
+ if (hitIndex === -1) return item;
12
+ const before = item.slice(0, hitIndex);
13
+ const match = item.slice(hitIndex, hitIndex + keyword.length);
14
+ const after = item.slice(hitIndex + keyword.length);
15
+ return (
16
+ <span>
17
+ {before}
18
+ <b>{match}</b>
19
+ {after}
20
+ </span>
21
+ );
22
+ };
@@ -100,3 +100,5 @@ export * from './PieChart';
100
100
  export * from './CollapsibleNavBar';
101
101
  export * from './Filters';
102
102
  export * from './TableFilters';
103
+ export * from './Typeahead';
104
+ export * from './ImageItem';