Skip to content

Commit e5c5e62

Browse files
committed
Fixes to PR #919
1 parent 2bd62df commit e5c5e62

File tree

4 files changed

+55
-61
lines changed

4 files changed

+55
-61
lines changed

Diff for: apps/cyberstorm-storybook/stories/components/MultiSelectSearch.stories.tsx

+6-3
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,17 @@ const Template: StoryFn<typeof MultiSelectSearch> = (args) => {
3434
const defaultProps = {
3535
...args,
3636
onChange: (x: { label: string; value: string }[]) => setSelected(x),
37+
value: selected,
3738
};
3839
return (
3940
<div>
4041
<div style={{ color: "white" }}>
4142
Value in state:{" "}
42-
{selected.map((s) => {
43-
return s.label;
44-
})}
43+
{selected
44+
.map((s) => {
45+
return s.label;
46+
})
47+
.join(", ")}
4548
</div>
4649
<MultiSelectSearch {...defaultProps} />
4750
</div>

Diff for: packages/cyberstorm/src/components/MultiSelectSearch/MultiSelectSearch.module.css

+1-1
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,6 @@
136136
padding: var(--space--12) var(--space--16);
137137
}
138138

139-
.multiSelectItemWrapper:where(.highlighted) {
139+
.multiSelectItemWrapper:focus {
140140
background-color: var(--color-surface--6);
141141
}

Diff for: packages/cyberstorm/src/components/MultiSelectSearch/MultiSelectSearch.tsx

+46-55
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,7 @@ import {
1010
faCircleXmark,
1111
faXmark,
1212
} from "@fortawesome/pro-solid-svg-icons";
13-
import { useRef } from "react";
14-
import { assertIsNode } from "../../utils/type_guards";
13+
import { isNode } from "../../utils/type_guards";
1514

1615
type Option = {
1716
label: string;
@@ -20,8 +19,10 @@ type Option = {
2019
type Props = {
2120
options: Option[];
2221
// eslint-disable-next-line @typescript-eslint/no-explicit-any
23-
onChange: (v: any) => void;
22+
value: Option[];
23+
onChange: (v: Option[]) => void;
2424
onBlur: () => void;
25+
// TODO: Implement disabled state
2526
disabled?: boolean;
2627
name: string;
2728
placeholder?: string;
@@ -34,77 +35,66 @@ type Props = {
3435
*/
3536
export const MultiSelectSearch = React.forwardRef<HTMLInputElement, Props>(
3637
function MultiSelectSearch(props, ref) {
37-
const { options, onChange, onBlur, placeholder, color } = props;
38-
const menuRef = useRef<HTMLDivElement | null>(null);
39-
const inputRef = useRef<HTMLInputElement | null>(null);
38+
const { name, options, value, onChange, onBlur, placeholder, color } =
39+
props;
40+
const menuRef = React.useRef<HTMLDivElement | null>(null);
41+
const inputRef = React.useRef<HTMLInputElement | null>(null);
4042

4143
const [isVisible, setIsVisible] = React.useState(false);
4244
const [search, setSearch] = React.useState("");
43-
const [selected, setList] = React.useState<Option[]>([]);
4445
const [filteredOptions, setFilteredOptions] = React.useState(options);
4546

4647
function add(incomingOption: Option) {
47-
if (!selected.some((option) => option.value === incomingOption.value)) {
48-
setList([...selected, incomingOption]);
48+
if (!value.some((option) => option.value === incomingOption.value)) {
49+
onChange([...value, incomingOption]);
4950
}
5051
}
5152

5253
function remove(incomingOption: Option) {
53-
setList(
54-
selected.filter((option) => option.value !== incomingOption.value)
55-
);
56-
}
57-
58-
// Helper function for preventing focusing on input, when clicking on a child.
59-
function handleParentClick(
60-
event: React.MouseEvent | React.KeyboardEvent,
61-
onClick: () => void
62-
) {
63-
onClick();
64-
event.stopPropagation();
54+
onChange(value.filter((option) => option.value !== incomingOption.value));
6555
}
6656

67-
const closeOpenMenu = React.useCallback(
57+
const hideMenu = React.useCallback(
6858
(e: MouseEvent | TouchEvent) => {
6959
if (
7060
menuRef.current &&
7161
isVisible &&
72-
!menuRef.current.contains(assertIsNode(e.target) ? e.target : null)
62+
!menuRef.current.contains(isNode(e.target) ? e.target : null)
7363
) {
7464
setIsVisible(false);
7565
onBlur();
7666
}
7767
},
78-
[setIsVisible, isVisible]
68+
[setIsVisible, isVisible, onBlur, menuRef]
7969
);
8070

8171
// Event listeners for closing menu when clicking or touching outside.
8272
React.useEffect(() => {
83-
document.addEventListener("mousedown", closeOpenMenu);
84-
document.addEventListener("touchstart", closeOpenMenu);
73+
document.addEventListener("mousedown", hideMenu);
74+
document.addEventListener("touchstart", hideMenu);
8575
return () => {
86-
document.removeEventListener("mousedown", closeOpenMenu);
87-
document.removeEventListener("touchstart", closeOpenMenu);
76+
document.removeEventListener("mousedown", hideMenu);
77+
document.removeEventListener("touchstart", hideMenu);
8878
};
8979
});
9080

9181
React.useEffect(() => {
92-
onChange(selected);
9382
const updatedOptions = options.filter(
94-
(option) => !selected.includes(option) && option.label.includes(search)
83+
(option) =>
84+
!value.includes(option) &&
85+
option.label.toLowerCase().includes(search.toLowerCase())
9586
);
9687
setFilteredOptions(updatedOptions);
97-
}, [selected, search]);
88+
}, [value, search, options, setFilteredOptions]);
9889

9990
return (
10091
<div className={styles.root} ref={ref}>
101-
<div className={styles.selected}>
102-
{selected.map((option, key) => {
92+
<div className={styles.value}>
93+
{value.map((option) => {
10394
return (
10495
<Button.Root
105-
key={key}
96+
key={option.value}
10697
onClick={() => remove(option)}
107-
colorScheme="default"
10898
paddingSize="small"
10999
style={{ gap: "0.5rem" }}
110100
>
@@ -116,20 +106,19 @@ export const MultiSelectSearch = React.forwardRef<HTMLInputElement, Props>(
116106
);
117107
})}
118108
</div>
119-
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events */}
120109
<div
121110
className={styles.search}
122-
onClick={(e) =>
123-
handleParentClick(e, () => {
124-
inputRef.current && inputRef.current.focus();
125-
})
126-
}
111+
onFocus={(e) => {
112+
e.stopPropagation();
113+
inputRef.current && inputRef.current.focus();
114+
}}
127115
role="button"
128116
tabIndex={0}
129117
ref={menuRef}
130118
>
131119
<div className={styles.inputContainer} data-color={color}>
132120
<input
121+
name={name}
133122
className={styles.input}
134123
value={search}
135124
data-color={color}
@@ -139,7 +128,10 @@ export const MultiSelectSearch = React.forwardRef<HTMLInputElement, Props>(
139128
placeholder={placeholder}
140129
/>
141130
<button
142-
onClick={(e) => handleParentClick(e, () => setList([]))}
131+
onClick={(e) => {
132+
e.stopPropagation();
133+
setSearch("");
134+
}}
143135
className={styles.removeAllButton}
144136
>
145137
<Icon inline>
@@ -148,9 +140,10 @@ export const MultiSelectSearch = React.forwardRef<HTMLInputElement, Props>(
148140
</button>
149141
<div className={styles.inputButtonDivider}></div>
150142
<button
151-
onClick={(e) =>
152-
handleParentClick(e, () => setIsVisible(!isVisible))
153-
}
143+
onClick={(e) => {
144+
e.stopPropagation();
145+
setIsVisible(!isVisible);
146+
}}
154147
className={styles.openMenuButton}
155148
>
156149
<Icon inline>
@@ -164,11 +157,14 @@ export const MultiSelectSearch = React.forwardRef<HTMLInputElement, Props>(
164157
isVisible ? styles.visible : null
165158
)}
166159
>
167-
{filteredOptions.map((option, key) => {
160+
{filteredOptions.map((option) => {
168161
return (
169162
<MultiSelectItem
170-
key={key}
171-
onClick={(e) => handleParentClick(e, () => add(option))}
163+
key={option.value}
164+
onClick={(e) => {
165+
e.stopPropagation();
166+
add(option);
167+
}}
172168
option={option}
173169
/>
174170
);
@@ -183,18 +179,13 @@ export const MultiSelectSearch = React.forwardRef<HTMLInputElement, Props>(
183179
MultiSelectSearch.displayName = "MultiSelectSearch";
184180

185181
const MultiSelectItem = (props: {
186-
key: number;
187182
onClick: (e: React.MouseEvent | React.KeyboardEvent) => void;
188183
option: Option;
189-
focus?: boolean;
190184
}) => {
191185
return (
192186
<div
193-
className={classnames(
194-
styles.multiSelectItemWrapper,
195-
props.focus ? styles.highlighted : null
196-
)}
197-
onClick={(e) => props.onClick(e)}
187+
className={styles.multiSelectItemWrapper}
188+
onClick={props.onClick}
198189
onKeyDown={(e) => (e.code === "Enter" ? props.onClick(e) : null)}
199190
tabIndex={0}
200191
role="button"

Diff for: packages/cyberstorm/src/utils/type_guards.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ export const isRecord = (obj: unknown): obj is Record<string, unknown> =>
44
export const isStringArray = (arr: unknown): arr is string[] =>
55
Array.isArray(arr) && arr.every((s) => typeof s === "string");
66

7-
export const assertIsNode = (e: EventTarget | null): e is Node => {
7+
export const isNode = (e: EventTarget | null): e is Node => {
88
if (!e || !("nodeType" in e)) {
9-
throw new Error(`Node expected`);
9+
return false;
1010
}
1111
return true;
1212
};

0 commit comments

Comments
 (0)