@onewelcome/react-lib-components 6.5.0 → 6.6.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 (113) hide show
  1. package/dist/cjs/Form/Select/MultiSelect/MultiOption.cjs.js +1 -1
  2. package/dist/cjs/Form/Select/MultiSelect/MultiOption.cjs.js.map +1 -1
  3. package/dist/cjs/Form/Select/MultiSelect/MultiSelect.cjs.js +1 -1
  4. package/dist/cjs/Form/Select/MultiSelect/MultiSelect.cjs.js.map +1 -1
  5. package/dist/cjs/Form/Select/MultiSelect/MultiSelect.module.cjs.js +1 -1
  6. package/dist/cjs/Form/Select/MultiSelect/SelectButton.cjs.js +1 -1
  7. package/dist/cjs/Form/Select/MultiSelect/SelectButton.cjs.js.map +1 -1
  8. package/dist/cjs/Form/Select/MultiSelect/SelectButton.module.cjs.js +1 -1
  9. package/dist/cjs/Form/Select/MultiSelect/SelectedOptions.cjs.js +1 -1
  10. package/dist/cjs/Form/Select/MultiSelect/SelectedOptions.cjs.js.map +1 -1
  11. package/dist/cjs/Form/Select/MultiSelect/useArrowNavigation.cjs.js +2 -0
  12. package/dist/cjs/Form/Select/MultiSelect/useArrowNavigation.cjs.js.map +1 -0
  13. package/dist/cjs/Form/Select/MultiSelect/useSearch.cjs.js +2 -0
  14. package/dist/cjs/Form/Select/MultiSelect/useSearch.cjs.js.map +1 -0
  15. package/dist/cjs/Form/Select/SingleSelect/Option.cjs.js +1 -1
  16. package/dist/cjs/Form/Select/SingleSelect/Option.cjs.js.map +1 -1
  17. package/dist/cjs/Form/Select/SingleSelect/Select.cjs.js +1 -1
  18. package/dist/cjs/Form/Select/SingleSelect/Select.cjs.js.map +1 -1
  19. package/dist/cjs/Form/Select/SingleSelect/Select.module.cjs.js +1 -1
  20. package/dist/cjs/Form/Select/SingleSelect/useArrowNavigation.cjs.js +2 -0
  21. package/dist/cjs/Form/Select/SingleSelect/useArrowNavigation.cjs.js.map +1 -0
  22. package/dist/cjs/Form/Select/SingleSelect/useSearch.cjs.js +2 -0
  23. package/dist/cjs/Form/Select/SingleSelect/useSearch.cjs.js.map +1 -0
  24. package/dist/cjs/Form/Select/useAddNewBtn.cjs.js +1 -1
  25. package/dist/cjs/Form/Select/useAddNewBtn.cjs.js.map +1 -1
  26. package/dist/cjs/Form/Select/useAddNewBtn.module.cjs.js +1 -1
  27. package/dist/cjs/Form/Select/useSelectPositionList.cjs.js +2 -0
  28. package/dist/cjs/Form/Select/useSelectPositionList.cjs.js.map +1 -0
  29. package/dist/cjs/src/components/Form/Select/MultiSelect/MultiSelect.test.d.ts +63 -1
  30. package/dist/cjs/src/components/Form/Select/MultiSelect/MultiSelectKeyboardNavigation.test.d.ts +1 -0
  31. package/dist/cjs/src/components/Form/Select/MultiSelect/SelectedOptions.d.ts +2 -1
  32. package/dist/cjs/src/components/Form/Select/MultiSelect/useArrowNavigation.d.ts +16 -0
  33. package/dist/cjs/src/components/Form/Select/MultiSelect/useSearch.d.ts +34 -0
  34. package/dist/cjs/src/components/Form/Select/Select.interfaces.d.ts +1 -0
  35. package/dist/cjs/src/components/Form/Select/SingleSelect/Option.d.ts +1 -0
  36. package/dist/cjs/src/components/Form/Select/SingleSelect/useArrowNavigation.d.ts +5 -0
  37. package/dist/cjs/src/components/Form/Select/{useSearch.d.ts → SingleSelect/useSearch.d.ts} +2 -1
  38. package/dist/cjs/src/components/Form/Select/useAddNewBtn.d.ts +10 -3
  39. package/dist/cjs/src/components/Form/Select/useSelectPositionList.d.ts +12 -0
  40. package/dist/cjs/src/util/helper.cjs.js +1 -1
  41. package/dist/cjs/src/util/helper.cjs.js.map +1 -1
  42. package/dist/cjs/src/util/helper.d.ts +1 -0
  43. package/dist/esm/Form/Select/MultiSelect/MultiOption.esm.js +1 -1
  44. package/dist/esm/Form/Select/MultiSelect/MultiOption.esm.js.map +1 -1
  45. package/dist/esm/Form/Select/MultiSelect/MultiSelect.esm.js +1 -1
  46. package/dist/esm/Form/Select/MultiSelect/MultiSelect.esm.js.map +1 -1
  47. package/dist/esm/Form/Select/MultiSelect/MultiSelect.module.esm.js +1 -1
  48. package/dist/esm/Form/Select/MultiSelect/SelectButton.esm.js +1 -1
  49. package/dist/esm/Form/Select/MultiSelect/SelectButton.esm.js.map +1 -1
  50. package/dist/esm/Form/Select/MultiSelect/SelectButton.module.esm.js +1 -1
  51. package/dist/esm/Form/Select/MultiSelect/SelectedOptions.esm.js +1 -1
  52. package/dist/esm/Form/Select/MultiSelect/SelectedOptions.esm.js.map +1 -1
  53. package/dist/esm/Form/Select/MultiSelect/useArrowNavigation.esm.js +2 -0
  54. package/dist/esm/Form/Select/MultiSelect/useArrowNavigation.esm.js.map +1 -0
  55. package/dist/esm/Form/Select/MultiSelect/useSearch.esm.js +2 -0
  56. package/dist/esm/Form/Select/MultiSelect/useSearch.esm.js.map +1 -0
  57. package/dist/esm/Form/Select/SingleSelect/Option.esm.js +1 -1
  58. package/dist/esm/Form/Select/SingleSelect/Option.esm.js.map +1 -1
  59. package/dist/esm/Form/Select/SingleSelect/Select.esm.js +1 -1
  60. package/dist/esm/Form/Select/SingleSelect/Select.esm.js.map +1 -1
  61. package/dist/esm/Form/Select/SingleSelect/Select.module.esm.js +1 -1
  62. package/dist/esm/Form/Select/SingleSelect/useArrowNavigation.esm.js +2 -0
  63. package/dist/esm/Form/Select/SingleSelect/useArrowNavigation.esm.js.map +1 -0
  64. package/dist/esm/Form/Select/SingleSelect/useSearch.esm.js +2 -0
  65. package/dist/esm/Form/Select/SingleSelect/useSearch.esm.js.map +1 -0
  66. package/dist/esm/Form/Select/useAddNewBtn.esm.js +1 -1
  67. package/dist/esm/Form/Select/useAddNewBtn.esm.js.map +1 -1
  68. package/dist/esm/Form/Select/useAddNewBtn.module.esm.js +1 -1
  69. package/dist/esm/Form/Select/useSelectPositionList.esm.js +2 -0
  70. package/dist/esm/Form/Select/useSelectPositionList.esm.js.map +1 -0
  71. package/dist/esm/src/components/Form/Select/MultiSelect/MultiSelect.test.d.ts +63 -1
  72. package/dist/esm/src/components/Form/Select/MultiSelect/MultiSelectKeyboardNavigation.test.d.ts +1 -0
  73. package/dist/esm/src/components/Form/Select/MultiSelect/SelectedOptions.d.ts +2 -1
  74. package/dist/esm/src/components/Form/Select/MultiSelect/useArrowNavigation.d.ts +16 -0
  75. package/dist/esm/src/components/Form/Select/MultiSelect/useSearch.d.ts +34 -0
  76. package/dist/esm/src/components/Form/Select/Select.interfaces.d.ts +1 -0
  77. package/dist/esm/src/components/Form/Select/SingleSelect/Option.d.ts +1 -0
  78. package/dist/esm/src/components/Form/Select/SingleSelect/useArrowNavigation.d.ts +5 -0
  79. package/dist/esm/src/components/Form/Select/{useSearch.d.ts → SingleSelect/useSearch.d.ts} +2 -1
  80. package/dist/esm/src/components/Form/Select/useAddNewBtn.d.ts +10 -3
  81. package/dist/esm/src/components/Form/Select/useSelectPositionList.d.ts +12 -0
  82. package/dist/esm/src/util/helper.d.ts +1 -0
  83. package/dist/esm/src/util/helper.esm.js +1 -1
  84. package/dist/esm/src/util/helper.esm.js.map +1 -1
  85. package/package.json +6 -6
  86. package/src/components/Form/Select/MultiSelect/MultiOption.tsx +36 -3
  87. package/src/components/Form/Select/MultiSelect/MultiSelect.module.scss +29 -19
  88. package/src/components/Form/Select/MultiSelect/MultiSelect.tsx +85 -62
  89. package/src/components/Form/Select/MultiSelect/SelectButton.module.scss +1 -1
  90. package/src/components/Form/Select/MultiSelect/SelectButton.tsx +1 -1
  91. package/src/components/Form/Select/MultiSelect/SelectedOptions.tsx +5 -4
  92. package/src/components/Form/Select/MultiSelect/useArrowNavigation.ts +128 -0
  93. package/src/components/Form/Select/MultiSelect/useSearch.tsx +126 -0
  94. package/src/components/Form/Select/Select.interfaces.ts +1 -0
  95. package/src/components/Form/Select/SingleSelect/Option.tsx +15 -4
  96. package/src/components/Form/Select/SingleSelect/Select.module.scss +13 -2
  97. package/src/components/Form/Select/SingleSelect/Select.tsx +7 -3
  98. package/src/components/Form/Select/{SelectService.ts → SingleSelect/useArrowNavigation.ts} +1 -101
  99. package/src/components/Form/Select/{useSearch.tsx → SingleSelect/useSearch.tsx} +3 -2
  100. package/src/components/Form/Select/useAddNewBtn.module.scss +18 -4
  101. package/src/components/Form/Select/useAddNewBtn.tsx +42 -8
  102. package/src/components/Form/Select/useSelectPositionList.ts +113 -0
  103. package/src/util/helper.tsx +2 -0
  104. package/dist/cjs/Form/Select/SelectService.cjs.js +0 -2
  105. package/dist/cjs/Form/Select/SelectService.cjs.js.map +0 -1
  106. package/dist/cjs/Form/Select/useSearch.cjs.js +0 -2
  107. package/dist/cjs/Form/Select/useSearch.cjs.js.map +0 -1
  108. package/dist/cjs/src/components/Form/Select/SelectService.d.ts +0 -17
  109. package/dist/esm/Form/Select/SelectService.esm.js +0 -2
  110. package/dist/esm/Form/Select/SelectService.esm.js.map +0 -1
  111. package/dist/esm/Form/Select/useSearch.esm.js +0 -2
  112. package/dist/esm/Form/Select/useSearch.esm.js.map +0 -1
  113. package/dist/esm/src/components/Form/Select/SelectService.d.ts +0 -17
@@ -0,0 +1,128 @@
1
+ /*
2
+ * Copyright 2022 OneWelcome B.V.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ interface UseArrowNavigationParams {
18
+ expanded: boolean;
19
+ setExpanded: React.Dispatch<React.SetStateAction<boolean>>;
20
+ setFocusedSelectItem: React.Dispatch<React.SetStateAction<number>>;
21
+ childrenCount: number;
22
+ setShouldClick: React.Dispatch<React.SetStateAction<boolean>>;
23
+ addBtnRef: React.RefObject<HTMLButtonElement>;
24
+ searchInputRef: React.RefObject<HTMLInputElement>;
25
+ customSelectButtonRef: React.RefObject<HTMLButtonElement>;
26
+ onClose: () => void;
27
+ }
28
+
29
+ /** @scope .*/
30
+ export const useArrowNavigation = ({
31
+ expanded,
32
+ setExpanded,
33
+ setFocusedSelectItem,
34
+ childrenCount,
35
+ setShouldClick,
36
+ addBtnRef,
37
+ searchInputRef,
38
+ customSelectButtonRef,
39
+ onClose
40
+ }: UseArrowNavigationParams) => {
41
+ const onArrowNavigation = (event: React.KeyboardEvent) => {
42
+ const codesToPreventDefault = ["ArrowDown", "ArrowUp", "Escape", "End", "Home"];
43
+ const hasAddBtn = !!addBtnRef?.current;
44
+ const childrenCountWithAddButton = childrenCount + (hasAddBtn ? 1 : 0);
45
+ const isSearchEvent = event.target === searchInputRef.current;
46
+ const isSelectButtonEvent = event.target === customSelectButtonRef.current;
47
+ const isSearchOrSelectButtonEvent = isSelectButtonEvent || isSearchEvent;
48
+
49
+ if (expanded) {
50
+ codesToPreventDefault.push("Tab");
51
+ codesToPreventDefault.push("Enter");
52
+ }
53
+
54
+ if (!expanded) {
55
+ codesToPreventDefault.push("Space");
56
+ }
57
+
58
+ if (codesToPreventDefault.includes(event.code) && !event.metaKey) {
59
+ event.preventDefault();
60
+ }
61
+
62
+ switch (event.code) {
63
+ case "ArrowDown":
64
+ if (!expanded) {
65
+ setExpanded(true);
66
+ setFocusedSelectItem(0);
67
+ break;
68
+ }
69
+ setFocusedSelectItem(prevState =>
70
+ prevState + 1 > childrenCountWithAddButton - 1 ? 0 : prevState + 1
71
+ );
72
+ break;
73
+ case "ArrowUp":
74
+ if (!expanded) {
75
+ setExpanded(true);
76
+ setFocusedSelectItem(childrenCountWithAddButton - 1);
77
+ break;
78
+ }
79
+
80
+ setFocusedSelectItem(prevState =>
81
+ prevState - 1 < 0 ? childrenCountWithAddButton - 1 : prevState - 1
82
+ );
83
+ break;
84
+ case "Enter":
85
+ if (expanded) {
86
+ setShouldClick(true);
87
+ } else {
88
+ setFocusedSelectItem(0);
89
+ }
90
+ if (isSearchOrSelectButtonEvent) {
91
+ setExpanded(!expanded);
92
+ }
93
+ break;
94
+ case "Space":
95
+ if (!expanded) {
96
+ setExpanded(true);
97
+ setFocusedSelectItem(0);
98
+ }
99
+ break;
100
+ case "Home":
101
+ setFocusedSelectItem(0);
102
+ break;
103
+ case "Escape":
104
+ if (expanded) {
105
+ setExpanded(false);
106
+ onClose();
107
+ }
108
+ break;
109
+ case "End":
110
+ setFocusedSelectItem(childrenCount - 1);
111
+ break;
112
+ case "ArrowLeft":
113
+ if (event.metaKey && expanded) {
114
+ event.preventDefault();
115
+ setFocusedSelectItem(0);
116
+ }
117
+ break;
118
+ case "ArrowRight":
119
+ if (event.metaKey && expanded) {
120
+ event.preventDefault();
121
+ setFocusedSelectItem(childrenCount - 1);
122
+ }
123
+ break;
124
+ }
125
+ };
126
+
127
+ return { onArrowNavigation };
128
+ };
@@ -0,0 +1,126 @@
1
+ /*
2
+ * Copyright 2022 OneWelcome B.V.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ import React, { useEffect, useRef, useState } from "react";
18
+ import { PartialInputProps, SearchProps } from "../Select.interfaces";
19
+
20
+ interface Props {
21
+ selectId: string;
22
+ search?: SearchProps;
23
+ optionsCount: number;
24
+ /**
25
+ * @deprecated
26
+ */
27
+ searchPlaceholder?: string;
28
+ /**
29
+ * @deprecated
30
+ */
31
+ searchInputProps?: PartialInputProps & { reset?: boolean };
32
+ searchInputClassName: string;
33
+ expanded: boolean;
34
+ setFocusedSelectItem: (idx: number) => void;
35
+ focusedSelectItem: number;
36
+ describedBy?: string;
37
+ getOptionId: (multiSelectId: string, optionIndex: number) => string;
38
+ getListboxId: (multiSelectId: string) => string;
39
+ }
40
+
41
+ /** @scope .*/
42
+ export const useSearch = ({
43
+ selectId,
44
+ search,
45
+ optionsCount,
46
+ searchPlaceholder,
47
+ searchInputProps,
48
+ searchInputClassName,
49
+ expanded,
50
+ setFocusedSelectItem,
51
+ focusedSelectItem,
52
+ describedBy,
53
+ getOptionId,
54
+ getListboxId
55
+ }: Props) => {
56
+ const [filter, setFilter] = useState("");
57
+
58
+ const DEFAULT_RENDER_THRESHOLD = 0;
59
+
60
+ const searchInputRef = useRef<HTMLInputElement>(null);
61
+
62
+ const threshold = search?.renderThreshold ?? DEFAULT_RENDER_THRESHOLD;
63
+ const hasEnoughChildren = optionsCount >= threshold;
64
+
65
+ const shouldRenderSearch = () => {
66
+ if (search?.enabled) {
67
+ return hasEnoughChildren;
68
+ }
69
+
70
+ if (search) {
71
+ return search.enabled as boolean;
72
+ }
73
+
74
+ return optionsCount > DEFAULT_RENDER_THRESHOLD;
75
+ };
76
+
77
+ const renderSearch = () => {
78
+ return (
79
+ <input
80
+ {...((search?.searchInputProps as any) ?? searchInputProps ?? {})}
81
+ ref={searchInputRef}
82
+ value={filter}
83
+ onChange={event => setFilter(event.currentTarget.value)}
84
+ className={[searchInputClassName, searchInputProps?.className].join(" ")}
85
+ style={{
86
+ display: expanded ? "block" : "none"
87
+ }}
88
+ type="text"
89
+ name="search-option"
90
+ placeholder={search?.searchPlaceholder ?? searchPlaceholder}
91
+ role="combobox"
92
+ autoComplete="off"
93
+ autoCorrect="off"
94
+ spellCheck={false}
95
+ aria-controls={getListboxId(selectId)}
96
+ aria-describedby={describedBy}
97
+ aria-autocomplete="none"
98
+ aria-expanded={expanded}
99
+ aria-activedescendant={getOptionId(selectId, focusedSelectItem)}
100
+ aria-haspopup={true}
101
+ />
102
+ );
103
+ };
104
+
105
+ const resetSearchState = () => {
106
+ setFilter("");
107
+ setFocusedSelectItem(0);
108
+ };
109
+
110
+ const visible = shouldRenderSearch();
111
+
112
+ useEffect(() => {
113
+ if (search?.searchInputProps?.reset || searchInputProps?.reset) {
114
+ resetSearchState();
115
+ }
116
+ }, [searchInputProps?.reset, search?.searchInputProps?.reset]);
117
+
118
+ return {
119
+ renderSearch,
120
+ resetSearchState,
121
+ searchVisible: visible,
122
+ searchThreshold: threshold,
123
+ searchInputRef,
124
+ filter
125
+ };
126
+ };
@@ -31,6 +31,7 @@ export interface AddNewProps {
31
31
  label: string;
32
32
  onAddNew: (value: string) => void;
33
33
  btnProps?: React.ButtonHTMLAttributes<HTMLButtonElement>;
34
+ alwaysEnabled?: boolean;
34
35
  }
35
36
 
36
37
  export interface SelectProps<V extends string | readonly string[] | undefined>
@@ -20,15 +20,18 @@ import React, {
20
20
  createRef,
21
21
  RefObject,
22
22
  useEffect,
23
- ReactNode
23
+ ReactNode,
24
+ useRef
24
25
  } from "react";
25
26
  import classes from "./Select.module.scss";
27
+ import { generateID } from "../../../../util/helper";
26
28
 
27
29
  export interface Props extends ComponentPropsWithRef<"li"> {
28
30
  children: ReactNode;
29
31
  value: string;
30
32
  disabled?: boolean;
31
33
  isSelected?: boolean;
34
+ disableDefaultSelectedStyle?: boolean;
32
35
  selectOpened?: boolean;
33
36
  hasFocus?: boolean;
34
37
  shouldClick?: boolean;
@@ -42,9 +45,11 @@ export interface Props extends ComponentPropsWithRef<"li"> {
42
45
 
43
46
  const OptionComponent: ForwardRefRenderFunction<HTMLLIElement, Props> = (
44
47
  {
48
+ id,
45
49
  children,
46
50
  className,
47
51
  isSelected = false,
52
+ disableDefaultSelectedStyle,
48
53
  shouldClick,
49
54
  hasFocus,
50
55
  selectOpened,
@@ -59,8 +64,15 @@ const OptionComponent: ForwardRefRenderFunction<HTMLLIElement, Props> = (
59
64
  }: Props,
60
65
  ref
61
66
  ) => {
67
+ const defaultOptionId = useRef(generateID(20));
68
+ const optionId = id ?? defaultOptionId.current;
62
69
  let innerOptionRef = (ref as RefObject<HTMLLIElement>) || createRef<HTMLLIElement>();
63
70
 
71
+ const additionalClasses = [];
72
+ className && additionalClasses.push(className);
73
+ isSelected && !disableDefaultSelectedStyle && additionalClasses.push(classes["selected-option"]);
74
+ disabled && additionalClasses.push(classes["disabled"]);
75
+
64
76
  useEffect(() => {
65
77
  if (isSelected && innerOptionRef.current && shouldClick) {
66
78
  innerOptionRef.current.click();
@@ -79,11 +91,10 @@ const OptionComponent: ForwardRefRenderFunction<HTMLLIElement, Props> = (
79
91
  return (
80
92
  <li
81
93
  {...rest}
94
+ id={optionId}
82
95
  ref={innerOptionRef}
83
96
  data-value={value}
84
- className={`${isSelected ? classes["selected-option"] : ""} ${
85
- disabled ? classes.disabled : ""
86
- } ${className ?? ""}`}
97
+ className={additionalClasses.join(" ")}
87
98
  onClick={onSelectHandler}
88
99
  onKeyDown={event => {
89
100
  if (event.code === "Enter") {
@@ -125,8 +125,19 @@ $listItemHeight: 2.5rem;
125
125
  cursor: pointer;
126
126
 
127
127
  &:focus {
128
- outline: var(--input-border-width-focus) solid var(--color-primary300);
129
- outline-offset: var(--input-border-width-focus);
128
+ outline: none;
129
+
130
+ &::before {
131
+ @include mixins.focusVisibleOutline($outlineOffset: 0, $selectors: null);
132
+ content: "";
133
+ position: absolute;
134
+ top: 0;
135
+ left: 0.125rem;
136
+ height: 100%;
137
+ width: calc(100% - 0.25rem);
138
+ opacity: 1;
139
+ z-index: calc(variables.$popover-z-index + 1);
140
+ }
130
141
  }
131
142
 
132
143
  &:hover {
@@ -31,9 +31,10 @@ import readyclasses from "../../../../readyclasses.module.scss";
31
31
  import { filterProps } from "../../../../util/helper";
32
32
  import { Icon, Icons } from "../../../Icon/Icon";
33
33
  import { SingleSelectProps } from "../Select.interfaces";
34
- import { useArrowNavigation, useSelectPositionList } from "../SelectService";
34
+ import { useSelectPositionList } from "../useSelectPositionList";
35
35
  import { useAddNewBtn } from "../useAddNewBtn";
36
- import { useSearch } from "../useSearch";
36
+ import { useSearch } from "./useSearch";
37
+ import { useArrowNavigation } from "./useArrowNavigation";
37
38
 
38
39
  const SelectComponent: ForwardRefRenderFunction<HTMLSelectElement, SingleSelectProps> = (
39
40
  {
@@ -87,7 +88,10 @@ const SelectComponent: ForwardRefRenderFunction<HTMLSelectElement, SingleSelectP
87
88
  });
88
89
  const { addBtnRef, addNewBtnOptionsContainerClassName, renderAddNew } = useAddNewBtn({
89
90
  addNew,
90
- filter
91
+ filter,
92
+ focusedSelectItem,
93
+ optionsCount,
94
+ searchInputRef
91
95
  });
92
96
 
93
97
  const nativeSelect = (ref as React.RefObject<HTMLSelectElement>) || createRef();
@@ -14,12 +14,7 @@
14
14
  * limitations under the License.
15
15
  */
16
16
 
17
- import React, { useEffect, useState } from "react";
18
- import {
19
- Position,
20
- UseArrowNavigationParams,
21
- UseSelectPositionListParams
22
- } from "./Select.interfaces";
17
+ import { UseArrowNavigationParams } from "../Select.interfaces";
23
18
 
24
19
  /** @scope .*/
25
20
  export const useArrowNavigation = ({
@@ -175,98 +170,3 @@ export const useArrowNavigation = ({
175
170
 
176
171
  return { onArrowNavigation };
177
172
  };
178
-
179
- /** @scope .*/
180
- export const useSelectPositionList = ({
181
- expanded,
182
- optionListReference,
183
- addBtnRef,
184
- containerReference
185
- }: UseSelectPositionListParams) => {
186
- const [optionsListMaxHeight, setOptionsListMaxHeight] = useState<{
187
- wrapper?: string;
188
- list?: string;
189
- }>({
190
- wrapper: undefined,
191
- list: undefined
192
- });
193
- const [opacity, setOpacity] = useState(0); // We set opacity because otherwise if we calculate the max height you see the list full height for a split second and then it shortens.
194
- const [listPosition, setListPosition] = useState<Partial<Position>>({});
195
-
196
- useEffect(() => {
197
- rePositionList();
198
- }, [expanded]);
199
-
200
- const rePositionList = () => {
201
- if (!expanded || !optionListReference.current || !containerReference.current) {
202
- return;
203
- }
204
-
205
- // Check whether there is more space above or below the select
206
- // Check space between the bottom of select and top of viewport
207
- const spaceOnTopOfSelect = containerReference.current.getBoundingClientRect().bottom;
208
-
209
- // Check space between the top of the select and bottom of viewport
210
- const spaceOnBottomOfSelect =
211
- window.innerHeight - containerReference.current.getBoundingClientRect().top;
212
-
213
- // Set position as if there's more space on the bottom
214
- let position: Position = { top: "2.75rem", bottom: "initial" };
215
-
216
- // Set the position of the select
217
- if (spaceOnTopOfSelect > spaceOnBottomOfSelect) {
218
- position = { top: "initial", bottom: "2.75rem" };
219
- }
220
-
221
- setListPosition(position);
222
-
223
- // Calculate the potential max height of the options list
224
- calculateOptionListMaxHeight(position);
225
- };
226
-
227
- const calculateOptionListMaxHeight = (position: Position) => {
228
- // Calculate max height if there's more space below the select
229
- const listHeight = optionListReference.current?.getBoundingClientRect().height;
230
- const addNewButtonHeightWithMargin = addBtnRef.current
231
- ? addBtnRef.current.getBoundingClientRect().height +
232
- parseInt(getComputedStyle(addBtnRef.current).marginBottom)
233
- : 0;
234
- const transformOrigin = position.top !== "initial" ? "top" : "bottom";
235
-
236
- if (!containerReference.current) {
237
- console.error(
238
- "The containerReference is empty for some reason in the SelectService.ts for the Select component in react-lib-components. We are trying to calculate the option list max height on expand"
239
- );
240
- return;
241
- }
242
-
243
- const availableSpace =
244
- transformOrigin === "top"
245
- ? window.innerHeight - containerReference.current.getBoundingClientRect().bottom - 16
246
- : containerReference.current.getBoundingClientRect().top - 16;
247
-
248
- if (listHeight && availableSpace < listHeight + addNewButtonHeightWithMargin) {
249
- const maxHeightObject = {
250
- wrapper: `${availableSpace}px`,
251
- list:
252
- addNewButtonHeightWithMargin > 0
253
- ? `${availableSpace - addNewButtonHeightWithMargin}px`
254
- : "none"
255
- };
256
- setOptionsListMaxHeight(maxHeightObject);
257
- setOpacity(100);
258
- return;
259
- }
260
-
261
- setOptionsListMaxHeight({ wrapper: undefined, list: undefined });
262
- setOpacity(100);
263
- };
264
-
265
- return {
266
- optionsListMaxHeight,
267
- opacity,
268
- setOpacity,
269
- listPosition,
270
- setListPosition
271
- };
272
- };
@@ -15,8 +15,8 @@
15
15
  */
16
16
 
17
17
  import React, { useEffect, useRef, useState } from "react";
18
- import { PartialInputProps, SearchProps } from "./Select.interfaces";
19
- import { Input } from "../Input/Input";
18
+ import { PartialInputProps, SearchProps } from "../Select.interfaces";
19
+ import { Input } from "../../Input/Input";
20
20
 
21
21
  interface Props {
22
22
  search?: SearchProps;
@@ -34,6 +34,7 @@ interface Props {
34
34
  setFocusedSelectItem: (idx: number) => void;
35
35
  }
36
36
 
37
+ /** @scope .*/
37
38
  export const useSearch = ({
38
39
  search,
39
40
  optionsCount,
@@ -13,12 +13,15 @@
13
13
  * See the License for the specific language governing permissions and
14
14
  * limitations under the License.
15
15
  */
16
+ @use "src/mixins.module";
17
+ @use "src/variables";
16
18
 
17
19
  ul.has-sibling {
18
20
  padding-bottom: 2px;
19
21
  }
20
22
 
21
23
  .action-button {
24
+ position: relative;
22
25
  border: none;
23
26
  border-top: 1px solid var(--color-blue-grey50);
24
27
  width: 100%;
@@ -35,10 +38,21 @@ ul.has-sibling {
35
38
  font-family: var(--font-family);
36
39
  font-size: var(--form-control-font-size);
37
40
 
38
- &:focus {
39
- outline: 1px solid;
40
- outline-offset: 0;
41
- border-radius: 0;
41
+ &:focus,
42
+ &.focus {
43
+ outline: none;
44
+
45
+ &::before {
46
+ @include mixins.focusVisibleOutline($outlineOffset: 0, $selectors: null);
47
+ content: "";
48
+ position: absolute;
49
+ top: 0;
50
+ left: 0.125rem;
51
+ height: 100%;
52
+ width: calc(100% - 0.25rem);
53
+ opacity: 1;
54
+ z-index: calc(variables.$popover-z-index + 1);
55
+ }
42
56
  }
43
57
 
44
58
  &:hover {
@@ -16,28 +16,62 @@
16
16
 
17
17
  import classes from "./useAddNewBtn.module.scss";
18
18
 
19
- import React, { useRef } from "react";
19
+ import React, { RefObject, useEffect, useRef } from "react";
20
20
  import { AddNewProps } from "./Select.interfaces";
21
21
 
22
22
  interface Props {
23
+ id?: string;
23
24
  addNew?: AddNewProps;
24
25
  filter: string;
26
+ focusedSelectItem: number;
27
+ optionsCount: number;
28
+ searchInputRef: RefObject<HTMLInputElement>;
29
+ shouldClick?: boolean;
30
+ onClickCallback?: () => void;
25
31
  }
26
32
 
27
- export const useAddNewBtn = ({ addNew, filter }: Props) => {
33
+ /** @scope .*/
34
+ export const useAddNewBtn = ({
35
+ id,
36
+ addNew,
37
+ filter,
38
+ focusedSelectItem,
39
+ optionsCount,
40
+ searchInputRef,
41
+ shouldClick,
42
+ onClickCallback
43
+ }: Props) => {
28
44
  const addBtnRef = useRef<HTMLButtonElement>(null);
29
-
30
45
  const addNewLabel = addNew?.label ?? "Create new";
46
+ const isProgrammaticallyFocused = focusedSelectItem === optionsCount;
47
+ const isSearchDisabled = !searchInputRef.current;
48
+ const hasSearchQuery = typeof filter === "string" && !!filter.trim() && !!searchInputRef.current;
49
+ const shouldRender = addNew && (isSearchDisabled || hasSearchQuery || addNew.alwaysEnabled);
50
+
51
+ const additionalClasses = [classes["action-button"]];
52
+ addNew?.btnProps?.className && additionalClasses.push(addNew?.btnProps?.className);
53
+ isProgrammaticallyFocused && additionalClasses.push(classes["focus"]);
54
+
55
+ useEffect(() => {
56
+ const addBtnClicked = addBtnRef.current && isProgrammaticallyFocused && shouldClick;
57
+ if (addBtnClicked) {
58
+ addBtnRef.current.click();
59
+ }
60
+ }, [addBtnRef.current, isProgrammaticallyFocused, shouldClick]);
31
61
 
32
62
  const renderAddNew = () =>
33
- addNew && (
63
+ shouldRender && (
34
64
  <button
35
- data-testid={"select-action-button"}
36
- className={classes["action-button"]}
37
- onClick={() => addNew.onAddNew(filter)}
65
+ data-testid="select-action-button"
66
+ {...addNew.btnProps}
38
67
  ref={addBtnRef}
39
68
  type="button"
40
- {...addNew.btnProps}
69
+ className={additionalClasses.join(" ")}
70
+ onClick={() => {
71
+ addNew.onAddNew(filter);
72
+ onClickCallback?.();
73
+ }}
74
+ id={id}
41
75
  >
42
76
  {!filter && addNewLabel}
43
77
  {filter && <span style={{ fontWeight: "700" }}>{`"${filter}"`}</span>}