@redocly/theme 0.42.2 → 0.43.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.
Files changed (112) hide show
  1. package/lib/components/Dropdown/DropdownMenu.d.ts +2 -0
  2. package/lib/components/Dropdown/DropdownMenu.js +3 -1
  3. package/lib/components/Loaders/SpinnerLoader.d.ts +5 -0
  4. package/lib/components/Loaders/SpinnerLoader.js +32 -0
  5. package/lib/components/Search/FilterFields/SearchFilterFieldSelect.d.ts +12 -0
  6. package/lib/components/Search/FilterFields/SearchFilterFieldSelect.js +113 -0
  7. package/lib/components/Search/FilterFields/SearchFilterFieldTags.d.ts +10 -0
  8. package/lib/components/Search/FilterFields/SearchFilterFieldTags.js +37 -0
  9. package/lib/components/Search/Search.js +1 -1
  10. package/lib/components/Search/SearchDialog.js +109 -27
  11. package/lib/components/Search/SearchFilter.d.ts +11 -0
  12. package/lib/components/Search/SearchFilter.js +71 -0
  13. package/lib/components/Search/SearchFilterField.d.ts +11 -0
  14. package/lib/components/Search/SearchFilterField.js +43 -0
  15. package/lib/components/Search/SearchGroups.d.ts +9 -0
  16. package/lib/components/Search/SearchGroups.js +69 -0
  17. package/lib/components/Search/SearchHighlight.d.ts +1 -1
  18. package/lib/components/Search/SearchHighlight.js +28 -5
  19. package/lib/components/Search/SearchInput.d.ts +1 -1
  20. package/lib/components/Search/SearchInput.js +5 -2
  21. package/lib/components/Search/SearchItem.d.ts +2 -2
  22. package/lib/components/Search/SearchItem.js +24 -15
  23. package/lib/components/Search/variables.js +48 -2
  24. package/lib/components/Segmented/Segmented.d.ts +2 -5
  25. package/lib/components/Select/Select.d.ts +2 -36
  26. package/lib/components/Select/Select.js +136 -98
  27. package/lib/components/Select/SelectInput.d.ts +23 -0
  28. package/lib/components/Select/SelectInput.js +129 -0
  29. package/lib/components/Select/variables.js +12 -1
  30. package/lib/components/Tag/Tag.d.ts +4 -2
  31. package/lib/components/Tag/Tag.js +40 -4
  32. package/lib/components/Tag/variables.dark.js +20 -5
  33. package/lib/components/Tag/variables.js +49 -17
  34. package/lib/components/VersionPicker/VersionPicker.d.ts +2 -3
  35. package/lib/components/VersionPicker/VersionPicker.js +13 -30
  36. package/lib/core/hooks/__mocks__/index.d.ts +2 -1
  37. package/lib/core/hooks/__mocks__/index.js +2 -1
  38. package/lib/core/hooks/__mocks__/search/use-search-filter.d.ts +9 -0
  39. package/lib/core/hooks/__mocks__/search/use-search-filter.js +14 -0
  40. package/lib/core/hooks/__mocks__/use-theme-hooks.d.ts +6 -1
  41. package/lib/core/hooks/__mocks__/use-theme-hooks.js +6 -1
  42. package/lib/core/hooks/index.d.ts +2 -1
  43. package/lib/core/hooks/index.js +2 -1
  44. package/lib/core/hooks/search/use-recent-searches.js +2 -0
  45. package/lib/core/hooks/{use-search.d.ts → search/use-search-dialog.d.ts} +1 -1
  46. package/lib/core/hooks/{use-search.js → search/use-search-dialog.js} +5 -5
  47. package/lib/core/hooks/search/use-search-filter.d.ts +9 -0
  48. package/lib/core/hooks/search/use-search-filter.js +50 -0
  49. package/lib/core/types/hooks.d.ts +17 -4
  50. package/lib/core/types/index.d.ts +1 -1
  51. package/lib/core/types/index.js +1 -1
  52. package/lib/core/types/l10n.d.ts +1 -2
  53. package/lib/core/types/search.d.ts +42 -2
  54. package/lib/core/types/select.d.ts +31 -0
  55. package/lib/core/types/{select-option.js → select.js} +1 -1
  56. package/lib/core/utils/index.d.ts +1 -0
  57. package/lib/core/utils/index.js +1 -0
  58. package/lib/core/utils/text-trimmer.d.ts +1 -0
  59. package/lib/core/utils/text-trimmer.js +16 -0
  60. package/lib/icons/ResetIcon/ResetIcon.d.ts +9 -0
  61. package/lib/icons/ResetIcon/ResetIcon.js +22 -0
  62. package/lib/icons/SettingsIcon/SettingsIcon.d.ts +9 -0
  63. package/lib/icons/SettingsIcon/SettingsIcon.js +23 -0
  64. package/lib/index.d.ts +8 -1
  65. package/lib/index.js +8 -1
  66. package/lib/markdoc/components/Cards/Card.js +6 -6
  67. package/package.json +3 -3
  68. package/src/components/Dropdown/DropdownMenu.tsx +2 -1
  69. package/src/components/Filter/FilterSelect.tsx +3 -3
  70. package/src/components/Loaders/SpinnerLoader.tsx +31 -0
  71. package/src/components/Search/FilterFields/SearchFilterFieldSelect.tsx +135 -0
  72. package/src/components/Search/FilterFields/SearchFilterFieldTags.tsx +61 -0
  73. package/src/components/Search/Search.tsx +2 -2
  74. package/src/components/Search/SearchDialog.tsx +183 -41
  75. package/src/components/Search/SearchFilter.tsx +90 -0
  76. package/src/components/Search/SearchFilterField.tsx +84 -0
  77. package/src/components/Search/SearchGroups.tsx +81 -0
  78. package/src/components/Search/SearchHighlight.tsx +29 -2
  79. package/src/components/Search/SearchInput.tsx +9 -3
  80. package/src/components/Search/SearchItem.tsx +39 -24
  81. package/src/components/Search/variables.ts +48 -2
  82. package/src/components/Segmented/Segmented.tsx +2 -2
  83. package/src/components/Select/Select.tsx +208 -157
  84. package/src/components/Select/SelectInput.tsx +201 -0
  85. package/src/components/Select/variables.ts +12 -1
  86. package/src/components/Tag/Tag.tsx +57 -6
  87. package/src/components/Tag/variables.dark.ts +20 -5
  88. package/src/components/Tag/variables.ts +49 -17
  89. package/src/components/VersionPicker/VersionPicker.tsx +15 -39
  90. package/src/core/hooks/__mocks__/index.ts +2 -1
  91. package/src/core/hooks/__mocks__/search/use-search-filter.ts +10 -0
  92. package/src/core/hooks/__mocks__/use-theme-hooks.ts +6 -1
  93. package/src/core/hooks/index.ts +2 -1
  94. package/src/core/hooks/search/use-recent-searches.ts +3 -0
  95. package/src/core/hooks/{use-search.ts → search/use-search-dialog.ts} +1 -1
  96. package/src/core/hooks/search/use-search-filter.ts +57 -0
  97. package/src/core/types/hooks.ts +25 -4
  98. package/src/core/types/index.ts +1 -1
  99. package/src/core/types/l10n.ts +110 -38
  100. package/src/core/types/search.ts +53 -2
  101. package/src/core/types/select.ts +33 -0
  102. package/src/core/utils/index.ts +1 -0
  103. package/src/core/utils/text-trimmer.ts +7 -0
  104. package/src/icons/ResetIcon/ResetIcon.tsx +26 -0
  105. package/src/icons/SettingsIcon/SettingsIcon.tsx +30 -0
  106. package/src/index.ts +8 -1
  107. package/src/markdoc/components/Cards/Card.tsx +15 -15
  108. package/lib/core/types/select-option.d.ts +0 -4
  109. package/src/core/types/select-option.ts +0 -4
  110. /package/lib/components/{Loading → Loaders}/Loading.d.ts +0 -0
  111. /package/lib/components/{Loading → Loaders}/Loading.js +0 -0
  112. /package/src/components/{Loading → Loaders}/Loading.tsx +0 -0
@@ -1,17 +1,18 @@
1
1
  import React, { useEffect, useRef } from 'react';
2
2
  import styled from 'styled-components';
3
3
 
4
- import type { SearchDocument } from '@redocly/theme/core/types';
4
+ import type { SearchItemData } from '@redocly/theme/core/types';
5
5
 
6
6
  import { HttpTag } from '@redocly/theme/components/Tags/HttpTag';
7
7
  import { Link } from '@redocly/theme/components/Link/Link';
8
8
  import { Image } from '@redocly/theme/components/Image/Image';
9
- import { highlight } from '@redocly/theme/components/Search/SearchHighlight';
9
+ import { searchHighlight } from '@redocly/theme/components/Search/SearchHighlight';
10
10
  import { Badge } from '@redocly/theme/components/Badge/Badge';
11
+ import { trimText } from '@redocly/theme/core/utils';
11
12
 
12
13
  type ActiveItem<T> = T & { active?: boolean };
13
14
  export type SearchItemProps = {
14
- item: ActiveItem<SearchDocument>;
15
+ item: ActiveItem<SearchItemData>;
15
16
  product?: {
16
17
  name: string;
17
18
  icon?: string;
@@ -28,12 +29,16 @@ export function SearchItem({ item, className, product }: SearchItemProps): JSX.E
28
29
  }
29
30
  }, [item.active]);
30
31
 
31
- const itemParam = item.parameters?.[0];
32
+ const { document, highlight } = item;
33
+
34
+ const parameter =
35
+ highlight.parameters && highlight.parameters.length ? highlight.parameters[0] : null;
36
+ const shouldShowPath = document.path && document.path?.length > 1;
32
37
 
33
38
  return (
34
39
  <SearchItemWrapper
35
40
  className={className}
36
- to={item.url}
41
+ to={document.url}
37
42
  tabIndex={0}
38
43
  innerRef={ref}
39
44
  data-component-name="Search/SearchItem"
@@ -45,41 +50,49 @@ export function SearchItem({ item, className, product }: SearchItemProps): JSX.E
45
50
  {product.name}
46
51
  </SearchItemProductTag>
47
52
  )}
48
- {item.httpVerb ? (
53
+ {document.httpMethod ? (
49
54
  <SearchItemOperation>
50
- <SearchItemOperationTag color={item.httpVerb}>
51
- {item.httpVerb.toUpperCase()}
55
+ <SearchItemOperationTag color={document.httpMethod}>
56
+ {document.httpMethod.toUpperCase()}
52
57
  </SearchItemOperationTag>
53
- {item.pathName ? highlight(item.pathName) : null}
58
+ {highlight.httpPath ? searchHighlight(highlight.httpPath) : document.httpPath}
54
59
  </SearchItemOperation>
55
60
  ) : null}
56
61
  <SearchItemTitleWrapper>
57
- <SearchItemTitle>{highlight(item.title)}</SearchItemTitle>
58
- {item.deprecated ? <SearchItemBadge deprecated>Deprecated</SearchItemBadge> : null}
59
- {item.badges
60
- ? item.badges.map(({ name, color }) => (
62
+ <SearchItemTitle>
63
+ {highlight.title ? searchHighlight(highlight.title) : document.title}
64
+ </SearchItemTitle>
65
+ {document.deprecated ? <SearchItemBadge deprecated>Deprecated</SearchItemBadge> : null}
66
+ {document.badges
67
+ ? document.badges.map(({ name, color }) => (
61
68
  <SearchItemBadge color={color || 'var(--color-info-base)'} key={name}>
62
69
  {name}
63
70
  </SearchItemBadge>
64
71
  ))
65
72
  : null}
66
73
  </SearchItemTitleWrapper>
67
- {Array.isArray(item.text) ? (
68
- <SearchItemDescription>{highlight(item.text)}</SearchItemDescription>
69
- ) : null}
74
+ <SearchItemDescription>
75
+ {highlight.text ? searchHighlight(highlight.text) : trimText(document.text)}
76
+ </SearchItemDescription>
70
77
  </SearchItemHeader>
71
- {itemParam ? (
78
+ {parameter ? (
72
79
  <SearchItemPlace>
73
80
  <div>
74
- {`${itemParam.place} → ${
75
- itemParam.path?.length ? itemParam.path?.join(' ') + ' → ' : ''
76
- }`}
77
- {highlight(itemParam.name)}
81
+ {searchHighlight(parameter.place)}
82
+ {``}
83
+ {parameter.path?.length ? searchHighlight(parameter.path?.join(' → ')) + ' → ' : ''}
84
+ {searchHighlight(parameter.name)}
78
85
  </div>
79
- <div>{highlight(itemParam.description)}</div>
86
+ <div>{searchHighlight(parameter.description)}</div>
80
87
  </SearchItemPlace>
81
88
  ) : (
82
- <SearchItemPath>{item.path?.join(' ')}</SearchItemPath>
89
+ shouldShowPath && (
90
+ <SearchItemPath>
91
+ {highlight.path && highlight.path.length
92
+ ? searchHighlight(highlight.path.join(' → '))
93
+ : document.path?.join(' → ')}
94
+ </SearchItemPath>
95
+ )
83
96
  )}
84
97
  </SearchItemWrapper>
85
98
  );
@@ -92,7 +105,7 @@ const SearchItemWrapper = styled(Link)`
92
105
  background-color: var(--search-item-bg-color);
93
106
  transition: background-color 0.3s ease;
94
107
  text-decoration: none;
95
- white-space: nowrap;
108
+ white-space: normal;
96
109
  outline: none;
97
110
  border-top: 1px solid var(--search-item-border-color);
98
111
  border-bottom: 1px solid var(--search-item-border-color);
@@ -160,6 +173,7 @@ const SearchItemPath = styled.div`
160
173
  font-size: var(--font-size-sm);
161
174
  line-height: var(--line-height-sm);
162
175
  font-weight: var(--font-weight-regular);
176
+ margin-top: var(--spacing-sm);
163
177
  `;
164
178
 
165
179
  const SearchItemPlace = styled.div`
@@ -170,6 +184,7 @@ const SearchItemPlace = styled.div`
170
184
  font-size: var(--font-size-sm);
171
185
  line-height: var(--line-height-sm);
172
186
  font-weight: var(--font-weight-regular);
187
+ margin-top: var(--spacing-sm);
173
188
 
174
189
  &:first-child {
175
190
  padding-top: 0;
@@ -4,11 +4,18 @@ export const search = css`
4
4
  /**
5
5
  * @tokens Search
6
6
  */
7
- --search-modal-width: 700px;
8
- --search-modal-min-height: 530px;
7
+ --search-modal-width: 1100px;
8
+ --search-modal-min-height: 705px;
9
9
  --search-modal-bg-color: var(--modal-bg-color);
10
10
  --search-modal-box-shadow: var(--modal-box-shadow);
11
11
  --search-modal-border-radius: var(--border-radius-lg);
12
+ --search-modal-border: 1px solid var(--border-color-secondary);
13
+
14
+ --search-modal-header-bg-color: var(--bg-color-tonal); // @presenter Color
15
+ --search-modal-header-padding: var(--spacing-sm);
16
+
17
+ --search-modal-footer-padding: var(--spacing-sm);
18
+ --search-modal-footer-gap: var(--spacing-md);
12
19
 
13
20
  --search-highlight-bg-color: none; // @presenter Color
14
21
  --search-highlight-text-color: var(--color-info-base); // @presenter Color
@@ -27,6 +34,11 @@ export const search = css`
27
34
  --search-input-border: none;
28
35
  --search-input-border-radius: var(--border-radius); // @presenter BorderRadius
29
36
 
37
+ --search-group-title-padding: var(--spacing-sm);
38
+ --search-group-title-bg-color: var(--bg-color-tonal); // @presenter Color
39
+ --search-group-footer-padding: var(--spacing-sm);
40
+ --search-group-footer-text-color: var(--link-color-primary); // @presenter Color
41
+
30
42
  --search-item-text-color: var(--text-color-secondary); // @presenter Color
31
43
  --search-item-text-color-hover: var(--text-color-primary); // @presenter Color
32
44
  --search-item-title-font-size: var(--font-size-base); // @presenter FontSize
@@ -74,6 +86,40 @@ export const search = css`
74
86
  --search-suggested-item-bg-color-active: var(--layer-color); // @presenter Color
75
87
  --search-suggested-item-border-color-focused: var(--color-blue-4); // @presenter Color
76
88
 
89
+ --search-shortcuts-gap: var(--spacing-xs);
90
+
91
+ --search-message-font-size: var(--font-size-lg); // @presenter FontSize
92
+ --search-message-font-weight: var(--font-weight-regular); // @presenter FontWeight
93
+ --search-message-line-height: var(--line-height-lg); // @presenter LineHeight
94
+ --search-message-text-color: var(--text-color-secondary); // @presenter Color
95
+ --search-message-gap: var(--spacing-md);
96
+
97
+ /**
98
+ * @tokens Search filter
99
+ */
100
+ --search-filter-width: 450px;
101
+ --search-filter-bg-color: var(--search-modal-bg-color);
102
+ --search-filter-padding: var(--spacing-lg);
103
+ --search-filter-font-size: var(--font-size-base);
104
+ --search-filter-font-weight: var(--font-weight-regular);
105
+ --search-filter-line-height: var(--line-height-base);
106
+
107
+ --search-filter-header-padding: var(--spacing-md) 0;
108
+ --search-filter-header-text-color: var(--text-color-helper);
109
+ --search-filter-header-z-index: var(--z-index-raised);
110
+
111
+ --search-filter-fields-gap: var(--spacing-base);
112
+
113
+ --search-filter-field-select-option-gap: var(--spacing-unit);
114
+ --search-filter-field-select-option-text-padding: 0 0 0 var(--spacing-unit);
115
+ --search-filter-field-select-footer-padding: 0 0 0 var(--spacing-unit);
116
+ --search-filter-field-select-footer-text-color: var(--text-color-helper);
117
+
118
+ --search-filter-field-tags-gap: var(--spacing-unit);
119
+ --search-filter-field-tags-tag-margin: var(--spacing-unit) 0 0 0;
120
+
121
+ // @tokens End
122
+
77
123
  /**
78
124
  * @tokens Navbar Search Trigger
79
125
  */
@@ -2,14 +2,14 @@ import React from 'react';
2
2
  import styled, { css } from 'styled-components';
3
3
 
4
4
  import type { ReactElement } from 'react';
5
- import type { SelectOption } from '@redocly/theme/core/types/select-option';
5
+ import type { SelectOption } from '@redocly/theme/core/types/select';
6
6
 
7
7
  import { typedMemo } from '@redocly/theme/core/hoc/typedMemo';
8
8
 
9
9
  export type SegmentedProps<T> = {
10
10
  options: SelectOption<T>[];
11
11
  value: T;
12
- onChange: ({ label, value }: { label: string; value: T }) => void;
12
+ onChange: ({ label, value }: SelectOption<T>) => void;
13
13
  className?: string;
14
14
  size?: 'regular' | 'small';
15
15
  };
@@ -1,34 +1,14 @@
1
- import React, { useRef, useState } from 'react';
1
+ import React, { Fragment, useEffect, useId, useRef, useState } from 'react';
2
2
  import styled from 'styled-components';
3
3
 
4
- import { useOutsideClick } from '@redocly/theme/core/hooks';
5
- import { ChevronDownIcon } from '@redocly/theme/icons/ChevronDownIcon/ChevronDownIcon';
6
- import { ChevronUpIcon } from '@redocly/theme/icons/ChevronUpIcon/ChevronUpIcon';
7
- import { CheckmarkIcon } from '@redocly/theme/icons/CheckmarkIcon/CheckmarkIcon';
4
+ import type { SelectOption, SelectProps } from '@redocly/theme/core/types/select';
8
5
 
9
- export type SelectProps<T = any> = {
10
- value: T;
11
- options: {
12
- element: React.ReactNode | JSX.Element | string;
13
- value: T;
14
- label?: string;
15
- }[];
16
- dataAttributes?: Record<string, string>;
17
- className?: string;
18
- withArrow?: boolean;
19
- triggerEvent?: 'click' | 'hover';
20
- onChange?: (value: any) => void;
21
- placement?: 'top' | 'bottom';
22
- alignment?: 'start' | 'end';
23
- icon?: React.ReactNode;
24
- onlyIcon?: boolean;
25
- placeholder?: string;
26
- disabled?: boolean;
27
- hideCheckmarkIcon?: boolean;
28
- dataTestId?: string;
29
- renderInput?: (props: { isOpen: boolean }) => React.ReactElement;
30
- renderDivider?: () => React.ReactElement;
31
- };
6
+ import { CheckmarkIcon } from '@redocly/theme/icons/CheckmarkIcon/CheckmarkIcon';
7
+ import { SelectInput } from '@redocly/theme/components/Select/SelectInput';
8
+ import { Dropdown } from '@redocly/theme/components/Dropdown/Dropdown';
9
+ import { DropdownMenu } from '@redocly/theme/components/Dropdown/DropdownMenu';
10
+ import { DropdownMenuItem } from '@redocly/theme/components/Dropdown/DropdownMenuItem';
11
+ import { useOutsideClick } from '@redocly/theme/core/hooks';
32
12
 
33
13
  export function Select<T>(props: SelectProps<T>): JSX.Element {
34
14
  const {
@@ -38,7 +18,6 @@ export function Select<T>(props: SelectProps<T>): JSX.Element {
38
18
  dataAttributes,
39
19
  withArrow = true,
40
20
  triggerEvent = 'click',
41
- onChange,
42
21
  placement,
43
22
  alignment,
44
23
  icon,
@@ -46,102 +25,229 @@ export function Select<T>(props: SelectProps<T>): JSX.Element {
46
25
  disabled,
47
26
  placeholder,
48
27
  hideCheckmarkIcon,
28
+ checkmarkIconPosition,
29
+ dataTestId = 'select',
30
+ multiple,
31
+ searchable,
32
+ clearable,
33
+ footer,
34
+ onChange,
35
+ onSearch,
49
36
  renderInput,
50
37
  renderDivider,
51
- dataTestId = 'select',
52
38
  } = props;
53
39
 
54
- const containerRef = useRef<HTMLDivElement | null>(null);
40
+ const getSelectedOptionsFromPropsValue = () => {
41
+ const values = Array.isArray(value) ? value : [value];
42
+ return values
43
+ .map((value) => {
44
+ const selectedOption = options.find((option) => option.value === value);
45
+ return selectedOption || typeof value === 'string'
46
+ ? ({ value } as SelectOption<T>)
47
+ : (value as SelectOption<T>);
48
+ })
49
+ .filter((option) => !!option);
50
+ };
55
51
 
56
- const [isOpen, setIsOpen] = useState<boolean>(false);
57
- // const [selectedIdx, setSelectedIdx] = useState<React.ReactNode | string>(selected);
52
+ const [selectedOptions, setSelectedOptions] = useState<SelectOption<T>[]>(
53
+ getSelectedOptionsFromPropsValue(),
54
+ );
55
+ const selectRef = useRef<HTMLDivElement | null>(null);
56
+ const [searchValue, setSearchValue] = useState<string | null>(null);
57
+ const [dropdownActive, setDropdownActive] = useState<boolean | undefined>(false);
58
+ const [filteredOptions, setFilteredOptions] = useState<SelectOption<T>[]>(options);
59
+ const [stickyInputValue, setStickyInputValue] = useState<string>(placeholder || '');
60
+ const inputId = useId();
58
61
 
59
- const handleOpen = () => {
60
- setIsOpen(true);
62
+ useOutsideClick(selectRef, () => {
63
+ setDropdownActive(false);
64
+ });
65
+
66
+ useEffect(() => {
67
+ setFilteredOptions(options);
68
+ }, [options]);
69
+
70
+ useEffect(() => {
71
+ setSelectedOptions(getSelectedOptionsFromPropsValue());
72
+ // eslint-disable-next-line react-hooks/exhaustive-deps
73
+ }, [multiple, value]);
74
+
75
+ useEffect(() => {
76
+ if (onSearch) {
77
+ onSearch?.(searchValue as T);
78
+ } else {
79
+ if (typeof searchValue === 'string') {
80
+ const filteredOptions = options.filter((option) => {
81
+ const valueForSearch = String(option.label ?? option.value ?? '');
82
+ return (
83
+ !valueForSearch || valueForSearch.toLowerCase().indexOf(searchValue.toLowerCase()) > -1
84
+ );
85
+ });
86
+ setFilteredOptions(filteredOptions);
87
+ } else {
88
+ setFilteredOptions(options);
89
+ }
90
+ }
91
+ // eslint-disable-next-line react-hooks/exhaustive-deps
92
+ }, [searchValue]);
93
+
94
+ const selectHandler = (selectedOption: SelectOption<T>) => {
95
+ const newSelectedOptions = multiple
96
+ ? isSelected(selectedOption)
97
+ ? selectedOptions.filter((option) => option.value !== selectedOption.value)
98
+ : [...selectedOptions, selectedOption]
99
+ : [selectedOption];
100
+
101
+ const newSelectedValues = newSelectedOptions.length
102
+ ? multiple
103
+ ? newSelectedOptions.map((o) => o.value)
104
+ : newSelectedOptions[0].value
105
+ : multiple
106
+ ? []
107
+ : ('' as T);
108
+
109
+ setSelectedOptions(newSelectedOptions);
110
+ onChange?.(newSelectedValues);
111
+ setSearchValue(null);
112
+ setDropdownActive(false);
113
+ if (!multiple) {
114
+ setStickyInputValue('');
115
+ }
61
116
  };
62
117
 
63
- const handleClose = () => {
64
- setIsOpen(false);
118
+ const searchHandler = (e: React.ChangeEvent<HTMLInputElement>) => {
119
+ const targetValue = e.target.value;
120
+ setSearchValue(targetValue);
121
+ setDropdownActive(true);
65
122
  };
66
123
 
67
- const handleToggle = () => {
68
- setIsOpen(!isOpen);
124
+ const clearHandler = (option?: SelectOption<T>) => {
125
+ if (option) {
126
+ selectHandler(option);
127
+ } else {
128
+ if (!multiple) {
129
+ setStickyInputValue('');
130
+ }
131
+ setSelectedOptions([]);
132
+ onChange?.(multiple ? [] : ('' as T));
133
+ }
69
134
  };
70
135
 
71
- const handleSelect = (value: T) => {
72
- // setSelectedIdx(options.findIndex(o => o.value === value));
73
- setIsOpen(false);
74
- onChange?.(value);
136
+ const inputBlurHandler = (e: React.FocusEvent) => {
137
+ const relatedTarget = e.relatedTarget as HTMLElement;
138
+ const isDropdownItem =
139
+ relatedTarget?.attributes.getNamedItem('data-component-name')?.value ===
140
+ 'Dropdown/DropdownMenuItem';
141
+ if (!isDropdownItem) {
142
+ if (!e.relatedTarget || e.relatedTarget?.id !== inputId) {
143
+ setDropdownActive(false);
144
+ }
145
+ setSearchValue(null);
146
+ setStickyInputValue('');
147
+ }
75
148
  };
76
149
 
77
- useOutsideClick(containerRef, handleClose);
78
-
79
- // useEffect(() => {
80
- // handleSelect(selected);
81
- // // eslint-disable-next-line react-hooks/exhaustive-deps
82
- // }, [selected]);
83
-
84
- const selectedOption = options.find((option) => option.value === value);
85
-
86
- const renderDefaultInput = () => (
87
- <SelectInput>
88
- {!onlyIcon && (
89
- <SelectInputValue>
90
- {selectedOption?.label || selectedOption?.element || placeholder}
91
- </SelectInputValue>
92
- )}
93
- {icon}
94
- {withArrow ? isOpen ? <ChevronUpIcon size="14px" /> : <ChevronDownIcon size="14px" /> : null}
95
- </SelectInput>
96
- );
150
+ const inputFocusHandler = () => {
151
+ if (!multiple && selectedOptions.length) {
152
+ setStickyInputValue(selectedOptions[0].label || (selectedOptions[0].value as string));
153
+ }
154
+ };
155
+
156
+ const inputClickHandler = () => {
157
+ setDropdownActive(!dropdownActive);
158
+ };
159
+
160
+ const isSelected = (option: SelectOption<T>) => {
161
+ return !!selectedOptions.find(
162
+ (selectOption) => selectOption.value === option.value || selectOption.value === option.label,
163
+ );
164
+ };
165
+
166
+ const renderDefaultInput = () => {
167
+ return (
168
+ <SelectInput
169
+ id={inputId}
170
+ selectedOptions={selectedOptions}
171
+ searchValue={searchValue}
172
+ placeholder={placeholder}
173
+ stickyValue={stickyInputValue}
174
+ multiple={multiple}
175
+ searchable={searchable}
176
+ clearable={clearable}
177
+ customIcon={icon}
178
+ onlyIcon={onlyIcon}
179
+ clearHandler={clearHandler}
180
+ searchHandler={searchHandler}
181
+ inputBlurHandler={inputBlurHandler}
182
+ inputFocusHandler={inputFocusHandler}
183
+ clickHandler={inputClickHandler}
184
+ />
185
+ );
186
+ };
97
187
 
98
188
  return (
99
- <SelectContainer
189
+ <SelectWrapper
100
190
  data-component-name="Select/Select"
191
+ ref={selectRef}
101
192
  {...dataAttributes}
102
193
  disabled={disabled}
103
194
  data-testid={dataTestId}
104
195
  className={className}
105
- ref={containerRef}
106
- onPointerEnter={triggerEvent === 'hover' ? handleOpen : undefined}
107
- onPointerLeave={triggerEvent === 'hover' ? handleClose : undefined}
108
- onClick={triggerEvent === 'click' ? handleToggle : undefined}
109
196
  >
110
- {renderInput ? renderInput({ isOpen }) : renderDefaultInput()}
111
- {isOpen && (
112
- <SelectList placement={placement} alignment={alignment}>
113
- {options.map((option, index) => {
114
- const selected = option.value === value;
115
- return (
116
- <>
117
- <SelectListItem
118
- key={index}
119
- onClick={() => handleSelect(option.value)}
120
- selected={selected}
121
- >
122
- {typeof option.element === 'string' ? (
123
- <div>{option.element}</div>
124
- ) : (
125
- option.element
126
- )}
127
- {!hideCheckmarkIcon && selected && <CheckmarkIcon />}
128
- </SelectListItem>
129
- {renderDivider && index !== options.length - 1 ? renderDivider() : null}
130
- </>
131
- );
132
- })}
133
- </SelectList>
134
- )}
135
- </SelectContainer>
197
+ <SelectDropdown
198
+ closeOnClick={!multiple}
199
+ withArrow={withArrow}
200
+ trigger={renderInput ? renderInput() : renderDefaultInput()}
201
+ triggerEvent={triggerEvent}
202
+ placement={placement}
203
+ alignment={alignment}
204
+ active={!renderInput ? dropdownActive : undefined}
205
+ >
206
+ <SelectDropdownMenu footer={footer}>
207
+ {filteredOptions.length === 0 ? (
208
+ <DropdownMenuItem disabled>No results</DropdownMenuItem>
209
+ ) : (
210
+ filteredOptions.map((option, index) => {
211
+ return (
212
+ <Fragment key={index}>
213
+ <DropdownMenuItem
214
+ onAction={() => selectHandler(option)}
215
+ prefix={
216
+ !hideCheckmarkIcon &&
217
+ (checkmarkIconPosition ? checkmarkIconPosition === 'start' : false) &&
218
+ isSelected(option) && <CheckmarkIcon />
219
+ }
220
+ suffix={
221
+ !hideCheckmarkIcon &&
222
+ (checkmarkIconPosition ? checkmarkIconPosition === 'end' : true) &&
223
+ isSelected(option) && <CheckmarkIcon />
224
+ }
225
+ >
226
+ {typeof option.element === 'string' ? (
227
+ <div>{option.element}</div>
228
+ ) : (
229
+ option.element
230
+ )}
231
+ </DropdownMenuItem>
232
+ {renderDivider && index !== options.length - 1 ? renderDivider() : null}
233
+ </Fragment>
234
+ );
235
+ }) || 'No results'
236
+ )}
237
+ </SelectDropdownMenu>
238
+ </SelectDropdown>
239
+ </SelectWrapper>
136
240
  );
137
241
  }
138
242
 
139
- export const SelectContainer = styled.div<{ disabled?: boolean }>`
243
+ export const SelectWrapper = styled.div<{ disabled?: boolean }>`
244
+ display: flex;
140
245
  position: relative;
141
246
  font-size: var(--select-font-size);
142
247
  font-weight: var(--select-font-weight);
143
248
  line-height: var(--select-line-height);
144
249
  border-radius: var(--select-border-radius);
250
+ border: var(--select-border);
145
251
  color: var(--select-text-color);
146
252
  min-width: 0;
147
253
 
@@ -151,71 +257,16 @@ export const SelectContainer = styled.div<{ disabled?: boolean }>`
151
257
  opacity: 0.59;
152
258
  pointer-events: none;
153
259
  `}
154
- a {
155
- display: block;
156
- text-decoration: none;
157
- color: var(--select-text-color);
158
- }
159
- `;
160
-
161
- export const SelectInput = styled.div`
162
- display: flex;
163
- align-items: center;
164
- justify-content: space-between;
165
- border-radius: var(--select-input-border-radius);
166
- padding: var(--select-input-padding);
167
- cursor: pointer;
168
- gap: 8px;
169
260
  `;
170
261
 
171
- export const SelectInputValue = styled.div`
172
- pointer-events: none;
173
- min-width: 0;
174
- text-overflow: ellipsis;
175
- overflow: hidden;
176
- `;
177
-
178
- export const SelectList = styled.ul<{ placement?: string; alignment?: string }>`
179
- position: absolute;
180
- top: ${({ placement }) => (placement === 'top' ? 'auto' : '100%')};
181
- bottom: ${({ placement }) => (placement === 'top' ? '100%' : 'auto')};
182
- left: ${({ alignment }) => (alignment === 'start' ? '0' : 'auto')};
183
- right: ${({ alignment }) => (alignment === 'end' ? '0' : 'auto')};
184
- margin: 0;
185
- min-width: var(--select-list-min-width);
186
- max-width: var(--select-list-max-width);
187
- padding: var(--select-list-padding);
188
- background-color: var(--select-list-bg-color);
189
- border-radius: var(--select-list-border-radius);
190
- box-shadow: var(--select-list-box-shadow);
191
- list-style-type: none;
192
- cursor: pointer;
193
- white-space: nowrap;
194
- overflow: hidden;
195
- z-index: 1;
196
- `;
262
+ const SelectDropdown = styled(Dropdown)`
263
+ width: 100%;
197
264
 
198
- export const SelectListItem = styled.li<{ selected: boolean }>`
199
- display: flex;
200
- align-items: center;
201
- justify-content: space-between;
202
- border-radius: var(--select-list-item-border-radius);
203
- gap: var(--select-list-item-gap);
204
- padding: var(--select-list-item-padding);
205
- ${({ selected }) =>
206
- selected &&
207
- `
208
- font-weight: var(--select-list-item-font-weight-active);
209
- background-color: var(--select-list-item-bg-color-active);
210
- &:hover { background-color: var(--select-list-item-bg-color-active)};
211
- `}
212
-
213
- & > * {
214
- overflow: hidden;
215
- text-overflow: ellipsis;
265
+ > * {
266
+ width: 100%;
216
267
  }
268
+ `;
217
269
 
218
- &:hover {
219
- background-color: var(--select-list-item-bg-color-hover);
220
- }
270
+ const SelectDropdownMenu = styled(DropdownMenu)`
271
+ width: 100%;
221
272
  `;