react-miui 0.33.0 → 0.34.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.
Files changed (207) hide show
  1. package/.claude/settings.json +12 -0
  2. package/.claude/settings.local.json +4 -1
  3. package/.storybook/preview.tsx +10 -4
  4. package/CHANGELOG.md +9 -0
  5. package/dist/components/ui/drawer/Drawer.d.ts +10 -1
  6. package/dist/components/ui/drawer/Drawer.d.ts.map +1 -1
  7. package/dist/components/ui/drawer/Drawer.js +135 -15
  8. package/dist/components/ui/drawer/Drawer.js.map +1 -1
  9. package/dist/components/ui/drawer/Drawer.styled.d.ts +86 -1
  10. package/dist/components/ui/drawer/Drawer.styled.d.ts.map +1 -1
  11. package/dist/components/ui/drawer/Drawer.styled.js +13 -1
  12. package/dist/components/ui/drawer/Drawer.styled.js.map +1 -1
  13. package/dist/components/ui/toaster/Toaster.d.ts.map +1 -1
  14. package/dist/components/ui/toaster/Toaster.js +7 -1
  15. package/dist/components/ui/toaster/Toaster.js.map +1 -1
  16. package/dist/components/ui/tooltip/Tooltip.d.ts +30 -0
  17. package/dist/components/ui/tooltip/Tooltip.d.ts.map +1 -0
  18. package/dist/components/ui/tooltip/Tooltip.js +81 -0
  19. package/dist/components/ui/tooltip/Tooltip.js.map +1 -0
  20. package/dist/components/ui/tooltip/Tooltip.styled.d.ts +173 -0
  21. package/dist/components/ui/tooltip/Tooltip.styled.d.ts.map +1 -0
  22. package/dist/components/ui/tooltip/Tooltip.styled.js +65 -0
  23. package/dist/components/ui/tooltip/Tooltip.styled.js.map +1 -0
  24. package/dist/index.d.ts +1 -0
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +1 -0
  27. package/dist/index.js.map +1 -1
  28. package/docs/assets/highlight.css +7 -0
  29. package/docs/assets/navigation.js +1 -1
  30. package/docs/assets/search.js +1 -1
  31. package/docs/classes/index.Pop.html +7 -7
  32. package/docs/documents/Test.html +2 -2
  33. package/docs/enums/index.ICON.html +2 -2
  34. package/docs/functions/index.Action.html +3 -3
  35. package/docs/functions/index.Button.html +4 -4
  36. package/docs/functions/index.Card.html +3 -3
  37. package/docs/functions/index.Checkbox.html +3 -3
  38. package/docs/functions/index.Choice.html +2 -2
  39. package/docs/functions/index.ColorPicker.html +3 -3
  40. package/docs/functions/index.CoveringLoader.html +3 -3
  41. package/docs/functions/index.DirectionPad.html +2 -2
  42. package/docs/functions/index.Drawer.html +2 -2
  43. package/docs/functions/index.EqualActions.html +2 -2
  44. package/docs/functions/index.FullLoader.html +3 -3
  45. package/docs/functions/index.Gap.html +3 -3
  46. package/docs/functions/index.HandleEsc.html +3 -3
  47. package/docs/functions/index.Header.html +3 -3
  48. package/docs/functions/index.HeaderIconAction.html +3 -3
  49. package/docs/functions/index.Icon-1.html +2 -2
  50. package/docs/functions/index.If.html +3 -3
  51. package/docs/functions/index.Input.html +1 -1
  52. package/docs/functions/index.KeyValue.html +2 -2
  53. package/docs/functions/index.Label.html +2 -2
  54. package/docs/functions/index.Line.html +4 -4
  55. package/docs/functions/index.List.html +2 -2
  56. package/docs/functions/index.Loader.html +3 -3
  57. package/docs/functions/index.Loading.html +3 -3
  58. package/docs/functions/index.Message.html +4 -4
  59. package/docs/functions/index.Modal.html +2 -2
  60. package/docs/functions/index.ModalButtons.html +3 -3
  61. package/docs/functions/index.PopLoader.html +3 -3
  62. package/docs/functions/index.PopOption.html +2 -2
  63. package/docs/functions/index.Progress.html +2 -2
  64. package/docs/functions/index.SearchContainer.html +3 -3
  65. package/docs/functions/index.Section.html +4 -4
  66. package/docs/functions/index.Select.html +3 -3
  67. package/docs/functions/index.Selector.html +2 -2
  68. package/docs/functions/index.Spacer.html +3 -3
  69. package/docs/functions/index.Stats.html +2 -2
  70. package/docs/functions/index.StickyHeader.html +4 -4
  71. package/docs/functions/index.Table.html +3 -3
  72. package/docs/functions/index.TextArea.html +2 -2
  73. package/docs/functions/index.ToasterProvider.html +3 -3
  74. package/docs/functions/index.Toggle.html +3 -3
  75. package/docs/functions/index.ToolButton.html +4 -4
  76. package/docs/functions/index.Tooltip.html +18 -0
  77. package/docs/functions/index.TooltipProvider.html +6 -0
  78. package/docs/functions/index.borderPxToRem.html +1 -1
  79. package/docs/functions/index.createTheme.html +1 -1
  80. package/docs/functions/index.css.html +1 -1
  81. package/docs/functions/index.dimensionsPxToRem.html +1 -1
  82. package/docs/functions/index.fontPxToRem.html +1 -1
  83. package/docs/functions/index.getCssText.html +1 -1
  84. package/docs/functions/index.globalCss.html +2 -2
  85. package/docs/functions/index.injectGlobalStyles.html +1 -1
  86. package/docs/functions/index.keyframes.html +1 -1
  87. package/docs/functions/index.pxToRem.html +1 -1
  88. package/docs/functions/index.styled.html +1 -1
  89. package/docs/functions/index.toast.html +2 -2
  90. package/docs/functions/index.useToaster.html +1 -1
  91. package/docs/index.html +2 -2
  92. package/docs/interfaces/index.IconProps.html +2 -2
  93. package/docs/interfaces/index.InputCustomProps.html +3 -3
  94. package/docs/interfaces/index.LoaderProps.html +6 -6
  95. package/docs/interfaces/index.StickyHeaderProps.html +4 -4
  96. package/docs/interfaces/index.ToasterProviderProps.html +3 -3
  97. package/docs/interfaces/index.TooltipProps.html +36 -0
  98. package/docs/interfaces/index.TooltipProviderProps.html +13 -0
  99. package/docs/modules/index.html +1 -1
  100. package/docs/modules.html +1 -1
  101. package/docs/types/index.ActionProps.html +1 -1
  102. package/docs/types/index.CardProps.html +1 -1
  103. package/docs/types/index.CheckboxProps.html +2 -2
  104. package/docs/types/index.ChoiceProps.html +1 -1
  105. package/docs/types/index.ColorPickerProps.html +1 -1
  106. package/docs/types/index.DirectionPadProps.html +1 -1
  107. package/docs/types/index.DrawerFrom.html +1 -0
  108. package/docs/types/index.DrawerProps.html +28 -1
  109. package/docs/types/index.EqualActionsProps.html +1 -1
  110. package/docs/types/index.HeaderProps.html +1 -1
  111. package/docs/types/index.InputProps.html +1 -1
  112. package/docs/types/index.KeyValueProps.html +1 -1
  113. package/docs/types/index.LabelProps.html +1 -1
  114. package/docs/types/index.OverwriteProps.html +1 -1
  115. package/docs/types/index.ProgressProps.html +2 -2
  116. package/docs/types/index.SelectProps.html +1 -1
  117. package/docs/types/index.SelectorProps.html +1 -1
  118. package/docs/types/index.Stat.html +1 -1
  119. package/docs/types/index.StatsProps.html +1 -1
  120. package/docs/types/index.TextAreaProps.html +1 -1
  121. package/docs/types/index.ThemeCSS.html +1 -1
  122. package/docs/types/index.ToggleProps.html +2 -2
  123. package/docs/variables/index.ActionBadgeSelector.html +1 -1
  124. package/docs/variables/index.ActionCircleSelector.html +1 -1
  125. package/docs/variables/index.CheckboxCheckmarkWrapperSelector.html +1 -1
  126. package/docs/variables/index.CheckboxTextLabelSelector.html +1 -1
  127. package/docs/variables/index.ChoiceItemSelector.html +1 -1
  128. package/docs/variables/index.ColorPickerColorDisplaySelector.html +1 -1
  129. package/docs/variables/index.DirectionPadButtonDotSelector.html +1 -1
  130. package/docs/variables/index.DirectionPadButtonSelector.html +1 -1
  131. package/docs/variables/index.DirectionPadLineSelector.html +1 -1
  132. package/docs/variables/index.DirectionPadMiddleSelector.html +1 -1
  133. package/docs/variables/index.DrawerContentSelector.html +1 -1
  134. package/docs/variables/index.HeaderAfterSelector.html +1 -1
  135. package/docs/variables/index.HeaderBeforeSelector.html +1 -1
  136. package/docs/variables/index.HeaderContentsSelector.html +1 -1
  137. package/docs/variables/index.HeaderIconActionIconSelector.html +1 -1
  138. package/docs/variables/index.InputContainerSelector.html +1 -1
  139. package/docs/variables/index.InputInputSelector.html +1 -1
  140. package/docs/variables/index.InputLabelSelector.html +1 -1
  141. package/docs/variables/index.InputPrefixSelector.html +1 -1
  142. package/docs/variables/index.InputSuffixSelector.html +1 -1
  143. package/docs/variables/index.KeyValueIconSelector.html +1 -1
  144. package/docs/variables/index.KeyValueItemSelector.html +1 -1
  145. package/docs/variables/index.KeyValueKeySelector.html +1 -1
  146. package/docs/variables/index.KeyValuePairSelector.html +1 -1
  147. package/docs/variables/index.KeyValueValueSelector.html +1 -1
  148. package/docs/variables/index.LabelTextSelector.html +1 -1
  149. package/docs/variables/index.ListItemInnerContainerClassNameSelector.html +1 -1
  150. package/docs/variables/index.ModalContainerSelector.html +1 -1
  151. package/docs/variables/index.ModalRemovePaddingSelector.html +1 -1
  152. package/docs/variables/index.ModalTitleSelector.html +1 -1
  153. package/docs/variables/index.PopListSelector.html +1 -1
  154. package/docs/variables/index.PopOptionButtonSelector.html +1 -1
  155. package/docs/variables/index.PopOptionIconSelector.html +1 -1
  156. package/docs/variables/index.PopOverlaySelector.html +1 -1
  157. package/docs/variables/index.ProgressBackgroundSelector.html +1 -1
  158. package/docs/variables/index.ProgressValueSelector.html +1 -1
  159. package/docs/variables/index.SelectorItemSelector.html +1 -1
  160. package/docs/variables/index.StatsItemSelector.html +1 -1
  161. package/docs/variables/index.StatsLabelSelector.html +1 -1
  162. package/docs/variables/index.StatsSeparatorSelector.html +1 -1
  163. package/docs/variables/index.StatsValueSelector.html +1 -1
  164. package/docs/variables/index.TextAreaLabelSelector.html +1 -1
  165. package/docs/variables/index.TextAreaTextAreaSelector.html +1 -1
  166. package/docs/variables/index.TextAreaWrapperSelector.html +1 -1
  167. package/docs/variables/index.ToggleStyledToggleSelector.html +1 -1
  168. package/docs/variables/index.TooltipContentSelector.html +1 -0
  169. package/docs/variables/index.config.html +1 -1
  170. package/docs/variables/index.cssReset.html +2 -2
  171. package/docs/variables/index.darkTheme.html +1 -1
  172. package/docs/variables/index.miuiScrollbars.html +1 -1
  173. package/docs/variables/index.theme.html +1 -1
  174. package/esm/components/ui/drawer/Drawer.d.ts +10 -1
  175. package/esm/components/ui/drawer/Drawer.d.ts.map +1 -1
  176. package/esm/components/ui/drawer/Drawer.js +139 -15
  177. package/esm/components/ui/drawer/Drawer.js.map +1 -1
  178. package/esm/components/ui/drawer/Drawer.styled.d.ts +86 -1
  179. package/esm/components/ui/drawer/Drawer.styled.d.ts.map +1 -1
  180. package/esm/components/ui/drawer/Drawer.styled.js +12 -1
  181. package/esm/components/ui/drawer/Drawer.styled.js.map +1 -1
  182. package/esm/components/ui/toaster/Toaster.d.ts.map +1 -1
  183. package/esm/components/ui/toaster/Toaster.js +8 -2
  184. package/esm/components/ui/toaster/Toaster.js.map +1 -1
  185. package/esm/components/ui/tooltip/Tooltip.d.ts +30 -0
  186. package/esm/components/ui/tooltip/Tooltip.d.ts.map +1 -0
  187. package/esm/components/ui/tooltip/Tooltip.js +43 -0
  188. package/esm/components/ui/tooltip/Tooltip.js.map +1 -0
  189. package/esm/components/ui/tooltip/Tooltip.styled.d.ts +173 -0
  190. package/esm/components/ui/tooltip/Tooltip.styled.d.ts.map +1 -0
  191. package/esm/components/ui/tooltip/Tooltip.styled.js +28 -0
  192. package/esm/components/ui/tooltip/Tooltip.styled.js.map +1 -0
  193. package/esm/index.d.ts +1 -0
  194. package/esm/index.d.ts.map +1 -1
  195. package/esm/index.js +1 -0
  196. package/esm/index.js.map +1 -1
  197. package/package.json +2 -1
  198. package/pnpm-workspace.yaml +3 -0
  199. package/src/bugfixes/ToastsFromModal.stories.tsx +59 -0
  200. package/src/components/ui/drawer/Drawer.stories.tsx +143 -59
  201. package/src/components/ui/drawer/Drawer.styled.ts +13 -0
  202. package/src/components/ui/drawer/Drawer.tsx +214 -20
  203. package/src/components/ui/toaster/Toaster.tsx +12 -2
  204. package/src/components/ui/tooltip/Tooltip.stories.tsx +285 -0
  205. package/src/components/ui/tooltip/Tooltip.styled.ts +36 -0
  206. package/src/components/ui/tooltip/Tooltip.tsx +195 -0
  207. package/src/index.ts +1 -0
@@ -1,12 +1,49 @@
1
1
  import React, { useCallback, useState } from "react";
2
2
 
3
3
  import type { Meta, StoryObj } from "@storybook/react-vite";
4
+ import type { DrawerProps } from "./Drawer";
4
5
 
5
6
  import { styled } from "../../../theme";
6
7
  import { Section } from "../../layout/section/Section";
7
8
  import { Button } from "../button/Button";
8
9
  import { Drawer } from "./Drawer";
9
10
 
11
+ type PlaygroundArgs = Omit<DrawerProps, "isOpen" | "onClose">;
12
+
13
+ const Playground = (args: PlaygroundArgs) => {
14
+ const [isOpen, setIsOpen] = useState(false);
15
+
16
+ const handleToggleOpen = useCallback(() => {
17
+ setIsOpen((prev) => !prev);
18
+ }, []);
19
+
20
+ const handleClose = useCallback(() => {
21
+ setIsOpen(false);
22
+ }, []);
23
+
24
+ return (
25
+ <div style={{ padding: "20px" }}>
26
+ <h2>Page content</h2>
27
+ <p>Open the drawer and tweak its props in the Controls panel.</p>
28
+ {Array(8).fill(0).map((_: number, i) => (
29
+ <Section key={i} vertical={true} horizontal={true}> {/* eslint-disable-line react/no-array-index-key */}
30
+ <p>Filler row {i + 1}</p>
31
+ </Section>
32
+ ))}
33
+ <Button onClick={handleToggleOpen}>
34
+ {isOpen ? "Close Drawer" : "Open Drawer"}
35
+ </Button>
36
+ <Drawer {...args} isOpen={isOpen} onClose={handleClose}>
37
+ <div style={{ padding: "20px" }}>
38
+ <h2>Drawer</h2>
39
+ <p>Adjust controls to customize behavior.</p>
40
+ <Button onClick={handleClose}>Close</Button>
41
+ </div>
42
+ </Drawer>
43
+ </div>
44
+ );
45
+ };
46
+
10
47
  const meta: Meta<typeof Drawer> = {
11
48
  title: "UI/Drawer",
12
49
  component: Drawer,
@@ -14,72 +51,111 @@ const meta: Meta<typeof Drawer> = {
14
51
  layout: "fullscreen",
15
52
  },
16
53
  tags: ["autodocs"],
54
+ argTypes: {
55
+ from: {
56
+ control: "inline-radio",
57
+ options: ["top", "right", "bottom", "left"],
58
+ },
59
+ size: {
60
+ control: "text",
61
+ description: "CSS length, e.g. \"200px\", \"50%\", \"calc(100% - 80px)\". Leave empty for full viewport.",
62
+ },
63
+ scaleContent: {
64
+ control: "boolean",
65
+ },
66
+ scaleSelectors: {
67
+ control: "object",
68
+ },
69
+ scaleSelectorsMode: {
70
+ control: "inline-radio",
71
+ options: ["first", "all"],
72
+ },
73
+ onOverlayClick: {
74
+ control: "select",
75
+ options: ["close", null],
76
+ },
77
+ closeOnEsc: {
78
+ control: "boolean",
79
+ },
80
+ isOpen: { table: { disable: true } },
81
+ onClose: { table: { disable: true } },
82
+ children: { table: { disable: true } },
83
+ portal: { table: { disable: true } },
84
+ className: { table: { disable: true } },
85
+ },
86
+ args: {
87
+ from: "bottom",
88
+ closeOnEsc: true,
89
+ onOverlayClick: "close",
90
+ scaleContent: false,
91
+ scaleSelectorsMode: "first",
92
+ },
93
+ render: (args) => <Playground {...args as PlaygroundArgs} />,
17
94
  };
18
95
 
19
96
  type Story = StoryObj<typeof Drawer>;
20
97
 
21
98
  /**
22
- * Basic implementation of the Drawer component with a toggle button.
99
+ * Default full-viewport drawer sliding up from the bottom.
23
100
  */
24
- const Default: Story = {
25
- render: () => {
26
- const [isOpen, setisOpen] = useState(false);
101
+ const Default: Story = {};
27
102
 
28
- const handleToggleOpen = useCallback(() => {
29
- setisOpen((prev) => !prev);
30
- }, []);
103
+ /**
104
+ * ESC key is disabled, drawer can only be closed via its own UI.
105
+ */
106
+ const NoEscClose: Story = {
107
+ args: { closeOnEsc: false },
108
+ };
31
109
 
32
- const handleClose = useCallback(() => {
33
- setisOpen(false);
34
- }, []);
110
+ /**
111
+ * Slides in from the top, partial height.
112
+ */
113
+ const FromTop: Story = {
114
+ args: { from: "top", size: "240px" },
115
+ };
35
116
 
36
- return (
37
- <div style={{ padding: "20px" }}>
38
- <Drawer isOpen={isOpen} onClose={handleClose}>
39
- <div style={{ padding: "20px" }}>
40
- <h2>Drawer Content</h2>
41
- <p>This is the content inside the drawer.</p>
42
- <p>Press ESC to close.</p>
43
- </div>
44
- </Drawer>
45
- <Button onClick={handleToggleOpen}>
46
- {isOpen ? "Close Drawer" : "Open Drawer"}
47
- </Button>
48
- </div>
49
- );
50
- },
117
+ /**
118
+ * Slides in from the right side.
119
+ */
120
+ const FromRight: Story = {
121
+ args: { from: "right", size: "320px" },
51
122
  };
52
123
 
53
124
  /**
54
- * Drawer with custom close behavior where ESC key doesn't close the drawer.
125
+ * Slides in from the left side.
55
126
  */
56
- const NoEscClose: Story = {
57
- render: () => {
58
- const [open, setOpen] = useState(false);
127
+ const FromLeft: Story = {
128
+ args: { from: "left", size: "260px" },
129
+ };
59
130
 
60
- const handleToggleOpen = useCallback(() => {
61
- setOpen((prev) => !prev);
62
- }, []);
131
+ /**
132
+ * Partial-size drawer with the new rounded corner treatment on the visible edge.
133
+ */
134
+ const PartialSize: Story = {
135
+ args: { from: "bottom", size: "200px" },
136
+ };
63
137
 
64
- const handleClose = useCallback(() => {
65
- setOpen(false);
66
- }, []);
138
+ /**
139
+ * Drawer that leaves 80px of the page visible at the top while scaling the page
140
+ * content down — the classic stacked "3D" sheet look.
141
+ */
142
+ const ScaleContent: Story = {
143
+ args: {
144
+ from: "bottom",
145
+ size: "calc(100% - 80px)",
146
+ scaleContent: true,
147
+ },
148
+ };
67
149
 
68
- return (
69
- <div style={{ padding: "20px" }}>
70
- <Drawer isOpen={open} onClose={handleClose} closeOnEsc={false}>
71
- <div style={{ padding: "20px" }}>
72
- <h2>Drawer Without ESC Close</h2>
73
- <p>This drawer will not close when you press ESC.</p>
74
- <p>Use the button below to close it.</p>
75
- <Button onClick={handleClose}>Close Drawer</Button>
76
- </div>
77
- </Drawer>
78
- <Button onClick={handleToggleOpen}>
79
- {open ? "Close Drawer" : "Open Drawer (No ESC Close)"}
80
- </Button>
81
- </div>
82
- );
150
+ /**
151
+ * Overlay clicks are passed through to the underlying page (`onOverlayClick={null}`),
152
+ * so users can keep interacting with content next to the drawer.
153
+ */
154
+ const PassThroughOverlay: Story = {
155
+ args: {
156
+ from: "bottom",
157
+ size: "200px",
158
+ onOverlayClick: null,
83
159
  },
84
160
  };
85
161
 
@@ -87,7 +163,7 @@ const NoEscClose: Story = {
87
163
  * Drawer with content that demonstrates scrolling behavior.
88
164
  */
89
165
  const WithLongContent: Story = {
90
- render: () => {
166
+ render: (args) => {
91
167
  const [isOpen, setIsOpen] = useState(false);
92
168
 
93
169
  const handleToggleOpen = useCallback(() => {
@@ -100,7 +176,7 @@ const WithLongContent: Story = {
100
176
 
101
177
  return (
102
178
  <div style={{ padding: "20px" }}>
103
- <Drawer isOpen={isOpen} onClose={handleClose}>
179
+ <Drawer {...args as PlaygroundArgs} isOpen={isOpen} onClose={handleClose}>
104
180
  <div style={{ padding: "20px" }}>
105
181
  <h2>Drawer with Long Content</h2>
106
182
  <p>This drawer has enough content to demonstrate scrolling behavior.</p>
@@ -120,9 +196,6 @@ const WithLongContent: Story = {
120
196
  },
121
197
  };
122
198
 
123
- /**
124
- * Custom styled Drawer with a different background color and border radius.
125
- */
126
199
  const CustomStyledDrawer = styled(Drawer, {
127
200
  "&&": {
128
201
  background: "linear-gradient(to bottom, #c5f7fa, #63cfe2)",
@@ -130,10 +203,10 @@ const CustomStyledDrawer = styled(Drawer, {
130
203
  });
131
204
 
132
205
  /**
133
- * A custom styled version of the Drawer component with a gradient background and rounded top corners.
206
+ * A custom styled version of the Drawer component with a gradient background.
134
207
  */
135
208
  const CustomStyled: Story = {
136
- render: () => {
209
+ render: (args) => {
137
210
  const [isOpen, setIsOpen] = useState(false);
138
211
 
139
212
  const handleToggleOpen = useCallback(() => {
@@ -146,7 +219,7 @@ const CustomStyled: Story = {
146
219
 
147
220
  return (
148
221
  <div style={{ padding: "20px" }}>
149
- <CustomStyledDrawer isOpen={isOpen} onClose={handleClose}>
222
+ <CustomStyledDrawer {...args as PlaygroundArgs} isOpen={isOpen} onClose={handleClose}>
150
223
  <div style={{ padding: "20px" }}>
151
224
  <h2>Custom Styled Drawer</h2>
152
225
  <p>This drawer has custom styling with a gradient background</p>
@@ -161,5 +234,16 @@ const CustomStyled: Story = {
161
234
  },
162
235
  };
163
236
 
164
- export { Default, NoEscClose, WithLongContent, CustomStyled };
237
+ export {
238
+ Default,
239
+ NoEscClose,
240
+ FromTop,
241
+ FromRight,
242
+ FromLeft,
243
+ PartialSize,
244
+ ScaleContent,
245
+ PassThroughOverlay,
246
+ WithLongContent,
247
+ CustomStyled,
248
+ };
165
249
  export default meta;
@@ -12,11 +12,24 @@ const StyledDrawer = styled("div", {
12
12
  overflow: "auto",
13
13
  });
14
14
 
15
+ const Tint = styled("div", {
16
+ position: "fixed",
17
+ top: 0,
18
+ left: 0,
19
+ right: 0,
20
+ bottom: 0,
21
+ background: "$text",
22
+ opacity: 0,
23
+ transition: "opacity 300ms",
24
+ zIndex: 0,
25
+ });
26
+
15
27
  const Content = styled("div", {
16
28
  height: "100%",
17
29
  });
18
30
 
19
31
  export {
20
32
  StyledDrawer,
33
+ Tint,
21
34
  Content,
22
35
  };
@@ -1,22 +1,147 @@
1
- import React, { forwardRef, useEffect, useRef, useState } from "react";
1
+ import React, { forwardRef, useCallback, useEffect, useRef, useState } from "react";
2
+ import { createPortal } from "react-dom";
2
3
 
4
+ import { useForwardedRef } from "@bedrock-layout/use-forwarded-ref";
3
5
  import { Timeout } from "oop-timers";
4
6
 
5
7
  import { HandleEsc } from "../../utils/HandleEsc";
6
- import { Content, StyledDrawer } from "./Drawer.styled";
8
+ import { Content, StyledDrawer, Tint } from "./Drawer.styled";
7
9
 
8
10
  const RENDER_TIMEOUT = 500;
11
+ const TRANSITION_MS = 300;
12
+ const DEFAULT_CONTENT_SCALE = 0.92;
13
+ const DEFAULT_TINT_OPACITY = 0.1;
14
+ const PARTIAL_RADIUS = "16px";
15
+ const DEFAULT_SCALE_SELECTORS = ["#__next", "#root", "#storybook-root", "body>*:first-child"];
16
+
17
+ type DrawerFrom = "top" | "right" | "bottom" | "left";
18
+ type ScaleMode = "first" | "all";
9
19
 
10
20
  type DrawerProps = {
11
21
  isOpen: boolean;
12
22
  closeOnEsc?: boolean;
13
23
  onClose: () => void;
14
24
  className?: string;
25
+ /**
26
+ * Which edge the drawer slides in from. Defaults to `"bottom"`.
27
+ */
28
+ from?: DrawerFrom;
29
+ /**
30
+ * How far the drawer extends along its slide axis. Defaults to filling the whole viewport.
31
+ * - positive number — size in pixels (e.g. `300` ⇒ 300px tall/wide).
32
+ * - negative number — viewport minus pixels (e.g. `-100` ⇒ `calc(100% - 100px)`).
33
+ * - string — any CSS length (e.g. `"50%"`, `"20rem"`).
34
+ *
35
+ * When set, the drawer's visible edge gets rounded corners.
36
+ */
37
+ size?: number | string;
38
+ /**
39
+ * When set, scales the targeted page content down while the drawer is open
40
+ * (use with `scaleSelectors`) and fades in a subtle tint over it for a
41
+ * stacked "3D" look. `true` uses the default scale (~0.92), or pass a number
42
+ * to customize (e.g. `0.95`).
43
+ */
44
+ scaleContent?: boolean | number;
45
+ /**
46
+ * CSS selector (or array tried in order) for what to scale when `scaleContent`
47
+ * is set. The first selector that matches anything wins. Defaults to common
48
+ * app-root selectors `["#__next", "#root", "body>*:first-child"]`.
49
+ */
50
+ scaleSelectors?: string | string[];
51
+ /**
52
+ * Whether to scale only the first match of the chosen selector (`"first"`,
53
+ * default) or every match (`"all"`).
54
+ */
55
+ scaleSelectorsMode?: ScaleMode;
56
+ /**
57
+ * What happens when the user clicks (or taps) on the area outside the drawer.
58
+ * Mirrors `Modal`'s prop: `"close"` (default) calls `onClose`, a function is
59
+ * invoked directly, and `null` makes the overlay transparent to pointer/touch
60
+ * events so the underlying page stays interactive. The overlay only renders
61
+ * when there is an "outside" — i.e. `size` is set or `scaleContent` is enabled.
62
+ */
63
+ onOverlayClick?: (() => void) | "close" | null;
64
+ /**
65
+ * Where to portal the drawer. `true` (default) portals to `document.body` so the
66
+ * drawer's `position: fixed` always anchors to the viewport — needed when any
67
+ * ancestor establishes a containing block (e.g. via `transform`, `filter`, or
68
+ * `will-change`). Pass an `HTMLElement` to portal to a specific node, or `false`
69
+ * to render in place.
70
+ */
71
+ portal?: boolean | HTMLElement;
15
72
  children: React.ReactNode;
16
73
  };
17
74
 
75
+ const closedTransform: Record<DrawerFrom, string> = {
76
+ bottom: "translate(0, 100%)",
77
+ top: "translate(0, -100%)",
78
+ left: "translate(-100%, 0)",
79
+ right: "translate(100%, 0)",
80
+ };
81
+
82
+ const getSizeCss = (size: number | string | undefined): string | undefined => {
83
+ if (size === undefined) {
84
+ return undefined;
85
+ }
86
+ if (typeof size === "string") {
87
+ return size;
88
+ }
89
+ if (size >= 0) {
90
+ return `${size}px`;
91
+ }
92
+ return `calc(100% + ${size}px)`;
93
+ };
94
+
95
+ const positioningPresets: Record<DrawerFrom, (size: string) => React.CSSProperties> = {
96
+ bottom: (size) => ({ top: "auto", height: size }),
97
+ top: (size) => ({ bottom: "auto", height: size }),
98
+ left: (size) => ({ right: "auto", width: size }),
99
+ right: (size) => ({ left: "auto", width: size }),
100
+ };
101
+
102
+ const radiusPresets: Record<DrawerFrom, React.CSSProperties> = {
103
+ bottom: { borderTopLeftRadius: PARTIAL_RADIUS, borderTopRightRadius: PARTIAL_RADIUS },
104
+ top: { borderBottomLeftRadius: PARTIAL_RADIUS, borderBottomRightRadius: PARTIAL_RADIUS },
105
+ left: { borderTopRightRadius: PARTIAL_RADIUS, borderBottomRightRadius: PARTIAL_RADIUS },
106
+ right: { borderTopLeftRadius: PARTIAL_RADIUS, borderBottomLeftRadius: PARTIAL_RADIUS },
107
+ };
108
+
109
+ const getStaticStyle = (from: DrawerFrom, size: number | string | undefined): React.CSSProperties => {
110
+ const sizeCss = getSizeCss(size);
111
+ if (sizeCss === undefined) {
112
+ return {};
113
+ }
114
+ return { ...positioningPresets[from](sizeCss), ...radiusPresets[from] };
115
+ };
116
+
117
+ const findScaleTargets = (selectors: string[], mode: ScaleMode): HTMLElement[] => {
118
+ for (const sel of selectors) {
119
+ if (mode === "first") {
120
+ const el = document.querySelector<HTMLElement>(sel);
121
+ if (el) {
122
+ return [el];
123
+ }
124
+ }
125
+ else {
126
+ const els = Array.from(document.querySelectorAll<HTMLElement>(sel));
127
+ if (els.length > 0) {
128
+ return els;
129
+ }
130
+ }
131
+ }
132
+ return [];
133
+ };
134
+
135
+ const applyScale = (target: HTMLElement, transform: string) => {
136
+ target.style.setProperty("transition", `transform ${TRANSITION_MS}ms`);
137
+ target.style.setProperty("transform-origin", "50% 0");
138
+ target.style.setProperty("transform", transform);
139
+ };
140
+
18
141
  const Drawer = forwardRef<HTMLDivElement, DrawerProps>((props, ref) => {
142
+ const drawerRef = useForwardedRef(ref);
19
143
  const [shouldRenderWhenClosed, setShouldRenderWhenClosed] = useState(false);
144
+ const [isVisible, setIsVisible] = useState(props.isOpen);
20
145
  const timeoutRef = useRef<Timeout | null>(null);
21
146
 
22
147
  useEffect(() => {
@@ -31,36 +156,105 @@ const Drawer = forwardRef<HTMLDivElement, DrawerProps>((props, ref) => {
31
156
  }, []);
32
157
 
33
158
  useEffect(() => {
34
- if (props.isOpen) {
35
- timeoutRef.current?.stop();
36
- // eslint-disable-next-line react-hooks/set-state-in-effect
37
- setShouldRenderWhenClosed(true);
38
- }
39
- else {
159
+ if (!props.isOpen) {
40
160
  timeoutRef.current?.start();
161
+ // eslint-disable-next-line react-hooks/set-state-in-effect
162
+ setIsVisible(false);
163
+ return undefined;
41
164
  }
165
+ timeoutRef.current?.stop();
166
+ setShouldRenderWhenClosed(true);
167
+ const id = requestAnimationFrame(() => {
168
+ setIsVisible(true);
169
+ });
170
+ return () => {
171
+ cancelAnimationFrame(id);
172
+ };
42
173
  }, [props.isOpen]);
43
174
 
44
- // eslint-disable-next-line @typescript-eslint/no-magic-numbers
45
- const y = props.isOpen ? 0 : 100;
175
+ const { scaleContent, scaleSelectors, scaleSelectorsMode } = props;
176
+
177
+ useEffect(() => {
178
+ if (!scaleContent || !props.isOpen) {
179
+ return undefined;
180
+ }
181
+ const selectors = scaleSelectors === undefined
182
+ ? DEFAULT_SCALE_SELECTORS
183
+ : (Array.isArray(scaleSelectors) ? scaleSelectors : [scaleSelectors]);
184
+ const mode = scaleSelectorsMode ?? "first";
185
+ const targets = findScaleTargets(selectors, mode);
186
+ if (targets.length === 0) {
187
+ return undefined;
188
+ }
189
+ const scale = typeof scaleContent === "number" ? scaleContent : DEFAULT_CONTENT_SCALE;
190
+ targets.forEach((el) => {
191
+ applyScale(el, `scale(${scale})`);
192
+ });
193
+ return () => {
194
+ targets.forEach((el) => {
195
+ applyScale(el, "");
196
+ });
197
+ };
198
+ }, [props.isOpen, scaleContent, scaleSelectors, scaleSelectorsMode]);
199
+
200
+ const from = props.from ?? "bottom";
46
201
  const style = {
47
- transform: `translateY(${y}%)`,
202
+ ...getStaticStyle(from, props.size),
203
+ transform: isVisible ? "translate(0, 0)" : closedTransform[from],
204
+ transition: props.isOpen && !isVisible ? "none" : undefined,
48
205
  };
49
206
 
50
207
  const shouldRender = props.isOpen || shouldRenderWhenClosed;
51
208
 
52
209
  const closeOnEsc = props.closeOnEsc ?? true;
53
210
  const esc = closeOnEsc && <HandleEsc onPress={props.onClose} />;
211
+ const portal = props.portal ?? true;
212
+
213
+ const overlayClick = props.onOverlayClick === undefined ? "close" : props.onOverlayClick;
214
+ const { onClose } = props;
215
+ const handleOverlayClick = useCallback(() => {
216
+ if (overlayClick === "close") {
217
+ onClose();
218
+ return;
219
+ }
220
+ if (typeof overlayClick === "function") {
221
+ overlayClick();
222
+ }
223
+ }, [overlayClick, onClose]);
54
224
 
55
- return (
56
- <StyledDrawer className={props.className} style={style} ref={ref}>
57
- {esc}
58
- <Content>
59
- {/* eslint-disable-next-line react/jsx-no-leaked-render */}
60
- {shouldRender && props.children}
61
- </Content>
62
- </StyledDrawer>
225
+ const hasOverlay = props.size !== undefined || Boolean(scaleContent);
226
+ const overlayInteractive = overlayClick !== null && props.isOpen;
227
+ const tintStyle = {
228
+ opacity: isVisible ? DEFAULT_TINT_OPACITY : 0,
229
+ pointerEvents: (overlayInteractive ? "auto" : "none") as React.CSSProperties["pointerEvents"],
230
+ };
231
+
232
+ const tree = (
233
+ <>
234
+ {/* eslint-disable-next-line react/jsx-no-leaked-render */}
235
+ {hasOverlay && (
236
+ <Tint
237
+ style={tintStyle}
238
+ onClick={handleOverlayClick}
239
+ aria-hidden={true}
240
+ />
241
+ )}
242
+ <StyledDrawer className={props.className} style={style} ref={drawerRef}>
243
+ {esc}
244
+ <Content>
245
+ {/* eslint-disable-next-line react/jsx-no-leaked-render */}
246
+ {shouldRender && props.children}
247
+ </Content>
248
+ </StyledDrawer>
249
+ </>
63
250
  );
251
+
252
+ if (portal) {
253
+ const root = typeof portal === "boolean" ? document.body : portal;
254
+ return createPortal(tree, root);
255
+ }
256
+
257
+ return tree;
64
258
  });
65
259
 
66
260
  Drawer.displayName = "Drawer";
@@ -69,4 +263,4 @@ Drawer.toString = () => StyledDrawer.toString();
69
263
  const DrawerContentSelector = Content.toString();
70
264
 
71
265
  export { Drawer, DrawerContentSelector };
72
- export type { DrawerProps };
266
+ export type { DrawerProps, DrawerFrom };
@@ -1,4 +1,5 @@
1
- import React, { useCallback } from "react";
1
+ import React, { useCallback, useEffect, useState } from "react";
2
+ import { createPortal } from "react-dom";
2
3
 
3
4
  import { toast, Toaster as SonnerToaster } from "sonner";
4
5
 
@@ -9,10 +10,19 @@ interface ToasterProviderProps extends SonnerToasterProps {
9
10
  }
10
11
 
11
12
  const ToasterProvider: React.FC<ToasterProviderProps> = ({ children, position = "bottom-center", ...rest }) => {
13
+ const [body, setBody] = useState<HTMLElement | null>(null);
14
+
15
+ useEffect(() => {
16
+ // eslint-disable-next-line react-hooks/set-state-in-effect
17
+ setBody(document.body);
18
+ }, []);
19
+
20
+ const toaster = <SonnerToaster position={position} {...rest} />;
21
+
12
22
  return (
13
23
  <>
14
24
  {children}
15
- <SonnerToaster position={position} {...rest} />
25
+ {body ? createPortal(toaster, body) : null}
16
26
  </>
17
27
  );
18
28
  };