Skip to content

Commit 0db6569

Browse files
refactor: ♻️ ref based navigation (#346)
* refactor: ♻️ ref based navigation * refactor: ♻️ zero dependency * refactor: ♻️ fixed search * docs: 📝 updated documentation
1 parent 80eae41 commit 0db6569

21 files changed

+583
-411
lines changed

.storybook/main.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1+
const path = require("path");
2+
13
module.exports = {
24
stories: ["../stories/**/*.stories.@(ts|tsx|js|jsx)"],
35
addons: [
46
"@storybook/addon-essentials",
5-
"@storybook/addon-links",
67
"@storybook/addon-knobs",
7-
"@storybook/addon-storysource",
8+
"@storybook/addon-links",
89
],
910
// https://storybook.js.org/docs/react/configure/typescript#mainjs-configuration
1011
typescript: {

README.md

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ Simple and lightweight multiple selection dropdown component with `checkboxes`,
99

1010
## ✨ Features
1111

12+
- 🕶 Zero Dependency
1213
- 🍃 Lightweight (<5KB)
1314
- 💅 Themeable
1415
- ✌ Written w/ TypeScript
@@ -28,7 +29,7 @@ yarn add react-multi-select-component # yarn
2829
import React, { useState } from "react";
2930
import MultiSelect from "react-multi-select-component";
3031

31-
const Example: React.FC = () => {
32+
const Example = () => {
3233
const options = [
3334
{ label: "Grapes 🍇", value: "grapes" },
3435
{ label: "Mango 🥭", value: "mango" },
@@ -51,7 +52,7 @@ const Example: React.FC = () => {
5152
options={options}
5253
value={selected}
5354
onChange={setSelected}
54-
labelledBy={"Select"}
55+
labelledBy="Select"
5556
/>
5657
</div>
5758
);
@@ -67,7 +68,6 @@ export default Example;
6768
| `labelledBy` | value for `aria-labelledby` | `string` | |
6869
| `options` | options for dropdown | `[{label, value, disabled}]` | |
6970
| `value` | pre-selected rows | `[{label, value}]` | `[]` |
70-
| `focusSearchOnOpen` | focus on search input when opening | `boolean` | `true` |
7171
| `hasSelectAll` | toggle 'Select All' option | `boolean` | `true` |
7272
| `isLoading` | show spinner on select | `boolean` | `false` |
7373
| `shouldToggleOnHover` | toggle dropdown on hover option | `boolean` | `false` |
@@ -80,10 +80,10 @@ export default Example;
8080
| `className` | class name for parent component | `string` | `multi-select` |
8181
| `valueRenderer` | custom dropdown header [docs](#-custom-value-renderer) | `function` | |
8282
| `ItemRenderer` | custom dropdown option [docs](#-custom-item-renderer) | `function` | |
83-
| `ClearIcon` | Custom Clear Icon for Search | `JSX.element` | |
84-
| `ArrowRenderer` | Custom Arrow Icon for Dropdown | `JSX.element` | |
83+
| `ClearIcon` | Custom Clear Icon for Search | `ReactNode` | |
84+
| `ArrowRenderer` | Custom Arrow Icon for Dropdown | `ReactNode` | |
8585
| `debounceDuration` | debounce duraion for Search | `number` | `300` |
86-
| `ClearSelectedIcon` | Custom Clear Icon for Selected Items | `JSX.element` | `function` |
86+
| `ClearSelectedIcon` | Custom Clear Icon for Selected Items | `ReactNode` | |
8787

8888
## 🔍 Custom filter logic
8989

@@ -159,7 +159,6 @@ You can override CSS variables to customize the appearance
159159
- This project gets inspiration and several pieces of logical code from [react-multiple-select](https://github.com/Khan/react-multi-select/)
160160
- [TypeScript](https://github.com/microsoft/typescript)
161161
- [TSDX](https://github.com/jaredpalmer/tsdx)
162-
- [Goober](https://github.com/cristianbote/goober)
163162

164163
## 📜 License
165164

package.json

Lines changed: 17 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
"main": "dist/index.js",
99
"module": "dist/react-multi-select-component.esm.js",
1010
"typings": "dist/index.d.ts",
11-
"sideEffects": false,
1211
"scripts": {
1312
"start": "tsdx watch",
1413
"build": "tsdx build",
@@ -24,31 +23,31 @@
2423
"peerDependencies": {
2524
"react": ">=17"
2625
},
27-
"dependencies": {
28-
"goober": "^2.0.30"
29-
},
26+
"dependencies": {},
3027
"devDependencies": {
31-
"@babel/core": "^7.12.13",
32-
"@size-limit/preset-small-lib": "^4.9.2",
33-
"@storybook/addon-essentials": "^6.1.17",
28+
"@babel/core": "^7.13.10",
29+
"@size-limit/preset-small-lib": "^4.10.1",
30+
"@storybook/addon-essentials": "^6.1.21",
3431
"@storybook/addon-info": "^5.3.21",
35-
"@storybook/addon-knobs": "^6.1.17",
36-
"@storybook/addon-links": "^6.1.17",
37-
"@storybook/addon-storysource": "^6.1.17",
38-
"@storybook/addons": "^6.1.17",
39-
"@storybook/react": "^6.1.17",
40-
"@types/react": "^17.0.1",
41-
"@types/react-dom": "^17.0.0",
32+
"@storybook/addon-knobs": "^6.1.21",
33+
"@storybook/addon-links": "^6.1.21",
34+
"@storybook/addons": "^6.1.21",
35+
"@storybook/react": "^6.1.21",
36+
"@types/react": "^17.0.3",
37+
"@types/react-dom": "^17.0.3",
4238
"babel-loader": "^8.2.2",
4339
"eslint-plugin-prettier": "^3.3.1",
4440
"husky": "^4.3.8",
41+
"postcss": "^8.2.8",
4542
"react": "^17.0.1",
46-
"react-dom": "^17.0.1",
47-
"react-is": "^17.0.1",
48-
"size-limit": "^4.9.2",
43+
"react-dom": "^17.0.2",
44+
"react-is": "^17.0.2",
45+
"rollup-plugin-postcss": "^4.0.0",
46+
"size-limit": "^4.10.1",
47+
"style-loader": "^2.0.0",
4948
"tsdx": "^0.14.1",
5049
"tslib": "^2.1.0",
51-
"typescript": "^4.1.5"
50+
"typescript": "^4.2.3"
5251
},
5352
"browserslist": [
5453
"defaults",

src/hooks/use-key.tsx

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/*
2+
* copied from https://github.com/imbhargav5/rooks/blob/master/packages/shared/useKeyRef.ts
3+
*/
4+
5+
import { Ref, useEffect, useCallback, useRef, useMemo } from "react";
6+
7+
interface Options {
8+
/**
9+
* Condition which if true, will enable the event listeners
10+
*/
11+
when?: boolean;
12+
/**
13+
* Keyboardevent types to listen for. Valid options are keyDown, keyPress and keyUp
14+
*/
15+
eventTypes?: Array<string | number>;
16+
/**
17+
* target ref on which the events should be listened. If no target is specified,
18+
* events are listened to on the window
19+
*/
20+
target?: Ref<HTMLElement> | null;
21+
}
22+
23+
const defaultOptions = {
24+
when: true,
25+
eventTypes: ["keydown"],
26+
};
27+
28+
/**
29+
* useKey hook
30+
*
31+
* Fires a callback on keyboard events like keyDown, keyPress and keyUp
32+
*
33+
* @param {[string|number]} keyList
34+
* @param {function} callback
35+
* @param {Options} options
36+
*/
37+
function useKey(
38+
input: string | number | Array<string | number>,
39+
callback: (e: KeyboardEvent) => any,
40+
opts?: Options
41+
): void {
42+
const keyList: Array<string | number> = useMemo(
43+
() => (Array.isArray(input) ? input : [input]),
44+
[input]
45+
);
46+
const options = Object.assign({}, defaultOptions, opts);
47+
const { when, eventTypes } = options;
48+
const callbackRef = useRef<(e: KeyboardEvent) => any>(callback);
49+
let { target } = options;
50+
51+
useEffect(() => {
52+
callbackRef.current = callback;
53+
});
54+
55+
const handle = useCallback(
56+
(e: KeyboardEvent) => {
57+
if (keyList.some((k) => e.key === k || e.code === k)) {
58+
callbackRef.current(e);
59+
}
60+
},
61+
[keyList]
62+
);
63+
64+
useEffect((): any => {
65+
if (when && typeof window !== "undefined") {
66+
const targetNode = target ? target["current"] : window;
67+
eventTypes.forEach((eventType) => {
68+
targetNode && targetNode.addEventListener(eventType, handle);
69+
});
70+
return () => {
71+
eventTypes.forEach((eventType) => {
72+
targetNode && targetNode.removeEventListener(eventType, handle);
73+
});
74+
};
75+
}
76+
}, [when, eventTypes, keyList, target, callback]);
77+
}
78+
79+
export { useKey };

src/hooks/use-multi-select.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ const defaultStrings = {
1414

1515
const defaultProps: Partial<ISelectProps> = {
1616
value: [],
17-
focusSearchOnOpen: true,
1817
hasSelectAll: true,
1918
className: "multi-select",
2019
debounceDuration: 200,

src/lib/classnames.ts

Lines changed: 0 additions & 6 deletions
This file was deleted.

src/lib/constants.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export const KEY = {
2+
ARROW_DOWN: "ArrowDown",
3+
ARROW_UP: "ArrowUp",
4+
ENTER: "Enter",
5+
ESCAPE: "Escape",
6+
SPACE: "Space",
7+
};

src/lib/interfaces.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ export interface Option {
1010
export interface ISelectProps {
1111
options: Option[];
1212
value: Option[];
13-
focusSearchOnOpen?: boolean;
1413
onChange?;
1514
valueRenderer?: (selected: Option[], options: Option[]) => ReactNode;
1615
ItemRenderer?: Function;

src/multi-select/dropdown.tsx

Lines changed: 15 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -3,70 +3,18 @@
33
* and hosts it in the component. When the component is selected, it
44
* drops-down the contentComponent and applies the contentProps.
55
*/
6-
import { css } from "goober";
76
import React, { useEffect, useRef, useState } from "react";
87

98
import { useDidUpdateEffect } from "../hooks/use-did-update-effect";
9+
import { useKey } from "../hooks/use-key";
1010
import { useMultiSelect } from "../hooks/use-multi-select";
11-
import { cn } from "../lib/classnames";
11+
import { KEY } from "../lib/constants";
1212
import SelectPanel from "../select-panel";
1313
import { Cross } from "../select-panel/cross";
1414
import { Arrow } from "./arrow";
1515
import { DropdownHeader } from "./header";
1616
import { Loading } from "./loading";
1717

18-
const PanelContainer = css({
19-
position: "absolute",
20-
zIndex: 1,
21-
top: "100%",
22-
width: "100%",
23-
paddingTop: "8px",
24-
".panel-content": {
25-
maxHeight: "300px",
26-
overflowY: "auto",
27-
borderRadius: "var(--rmsc-radius)",
28-
background: "var(--rmsc-bg)",
29-
boxShadow: "0 0 0 1px rgba(0, 0, 0, 0.1), 0 4px 11px rgba(0, 0, 0, 0.1)",
30-
},
31-
});
32-
33-
const DropdownContainer = css({
34-
position: "relative",
35-
outline: 0,
36-
backgroundColor: "var(--rmsc-bg)",
37-
border: "1px solid var(--rmsc-border)",
38-
borderRadius: "var(--rmsc-radius)",
39-
"&:focus-within": {
40-
boxShadow: "var(--rmsc-main) 0 0 0 1px",
41-
borderColor: "var(--rmsc-main)",
42-
},
43-
});
44-
45-
const DropdownHeading = css({
46-
position: "relative",
47-
padding: "0 var(--rmsc-p)",
48-
display: "flex",
49-
alignItems: "center",
50-
width: "100%",
51-
height: "var(--rmsc-h)",
52-
cursor: "default",
53-
outline: 0,
54-
".dropdown-heading-value": {
55-
overflow: "hidden",
56-
textOverflow: "ellipsis",
57-
whiteSpace: "nowrap",
58-
flex: 1,
59-
},
60-
});
61-
62-
const ClearSelectedButton = css({
63-
cursor: "pointer",
64-
background: "none",
65-
border: 0,
66-
padding: 0,
67-
display: "flex",
68-
});
69-
7018
const Dropdown = () => {
7119
const {
7220
t,
@@ -103,24 +51,20 @@ const Dropdown = () => {
10351

10452
const handleKeyDown = (e) => {
10553
if (isInternalExpand) {
106-
switch (e.which) {
107-
case 27: // Escape
108-
case 38: // Up Arrow
109-
setExpanded(false);
110-
wrapper?.current?.focus();
111-
break;
112-
case 32: // Space
113-
case 13: // Enter Key
114-
case 40: // Down Arrow
115-
setExpanded(true);
116-
break;
117-
default:
118-
return;
54+
if (e.code === KEY.ESCAPE) {
55+
setExpanded(false);
56+
wrapper?.current?.focus();
57+
} else {
58+
setExpanded(true);
11959
}
12060
}
12161
e.preventDefault();
12262
};
12363

64+
useKey([KEY.ENTER, KEY.ARROW_DOWN, KEY.SPACE, KEY.ESCAPE], handleKeyDown, {
65+
target: wrapper,
66+
});
67+
12468
const handleHover = (iexpanded: boolean) => {
12569
isInternalExpand && shouldToggleOnHover && setExpanded(iexpanded);
12670
};
@@ -151,30 +95,26 @@ const Dropdown = () => {
15195
return (
15296
<div
15397
tabIndex={0}
154-
className={cn(DropdownContainer, "dropdown-container")}
98+
className="dropdown-container"
15599
aria-labelledby={labelledBy}
156100
aria-expanded={expanded}
157101
aria-readonly={true}
158102
aria-disabled={disabled}
159103
ref={wrapper}
160-
onKeyDown={handleKeyDown}
161104
onFocus={handleFocus}
162105
onBlur={handleBlur}
163106
onMouseEnter={handleMouseEnter}
164107
onMouseLeave={handleMouseLeave}
165108
>
166-
<div
167-
className={cn(DropdownHeading, "dropdown-heading")}
168-
onClick={toggleExpanded}
169-
>
109+
<div className="dropdown-heading" onClick={toggleExpanded}>
170110
<div className="dropdown-heading-value">
171111
<DropdownHeader />
172112
</div>
173113
{isLoading && <Loading />}
174114
{value.length > 0 && (
175115
<button
176116
type="button"
177-
className={cn(ClearSelectedButton, "clear-selected-button")}
117+
className="clear-selected-button"
178118
onClick={handleClearSelected}
179119
disabled={disabled}
180120
aria-label={t("clearSelected")}
@@ -185,7 +125,7 @@ const Dropdown = () => {
185125
<FinalArrow expanded={expanded} />
186126
</div>
187127
{expanded && (
188-
<div className={cn(PanelContainer, "dropdown-content")}>
128+
<div className="dropdown-content">
189129
<div className="panel-content">
190130
<SelectPanel />
191131
</div>

0 commit comments

Comments
 (0)