reshaped 2.11.9 → 2.11.11
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.
- package/CHANGELOG.md +5 -0
- package/bundle.css +1 -1
- package/bundle.js +10 -10
- package/components/Alert/Alert.js +1 -1
- package/components/Badge/Badge.types.d.ts +1 -1
- package/components/Modal/Modal.js +3 -2
- package/components/Modal/Modal.module.css +1 -1
- package/components/Modal/Modal.types.d.ts +3 -1
- package/components/Modal/tests/Modal.stories.d.ts +2 -1
- package/components/Modal/tests/Modal.stories.js +10 -4
- package/components/Overlay/Overlay.js +5 -1
- package/components/Tooltip/Tooltip.js +3 -1
- package/components/Tooltip/Tooltip.types.d.ts +3 -2
- package/components/Tooltip/tests/Tooltip.stories.d.ts +1 -0
- package/components/Tooltip/tests/Tooltip.stories.js +20 -1
- package/components/_private/Flyout/Flyout.types.d.ts +35 -5
- package/components/_private/Flyout/FlyoutControlled.js +23 -18
- package/components/_private/Flyout/useFlyout.d.ts +21 -0
- package/{hooks/_private → components/_private/Flyout}/useFlyout.js +33 -120
- package/components/_private/Flyout/utilities/calculatePosition.d.ts +18 -0
- package/components/_private/Flyout/utilities/calculatePosition.js +97 -0
- package/hooks/_private/useSingletonHotkeys.d.ts +8 -11
- package/hooks/_private/useSingletonHotkeys.js +23 -56
- package/package.json +1 -1
- package/utilities/a11y/TrapFocus.d.ts +1 -0
- package/utilities/a11y/TrapFocus.js +3 -3
- package/hooks/_private/useFlyout.d.ts +0 -27
@@ -0,0 +1,18 @@
|
|
1
|
+
import type * as T from "../Flyout.types";
|
2
|
+
/**
|
3
|
+
* Calculate styles for the current position
|
4
|
+
*/
|
5
|
+
declare const calculatePosition: (args: T.Options & {
|
6
|
+
triggerBounds: DOMRect;
|
7
|
+
flyoutBounds: DOMRect;
|
8
|
+
scopeOffset: Record<"left" | "top", number>;
|
9
|
+
}) => {
|
10
|
+
styles: {
|
11
|
+
left: number;
|
12
|
+
top: number;
|
13
|
+
width: number;
|
14
|
+
height: number;
|
15
|
+
};
|
16
|
+
position: T.Position;
|
17
|
+
};
|
18
|
+
export default calculatePosition;
|
@@ -0,0 +1,97 @@
|
|
1
|
+
const SCREEN_OFFSET = 16;
|
2
|
+
const getRTLPosition = (position) => {
|
3
|
+
if (position.includes("start"))
|
4
|
+
return position.replace("start", "end");
|
5
|
+
if (position.includes("end"))
|
6
|
+
return position.replace("end", "start");
|
7
|
+
return position;
|
8
|
+
};
|
9
|
+
/**
|
10
|
+
* Get a position value which centers 2 elements vertically or horizontally
|
11
|
+
*/
|
12
|
+
const centerBySize = (originSize, targetSize) => {
|
13
|
+
return Math.floor(originSize / 2 - targetSize / 2);
|
14
|
+
};
|
15
|
+
/**
|
16
|
+
* Calculate styles for the current position
|
17
|
+
*/
|
18
|
+
const calculatePosition = (args) => {
|
19
|
+
const { triggerBounds, flyoutBounds, scopeOffset, position: passedPosition, rtl, width } = args;
|
20
|
+
let left = 0;
|
21
|
+
let top = 0;
|
22
|
+
let position = passedPosition;
|
23
|
+
if (rtl)
|
24
|
+
position = getRTLPosition(position);
|
25
|
+
if (width === "full" || width === "trigger") {
|
26
|
+
position = position.includes("top") ? "top" : "bottom";
|
27
|
+
}
|
28
|
+
switch (position) {
|
29
|
+
case "bottom":
|
30
|
+
case "top":
|
31
|
+
left = centerBySize(triggerBounds.width, flyoutBounds.width) + triggerBounds.left;
|
32
|
+
break;
|
33
|
+
case "start":
|
34
|
+
case "start-top":
|
35
|
+
case "start-bottom":
|
36
|
+
left = triggerBounds.left - triggerBounds.width;
|
37
|
+
break;
|
38
|
+
case "end":
|
39
|
+
case "end-top":
|
40
|
+
case "end-bottom":
|
41
|
+
left = triggerBounds.right;
|
42
|
+
break;
|
43
|
+
case "top-start":
|
44
|
+
case "bottom-start":
|
45
|
+
left = triggerBounds.left;
|
46
|
+
break;
|
47
|
+
case "top-end":
|
48
|
+
case "bottom-end":
|
49
|
+
left = triggerBounds.right - flyoutBounds.width;
|
50
|
+
break;
|
51
|
+
default:
|
52
|
+
break;
|
53
|
+
}
|
54
|
+
switch (position) {
|
55
|
+
case "top":
|
56
|
+
case "top-start":
|
57
|
+
case "top-end":
|
58
|
+
top = triggerBounds.top - flyoutBounds.height;
|
59
|
+
break;
|
60
|
+
case "bottom":
|
61
|
+
case "bottom-start":
|
62
|
+
case "bottom-end":
|
63
|
+
top = triggerBounds.bottom;
|
64
|
+
break;
|
65
|
+
case "start":
|
66
|
+
case "end":
|
67
|
+
top = centerBySize(triggerBounds.height, flyoutBounds.height) + triggerBounds.top;
|
68
|
+
break;
|
69
|
+
case "start-top":
|
70
|
+
case "end-top":
|
71
|
+
top = triggerBounds.top;
|
72
|
+
break;
|
73
|
+
case "start-bottom":
|
74
|
+
case "end-bottom":
|
75
|
+
top = triggerBounds.bottom - triggerBounds.height;
|
76
|
+
break;
|
77
|
+
default:
|
78
|
+
break;
|
79
|
+
}
|
80
|
+
if (top === undefined || left === undefined) {
|
81
|
+
throw Error(`[Reshaped, flyout]: ${position} position is not valid`);
|
82
|
+
}
|
83
|
+
top = Math.round(top + (window.scrollY || 0) - scopeOffset.top);
|
84
|
+
left = Math.round(left + (window.scrollX || 0) - scopeOffset.left);
|
85
|
+
let widthStyle = Math.ceil(flyoutBounds.width);
|
86
|
+
const height = Math.ceil(flyoutBounds.height);
|
87
|
+
if (width === "full") {
|
88
|
+
left = SCREEN_OFFSET;
|
89
|
+
widthStyle = window.innerWidth - SCREEN_OFFSET * 2;
|
90
|
+
}
|
91
|
+
else if (width === "trigger") {
|
92
|
+
widthStyle = triggerBounds.width;
|
93
|
+
}
|
94
|
+
const styles = { left, top, width: widthStyle, height };
|
95
|
+
return { styles, position };
|
96
|
+
};
|
97
|
+
export default calculatePosition;
|
@@ -3,7 +3,7 @@ import React from "react";
|
|
3
3
|
* Types
|
4
4
|
*/
|
5
5
|
type Callback = (e: KeyboardEvent) => void;
|
6
|
-
type
|
6
|
+
type PressedMap = Record<string, KeyboardEvent>;
|
7
7
|
type Hotkeys = Record<string, Callback | null>;
|
8
8
|
type HotkeyOptions = {
|
9
9
|
preventDefault?: boolean;
|
@@ -12,20 +12,17 @@ type Context = {
|
|
12
12
|
isPressed: (key: string) => boolean;
|
13
13
|
addHotkeys: (hotkeys: Hotkeys, ref: React.RefObject<HTMLElement | null>, options?: HotkeyOptions) => (() => void) | undefined;
|
14
14
|
};
|
15
|
+
type HotkeyData = {
|
16
|
+
callback: Callback;
|
17
|
+
ref: React.RefObject<HTMLElement | null>;
|
18
|
+
options: HotkeyOptions;
|
19
|
+
};
|
15
20
|
export declare class HotkeyStore {
|
16
|
-
hotkeyMap: Record<string,
|
17
|
-
data: Set<{
|
18
|
-
callback: Callback;
|
19
|
-
ref: React.RefObject<HTMLElement | null>;
|
20
|
-
options: HotkeyOptions;
|
21
|
-
}>;
|
22
|
-
used: boolean;
|
23
|
-
}>;
|
21
|
+
hotkeyMap: Record<string, Set<HotkeyData>>;
|
24
22
|
getSize: () => number;
|
25
23
|
bindHotkeys: (hotkeys: Hotkeys, ref: React.RefObject<HTMLElement | null>, options: HotkeyOptions) => void;
|
26
24
|
unbindHotkeys: (hotkeys: Hotkeys) => void;
|
27
|
-
handleKeyDown: (pressedMap:
|
28
|
-
handleKeyUp: (e: KeyboardEvent) => void;
|
25
|
+
handleKeyDown: (pressedMap: PressedMap, e: KeyboardEvent) => void;
|
29
26
|
}
|
30
27
|
/**
|
31
28
|
* Components / Hooks
|
@@ -24,29 +24,6 @@ const getEventKey = (e) => {
|
|
24
24
|
}
|
25
25
|
return e.key.toLowerCase();
|
26
26
|
};
|
27
|
-
const getCombinations = (pressedKeys) => {
|
28
|
-
const result = [];
|
29
|
-
const hotkey = pressedKeys.join(COMBINATION_DELIMETER);
|
30
|
-
const id = getHotkeyId(hotkey);
|
31
|
-
const sortedKeys = id.split(COMBINATION_DELIMETER);
|
32
|
-
const f = (prefix, items) => {
|
33
|
-
items.forEach((item, i) => {
|
34
|
-
const nextId = prefix ? `${prefix}+${item}` : item;
|
35
|
-
result.push(nextId);
|
36
|
-
f(nextId, items.slice(i + 1));
|
37
|
-
});
|
38
|
-
};
|
39
|
-
f("", sortedKeys);
|
40
|
-
return result;
|
41
|
-
};
|
42
|
-
const walkPressedCombinations = (pressed, cb) => {
|
43
|
-
const pressedKeys = Object.keys(pressed);
|
44
|
-
if (!pressedKeys.length)
|
45
|
-
return;
|
46
|
-
getCombinations(pressedKeys).forEach((pressedId) => {
|
47
|
-
cb(pressedId);
|
48
|
-
});
|
49
|
-
};
|
50
27
|
const walkHotkeys = (hotkeys, cb) => {
|
51
28
|
Object.keys(hotkeys).forEach((key) => {
|
52
29
|
key.split(",").forEach((hotkey) => {
|
@@ -66,9 +43,9 @@ export class HotkeyStore {
|
|
66
43
|
if (!hotkeyData)
|
67
44
|
return;
|
68
45
|
if (!this.hotkeyMap[id]) {
|
69
|
-
this.hotkeyMap[id] =
|
46
|
+
this.hotkeyMap[id] = new Set();
|
70
47
|
}
|
71
|
-
this.hotkeyMap[id].
|
48
|
+
this.hotkeyMap[id].add({ callback: hotkeyData, ref, options });
|
72
49
|
});
|
73
50
|
};
|
74
51
|
this.unbindHotkeys = (hotkeys) => {
|
@@ -76,23 +53,34 @@ export class HotkeyStore {
|
|
76
53
|
var _a, _b;
|
77
54
|
if (!hotkeyCallback)
|
78
55
|
return;
|
79
|
-
(_a = this.hotkeyMap[id]) === null || _a === void 0 ? void 0 : _a.
|
56
|
+
(_a = this.hotkeyMap[id]) === null || _a === void 0 ? void 0 : _a.forEach((data) => {
|
80
57
|
if (data.callback === hotkeyCallback) {
|
81
|
-
this.hotkeyMap[id].
|
58
|
+
this.hotkeyMap[id].delete(data);
|
82
59
|
}
|
83
60
|
});
|
84
|
-
if (!((_b = this.hotkeyMap[id]) === null || _b === void 0 ? void 0 : _b.
|
61
|
+
if (!((_b = this.hotkeyMap[id]) === null || _b === void 0 ? void 0 : _b.size)) {
|
85
62
|
delete this.hotkeyMap[id];
|
86
63
|
}
|
87
64
|
});
|
88
65
|
};
|
89
66
|
this.handleKeyDown = (pressedMap, e) => {
|
90
|
-
|
91
|
-
|
92
|
-
|
67
|
+
const pressedKeys = Object.keys(pressedMap);
|
68
|
+
if (!pressedKeys.length)
|
69
|
+
return;
|
70
|
+
const pressedId = getHotkeyId(pressedKeys.join(COMBINATION_DELIMETER));
|
71
|
+
const pressedFormattedKeys = pressedId.split(COMBINATION_DELIMETER);
|
72
|
+
const hotkeyData = this.hotkeyMap[pressedId];
|
73
|
+
/**
|
74
|
+
* Support for `mod` that represents both Mac and Win keyboards
|
75
|
+
*/
|
76
|
+
const hotkeyControlModData = pressedFormattedKeys.includes("control") &&
|
77
|
+
this.hotkeyMap[pressedId.replace("control", "mod")];
|
78
|
+
const hotkeyMetaModData = pressedFormattedKeys.includes("meta") && this.hotkeyMap[pressedId.replace("meta", "mod")];
|
79
|
+
[hotkeyData, hotkeyControlModData, hotkeyMetaModData].forEach((hotkeyData) => {
|
80
|
+
if (!hotkeyData)
|
93
81
|
return;
|
94
|
-
if (hotkeyData === null || hotkeyData === void 0 ? void 0 : hotkeyData.
|
95
|
-
hotkeyData.
|
82
|
+
if (hotkeyData === null || hotkeyData === void 0 ? void 0 : hotkeyData.size) {
|
83
|
+
hotkeyData.forEach((data) => {
|
96
84
|
var _a;
|
97
85
|
if (((_a = data.ref) === null || _a === void 0 ? void 0 : _a.current) &&
|
98
86
|
!(e.target === data.ref.current || data.ref.current.contains(e.target))) {
|
@@ -103,24 +91,10 @@ export class HotkeyStore {
|
|
103
91
|
resolvedEvent === null || resolvedEvent === void 0 ? void 0 : resolvedEvent.preventDefault();
|
104
92
|
}
|
105
93
|
data.callback(resolvedEvent);
|
106
|
-
/**
|
107
|
-
* While meta is pressed - other keys keyup won't trigger and
|
108
|
-
* we want to allow calling the same shortcut multiple times while meta was pressed
|
109
|
-
*/
|
110
|
-
if (!(resolvedEvent === null || resolvedEvent === void 0 ? void 0 : resolvedEvent.metaKey))
|
111
|
-
this.hotkeyMap[pressedId].used = true;
|
112
94
|
});
|
113
95
|
}
|
114
96
|
});
|
115
97
|
};
|
116
|
-
this.handleKeyUp = (e) => {
|
117
|
-
const id = getHotkeyId(e.key);
|
118
|
-
walkHotkeys(this.hotkeyMap, (hotkeyId, data) => {
|
119
|
-
const hotkeyIds = hotkeyId.split(COMBINATION_DELIMETER);
|
120
|
-
if (hotkeyIds.includes(id))
|
121
|
-
data.used = false;
|
122
|
-
});
|
123
|
-
};
|
124
98
|
}
|
125
99
|
}
|
126
100
|
const globalHotkeyStore = new HotkeyStore();
|
@@ -141,18 +115,13 @@ export const SingletonHotkeysProvider = (props) => {
|
|
141
115
|
if (!eventKey)
|
142
116
|
return;
|
143
117
|
pressedMap[eventKey] = e;
|
144
|
-
if (eventKey === "meta" || eventKey === "control") {
|
145
|
-
pressedMap.mod = e;
|
146
|
-
}
|
147
118
|
setTriggerCount(Object.keys(pressedMap).length);
|
148
119
|
// Key up won't trigger for other keys while Meta is pressed so we need to cache them
|
149
120
|
// and remove on Meta keyup
|
150
|
-
if (
|
121
|
+
if (e.metaKey)
|
151
122
|
modifiedKeys.push(...Object.keys(pressedMap));
|
152
|
-
|
153
|
-
if (pressedMap.Meta) {
|
123
|
+
if (pressedMap.Meta)
|
154
124
|
modifiedKeys.push(eventKey);
|
155
|
-
}
|
156
125
|
}, [hooksCount]);
|
157
126
|
const removePressedKey = React.useCallback((e) => {
|
158
127
|
if (hooksCount === 0)
|
@@ -168,7 +137,6 @@ export const SingletonHotkeysProvider = (props) => {
|
|
168
137
|
modifiedKeys.forEach((key) => {
|
169
138
|
if (!pressedMap[key])
|
170
139
|
return;
|
171
|
-
globalHotkeyStore.handleKeyUp(pressedMap[key]);
|
172
140
|
delete pressedMap[key];
|
173
141
|
});
|
174
142
|
modifiedKeys = [];
|
@@ -200,7 +168,6 @@ export const SingletonHotkeysProvider = (props) => {
|
|
200
168
|
if (!e.key)
|
201
169
|
return;
|
202
170
|
removePressedKey(e);
|
203
|
-
globalHotkeyStore.handleKeyUp(e);
|
204
171
|
}, [removePressedKey]);
|
205
172
|
React.useEffect(() => {
|
206
173
|
window.addEventListener("keydown", handleWindowKeyDown);
|
package/package.json
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
{
|
2
2
|
"name": "reshaped",
|
3
3
|
"description": "Professionally crafted design system in React & Figma for building products of any scale and complexity",
|
4
|
-
"version": "2.11.
|
4
|
+
"version": "2.11.11",
|
5
5
|
"license": "MIT",
|
6
6
|
"email": "hello@reshaped.so",
|
7
7
|
"homepage": "https://reshaped.so",
|
@@ -60,7 +60,7 @@ class TrapFocus {
|
|
60
60
|
* and create a chain item
|
61
61
|
*/
|
62
62
|
this.trap = (options = {}) => {
|
63
|
-
const { mode = "dialog", includeTrigger } = options;
|
63
|
+
const { mode = "dialog", includeTrigger, initialFocusEl } = options;
|
64
64
|
const trigger = getActiveElement();
|
65
65
|
const focusable = getFocusableElements(this.root, {
|
66
66
|
additionalElement: includeTrigger ? trigger : undefined,
|
@@ -84,14 +84,14 @@ class TrapFocus {
|
|
84
84
|
if (mode === "dialog")
|
85
85
|
this.screenReaderTrap.trap();
|
86
86
|
this.mutationObserver.observe(this.root, { childList: true, subtree: true });
|
87
|
-
if (!focusable.length)
|
87
|
+
if (!focusable.length && !initialFocusEl)
|
88
88
|
return;
|
89
89
|
this.addListeners();
|
90
90
|
// Don't add back to the chain if we're traversing back
|
91
91
|
const tailItem = TrapFocus.chain.tailId && TrapFocus.chain.get(TrapFocus.chain.tailId);
|
92
92
|
if (!tailItem || this.root !== tailItem.data.root) {
|
93
93
|
this.chainId = TrapFocus.chain.add(this);
|
94
|
-
focusElement(focusable[0], { pseudoFocus });
|
94
|
+
focusElement(initialFocusEl || focusable[0], { pseudoFocus });
|
95
95
|
}
|
96
96
|
this.trapped = true;
|
97
97
|
};
|
@@ -1,27 +0,0 @@
|
|
1
|
-
import React from "react";
|
2
|
-
/**
|
3
|
-
* Typings
|
4
|
-
*/
|
5
|
-
export type FlyoutPosition = "bottom" | "bottom-start" | "bottom-end" | "top" | "top-start" | "top-end" | "start" | "start-top" | "start-bottom" | "end" | "end-top" | "end-bottom";
|
6
|
-
export type FlyoutWidth = "trigger" | string;
|
7
|
-
type ElementRef = React.RefObject<HTMLElement>;
|
8
|
-
type PassedFlyoutOptions = {
|
9
|
-
width?: FlyoutWidth;
|
10
|
-
position?: FlyoutPosition;
|
11
|
-
defaultActive?: boolean;
|
12
|
-
forcePosition?: boolean;
|
13
|
-
};
|
14
|
-
type FlyoutStyles = React.CSSProperties;
|
15
|
-
type FlyoutState = {
|
16
|
-
styles: FlyoutStyles;
|
17
|
-
position?: FlyoutPosition;
|
18
|
-
status: "idle" | "rendered" | "positioned" | "visible" | "hidden";
|
19
|
-
};
|
20
|
-
type UseFlyout = (originRef: ElementRef, targetRef: ElementRef, options: PassedFlyoutOptions) => Pick<FlyoutState, "styles" | "position" | "status"> & {
|
21
|
-
updatePosition: () => void;
|
22
|
-
render: () => void;
|
23
|
-
hide: () => void;
|
24
|
-
remove: () => void;
|
25
|
-
};
|
26
|
-
declare const useFlyout: UseFlyout;
|
27
|
-
export default useFlyout;
|