@boxcustodia/library 2.0.0-alpha.19 → 2.0.0-alpha.20

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 (265) hide show
  1. package/dist/components/button/button.cjs.js +1 -1
  2. package/dist/components/button/button.es.js +19 -18
  3. package/dist/components/button/components/base-button.cjs.js +1 -1
  4. package/dist/components/button/components/base-button.es.js +20 -20
  5. package/dist/components/calendar/calendar.cjs.js +1 -1
  6. package/dist/components/calendar/calendar.es.js +1 -0
  7. package/dist/components/date-picker/date-input.cjs.js +1 -1
  8. package/dist/components/date-picker/date-input.es.js +92 -75
  9. package/dist/components/date-picker/date-picker.cjs.js +1 -1
  10. package/dist/components/date-picker/date-picker.es.js +104 -95
  11. package/dist/components/date-picker/date-picker.utils.cjs.js +1 -1
  12. package/dist/components/date-picker/date-picker.utils.es.js +51 -43
  13. package/dist/components/date-picker/use-hidden-field-value.cjs.js +1 -0
  14. package/dist/components/date-picker/use-hidden-field-value.es.js +11 -0
  15. package/dist/components/menu/menu.es.js +1 -9
  16. package/dist/components/otp/otp.cjs.js +2 -0
  17. package/dist/components/otp/otp.es.js +93 -0
  18. package/dist/components/password/password.cjs.js +1 -1
  19. package/dist/components/password/password.es.js +2 -2
  20. package/dist/components/select/select.cjs.js +1 -1
  21. package/dist/components/select/select.es.js +68 -60
  22. package/dist/hooks/internal/is-apple-device.cjs.js +1 -0
  23. package/dist/hooks/internal/is-apple-device.es.js +9 -0
  24. package/dist/hooks/internal/use-latest-ref.cjs.js +1 -0
  25. package/dist/hooks/internal/use-latest-ref.es.js +11 -0
  26. package/dist/hooks/use-array/use-array.cjs.js +1 -1
  27. package/dist/hooks/use-array/use-array.es.js +54 -42
  28. package/dist/hooks/use-async/use-async.cjs.js +1 -1
  29. package/dist/hooks/use-async/use-async.es.js +53 -20
  30. package/dist/hooks/use-boolean/use-boolean.cjs.js +1 -0
  31. package/dist/hooks/use-boolean/use-boolean.es.js +25 -0
  32. package/dist/hooks/use-click-outside/use-click-outside.cjs.js +1 -1
  33. package/dist/hooks/use-click-outside/use-click-outside.es.js +26 -12
  34. package/dist/hooks/use-debounce-callback/use-debounced-callback.cjs.js +1 -1
  35. package/dist/hooks/use-debounce-callback/use-debounced-callback.es.js +27 -10
  36. package/dist/hooks/use-debounce-value/use-debounced-value.cjs.js +1 -1
  37. package/dist/hooks/use-debounce-value/use-debounced-value.es.js +7 -9
  38. package/dist/hooks/use-disclosure/use-disclosure.cjs.js +1 -1
  39. package/dist/hooks/use-disclosure/use-disclosure.es.js +21 -11
  40. package/dist/hooks/use-document-title/use-document-title.cjs.js +1 -1
  41. package/dist/hooks/use-document-title/use-document-title.es.js +14 -12
  42. package/dist/hooks/use-event-listener/use-event-listener.cjs.js +1 -1
  43. package/dist/hooks/use-event-listener/use-event-listener.es.js +17 -9
  44. package/dist/hooks/use-hotkey/use-hotkey.cjs.js +1 -1
  45. package/dist/hooks/use-hotkey/use-hotkey.es.js +30 -14
  46. package/dist/hooks/use-hotkey/utils/is-input-field.cjs.js +1 -1
  47. package/dist/hooks/use-hotkey/utils/is-input-field.es.js +4 -2
  48. package/dist/hooks/use-hotkey/utils/match-and-run.cjs.js +1 -0
  49. package/dist/hooks/use-hotkey/utils/match-and-run.es.js +12 -0
  50. package/dist/hooks/use-hotkey/utils/match-key-modifiers.cjs.js +1 -1
  51. package/dist/hooks/use-hotkey/utils/match-key-modifiers.es.js +13 -12
  52. package/dist/hooks/use-hover/use-hover.cjs.js +1 -1
  53. package/dist/hooks/use-hover/use-hover.es.js +32 -17
  54. package/dist/hooks/use-is-visible/use-is-visible.cjs.js +1 -1
  55. package/dist/hooks/use-is-visible/use-is-visible.es.js +31 -27
  56. package/dist/hooks/use-local-storage/use-local-storage.cjs.js +1 -1
  57. package/dist/hooks/use-local-storage/use-local-storage.es.js +52 -20
  58. package/dist/hooks/use-media-query/use-media-query.cjs.js +1 -1
  59. package/dist/hooks/use-media-query/use-media-query.es.js +21 -11
  60. package/dist/hooks/use-mutation/use-mutation.cjs.js +1 -1
  61. package/dist/hooks/use-mutation/use-mutation.es.js +36 -22
  62. package/dist/hooks/use-object/use-object.cjs.js +1 -1
  63. package/dist/hooks/use-object/use-object.es.js +26 -22
  64. package/dist/hooks/use-prevent-page-close/use-prevent-page-close.cjs.js +1 -0
  65. package/dist/hooks/use-prevent-page-close/use-prevent-page-close.es.js +14 -0
  66. package/dist/hooks/use-step/use-step.cjs.js +1 -1
  67. package/dist/hooks/use-step/use-step.es.js +25 -24
  68. package/dist/index.cjs.js +1 -1
  69. package/dist/index.es.js +308 -300
  70. package/dist/src/components/date-picker/date-picker.utils.d.ts +17 -0
  71. package/dist/src/components/date-picker/use-hidden-field-value.d.ts +12 -0
  72. package/dist/src/components/index.d.ts +1 -0
  73. package/dist/src/hooks/index.d.ts +2 -2
  74. package/dist/src/hooks/internal/index.d.ts +2 -0
  75. package/dist/src/hooks/internal/is-apple-device.d.ts +12 -0
  76. package/dist/src/hooks/internal/use-latest-ref.d.ts +12 -0
  77. package/dist/src/hooks/use-array/use-array.d.ts +24 -11
  78. package/dist/src/hooks/use-async/use-async.d.ts +16 -13
  79. package/dist/src/hooks/use-boolean/index.d.ts +1 -0
  80. package/dist/src/hooks/use-boolean/use-boolean.d.ts +15 -0
  81. package/dist/src/hooks/use-boolean/use-boolean.test.d.ts +1 -0
  82. package/dist/src/hooks/use-click-outside/use-click-outside.d.ts +23 -1
  83. package/dist/src/hooks/use-debounce-callback/use-debounced-callback.d.ts +19 -1
  84. package/dist/src/hooks/use-debounce-value/use-debounced-value.d.ts +10 -1
  85. package/dist/src/hooks/use-disclosure/use-disclosure.d.ts +17 -8
  86. package/dist/src/hooks/use-document-title/use-document-title.d.ts +11 -0
  87. package/dist/src/hooks/use-event-listener/use-event-listener.d.ts +18 -1
  88. package/dist/src/hooks/use-hotkey/index.d.ts +2 -1
  89. package/dist/src/hooks/use-hotkey/use-hotkey.d.ts +62 -5
  90. package/dist/src/hooks/use-hotkey/utils/index.d.ts +4 -3
  91. package/dist/src/hooks/use-hotkey/utils/is-input-field.d.ts +12 -2
  92. package/dist/src/hooks/use-hotkey/utils/is-input-field.test.d.ts +1 -0
  93. package/dist/src/hooks/use-hotkey/utils/match-and-run.d.ts +36 -0
  94. package/dist/src/hooks/use-hotkey/utils/match-and-run.test.d.ts +1 -0
  95. package/dist/src/hooks/use-hotkey/utils/match-key-modifiers.d.ts +20 -6
  96. package/dist/src/hooks/use-hotkey/utils/match-key-modifiers.test.d.ts +1 -0
  97. package/dist/src/hooks/use-hover/use-hover.d.ts +8 -4
  98. package/dist/src/hooks/use-is-visible/use-is-visible.d.ts +28 -4
  99. package/dist/src/hooks/use-local-storage/use-local-storage.d.ts +13 -2
  100. package/dist/src/hooks/use-media-query/use-media-query.d.ts +10 -1
  101. package/dist/src/hooks/use-media-query/use-media-query.test.d.ts +1 -0
  102. package/dist/src/hooks/use-mutation/use-mutation.d.ts +18 -11
  103. package/dist/src/hooks/use-object/use-object.d.ts +15 -6
  104. package/dist/src/hooks/use-prevent-page-close/index.d.ts +1 -0
  105. package/dist/src/hooks/use-prevent-page-close/use-prevent-page-close.d.ts +10 -0
  106. package/dist/src/hooks/use-prevent-page-close/use-prevent-page-close.test.d.ts +1 -0
  107. package/dist/src/hooks/use-step/use-step.d.ts +18 -11
  108. package/dist/src/utils/form.d.ts +10 -0
  109. package/package.json +1 -1
  110. package/src/components/alert-dialog/alert-dialog.test.tsx +13 -9
  111. package/src/components/auto-complete/auto-complete.test.tsx +4 -14
  112. package/src/components/avatar/avatar.test.tsx +7 -12
  113. package/src/components/button/button.test.tsx +10 -15
  114. package/src/components/button/button.tsx +14 -9
  115. package/src/components/button/components/base-button.tsx +2 -4
  116. package/src/components/calendar/calendar.test.tsx +12 -19
  117. package/src/components/calendar/calendar.tsx +4 -0
  118. package/src/components/card/card.test.tsx +4 -6
  119. package/src/components/checkbox/checkbox.test.tsx +12 -8
  120. package/src/components/checkbox-group/checkbox-group.test.tsx +7 -8
  121. package/src/components/combobox/combobox.test.tsx +24 -21
  122. package/src/components/date-picker/date-input-form.test.tsx +77 -0
  123. package/src/components/date-picker/date-input.stories.tsx +30 -18
  124. package/src/components/date-picker/date-input.tsx +77 -44
  125. package/src/components/date-picker/date-picker.stories.tsx +31 -1
  126. package/src/components/date-picker/date-picker.test.tsx +3 -13
  127. package/src/components/date-picker/date-picker.tsx +35 -16
  128. package/src/components/date-picker/date-picker.utils.test.ts +32 -14
  129. package/src/components/date-picker/date-picker.utils.ts +33 -0
  130. package/src/components/date-picker/use-date-input-popover.test.ts +3 -1
  131. package/src/components/date-picker/use-hidden-field-value.ts +23 -0
  132. package/src/components/dialog/dialog.test.tsx +10 -8
  133. package/src/components/dropzone/dropzone.test.tsx +11 -13
  134. package/src/components/empty/empty.test.tsx +4 -3
  135. package/src/components/field/field.test.tsx +12 -13
  136. package/src/components/form/form.stories.tsx +16 -1
  137. package/src/components/index.ts +1 -0
  138. package/src/components/label/label.test.tsx +3 -3
  139. package/src/components/menu/menu.tsx +1 -5
  140. package/src/components/number-input/number-input.test.tsx +6 -2
  141. package/src/components/password/password.test.tsx +20 -6
  142. package/src/components/password/password.tsx +2 -2
  143. package/src/components/popover/popover.test.tsx +4 -4
  144. package/src/components/progress/progress.test.tsx +7 -8
  145. package/src/components/radio-group/radio-group.test.tsx +17 -11
  146. package/src/components/select/select.test.tsx +10 -10
  147. package/src/components/select/select.tsx +9 -1
  148. package/src/components/stepper/stepper.stories.tsx +11 -15
  149. package/src/components/stepper/stepper.test.tsx +6 -4
  150. package/src/components/switch/switch.test.tsx +3 -3
  151. package/src/components/table/table.test.tsx +9 -3
  152. package/src/components/tabs/tabs.test.tsx +6 -2
  153. package/src/components/tag/tag.test.tsx +1 -3
  154. package/src/components/textarea/textarea.test.tsx +4 -1
  155. package/src/components/timeline/timeline.test.tsx +10 -5
  156. package/src/components/toast/toast.test.tsx +11 -14
  157. package/src/components/tooltip/tooltip.test.tsx +1 -5
  158. package/src/components/tree/tree.test.tsx +3 -1
  159. package/src/hooks/index.ts +2 -2
  160. package/src/hooks/internal/index.ts +2 -0
  161. package/src/hooks/internal/is-apple-device.test.ts +41 -0
  162. package/src/hooks/internal/is-apple-device.ts +33 -0
  163. package/src/hooks/internal/use-isomorphic-layout-effect.ts +3 -1
  164. package/src/hooks/internal/use-latest-ref.ts +21 -0
  165. package/src/hooks/use-array/use-array.stories.tsx +435 -64
  166. package/src/hooks/use-array/use-array.test.tsx +398 -15
  167. package/src/hooks/use-array/use-array.ts +105 -66
  168. package/src/hooks/use-async/use-async.stories.tsx +255 -131
  169. package/src/hooks/use-async/use-async.test.ts +397 -0
  170. package/src/hooks/use-async/use-async.ts +117 -39
  171. package/src/hooks/use-boolean/index.ts +1 -0
  172. package/src/hooks/use-boolean/use-boolean.stories.tsx +377 -0
  173. package/src/hooks/use-boolean/use-boolean.test.tsx +177 -0
  174. package/src/hooks/use-boolean/use-boolean.ts +50 -0
  175. package/src/hooks/use-click-outside/use-click-outside.stories.tsx +188 -18
  176. package/src/hooks/use-click-outside/use-click-outside.test.tsx +89 -10
  177. package/src/hooks/use-click-outside/use-click-outside.ts +62 -16
  178. package/src/hooks/use-debounce-callback/use-debounced-callback.stories.tsx +141 -41
  179. package/src/hooks/use-debounce-callback/use-debounced-callback.test.ts +217 -9
  180. package/src/hooks/use-debounce-callback/use-debounced-callback.ts +71 -11
  181. package/src/hooks/use-debounce-value/use-debounced-value.stories.tsx +247 -47
  182. package/src/hooks/use-debounce-value/use-debounced-value.test.ts +105 -10
  183. package/src/hooks/use-debounce-value/use-debounced-value.ts +19 -10
  184. package/src/hooks/use-disclosure/use-disclosure.stories.tsx +305 -14
  185. package/src/hooks/use-disclosure/use-disclosure.test.ts +198 -50
  186. package/src/hooks/use-disclosure/use-disclosure.ts +49 -29
  187. package/src/hooks/use-document-title/use-document-title.stories.tsx +54 -0
  188. package/src/hooks/use-document-title/use-document-title.test.tsx +26 -0
  189. package/src/hooks/use-document-title/{use-document-title.tsx → use-document-title.ts} +17 -3
  190. package/src/hooks/use-event-listener/use-event-listener.stories.tsx +105 -9
  191. package/src/hooks/use-event-listener/use-event-listener.test.tsx +77 -10
  192. package/src/hooks/use-event-listener/use-event-listener.ts +71 -11
  193. package/src/hooks/use-focus-trap/use-focus-trap.test.ts +31 -6
  194. package/src/hooks/use-focus-trap/use-focus-trap.ts +3 -2
  195. package/src/hooks/use-hotkey/index.ts +9 -1
  196. package/src/hooks/use-hotkey/use-hotkey.stories.tsx +279 -74
  197. package/src/hooks/use-hotkey/use-hotkey.test.tsx +286 -34
  198. package/src/hooks/use-hotkey/use-hotkey.ts +141 -17
  199. package/src/hooks/use-hotkey/utils/index.ts +8 -3
  200. package/src/hooks/use-hotkey/utils/is-input-field.test.ts +78 -0
  201. package/src/hooks/use-hotkey/utils/is-input-field.ts +31 -10
  202. package/src/hooks/use-hotkey/utils/match-and-run.test.ts +203 -0
  203. package/src/hooks/use-hotkey/utils/match-and-run.ts +62 -0
  204. package/src/hooks/use-hotkey/utils/match-key-modifiers.test.ts +65 -0
  205. package/src/hooks/use-hotkey/utils/match-key-modifiers.ts +39 -12
  206. package/src/hooks/use-hover/use-hover.stories.tsx +258 -80
  207. package/src/hooks/use-hover/use-hover.test.tsx +266 -26
  208. package/src/hooks/use-hover/use-hover.tsx +93 -28
  209. package/src/hooks/use-is-visible/use-is-visible.stories.tsx +193 -46
  210. package/src/hooks/use-is-visible/use-is-visible.test.tsx +235 -7
  211. package/src/hooks/use-is-visible/use-is-visible.ts +114 -0
  212. package/src/hooks/use-local-storage/use-local-storage.stories.tsx +129 -29
  213. package/src/hooks/use-local-storage/use-local-storage.test.ts +106 -41
  214. package/src/hooks/use-local-storage/use-local-storage.ts +100 -31
  215. package/src/hooks/use-media-query/use-media-query.stories.tsx +86 -26
  216. package/src/hooks/use-media-query/use-media-query.test.ts +132 -0
  217. package/src/hooks/use-media-query/use-media-query.ts +39 -14
  218. package/src/hooks/use-memoized-fn/use-memoized-fn.ts +0 -1
  219. package/src/hooks/use-mutation/use-mutation.stories.tsx +260 -94
  220. package/src/hooks/use-mutation/use-mutation.test.ts +359 -0
  221. package/src/hooks/use-mutation/use-mutation.ts +97 -0
  222. package/src/hooks/use-object/use-object.stories.tsx +310 -79
  223. package/src/hooks/use-object/use-object.test.tsx +235 -56
  224. package/src/hooks/use-object/use-object.ts +59 -0
  225. package/src/hooks/use-pagination/use-pagination.tsx +0 -1
  226. package/src/hooks/use-prevent-page-close/index.ts +1 -0
  227. package/src/hooks/use-prevent-page-close/use-prevent-page-close.stories.tsx +39 -0
  228. package/src/hooks/use-prevent-page-close/use-prevent-page-close.test.ts +89 -0
  229. package/src/hooks/use-prevent-page-close/use-prevent-page-close.ts +27 -0
  230. package/src/hooks/use-range-pagination/use-range-pagination.test.tsx +1 -1
  231. package/src/hooks/use-range-pagination/use-range-pagination.tsx +1 -1
  232. package/src/hooks/use-selection/use-selection.ts +0 -1
  233. package/src/hooks/use-step/use-step.stories.tsx +178 -65
  234. package/src/hooks/use-step/use-step.test.ts +178 -53
  235. package/src/hooks/use-step/use-step.ts +57 -49
  236. package/src/utils/form.test.tsx +13 -8
  237. package/src/utils/form.tsx +10 -0
  238. package/src/utils/functions/getFormData.test.ts +1 -1
  239. package/dist/hooks/use-hotkey/utils/create-hotkey-listener.cjs.js +0 -1
  240. package/dist/hooks/use-hotkey/utils/create-hotkey-listener.es.js +0 -10
  241. package/dist/hooks/use-prevent-close-window/use-prevent-close-window.cjs.js +0 -1
  242. package/dist/hooks/use-prevent-close-window/use-prevent-close-window.es.js +0 -15
  243. package/dist/hooks/use-toggle/use-toggle.cjs.js +0 -1
  244. package/dist/hooks/use-toggle/use-toggle.es.js +0 -10
  245. package/dist/src/hooks/use-hotkey/utils/create-hotkey-listener.d.ts +0 -1
  246. package/dist/src/hooks/use-prevent-close-window/index.d.ts +0 -1
  247. package/dist/src/hooks/use-prevent-close-window/use-prevent-close-window.d.ts +0 -13
  248. package/dist/src/hooks/use-toggle/index.d.ts +0 -1
  249. package/dist/src/hooks/use-toggle/use-toggle.d.ts +0 -3
  250. package/src/hooks/use-async/use-async.test.tsx +0 -68
  251. package/src/hooks/use-hotkey/utils/create-hotkey-listener.ts +0 -25
  252. package/src/hooks/use-is-visible/use-is-visible.tsx +0 -49
  253. package/src/hooks/use-mutation/use-mutation.test.tsx +0 -83
  254. package/src/hooks/use-mutation/use-mutation.tsx +0 -59
  255. package/src/hooks/use-object/use-object.tsx +0 -46
  256. package/src/hooks/use-prevent-close-window/index.ts +0 -1
  257. package/src/hooks/use-prevent-close-window/use-prevent-close-window.stories.tsx +0 -32
  258. package/src/hooks/use-prevent-close-window/use-prevent-close-window.test.ts +0 -79
  259. package/src/hooks/use-prevent-close-window/use-prevent-close-window.ts +0 -33
  260. package/src/hooks/use-toggle/index.ts +0 -1
  261. package/src/hooks/use-toggle/use-toggle.stories.tsx +0 -25
  262. package/src/hooks/use-toggle/use-toggle.test.tsx +0 -64
  263. package/src/hooks/use-toggle/use-toggle.ts +0 -14
  264. /package/dist/src/{hooks/use-prevent-close-window/use-prevent-close-window.test.d.ts → components/date-picker/date-input-form.test.d.ts} +0 -0
  265. /package/dist/src/hooks/{use-toggle/use-toggle.test.d.ts → internal/is-apple-device.test.d.ts} +0 -0
@@ -1,39 +1,99 @@
1
- import { Meta } from "@storybook/react-vite";
1
+ import type { Meta } from "@storybook/react-vite";
2
2
  import { useMediaQuery } from "../use-media-query";
3
3
 
4
+ /**
5
+ * Subscribes to a CSS media query and returns whether it currently
6
+ * matches. Powered by `useSyncExternalStore` so multiple subscribers
7
+ * stay in sync and updates fire from the native `change` event. SSR-safe:
8
+ * pass `defaultValue` to control what is returned during server render
9
+ * and on the first client render, which avoids hydration mismatches.
10
+ */
4
11
  const meta: Meta = {
5
12
  title: "hooks/useMediaQuery",
13
+ tags: ["beta"],
6
14
  };
7
15
 
8
16
  export default meta;
9
17
 
10
- export const Default = () => {
11
- const isSmallDevice = useMediaQuery("(max-width: 768px)");
12
- const isMediumDevice = useMediaQuery(
13
- "(min-width: 769px) and (max-width: 992px)",
14
- );
15
- const isLargeDevice = useMediaQuery(
16
- "(min-width: 993px) and (max-width: 1200px)",
17
- );
18
- const isExtraLargeDevice = useMediaQuery("(min-width: 1201px)");
18
+ /**
19
+ * Resize the preview to cross each breakpoint and watch the active row
20
+ * light up. Each row is an independent `useMediaQuery` subscription.
21
+ */
22
+ export const Default = {
23
+ render: () => {
24
+ const breakpoints = [
25
+ { label: "Small", emoji: "📱", query: "(max-width: 768px)" },
26
+ {
27
+ label: "Medium",
28
+ emoji: "💻",
29
+ query: "(min-width: 769px) and (max-width: 992px)",
30
+ },
31
+ {
32
+ label: "Large",
33
+ emoji: "🖥️",
34
+ query: "(min-width: 993px) and (max-width: 1200px)",
35
+ },
36
+ { label: "Extra large", emoji: "🖥️", query: "(min-width: 1201px)" },
37
+ ];
19
38
 
20
- return (
21
- <div className="flex items-center justify-center h-screen">
22
- <div
23
- className={`
24
- p-8 rounded-lg text-white text-center transition-all duration-500
25
- ${isSmallDevice ? "bg-blue-500" : ""}
26
- ${isMediumDevice ? "bg-green-500" : ""}
27
- ${isLargeDevice ? "bg-yellow-500" : ""}
28
- ${isExtraLargeDevice ? "bg-red-500" : ""}
29
- `}
30
- >
31
- {isSmallDevice && "📱 You are using a Small Device (max-width: 768px)"}
32
- {isMediumDevice && "💻 You are using a Medium Device (769px - 992px)"}
33
- {isLargeDevice && "🖥️ You are using a Large Device (993px - 1200px)"}
34
- {isExtraLargeDevice &&
35
- "🖥️ You are using an Extra Large Device (min-width: 1201px)"}
39
+ return (
40
+ <div className="flex w-full max-w-sm flex-col gap-2">
41
+ {breakpoints.map((bp) => (
42
+ <BreakpointRow key={bp.query} {...bp} />
43
+ ))}
36
44
  </div>
45
+ );
46
+ },
47
+ };
48
+
49
+ function BreakpointRow({
50
+ label,
51
+ emoji,
52
+ query,
53
+ }: {
54
+ label: string;
55
+ emoji: string;
56
+ query: string;
57
+ }) {
58
+ const matches = useMediaQuery(query);
59
+
60
+ return (
61
+ <div
62
+ className={`flex items-center justify-between rounded-md border px-3 py-2 text-sm transition-colors ${
63
+ matches
64
+ ? "border-primary bg-primary/10 font-medium text-foreground"
65
+ : "border-input text-muted-foreground"
66
+ }`}
67
+ >
68
+ <span>
69
+ {emoji} {label}
70
+ </span>
71
+ <code className="text-xs">{query}</code>
37
72
  </div>
38
73
  );
74
+ }
75
+
76
+ /**
77
+ * Pass `defaultValue` to align the first render with whatever the
78
+ * server rendered. Common pattern for responsive layouts under SSR
79
+ * where the breakpoint is known from the request user-agent or a cookie.
80
+ *
81
+ * ```tsx
82
+ * const isMobile = useMediaQuery("(max-width: 768px)", {
83
+ * defaultValue: serverIsMobile,
84
+ * });
85
+ * ```
86
+ */
87
+ export const WithDefaultValue = {
88
+ render: () => {
89
+ const isMobile = useMediaQuery("(max-width: 768px)", {
90
+ defaultValue: true,
91
+ });
92
+
93
+ return (
94
+ <p className="text-sm">
95
+ Layout: <strong>{isMobile ? "mobile" : "desktop"}</strong>
96
+ </p>
97
+ );
98
+ },
39
99
  };
@@ -0,0 +1,132 @@
1
+ import { act, renderHook } from "@testing-library/react";
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
3
+ import { useMediaQuery } from "./use-media-query";
4
+
5
+ type Listener = (event: { matches: boolean }) => void;
6
+
7
+ interface MockMediaQueryList {
8
+ matches: boolean;
9
+ addEventListener: ReturnType<typeof vi.fn>;
10
+ removeEventListener: ReturnType<typeof vi.fn>;
11
+ setMatches: (next: boolean) => void;
12
+ }
13
+
14
+ function createMockMatchMedia() {
15
+ const listeners = new Map<string, Set<Listener>>();
16
+ const states = new Map<string, MockMediaQueryList>();
17
+
18
+ const matchMedia = vi.fn((query: string): MockMediaQueryList => {
19
+ let state = states.get(query);
20
+ if (state) return state;
21
+
22
+ const queryListeners = new Set<Listener>();
23
+ listeners.set(query, queryListeners);
24
+
25
+ state = {
26
+ matches: false,
27
+ addEventListener: vi.fn((_type: string, listener: Listener) => {
28
+ queryListeners.add(listener);
29
+ }),
30
+ removeEventListener: vi.fn((_type: string, listener: Listener) => {
31
+ queryListeners.delete(listener);
32
+ }),
33
+ setMatches(next: boolean) {
34
+ if (this.matches === next) return;
35
+ this.matches = next;
36
+ for (const listener of queryListeners) {
37
+ listener({ matches: next });
38
+ }
39
+ },
40
+ };
41
+ states.set(query, state);
42
+ return state;
43
+ });
44
+
45
+ return {
46
+ matchMedia,
47
+ getState: (query: string) => states.get(query),
48
+ };
49
+ }
50
+
51
+ describe("useMediaQuery hook", () => {
52
+ let mock: ReturnType<typeof createMockMatchMedia>;
53
+
54
+ beforeEach(() => {
55
+ mock = createMockMatchMedia();
56
+ vi.stubGlobal("matchMedia", mock.matchMedia);
57
+ Object.defineProperty(window, "matchMedia", {
58
+ configurable: true,
59
+ value: mock.matchMedia,
60
+ });
61
+ });
62
+
63
+ afterEach(() => {
64
+ vi.unstubAllGlobals();
65
+ });
66
+
67
+ it("returns the initial `matches` value from matchMedia", () => {
68
+ const query = "(max-width: 768px)";
69
+ const { result } = renderHook(() => useMediaQuery(query));
70
+
71
+ expect(result.current).toBe(false);
72
+ expect(mock.matchMedia).toHaveBeenCalledWith(query);
73
+ });
74
+
75
+ it("reflects later changes to the match state", () => {
76
+ const query = "(max-width: 768px)";
77
+ const { result } = renderHook(() => useMediaQuery(query));
78
+
79
+ act(() => {
80
+ mock.getState(query)?.setMatches(true);
81
+ });
82
+
83
+ expect(result.current).toBe(true);
84
+
85
+ act(() => {
86
+ mock.getState(query)?.setMatches(false);
87
+ });
88
+
89
+ expect(result.current).toBe(false);
90
+ });
91
+
92
+ it("subscribes on mount and unsubscribes on unmount", () => {
93
+ const query = "(max-width: 768px)";
94
+ const { unmount } = renderHook(() => useMediaQuery(query));
95
+
96
+ const state = mock.getState(query);
97
+ expect(state?.addEventListener).toHaveBeenCalledTimes(1);
98
+
99
+ unmount();
100
+
101
+ expect(state?.removeEventListener).toHaveBeenCalledTimes(1);
102
+ });
103
+
104
+ it("re-subscribes when the query changes", () => {
105
+ const { rerender } = renderHook(({ query }) => useMediaQuery(query), {
106
+ initialProps: { query: "(max-width: 768px)" },
107
+ });
108
+
109
+ rerender({ query: "(min-width: 1024px)" });
110
+
111
+ expect(
112
+ mock.getState("(max-width: 768px)")?.removeEventListener,
113
+ ).toHaveBeenCalled();
114
+ expect(
115
+ mock.getState("(min-width: 1024px)")?.addEventListener,
116
+ ).toHaveBeenCalled();
117
+ });
118
+
119
+ it("returns defaultValue when window.matchMedia is unavailable", () => {
120
+ vi.unstubAllGlobals();
121
+ Object.defineProperty(window, "matchMedia", {
122
+ configurable: true,
123
+ value: undefined,
124
+ });
125
+
126
+ const { result } = renderHook(() =>
127
+ useMediaQuery("(max-width: 768px)", { defaultValue: true }),
128
+ );
129
+
130
+ expect(result.current).toBe(true);
131
+ });
132
+ });
@@ -1,20 +1,45 @@
1
- import { useEffect, useState } from "react";
1
+ import { useCallback, useSyncExternalStore } from "react";
2
+ import { isBrowser } from "../internal";
2
3
 
3
- export const useMediaQuery = (query: string): boolean => {
4
- const [matches, setMatches] = useState<boolean>(false);
4
+ export interface UseMediaQueryOptions {
5
+ /**
6
+ * Value returned during server render and on the first client render
7
+ * before `matchMedia` has been read. Use this to avoid hydration
8
+ * mismatches in SSR consumers that already know the breakpoint.
9
+ * @default false
10
+ */
11
+ defaultValue?: boolean;
12
+ }
5
13
 
6
- useEffect(() => {
7
- const mediaQueryList = window.matchMedia(query);
8
- const documentChangeHandler = () => setMatches(mediaQueryList.matches);
14
+ function canMatch(): boolean {
15
+ return isBrowser && typeof window.matchMedia === "function";
16
+ }
9
17
 
10
- documentChangeHandler();
18
+ export function useMediaQuery(
19
+ query: string,
20
+ options: UseMediaQueryOptions = {},
21
+ ): boolean {
22
+ const { defaultValue = false } = options;
11
23
 
12
- mediaQueryList.addEventListener("change", documentChangeHandler);
24
+ const subscribe = useCallback(
25
+ (listener: () => void) => {
26
+ if (!canMatch()) return () => {};
27
+ const mql = window.matchMedia(query);
28
+ mql.addEventListener("change", listener);
29
+ return () => mql.removeEventListener("change", listener);
30
+ },
31
+ [query],
32
+ );
13
33
 
14
- return () => {
15
- mediaQueryList.removeEventListener("change", documentChangeHandler);
16
- };
17
- }, [query]);
34
+ const getSnapshot = useCallback((): boolean => {
35
+ if (!canMatch()) return defaultValue;
36
+ return window.matchMedia(query).matches;
37
+ }, [query, defaultValue]);
18
38
 
19
- return matches;
20
- };
39
+ const getServerSnapshot = useCallback(
40
+ (): boolean => defaultValue,
41
+ [defaultValue],
42
+ );
43
+
44
+ return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
45
+ }
@@ -27,4 +27,3 @@ export function useMemoizedFn<T extends noop>(fn: T) {
27
27
 
28
28
  return memoizedFn.current as T;
29
29
  }
30
-