@reshaped/headless 3.10.0-canary.9 → 3.10.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.
@@ -0,0 +1,4 @@
1
+ import React from "react";
2
+ import * as T from "./Accordion.types";
3
+ declare const AccordionContext: React.Context<T.ContextProps>;
4
+ export default AccordionContext;
@@ -0,0 +1,10 @@
1
+ "use client";
2
+ import React from "react";
3
+ const AccordionContext = React.createContext({
4
+ active: false,
5
+ disabled: false,
6
+ onToggle: () => { },
7
+ triggerId: "",
8
+ contentId: "",
9
+ });
10
+ export default AccordionContext;
@@ -0,0 +1,4 @@
1
+ import React from "react";
2
+ import * as T from "./Accordion.types";
3
+ declare const Accordion: React.FC<T.Props>;
4
+ export default Accordion;
@@ -0,0 +1,12 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import AccordionControlled from "./AccordionControlled.js";
4
+ import AccordionUncontrolled from "./AccordionUncontrolled.js";
5
+ const Accordion = (props) => {
6
+ const { active } = props;
7
+ if (active !== undefined)
8
+ return _jsx(AccordionControlled, { ...props });
9
+ return _jsx(AccordionUncontrolled, { ...props });
10
+ };
11
+ Accordion.displayName = "Headless.Accordion";
12
+ export default Accordion;
@@ -0,0 +1,87 @@
1
+ import type { Attributes } from "../../types/global";
2
+ import type { ClassName } from "@reshaped/utilities";
3
+ import type React from "react";
4
+ /**
5
+ * Render attributes and props
6
+ */
7
+ export type TriggerRenderProps = {
8
+ active: boolean;
9
+ disabled: boolean;
10
+ };
11
+ export type TriggerAttributes = Attributes<"button"> & {
12
+ className?: ClassName;
13
+ "aria-expanded": boolean;
14
+ "aria-controls": string;
15
+ id: string;
16
+ "data-active": string;
17
+ disabled?: boolean;
18
+ onClick: () => void;
19
+ };
20
+ export type ContentRenderProps = {
21
+ active: boolean;
22
+ };
23
+ export type ContentAttributes = Attributes<"div"> & {
24
+ "data-active": string;
25
+ "aria-labelledby": string;
26
+ id: string;
27
+ role: "region";
28
+ hidden: boolean;
29
+ };
30
+ /**
31
+ * Context
32
+ */
33
+ export type ContextProps = {
34
+ triggerId: string;
35
+ contentId: string;
36
+ active: boolean;
37
+ disabled: boolean;
38
+ onToggle?: (active: boolean) => void;
39
+ };
40
+ /**
41
+ * Props
42
+ */
43
+ export type BaseProps = {
44
+ /** Node for inserting the trigger and the content */
45
+ children?: React.ReactNode;
46
+ /** Callback when the accordion is expanded or collapsed */
47
+ onToggle?: (active: boolean) => void;
48
+ /** Disable the accordion from user interaction */
49
+ disabled?: boolean;
50
+ /** Additional classname for the root element */
51
+ className?: ClassName;
52
+ /** Additional attributes for the root element */
53
+ attributes?: Attributes<"div">;
54
+ };
55
+ export type TriggerProps = {
56
+ /** Additional classname for the root element */
57
+ className?: ClassName;
58
+ /** Additional attributes for the root element */
59
+ attributes?: Attributes<"button">;
60
+ /** Node or render function for inserting the trigger */
61
+ children?: React.ReactNode;
62
+ /** Render function for custom trigger rendering */
63
+ render?: (attributes: TriggerAttributes, props: TriggerRenderProps) => React.ReactNode;
64
+ };
65
+ export type ContentProps = {
66
+ /** Additional classname for the root element */
67
+ className?: ClassName;
68
+ /** Additional attributes for the root element */
69
+ attributes?: Attributes<"div">;
70
+ /** Node for inserting the expandable content */
71
+ children?: React.ReactNode;
72
+ /** Render function for custom content rendering */
73
+ render?: (attributes: ContentAttributes, props: ContentRenderProps) => React.ReactNode;
74
+ };
75
+ export type ControlledProps = BaseProps & {
76
+ /** Control whether the accordion is expanded or collapsed, enables controlled mode */
77
+ active: boolean;
78
+ /** Control whether the accordion is expanded or collapsed by default, enables uncontrolled mode */
79
+ defaultActive?: never;
80
+ };
81
+ export type UncontrolledProps = BaseProps & {
82
+ /** Control whether the accordion is expanded or collapsed, enables controlled mode */
83
+ active?: never;
84
+ /** Control whether the accordion is expanded or collapsed by default, enables uncontrolled mode */
85
+ defaultActive?: boolean;
86
+ };
87
+ export type Props = ControlledProps | UncontrolledProps;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,4 @@
1
+ import React from "react";
2
+ import type * as T from "./Accordion.types";
3
+ declare const AccordionContent: React.FC<T.ContentProps>;
4
+ export default AccordionContent;
@@ -0,0 +1,25 @@
1
+ "use client";
2
+ import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
3
+ import { classNames } from "@reshaped/utilities";
4
+ import React from "react";
5
+ import AccordionContext from "./Accordion.context.js";
6
+ const AccordionContent = (props) => {
7
+ const { children, render, className, attributes } = props;
8
+ const { active, triggerId, contentId } = React.useContext(AccordionContext);
9
+ const contentAttributes = {
10
+ ...attributes,
11
+ className: classNames(className),
12
+ "data-active": active ? "true" : "false",
13
+ "aria-labelledby": triggerId,
14
+ id: contentId,
15
+ role: "region",
16
+ hidden: !active,
17
+ };
18
+ const renderProps = { active };
19
+ if (render) {
20
+ return _jsx(_Fragment, { children: render(contentAttributes, renderProps) });
21
+ }
22
+ return _jsx("div", { ...contentAttributes, children: children });
23
+ };
24
+ AccordionContent.displayName = "Headless.Accordion.Content";
25
+ export default AccordionContent;
@@ -0,0 +1,4 @@
1
+ import React from "react";
2
+ import * as T from "./Accordion.types";
3
+ declare const AccordionControlled: React.FC<T.ControlledProps>;
4
+ export default AccordionControlled;
@@ -0,0 +1,21 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { classNames } from "@reshaped/utilities";
4
+ import React from "react";
5
+ import useHandlerRef from "../../hooks/useHandlerRef.js";
6
+ import AccordionContext from "./Accordion.context.js";
7
+ const AccordionControlled = (props) => {
8
+ const { children, onToggle, active, disabled = false, className, attributes } = props;
9
+ const id = React.useId();
10
+ const onToggleRef = useHandlerRef(onToggle);
11
+ const value = React.useMemo(() => ({
12
+ triggerId: `${id}-trigger`,
13
+ contentId: `${id}-content`,
14
+ active,
15
+ disabled,
16
+ onToggle: onToggleRef.current,
17
+ }), [active, disabled, id, onToggleRef]);
18
+ return (_jsx("div", { ...attributes, className: classNames(className), children: _jsx(AccordionContext.Provider, { value: value, children: children }) }));
19
+ };
20
+ AccordionControlled.displayName = "Headless.AccordionControlled";
21
+ export default AccordionControlled;
@@ -0,0 +1,4 @@
1
+ import React from "react";
2
+ import * as T from "./Accordion.types";
3
+ declare const AccordionTrigger: React.FC<T.TriggerProps>;
4
+ export default AccordionTrigger;
@@ -0,0 +1,31 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { classNames } from "@reshaped/utilities";
4
+ import React from "react";
5
+ import Actionable from "../Actionable/index.js";
6
+ import AccordionContext from "./Accordion.context.js";
7
+ const AccordionTrigger = (props) => {
8
+ const { children, render, className, attributes } = props;
9
+ const { active, onToggle, triggerId, contentId, disabled } = React.useContext(AccordionContext);
10
+ const handleClick = () => {
11
+ if (disabled)
12
+ return;
13
+ onToggle?.(!active);
14
+ };
15
+ const triggerAttributes = {
16
+ ...attributes,
17
+ className: classNames(className),
18
+ "aria-expanded": active,
19
+ "aria-controls": contentId,
20
+ id: triggerId,
21
+ "data-active": active ? "true" : "false",
22
+ disabled: disabled ? true : undefined,
23
+ onClick: handleClick,
24
+ };
25
+ const renderProps = { active, disabled };
26
+ if (render)
27
+ return render(triggerAttributes, renderProps);
28
+ return (_jsx(Actionable, { onClick: handleClick, attributes: triggerAttributes, render: render, children: children }));
29
+ };
30
+ AccordionTrigger.displayName = "Headless.Accordion.Trigger";
31
+ export default AccordionTrigger;
@@ -0,0 +1,4 @@
1
+ import React from "react";
2
+ import * as T from "./Accordion.types";
3
+ declare const AccordionUncontrolled: React.FC<T.UncontrolledProps>;
4
+ export default AccordionUncontrolled;
@@ -0,0 +1,15 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import React from "react";
4
+ import AccordionControlled from "./AccordionControlled.js";
5
+ const AccordionUncontrolled = (props) => {
6
+ const { defaultActive, onToggle, ...controlledProps } = props;
7
+ const [active, setActive] = React.useState(defaultActive || false);
8
+ const handleToggle = (active) => {
9
+ setActive(active);
10
+ onToggle?.(active);
11
+ };
12
+ return _jsx(AccordionControlled, { ...controlledProps, onToggle: handleToggle, active: active });
13
+ };
14
+ AccordionUncontrolled.displayName = "Headless.AccordionUncontrolled";
15
+ export default AccordionUncontrolled;
@@ -0,0 +1,9 @@
1
+ import Accordion from "./Accordion";
2
+ import AccordionContent from "./AccordionContent";
3
+ import AccordionTrigger from "./AccordionTrigger";
4
+ declare const AccordionRoot: typeof Accordion & {
5
+ Trigger: typeof AccordionTrigger;
6
+ Content: typeof AccordionContent;
7
+ };
8
+ export default AccordionRoot;
9
+ export type { Props as AccordionProps, TriggerProps as AccordionTriggerProps, TriggerAttributes as AccordionTriggerAttributes, ContentProps as AccordionContentProps, ContentAttributes as AccordionContentAttributes, } from "./Accordion.types";
@@ -0,0 +1,7 @@
1
+ import Accordion from "./Accordion.js";
2
+ import AccordionContent from "./AccordionContent.js";
3
+ import AccordionTrigger from "./AccordionTrigger.js";
4
+ const AccordionRoot = Accordion;
5
+ AccordionRoot.Trigger = AccordionTrigger;
6
+ AccordionRoot.Content = AccordionContent;
7
+ export default AccordionRoot;
@@ -0,0 +1,11 @@
1
+ import { StoryObj } from "@storybook/react-vite";
2
+ declare const _default: {
3
+ title: string;
4
+ parameters: {
5
+ chromatic: {
6
+ disableSnapshot: boolean;
7
+ };
8
+ };
9
+ };
10
+ export default _default;
11
+ export declare const base: StoryObj;
@@ -0,0 +1,30 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Accordion } from "../../../index.js";
3
+ export default {
4
+ title: "Headless/Components/Accordion",
5
+ parameters: {
6
+ chromatic: { disableSnapshot: true },
7
+ },
8
+ };
9
+ export const base = {
10
+ name: "base",
11
+ render: () => (_jsxs(Accordion, { attributes: {
12
+ style: {
13
+ padding: 16,
14
+ border: "1px solid rgba(255, 255, 255, 0.08)",
15
+ borderRadius: 8,
16
+ display: "flex",
17
+ flexDirection: "column",
18
+ gap: 16,
19
+ },
20
+ }, children: [_jsx(Accordion.Trigger, { attributes: {
21
+ style: {
22
+ background: "none",
23
+ border: "none",
24
+ width: "100%",
25
+ textAlign: "start",
26
+ cursor: "pointer",
27
+ padding: 0,
28
+ },
29
+ }, children: "Trigger" }), _jsx(Accordion.Content, { attributes: { style: { padding: 0, transition: "height 0.3s ease-in-out" } }, children: "Content" }), "r"] })),
30
+ };
@@ -68,5 +68,5 @@ const Actionable = forwardRef((props, ref) => {
68
68
  return render(tagAttributes);
69
69
  return _jsx(TagName, { ...tagAttributes });
70
70
  });
71
- Actionable.displayName = "Actionable";
71
+ Actionable.displayName = "Headless.Actionable";
72
72
  export default Actionable;
@@ -107,27 +107,23 @@ const globalHotkeyStore = new HotkeyStore();
107
107
  const HotkeyContext = React.createContext({});
108
108
  export const SingletonHotkeysProvider = (props) => {
109
109
  const { children } = props;
110
- // eslint-disable-next-line
111
- const [_, setTriggerCount] = React.useState(0);
112
- // Only handle key presses when there is at least one hook listening for hotkeys
113
- const [hooksCount, setHooksCount] = React.useState(0);
110
+ const hooksCountRef = React.useRef(0);
114
111
  const addPressedKey = React.useCallback((e) => {
115
- if (e.repeat || hooksCount === 0)
112
+ if (e.repeat || hooksCountRef.current === 0)
116
113
  return;
117
114
  const eventKey = getEventKey(e);
118
115
  if (!eventKey)
119
116
  return;
120
117
  pressedMap.set(eventKey, e);
121
- setTriggerCount(pressedMap.size);
122
118
  // Key up won't trigger for other keys while Meta is pressed so we need to cache them
123
119
  // and remove on Meta keyup
124
120
  if (e.metaKey)
125
121
  modifiedKeys.push(...pressedMap.keys());
126
122
  if (pressedMap.has("Meta"))
127
123
  modifiedKeys.push(eventKey);
128
- }, [hooksCount]);
124
+ }, []);
129
125
  const removePressedKey = React.useCallback((e) => {
130
- if (hooksCount === 0)
126
+ if (hooksCountRef.current === 0)
131
127
  return;
132
128
  const eventKey = getEventKey(e);
133
129
  if (!eventKey)
@@ -144,8 +140,7 @@ export const SingletonHotkeysProvider = (props) => {
144
140
  });
145
141
  modifiedKeys = [];
146
142
  }
147
- setTriggerCount(pressedMap.size);
148
- }, [hooksCount]);
143
+ }, []);
149
144
  const isPressed = (hotkey) => {
150
145
  const keys = formatHotkey(hotkey).split(COMBINATION_DELIMETER);
151
146
  if (keys.some((key) => !pressedMap.has(key)))
@@ -169,10 +164,10 @@ export const SingletonHotkeysProvider = (props) => {
169
164
  modifiedKeys = [];
170
165
  }, []);
171
166
  const addHotkeys = React.useCallback((hotkeys, ref, options = {}) => {
172
- setHooksCount((prev) => prev + 1);
167
+ hooksCountRef.current += 1;
173
168
  globalHotkeyStore.bindHotkeys(hotkeys, ref, options);
174
169
  return () => {
175
- setHooksCount((prev) => prev - 1);
170
+ hooksCountRef.current -= 1;
176
171
  globalHotkeyStore.unbindHotkeys(hotkeys);
177
172
  };
178
173
  }, []);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@reshaped/headless",
3
3
  "description": "Headless components and utilities for React",
4
- "version": "3.10.0-canary.9",
4
+ "version": "3.10.0",
5
5
  "license": "MIT",
6
6
  "homepage": "https://reshaped.so",
7
7
  "repository": {
@@ -39,7 +39,7 @@
39
39
  "react-dom": "^18 || ^19"
40
40
  },
41
41
  "dependencies": {
42
- "@reshaped/utilities": "3.10.0-canary.9"
42
+ "@reshaped/utilities": "3.10.0"
43
43
  },
44
44
  "scripts": {
45
45
  "clean": "rm -rf dist",