reshaped 3.2.0-canary.4 → 3.2.0-canary.6

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 (31) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/bundle.css +1 -1
  3. package/dist/bundle.js +11 -11
  4. package/dist/components/Actionable/Actionable.js +2 -2
  5. package/dist/components/Actionable/Actionable.module.css +1 -1
  6. package/dist/components/Actionable/Actionable.types.d.ts +1 -0
  7. package/dist/components/Autocomplete/Autocomplete.js +10 -4
  8. package/dist/components/Dismissible/Dismissible.module.css +1 -1
  9. package/dist/components/Overlay/tests/Overlay.stories.js +1 -1
  10. package/dist/components/Select/Select.js +1 -1
  11. package/dist/components/Table/Table.js +6 -4
  12. package/dist/components/Table/Table.types.d.ts +6 -1
  13. package/dist/components/Tooltip/Tooltip.js +1 -1
  14. package/dist/components/_private/Expandable/Expandable.js +9 -5
  15. package/dist/components/_private/Flyout/Flyout.types.d.ts +1 -0
  16. package/dist/components/_private/Flyout/FlyoutControlled.js +10 -6
  17. package/dist/components/_private/Flyout/useFlyout.js +2 -3
  18. package/dist/components/_private/Flyout/utilities/cooldown.d.ts +1 -1
  19. package/dist/components/_private/Flyout/utilities/cooldown.js +17 -5
  20. package/dist/hooks/_private/useOnClickOutside.js +3 -2
  21. package/dist/hooks/_private/useSingletonHotkeys.js +16 -13
  22. package/dist/hooks/tests/useHotkeys.stories.js +6 -0
  23. package/dist/tests/ShadowDOM.stories.d.ts +6 -0
  24. package/dist/tests/ShadowDOM.stories.js +110 -0
  25. package/dist/themes/_generator/tests/themes.stories.js +1 -1
  26. package/dist/utilities/a11y/TrapFocus.js +14 -5
  27. package/dist/utilities/a11y/focus.d.ts +1 -1
  28. package/dist/utilities/a11y/focus.js +10 -5
  29. package/dist/utilities/dom.d.ts +2 -1
  30. package/dist/utilities/dom.js +12 -2
  31. package/package.json +2 -1
@@ -5,8 +5,8 @@ import { classNames } from "../../utilities/helpers.js";
5
5
  import * as keys from "../../constants/keys.js";
6
6
  import s from "./Actionable.module.css";
7
7
  const Actionable = forwardRef((props, ref) => {
8
- const { children, href, onClick, type, disabled, insetFocus, borderRadius, as, fullWidth, className, attributes, } = props;
9
- const rootClassNames = classNames(s.root, className, disabled && s["--disabled"], borderRadius && s[`--radius-${borderRadius}`], insetFocus && s["--inset"], fullWidth && s["--full-width"]);
8
+ const { children, href, onClick, type, disabled, insetFocus, disableFocusRing, borderRadius, as, fullWidth, className, attributes, } = props;
9
+ const rootClassNames = classNames(s.root, className, disabled && s["--disabled"], borderRadius && s[`--radius-${borderRadius}`], insetFocus && s["--inset"], disableFocusRing && s["--disabled-focus-ring"], fullWidth && s["--full-width"]);
10
10
  const rootAttributes = { ...attributes };
11
11
  const hasClickHandler = onClick || attributes?.onClick;
12
12
  const hasFocusHandler = attributes?.onFocus || attributes?.onBlur;
@@ -1 +1 @@
1
- @layer rs.reset{.root{background:none;border:0;color:inherit;display:inline-block;font-size:inherit;font-weight:inherit;line-height:inherit;margin:0;padding:0;position:relative;text-align:initial;text-decoration:none;vertical-align:top;-webkit-tap-highlight-color:transparent}}.root:focus{outline:none}.root:focus-visible{box-shadow:var(--rs-focus-shadow);outline:none;z-index:var(--rs-z-index-raised)}.root.--inset:focus-visible{box-shadow:var(--rs-focus-inset-shadow)}.root.--radius-inherit:focus-visible{box-shadow:none}.root.--radius-inherit:focus-visible>*{box-shadow:var(--rs-focus-shadow)}.root.--radius-inherit.--inset:focus-visible>*{box-shadow:var(--rs-focus-inset-shadow)}[role=button].root,button.root{cursor:pointer;user-select:none}.root.--disabled,.root[disabled]{cursor:not-allowed}.root.--disabled:active,.root[disabled]:active{transform:none}.root.--full-width{width:100%}
1
+ @layer rs.reset{.root{background:none;border:0;color:inherit;display:inline-block;font-size:inherit;font-weight:inherit;line-height:inherit;margin:0;padding:0;position:relative;text-align:initial;text-decoration:none;vertical-align:top;-webkit-tap-highlight-color:transparent}}.root:focus{outline:none}.root:focus-visible{box-shadow:var(--rs-focus-shadow);outline:none;z-index:var(--rs-z-index-raised)}.root.--inset:focus-visible{box-shadow:var(--rs-focus-inset-shadow)}.root.--disabled-focus-ring:focus-visible{box-shadow:none}.root.--radius-inherit:focus-visible{box-shadow:none}.root.--radius-inherit:focus-visible>*{box-shadow:var(--rs-focus-shadow)}.root.--radius-inherit.--inset:focus-visible>*{box-shadow:var(--rs-focus-inset-shadow)}[role=button].root,button.root{cursor:pointer;user-select:none}.root.--disabled,.root[disabled]{cursor:not-allowed}.root.--disabled:active,.root[disabled]:active{transform:none}.root.--full-width{width:100%}
@@ -8,6 +8,7 @@ export type Props = {
8
8
  disabled?: boolean;
9
9
  fullWidth?: boolean;
10
10
  insetFocus?: boolean;
11
+ disableFocusRing?: boolean;
11
12
  borderRadius?: "inherit";
12
13
  as?: keyof JSX.IntrinsicElements;
13
14
  className?: G.ClassName;
@@ -19,7 +19,11 @@ const Autocomplete = (props) => {
19
19
  const [active, setActive] = React.useState(false);
20
20
  const hasChildren = !!React.Children.toArray(children).filter(Boolean).length;
21
21
  const lockedRef = React.useRef(false);
22
- const handleOpen = React.useCallback(() => setActive(true), []);
22
+ const handleOpen = React.useCallback(() => {
23
+ if (lockedRef.current)
24
+ return;
25
+ setActive(true);
26
+ }, []);
23
27
  const handleClose = () => setActive(false);
24
28
  useHotkeys({
25
29
  [keys.BACKSPACE]: () => onBackspaceRef.current?.(),
@@ -28,9 +32,11 @@ const Autocomplete = (props) => {
28
32
  disabled: !onBackspaceRef.current,
29
33
  });
30
34
  useHotkeys({
31
- [keys.DOWN]: () => handleOpen(),
35
+ [keys.DOWN]: () => {
36
+ handleOpen();
37
+ },
32
38
  [keys.ENTER]: () => {
33
- const el = getActiveElement();
39
+ const el = getActiveElement(inputRef.current);
34
40
  el?.click();
35
41
  },
36
42
  }, [handleOpen], { ref: inputRef, preventDefault: true });
@@ -50,7 +56,7 @@ const Autocomplete = (props) => {
50
56
  onInput?.({ value: e.currentTarget.value, name, event: e });
51
57
  textFieldProps.inputAttributes?.onInput?.(e);
52
58
  };
53
- return (_jsx(AutocompleteContext.Provider, { value: { onItemClick: handleItemClick }, children: _jsxs(DropdownMenu, { position: "bottom", width: "trigger", triggerType: "focus", trapFocusMode: "selection-menu", active: !lockedRef.current && hasChildren && active, onClose: handleClose, onOpen: handleOpen, containerRef: containerRef, disableHideAnimation: true, instanceRef: instanceRef, children: [_jsx(DropdownMenu.Trigger, { children: ({ ref, ...attributes }) => (_jsx(TextField, { ...textFieldProps, name: name, onChange: handleChange, focused: hasChildren && active,
59
+ return (_jsx(AutocompleteContext.Provider, { value: { onItemClick: handleItemClick }, children: _jsxs(DropdownMenu, { position: "bottom", width: "trigger", triggerType: "focus", trapFocusMode: "selection-menu", active: hasChildren && active, onClose: handleClose, onOpen: handleOpen, containerRef: containerRef, disableHideAnimation: true, instanceRef: instanceRef, children: [_jsx(DropdownMenu.Trigger, { children: ({ ref, ...attributes }) => (_jsx(TextField, { ...textFieldProps, name: name, onChange: handleChange, focused: hasChildren && active,
54
60
  // Ignoring the type check since TS can't infer the correct html element type
55
61
  attributes: { ...textFieldProps.attributes, ref }, inputAttributes: {
56
62
  ...textFieldProps.inputAttributes,
@@ -1 +1 @@
1
- .root{padding-inline-end:var(--rs-unit-x7);position:relative}.close{inset-block-start:0;inset-inline-end:0;position:absolute;z-index:5}.--hide-close,.--variant-media{padding:0}.--variant-media .close{inset-block-start:var(--rs-unit-x2);inset-inline-end:var(--rs-unit-x2)}.--align-center .close{inset-block-start:50%;transform:translateY(-50%)}
1
+ .root{min-height:var(--rs-unit-x5);padding-inline-end:var(--rs-unit-x7);position:relative}.close{inset-block-start:0;inset-inline-end:0;position:absolute;z-index:5}.--hide-close,.--variant-media{padding:0}.--variant-media .close{inset-block-start:var(--rs-unit-x2);inset-inline-end:var(--rs-unit-x2)}.--align-center .close{inset-block-start:50%;transform:translateY(-50%)}
@@ -9,7 +9,7 @@ export default {
9
9
  component: Overlay,
10
10
  parameters: {
11
11
  iframe: {
12
- url: "https://reshaped.so/docs/components/overlay",
12
+ url: "https://reshaped.so/docs/utilities/overlay",
13
13
  },
14
14
  },
15
15
  };
@@ -41,7 +41,7 @@ const Select = (props) => {
41
41
  return 6;
42
42
  return 4;
43
43
  }), svg: icon })) : (startSlot) }));
44
- return (_jsxs("div", { ...attributes, className: rootClassName, children: [options ? (_jsxs(_Fragment, { children: [startContent, _jsxs("select", { ...inputAttributes, onFocus: (onFocus || inputAttributes?.onFocus), onBlur: (onBlur || inputAttributes?.onBlur), className: s.input, disabled: disabled, name: name, value: value, defaultValue: defaultValue, onChange: handleChange, id: inputId, children: [placeholder && _jsx("option", { value: "", children: placeholder }), options.map((option) => (_jsx("option", { value: option.value, disabled: option.disabled, children: option.label }, option.value)))] })] })) : (_jsxs(_Fragment, { children: [_jsxs(Actionable, { className: s.input, disabled: disabled, onClick: onClick, attributes: {
44
+ return (_jsxs("div", { ...attributes, className: rootClassName, children: [options ? (_jsxs(_Fragment, { children: [startContent, _jsxs("select", { ...inputAttributes, onFocus: (onFocus || inputAttributes?.onFocus), onBlur: (onBlur || inputAttributes?.onBlur), className: s.input, disabled: disabled, name: name, value: value, defaultValue: defaultValue, onChange: handleChange, id: inputId, children: [placeholder && _jsx("option", { value: "", children: placeholder }), options.map((option) => (_jsx("option", { value: option.value, disabled: option.disabled, children: option.label }, option.value)))] })] })) : (_jsxs(_Fragment, { children: [_jsxs(Actionable, { className: s.input, disabled: disabled, disableFocusRing: true, onClick: onClick, attributes: {
45
45
  ...inputAttributes,
46
46
  onFocus: onFocus || inputAttributes?.onFocus,
47
47
  onBlur: onBlur || inputAttributes?.onBlur,
@@ -5,11 +5,11 @@ import getWidthStyles from "../../styles/width/index.js";
5
5
  import getMinWidthStyles from "../../styles/minWidth/index.js";
6
6
  import s from "./Table.module.css";
7
7
  const TableCellPrivate = (props) => {
8
- const { minWidth, rowSpan, colSpan, align, verticalAlign, tagName: TagName, padding, paddingInline, paddingBlock, children, attributes, } = props;
8
+ const { minWidth, rowSpan, colSpan, align, verticalAlign, tagName: TagName, padding, paddingInline, paddingBlock, children, className, attributes, } = props;
9
9
  const width = props.width === "auto" ? "0px" : props.width;
10
10
  const widthStyles = getWidthStyles(width);
11
11
  const minWidthStyles = getMinWidthStyles(minWidth || width);
12
- const headingClassNames = classNames(s.cell, widthStyles?.classNames, minWidthStyles?.classNames, (width === 0 || width === "0px") && s["cell--width-auto"], align && s[`cell--align-${align}`], verticalAlign && s[`cell--valign-${verticalAlign}`]);
12
+ const headingClassNames = classNames(s.cell, widthStyles?.classNames, minWidthStyles?.classNames, (width === 0 || width === "0px") && s["cell--width-auto"], align && s[`cell--align-${align}`], verticalAlign && s[`cell--valign-${verticalAlign}`], className);
13
13
  const headingStyle = {
14
14
  ...widthStyles?.variables,
15
15
  ...minWidthStyles?.variables,
@@ -30,10 +30,12 @@ const TableRow = (props) => {
30
30
  return (_jsx("tr", { ...attributes, className: rowClassNames, children: children }));
31
31
  };
32
32
  const TableBody = (props) => {
33
- return _jsx("tbody", { children: props.children });
33
+ const { children, attributes, className } = props;
34
+ return (_jsx("tbody", { ...attributes, className: classNames(className), children: children }));
34
35
  };
35
36
  const TableHead = (props) => {
36
- return _jsx("thead", { children: props.children });
37
+ const { children, attributes, className } = props;
38
+ return (_jsx("thead", { ...attributes, className: classNames(className), children: children }));
37
39
  };
38
40
  const Table = (props) => {
39
41
  const { children, border, columnBorder, className, attributes } = props;
@@ -23,6 +23,7 @@ export type CellProps = {
23
23
  width?: "auto" | string | number;
24
24
  minWidth?: string | number;
25
25
  children?: React.ReactNode;
26
+ className?: G.ClassName;
26
27
  attributes?: G.Attributes<"td">;
27
28
  };
28
29
  export type HeadingProps = CellProps & {
@@ -30,10 +31,14 @@ export type HeadingProps = CellProps & {
30
31
  };
31
32
  export type BodyProps = {
32
33
  children: React.ReactNode;
34
+ className?: G.ClassName;
35
+ attributes?: G.Attributes<"tbody">;
33
36
  };
34
37
  export type HeadProps = {
35
38
  children: React.ReactNode;
39
+ className?: G.ClassName;
40
+ attributes?: G.Attributes<"thead">;
36
41
  };
37
- export type PrivateCellProps = HeadingProps & {
42
+ export type PrivateCellProps = CellProps & {
38
43
  tagName: "td" | "th";
39
44
  };
@@ -8,6 +8,6 @@ const Tooltip = (props) => {
8
8
  const { id, text, children, onOpen, onClose, position = "bottom", containerRef, active, disabled, disableContentHover, } = props;
9
9
  if (!text)
10
10
  return _jsx(_Fragment, { children: children({}) });
11
- return (_jsxs(Flyout, { id: id, active: active, position: position, disabled: disabled, onOpen: onOpen, onClose: onClose, disableContentHover: disableContentHover, containerRef: containerRef, triggerType: "hover", children: [_jsx(Flyout.Trigger, { children: children }), _jsx(Flyout.Content, { children: _jsx(Theme, { colorMode: "inverted", children: _jsx(Text, { variant: "caption-1", className: s.root, children: text }) }) })] }));
11
+ return (_jsxs(Flyout, { id: id, active: active, position: position, disabled: disabled, onOpen: onOpen, onClose: onClose, disableContentHover: disableContentHover, containerRef: containerRef, triggerType: "hover", groupTimeouts: true, children: [_jsx(Flyout.Trigger, { children: children }), _jsx(Flyout.Content, { children: _jsx(Theme, { colorMode: "inverted", children: _jsx(Text, { variant: "caption-1", className: s.root, children: text }) }) })] }));
12
12
  };
13
13
  export default Tooltip;
@@ -26,16 +26,20 @@ const Expandable = (props) => {
26
26
  const rootEl = rootRef.current;
27
27
  if (!rootEl || !mountedRef.current)
28
28
  return;
29
- let targetHeight = 0;
30
29
  if (active) {
31
30
  rootEl.style.height = "auto";
32
- targetHeight = rootEl.clientHeight;
33
- rootEl.style.height = "0";
31
+ requestAnimationFrame(() => {
32
+ const targetHeight = rootEl.clientHeight;
33
+ rootEl.style.height = "0";
34
+ setAnimatedHeight(targetHeight);
35
+ });
34
36
  }
35
- if (!active) {
37
+ else {
36
38
  rootEl.style.height = `${rootEl.clientHeight}px`;
39
+ requestAnimationFrame(() => {
40
+ setAnimatedHeight(0);
41
+ });
37
42
  }
38
- setAnimatedHeight(targetHeight);
39
43
  }, [active]);
40
44
  return (_jsx("div", { ...attributes, className: contentClassNames, ref: rootRef, style: animatedHeight !== null
41
45
  ? { height: animatedHeight, overflow: animatedHeight === "auto" ? "visible" : undefined }
@@ -64,6 +64,7 @@ export type TriggerAttributes = {
64
64
  type BaseProps = {
65
65
  id?: string;
66
66
  triggerType?: "hover" | "click" | "focus";
67
+ groupTimeouts?: boolean;
67
68
  position?: Position;
68
69
  forcePosition?: boolean;
69
70
  trapFocusMode?: TrapMode;
@@ -16,7 +16,7 @@ import cooldown from "./utilities/cooldown.js";
16
16
  import { Provider, useFlyoutTriggerContext, useFlyoutContext, useFlyoutContentContext, } from "./Flyout.context.js";
17
17
  import useHandlerRef from "../../../hooks/useHandlerRef.js";
18
18
  const FlyoutRoot = (props) => {
19
- const { triggerType = "click", onOpen, onClose, children, disabled, forcePosition, trapFocusMode, width, disableHideAnimation, disableContentHover, disableCloseOnOutsideClick, contentGap, contentClassName, contentAttributes, position: passedPosition, active: passedActive, id: passedId, instanceRef, containerRef, } = props;
19
+ const { triggerType = "click", groupTimeouts, onOpen, onClose, children, disabled, forcePosition, trapFocusMode, width, disableHideAnimation, disableContentHover, disableCloseOnOutsideClick, contentGap, contentClassName, contentAttributes, position: passedPosition, active: passedActive, id: passedId, instanceRef, containerRef, } = props;
20
20
  const onOpenRef = useHandlerRef(onOpen);
21
21
  const onCloseRef = useHandlerRef(onClose);
22
22
  const resolvedActive = disabled === true ? false : passedActive;
@@ -118,11 +118,15 @@ const FlyoutRoot = (props) => {
118
118
  hoverTriggeredWithTouchEventRef.current = false;
119
119
  }
120
120
  else {
121
- timerRef.current = setTimeout(handleOpen, cooldown.timer || isSubmenu ? timeouts.mouseEnterShort : timeouts.mouseEnter);
122
- if (!isSubmenu && triggerType === "hover")
121
+ if (groupTimeouts)
123
122
  cooldown.warm();
123
+ timerRef.current = setTimeout(() => {
124
+ handleOpen();
125
+ }, groupTimeouts && cooldown.status === "warming"
126
+ ? timeouts.mouseEnter
127
+ : timeouts.mouseEnterShort);
124
128
  }
125
- }, [clearTimer, timerRef, handleOpen, isSubmenu, triggerType]);
129
+ }, [clearTimer, timerRef, handleOpen, groupTimeouts]);
126
130
  const handleMouseLeave = React.useCallback(() => {
127
131
  cooldown.cool();
128
132
  clearTimer();
@@ -180,14 +184,14 @@ const FlyoutRoot = (props) => {
180
184
  if (checkTransitions() &&
181
185
  !disableHideAnimation &&
182
186
  transitionStartedRef.current &&
183
- (cooldown.status !== "warm" || triggerType !== "hover")) {
187
+ (cooldown.status === "cooling" || !groupTimeouts)) {
184
188
  hide();
185
189
  }
186
190
  else {
187
191
  // In case transitions are disabled globally - remove from the DOM immediately
188
192
  remove();
189
193
  }
190
- }, [resolvedActive, render, hide, remove, disableHideAnimation, disabled]);
194
+ }, [resolvedActive, render, hide, remove, disableHideAnimation, disabled, groupTimeouts]);
191
195
  React.useEffect(() => {
192
196
  // Wait after positioning before show is triggered to animate flyout from the right side
193
197
  if (status === "positioned")
@@ -1,6 +1,6 @@
1
1
  import React from "react";
2
2
  import useRTL from "../../../hooks/useRTL.js";
3
- import { getClosestFlyoutTarget } from "../../../utilities/dom.js";
3
+ import { getClosestFlyoutTarget, getShadowRoot } from "../../../utilities/dom.js";
4
4
  import calculatePosition from "./utilities/calculatePosition.js";
5
5
  const topPos = ["top-start", "top", "top-end"];
6
6
  const bottomPos = ["bottom-start", "bottom", "bottom-end"];
@@ -78,8 +78,7 @@ const flyout = (args) => {
78
78
  targetClone.style.width = width;
79
79
  }
80
80
  }
81
- const rootNode = triggerEl?.getRootNode();
82
- const shadowRoot = rootNode instanceof ShadowRoot ? rootNode : null;
81
+ const shadowRoot = getShadowRoot(triggerEl);
83
82
  // Insert inside shadow root if possible to make sure styles are applied correctly
84
83
  (shadowRoot || document.body).appendChild(targetClone);
85
84
  const flyoutBounds = targetClone.getBoundingClientRect();
@@ -1,5 +1,5 @@
1
1
  declare class Cooldown {
2
- status: "cold" | "warm" | "cooling";
2
+ status: "warming" | "warm" | "cooling" | "cold";
3
3
  timer?: ReturnType<typeof setTimeout>;
4
4
  warm: () => void;
5
5
  cool: () => void;
@@ -1,18 +1,30 @@
1
+ import * as timeouts from "../Flyout.constants.js";
1
2
  class Cooldown {
2
3
  status = "cold";
3
4
  timer;
4
5
  warm = () => {
5
6
  clearTimeout(this.timer);
6
- this.status = "warm";
7
+ if (this.status === "cooling") {
8
+ this.status = "warm";
9
+ return;
10
+ }
11
+ this.status = "warming";
12
+ this.timer = setTimeout(() => {
13
+ this.status = "warm";
14
+ this.timer = undefined;
15
+ }, timeouts.mouseEnterShort);
7
16
  };
8
17
  cool = () => {
18
+ clearTimeout(this.timer);
19
+ if (this.status === "warming") {
20
+ this.status = "cold";
21
+ return;
22
+ }
9
23
  this.status = "cooling";
10
- const currentTimer = setTimeout(() => {
24
+ this.timer = setTimeout(() => {
11
25
  this.status = "cold";
12
- if (currentTimer === this.timer)
13
- this.timer = undefined;
26
+ this.timer = undefined;
14
27
  }, 500);
15
- this.timer = currentTimer;
16
28
  };
17
29
  }
18
30
  export default new Cooldown();
@@ -7,10 +7,11 @@ const useOnClickOutside = (refs, handler) => {
7
7
  return;
8
8
  const handleClick = (event) => {
9
9
  let isInside = false;
10
+ const clickedEl = event.composedPath()[0];
10
11
  refs.forEach((elRef) => {
11
12
  if (!elRef.current ||
12
- elRef.current === event.target ||
13
- elRef.current.contains(event.target)) {
13
+ elRef.current === clickedEl ||
14
+ elRef.current.contains(clickedEl)) {
14
15
  isInside = true;
15
16
  }
16
17
  });
@@ -19,7 +19,7 @@ const getEventKey = (e) => {
19
19
  if (!e.key)
20
20
  return;
21
21
  // Having alt pressed modifies e.key value, so relying on e.code for it
22
- if (e.altKey && e.key !== "Alt") {
22
+ if (e.altKey && /^[Key|Digit|Numpad]/.test(e.code)) {
23
23
  return e.code.toLowerCase().replace(/key|digit|numpad/, "");
24
24
  }
25
25
  return e.key.toLowerCase();
@@ -70,17 +70,20 @@ export class HotkeyStore {
70
70
  const hotkeyData = this.hotkeyMap[pressedId];
71
71
  /**
72
72
  * Support for `mod` that represents both Mac and Win keyboards
73
+ * We create the hotkeyId again to sort the mod key correctly
73
74
  */
74
- const hotkeyControlModData = pressedFormattedKeys.includes("control") &&
75
- this.hotkeyMap[pressedId.replace("control", "mod")];
76
- const hotkeyMetaModData = pressedFormattedKeys.includes("meta") && this.hotkeyMap[pressedId.replace("meta", "mod")];
75
+ const controlToModPressedId = getHotkeyId(pressedId.replace("control", "mod"));
76
+ const metaToModPressedId = getHotkeyId(pressedId.replace("meta", "mod"));
77
+ const hotkeyControlModData = pressedFormattedKeys.includes("control") && this.hotkeyMap[controlToModPressedId];
78
+ const hotkeyMetaModData = pressedFormattedKeys.includes("meta") && this.hotkeyMap[metaToModPressedId];
77
79
  [hotkeyData, hotkeyControlModData, hotkeyMetaModData].forEach((hotkeyData) => {
78
80
  if (!hotkeyData)
79
81
  return;
80
82
  if (hotkeyData?.size) {
81
83
  hotkeyData.forEach((data) => {
84
+ const eventTarget = e.composedPath()[0];
82
85
  if (data.ref?.current &&
83
- !(e.target === data.ref.current || data.ref.current.contains(e.target))) {
86
+ !(eventTarget === data.ref.current || data.ref.current.contains(eventTarget))) {
84
87
  return;
85
88
  }
86
89
  const resolvedEvent = pressedMap[pressedId];
@@ -146,14 +149,6 @@ export const SingletonHotkeysProvider = (props) => {
146
149
  return false;
147
150
  return true;
148
151
  };
149
- const addHotkeys = React.useCallback((hotkeys, ref, options = {}) => {
150
- setHooksCount((prev) => prev + 1);
151
- globalHotkeyStore.bindHotkeys(hotkeys, ref, options);
152
- return () => {
153
- setHooksCount((prev) => prev - 1);
154
- globalHotkeyStore.unbindHotkeys(hotkeys);
155
- };
156
- }, []);
157
152
  const handleWindowKeyDown = React.useCallback((e) => {
158
153
  // Browsers trigger keyboard event without passing e.key when you click on autocomplete
159
154
  if (!e.key)
@@ -166,6 +161,14 @@ export const SingletonHotkeysProvider = (props) => {
166
161
  return;
167
162
  removePressedKey(e);
168
163
  }, [removePressedKey]);
164
+ const addHotkeys = React.useCallback((hotkeys, ref, options = {}) => {
165
+ setHooksCount((prev) => prev + 1);
166
+ globalHotkeyStore.bindHotkeys(hotkeys, ref, options);
167
+ return () => {
168
+ setHooksCount((prev) => prev - 1);
169
+ globalHotkeyStore.unbindHotkeys(hotkeys);
170
+ };
171
+ }, []);
169
172
  React.useEffect(() => {
170
173
  window.addEventListener("keydown", handleWindowKeyDown);
171
174
  window.addEventListener("keyup", handleWindowKeyUp);
@@ -6,10 +6,16 @@ function Example() {
6
6
  "shift + b + n": () => console.log("pressed"),
7
7
  "c + v": () => console.log(111),
8
8
  "Meta + v": () => console.log(222),
9
+ "control + enter": () => console.log("control + enter"),
10
+ "meta + enter": () => console.log("meta + enter"),
11
+ "mod + enter": () => console.log("mod + enter"),
9
12
  "mod + ArrowRight": () => console.log("right"),
10
13
  "mod + ArrowUp": () => console.log("top"),
11
14
  "shift + ArrowRight": () => console.log("right"),
12
15
  "shift + ArrowUp": () => console.log("top"),
16
+ "alt+shift+n": () => console.log("alt+shift+n"),
17
+ "shift+alt+n": () => console.log("shift+alt+n"),
18
+ "alt+shiftLeft+n": () => console.log("alt+shiftLeft+n"),
13
19
  });
14
20
  const active = checkHotkeyState("shift + b + n");
15
21
  const shiftActive = checkHotkeyState("shift");
@@ -0,0 +1,6 @@
1
+ import React from "react";
2
+ declare const _default: {
3
+ title: string;
4
+ };
5
+ export default _default;
6
+ export declare const behavior: () => React.JSX.Element;
@@ -0,0 +1,110 @@
1
+ import React, { useEffect, useState, useRef, forwardRef } from "react";
2
+ import root from "react-shadow";
3
+ import { Example } from "../utilities/storybook/index.js";
4
+ import Autocomplete from "../components/Autocomplete/index.js";
5
+ import View from "../components/View/index.js";
6
+ import DropdownMenu from "../components/DropdownMenu/index.js";
7
+ import Reshaped from "../components/Reshaped/index.js";
8
+ import Select from "../components/Select/index.js";
9
+ import Button from "../components/Button/index.js";
10
+ import Tooltip from "../components/Tooltip/index.js";
11
+ export default {
12
+ title: "Meta/ShadowDOM",
13
+ };
14
+ const getStylesData = () => {
15
+ const sourceStylesContainer = document.head;
16
+ return Array.from(sourceStylesContainer.children).filter((x) => x instanceof HTMLStyleElement);
17
+ };
18
+ // Create a component to render inside the Shadow DOM
19
+ const ShadowDiv = forwardRef((props, ref) => {
20
+ const shadowRef = useRef(null);
21
+ // Load styles
22
+ useEffect(() => {
23
+ if (!shadowRef?.current)
24
+ return;
25
+ // Add styles to the shadow DOM
26
+ // const shadowEl = shadowRef?.current.shadowRoot;
27
+ const shadowEl = shadowRef?.current.shadowRoot;
28
+ if (!shadowEl)
29
+ return;
30
+ const sourceStylesContainer = document.head;
31
+ const observer = new MutationObserver(getStylesData);
32
+ observer.observe(sourceStylesContainer, {
33
+ characterData: true,
34
+ childList: true,
35
+ subtree: true,
36
+ });
37
+ let styleBlock = shadowEl.getElementById("custom-outer-style");
38
+ if (!styleBlock) {
39
+ styleBlock = document.createElement("span");
40
+ styleBlock.id = "custom-outer-style";
41
+ shadowEl.appendChild(styleBlock);
42
+ }
43
+ else {
44
+ styleBlock.innerHTML = "";
45
+ }
46
+ styleBlock.append(...getStylesData() // finds all <style> tags containing your web component ID
47
+ .map((x) => x.cloneNode(true)) // copies styles into the current instance of your web component. You might need to have multiple instances, so all of them need to track the styles.
48
+ );
49
+ return () => observer.disconnect();
50
+ }, []);
51
+ return (<Reshaped>
52
+ <root.div className="quote" ref={shadowRef}>
53
+ <div ref={ref}>
54
+ {/*
55
+ Adding padding here since otherwise mouseenter won't trigger on contents
56
+ when mouse is switching from outside the shadow dom
57
+ */}
58
+ <View padding={4}>{props.children}</View>
59
+ </div>
60
+ </root.div>
61
+ </Reshaped>);
62
+ });
63
+ // Main Component
64
+ const Component = () => {
65
+ const [valueAutoShadow, setValueAutoShadow] = useState("");
66
+ const [valueDropdownShadow, setValueDropdownShadow] = useState("");
67
+ const optionsAuto = ["Pizza", "Pie", "Ice-cream"];
68
+ const optionsDropdown = ["Turtle", "Cat", "Long-necked giraffe"];
69
+ const handleChangeAutoShadow = (args) => {
70
+ console.log("Autocomlete shadow value=", args);
71
+ setValueAutoShadow(args.value);
72
+ };
73
+ const handleChangeDropdownShadow = (val) => {
74
+ console.log("Dropdown shadow value=", val);
75
+ setValueDropdownShadow(val);
76
+ };
77
+ return (<View paddingBottom={50}>
78
+ <ShadowDiv>
79
+ <View gap={4} direction="row" wrap={false}>
80
+ <Autocomplete name="fruit-shadow" placeholder="Pick your food" value={valueAutoShadow} onChange={handleChangeAutoShadow}>
81
+ {optionsAuto.map((option) => (<Autocomplete.Item key={option} value={option} onClick={() => handleChangeAutoShadow({ value: option, name: "fruit-auto" })}>
82
+ {option}
83
+ </Autocomplete.Item>))}
84
+ </Autocomplete>
85
+
86
+ <DropdownMenu>
87
+ <DropdownMenu.Trigger>
88
+ {(attributes) => (<Select placeholder="Pick your animal" name="font" inputAttributes={attributes}>
89
+ {valueDropdownShadow}
90
+ </Select>)}
91
+ </DropdownMenu.Trigger>
92
+ <DropdownMenu.Content>
93
+ {optionsDropdown.map((option) => (<DropdownMenu.Item onClick={() => handleChangeDropdownShadow(option)} key={option}>
94
+ {option}
95
+ </DropdownMenu.Item>))}
96
+ </DropdownMenu.Content>
97
+ </DropdownMenu>
98
+
99
+ <Tooltip text="Tooltip for button">
100
+ {(attributes) => <Button attributes={attributes}>Hover me</Button>}
101
+ </Tooltip>
102
+ </View>
103
+ </ShadowDiv>
104
+ </View>);
105
+ };
106
+ export const behavior = () => (<Example>
107
+ <Example.Item title="base">
108
+ <Component />
109
+ </Example.Item>
110
+ </Example>);
@@ -13,7 +13,7 @@ import Link from "../../../components/Link/index.js";
13
13
  import Text from "../../../components/Text/index.js";
14
14
  import { getThemeCSS, generateThemeColors, baseThemeDefinition } from "../../index.js";
15
15
  export default {
16
- title: "Themes",
16
+ title: "Meta/Themes",
17
17
  parameters: {
18
18
  iframe: { url: "https://reshaped.so/docs/tokens/theming/runtime-theming" },
19
19
  },
@@ -2,6 +2,7 @@ import Chain from "../Chain.js";
2
2
  import * as keys from "../../constants/keys.js";
3
3
  import TrapScreenReader from "./TrapScreenReader.js";
4
4
  import { getActiveElement, getFocusableElements, focusElement, getFocusData } from "./focus.js";
5
+ import { getShadowRoot } from "../dom.js";
5
6
  import { checkKeyboardMode } from "./keyboardMode.js";
6
7
  class TrapFocus {
7
8
  static chain = new Chain();
@@ -34,7 +35,7 @@ class TrapFocus {
34
35
  const isDown = navigationMode === "arrows" && key === keys.DOWN;
35
36
  const isPrev = (isBackTab && navigationMode === "tabs") || isUp;
36
37
  const isNext = (isNextTab && navigationMode === "tabs") || isDown;
37
- const isFocusedOnTrigger = getActiveElement() === this.trigger;
38
+ const isFocusedOnTrigger = getActiveElement(this.root) === this.trigger;
38
39
  const focusData = getFocusData({
39
40
  root: this.root,
40
41
  target: isPrev ? "prev" : "next",
@@ -61,15 +62,23 @@ class TrapFocus {
61
62
  return;
62
63
  focusElement(focusData.el, { pseudoFocus });
63
64
  };
64
- addListeners = () => document.addEventListener("keydown", this.handleKeyDown);
65
- removeListeners = () => document.removeEventListener("keydown", this.handleKeyDown);
65
+ addListeners = () => {
66
+ const shadowRoot = getShadowRoot(this.root);
67
+ const el = shadowRoot ?? document;
68
+ el.addEventListener("keydown", this.handleKeyDown);
69
+ };
70
+ removeListeners = () => {
71
+ const shadowRoot = getShadowRoot(this.root);
72
+ const el = shadowRoot ?? document;
73
+ el.removeEventListener("keydown", this.handleKeyDown);
74
+ };
66
75
  /**
67
76
  * Trap the focus, add observer and keyboard event listeners
68
77
  * and create a chain item
69
78
  */
70
79
  trap = (options = {}) => {
71
80
  const { mode = "dialog", includeTrigger, initialFocusEl } = options;
72
- const trigger = getActiveElement();
81
+ const trigger = getActiveElement(this.root);
73
82
  const focusable = getFocusableElements(this.root, {
74
83
  additionalElement: includeTrigger ? trigger : undefined,
75
84
  });
@@ -77,7 +86,7 @@ class TrapFocus {
77
86
  this.options = { ...options, pseudoFocus };
78
87
  this.trigger = trigger;
79
88
  this.mutationObserver = new MutationObserver(() => {
80
- const currentActiveElement = getActiveElement();
89
+ const currentActiveElement = getActiveElement(this.root);
81
90
  // Focus stayed inside the wrapper, no need to refocus
82
91
  if (this.root.contains(currentActiveElement))
83
92
  return;
@@ -1,6 +1,6 @@
1
1
  import type { FocusableElement } from "./types";
2
2
  export declare const focusableSelector = "a,button,input:not([type=\"hidden\"]),textarea,select,details,[tabindex]:not([tabindex=\"-1\"])";
3
- export declare const getActiveElement: () => HTMLButtonElement;
3
+ export declare const getActiveElement: (originEl?: HTMLElement | null) => HTMLButtonElement;
4
4
  export declare const focusElement: (el: FocusableElement, options?: {
5
5
  pseudoFocus?: boolean;
6
6
  }) => void;
@@ -1,11 +1,16 @@
1
+ import { getShadowRoot } from "../dom.js";
1
2
  const pseudoFocusAttribute = "data-rs-focus";
2
3
  export const focusableSelector = 'a,button,input:not([type="hidden"]),textarea,select,details,[tabindex]:not([tabindex="-1"])';
3
- export const getActiveElement = () => {
4
- const pseudoFocusedEl = document.querySelector(`[${pseudoFocusAttribute}]`);
5
- return (pseudoFocusedEl || document.activeElement);
4
+ export const getActiveElement = (originEl) => {
5
+ const shadowRoot = originEl ? getShadowRoot(originEl) : null;
6
+ const rootEl = shadowRoot ?? document;
7
+ const pseudoFocusedEl = rootEl.querySelector(`[${pseudoFocusAttribute}]`);
8
+ return (pseudoFocusedEl || rootEl.activeElement);
6
9
  };
7
10
  export const focusElement = (el, options) => {
8
- document.querySelector(`[${pseudoFocusAttribute}]`)?.removeAttribute(pseudoFocusAttribute);
11
+ const shadowRoot = getShadowRoot(el);
12
+ const rootEl = shadowRoot ?? document;
13
+ rootEl.querySelector(`[${pseudoFocusAttribute}]`)?.removeAttribute(pseudoFocusAttribute);
9
14
  if (options?.pseudoFocus) {
10
15
  el.setAttribute(pseudoFocusAttribute, "true");
11
16
  }
@@ -59,7 +64,7 @@ export const getFocusData = (args) => {
59
64
  const { root, target, options } = args;
60
65
  const focusable = getFocusableElements(root, { additionalElement: options?.additionalElement });
61
66
  const focusableLimit = focusable.length - 1;
62
- const currentElement = getActiveElement();
67
+ const currentElement = getActiveElement(root);
63
68
  const currentIndex = focusable.indexOf(currentElement);
64
69
  const positions = {
65
70
  next: currentIndex + 1,
@@ -1,5 +1,6 @@
1
- export declare const getClosestFlyoutTarget: (el: HTMLElement | null) => HTMLElement;
1
+ export declare const getClosestFlyoutTarget: (el: HTMLElement | null, iteration?: number) => HTMLElement;
2
2
  export declare const disableUserSelect: () => void;
3
3
  export declare const enableUserSelect: () => void;
4
4
  export declare const disableScroll: () => void;
5
5
  export declare const enableScroll: () => void;
6
+ export declare const getShadowRoot: (el: HTMLElement | null) => ShadowRoot | null;