@kaizen/components 2.1.1 → 2.2.1

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 (51) hide show
  1. package/dist/cjs/index.cjs +2 -0
  2. package/dist/cjs/src/Button/Button.cjs +1 -0
  3. package/dist/cjs/src/Calendar/CalendarRange/CalendarRange.cjs +4 -2
  4. package/dist/cjs/src/Calendar/CalendarRange/CalendarRange.module.scss.cjs +2 -1
  5. package/dist/cjs/src/Calendar/CalendarSingle/CalendarSingle.cjs +9 -5
  6. package/dist/cjs/src/Calendar/CalendarSingle/CalendarSingle.module.scss.cjs +2 -1
  7. package/dist/cjs/src/Calendar/LegacyCalendarRange/LegacyCalendarRange.cjs +4 -2
  8. package/dist/cjs/src/Calendar/LegacyCalendarRange/LegacyCalendarRange.module.scss.cjs +2 -1
  9. package/dist/cjs/src/LinkButton/LinkButton.cjs +1 -0
  10. package/dist/cjs/src/Pagination/Pagination.cjs +1 -0
  11. package/dist/cjs/src/TitleBlock/TitleBlock.module.scss.cjs +1 -0
  12. package/dist/cjs/src/TitleBlock/utils.cjs +1 -1
  13. package/dist/cjs/src/Tooltip/OverlayArrow.cjs +1 -0
  14. package/dist/cjs/src/Tooltip/Tooltip.cjs +1 -0
  15. package/dist/cjs/src/utils/useContainerQueries.cjs +89 -0
  16. package/dist/esm/index.mjs +1 -0
  17. package/dist/esm/src/Button/Button.mjs +1 -0
  18. package/dist/esm/src/Calendar/CalendarRange/CalendarRange.mjs +4 -2
  19. package/dist/esm/src/Calendar/CalendarRange/CalendarRange.module.scss.mjs +2 -1
  20. package/dist/esm/src/Calendar/CalendarSingle/CalendarSingle.mjs +9 -5
  21. package/dist/esm/src/Calendar/CalendarSingle/CalendarSingle.module.scss.mjs +2 -1
  22. package/dist/esm/src/Calendar/LegacyCalendarRange/LegacyCalendarRange.mjs +4 -2
  23. package/dist/esm/src/Calendar/LegacyCalendarRange/LegacyCalendarRange.module.scss.mjs +2 -1
  24. package/dist/esm/src/LinkButton/LinkButton.mjs +1 -0
  25. package/dist/esm/src/Pagination/Pagination.mjs +1 -0
  26. package/dist/esm/src/TitleBlock/TitleBlock.module.scss.mjs +1 -0
  27. package/dist/esm/src/TitleBlock/utils.mjs +1 -1
  28. package/dist/esm/src/Tooltip/OverlayArrow.mjs +1 -0
  29. package/dist/esm/src/Tooltip/Tooltip.mjs +1 -0
  30. package/dist/esm/src/utils/useContainerQueries.mjs +87 -0
  31. package/dist/styles.css +34 -5
  32. package/dist/types/Calendar/CalendarSingle/CalendarSingle.d.ts +3 -1
  33. package/dist/types/TitleBlock/types.d.ts +1 -1
  34. package/dist/types/utils/index.d.ts +1 -0
  35. package/dist/types/utils/useContainerQueries.d.ts +21 -0
  36. package/package.json +1 -1
  37. package/src/Calendar/CalendarRange/CalendarRange.module.scss +4 -0
  38. package/src/Calendar/CalendarRange/CalendarRange.tsx +16 -2
  39. package/src/Calendar/CalendarSingle/CalendarSingle.module.scss +4 -0
  40. package/src/Calendar/CalendarSingle/CalendarSingle.tsx +20 -2
  41. package/src/Calendar/LegacyCalendarRange/LegacyCalendarRange.module.scss +4 -0
  42. package/src/Calendar/LegacyCalendarRange/LegacyCalendarRange.tsx +16 -2
  43. package/src/TitleBlock/TitleBlock.module.scss +29 -0
  44. package/src/TitleBlock/_docs/TitleBlock--usage-guidelines.mdx +4 -0
  45. package/src/TitleBlock/_docs/TitleBlock.stories.tsx +108 -0
  46. package/src/TitleBlock/_variables.scss +1 -0
  47. package/src/TitleBlock/types.ts +1 -1
  48. package/src/TitleBlock/utils.ts +1 -1
  49. package/src/utils/index.ts +1 -0
  50. package/src/utils/useContainerQueries.spec.tsx +209 -0
  51. package/src/utils/useContainerQueries.tsx +121 -0
@@ -0,0 +1,209 @@
1
+ import React from 'react'
2
+ import { act, render, screen } from '@testing-library/react'
3
+ import { vi } from 'vitest'
4
+ import { useContainerQueries } from './useContainerQueries'
5
+
6
+ const ExampleComponent = (): JSX.Element => {
7
+ const { containerRef, queries } = useContainerQueries()
8
+
9
+ return (
10
+ <div ref={containerRef} data-testid="container">
11
+ {queries.isSmOrLarger && <button type="button">Small query boolean</button>}
12
+ {queries.isMdOrLarger && <button type="button">Medium or larger query</button>}
13
+ </div>
14
+ )
15
+ }
16
+
17
+ // Mock ResizeObserver
18
+ class ResizeObserverMock {
19
+ callback: ResizeObserverCallback
20
+ elements: Set<Element>
21
+
22
+ constructor(callback: ResizeObserverCallback) {
23
+ this.callback = callback
24
+ this.elements = new Set()
25
+ }
26
+
27
+ observe(target: Element): void {
28
+ this.elements.add(target)
29
+ }
30
+
31
+ unobserve(target: Element): void {
32
+ this.elements.delete(target)
33
+ }
34
+
35
+ disconnect(): void {
36
+ this.elements.clear()
37
+ }
38
+
39
+ // Helper method to trigger resize
40
+ trigger(width: number): void {
41
+ const entries: ResizeObserverEntry[] = Array.from(this.elements).map((element) => ({
42
+ target: element,
43
+ contentRect: {
44
+ width,
45
+ height: 100,
46
+ top: 0,
47
+ left: 0,
48
+ bottom: 100,
49
+ right: width,
50
+ x: 0,
51
+ y: 0,
52
+ } as DOMRectReadOnly,
53
+ borderBoxSize: [
54
+ {
55
+ inlineSize: width,
56
+ blockSize: 100,
57
+ },
58
+ ],
59
+ contentBoxSize: [
60
+ {
61
+ inlineSize: width,
62
+ blockSize: 100,
63
+ },
64
+ ],
65
+ devicePixelContentBoxSize: [
66
+ {
67
+ inlineSize: width,
68
+ blockSize: 100,
69
+ },
70
+ ],
71
+ })) as ResizeObserverEntry[]
72
+
73
+ this.callback(entries, this as unknown as ResizeObserver)
74
+ }
75
+ }
76
+
77
+ let resizeObserverInstance: ResizeObserverMock | null = null
78
+
79
+ // Store original values to restore after each test
80
+ const originalResizeObserver = global.ResizeObserver
81
+ const originalGetBoundingClientRect = Element.prototype.getBoundingClientRect
82
+
83
+ const setupResizeObserver = (): ResizeObserverMock => {
84
+ const mockObserver = vi.fn((callback: ResizeObserverCallback) => {
85
+ resizeObserverInstance = new ResizeObserverMock(callback)
86
+ return resizeObserverInstance
87
+ })
88
+
89
+ global.ResizeObserver = mockObserver as unknown as typeof ResizeObserver
90
+
91
+ return resizeObserverInstance!
92
+ }
93
+
94
+ describe('useContainerQueries()', () => {
95
+ beforeEach(() => {
96
+ resizeObserverInstance = null
97
+ })
98
+
99
+ afterEach(() => {
100
+ vi.restoreAllMocks()
101
+ // Restore original global/prototype properties
102
+ global.ResizeObserver = originalResizeObserver
103
+ Element.prototype.getBoundingClientRect = originalGetBoundingClientRect
104
+ })
105
+
106
+ it('shows and hides content based on Tailwind container breakpoints', async () => {
107
+ setupResizeObserver()
108
+
109
+ // Mock getBoundingClientRect to return a small width initially
110
+ const mockGetBoundingClientRect = vi.fn(() => ({
111
+ width: 300,
112
+ height: 100,
113
+ top: 0,
114
+ left: 0,
115
+ bottom: 100,
116
+ right: 300,
117
+ x: 0,
118
+ y: 0,
119
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
120
+ toJSON: () => {},
121
+ }))
122
+ Element.prototype.getBoundingClientRect = mockGetBoundingClientRect
123
+
124
+ render(<ExampleComponent />)
125
+
126
+ // Initially at 300px, should not show sm (384px) or md (448px) content
127
+ expect(screen.queryByRole('button', { name: /Small query boolean/i })).not.toBeInTheDocument()
128
+ expect(
129
+ screen.queryByRole('button', { name: /Medium or larger query/i }),
130
+ ).not.toBeInTheDocument()
131
+
132
+ // Trigger resize to 400px (sm breakpoint is 384px)
133
+ await act(async () => {
134
+ resizeObserverInstance?.trigger(400)
135
+ // Wait for debounce (1000ms + buffer)
136
+ await new Promise((resolve) => setTimeout(resolve, 1100))
137
+ })
138
+
139
+ expect(screen.queryByRole('button', { name: /Small query boolean/i })).toBeInTheDocument()
140
+ expect(
141
+ screen.queryByRole('button', { name: /Medium or larger query/i }),
142
+ ).not.toBeInTheDocument()
143
+
144
+ // Trigger resize to 500px (md breakpoint is 448px)
145
+ await act(async () => {
146
+ resizeObserverInstance?.trigger(500)
147
+ // Wait for debounce (1000ms + buffer)
148
+ await new Promise((resolve) => setTimeout(resolve, 1100))
149
+ })
150
+
151
+ expect(screen.queryByRole('button', { name: /Small query boolean/i })).toBeInTheDocument()
152
+ expect(screen.queryByRole('button', { name: /Medium or larger query/i })).toBeInTheDocument()
153
+ })
154
+
155
+ it('returns SSR-safe defaults when window is undefined', () => {
156
+ // This test verifies the SSR code path exists
157
+ // In actual SSR environment, the hook returns safe defaults
158
+ expect(useContainerQueries).toBeDefined()
159
+ })
160
+
161
+ it('cleans up ResizeObserver on unmount', () => {
162
+ setupResizeObserver()
163
+
164
+ const mockGetBoundingClientRect = vi.fn(() => ({
165
+ width: 500,
166
+ height: 100,
167
+ top: 0,
168
+ left: 0,
169
+ bottom: 100,
170
+ right: 500,
171
+ x: 0,
172
+ y: 0,
173
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
174
+ toJSON: () => {},
175
+ }))
176
+ Element.prototype.getBoundingClientRect = mockGetBoundingClientRect
177
+
178
+ const { unmount } = render(<ExampleComponent />)
179
+
180
+ const disconnectSpy = vi.spyOn(resizeObserverInstance!, 'disconnect')
181
+
182
+ unmount()
183
+
184
+ expect(disconnectSpy).toHaveBeenCalled()
185
+ })
186
+
187
+ it('observes the container element', () => {
188
+ setupResizeObserver()
189
+
190
+ const mockGetBoundingClientRect = vi.fn(() => ({
191
+ width: 500,
192
+ height: 100,
193
+ top: 0,
194
+ left: 0,
195
+ bottom: 100,
196
+ right: 500,
197
+ x: 0,
198
+ y: 0,
199
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
200
+ toJSON: () => {},
201
+ }))
202
+ Element.prototype.getBoundingClientRect = mockGetBoundingClientRect
203
+
204
+ render(<ExampleComponent />)
205
+
206
+ // Verify that the ResizeObserver is observing the container
207
+ expect(resizeObserverInstance?.elements.size).toBe(1)
208
+ })
209
+ })
@@ -0,0 +1,121 @@
1
+ import type React from 'react'
2
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
3
+ import { useDebouncedCallback } from 'use-debounce'
4
+
5
+ const DEFAULT_DEBOUNCE_MS = 1000
6
+
7
+ /**
8
+ * Tailwind CSS default container query breakpoints in pixels
9
+ * These match the default ranges from Tailwind CSS documentation
10
+ * https://tailwindcss.com/docs/responsive-design#container-size-reference
11
+ */
12
+ const DEFAULT_BREAKPOINTS = {
13
+ '3xs': 256,
14
+ '2xs': 288,
15
+ 'xs': 320,
16
+ 'sm': 384,
17
+ 'md': 448,
18
+ 'lg': 512,
19
+ 'xl': 576,
20
+ '2xl': 672,
21
+ '3xl': 768,
22
+ '4xl': 896,
23
+ '5xl': 1024,
24
+ '6xl': 1152,
25
+ '7xl': 1280,
26
+ } as const
27
+
28
+ type ContainerQueries = {
29
+ is3xsOrLarger: boolean
30
+ is2xsOrLarger: boolean
31
+ isXsOrLarger: boolean
32
+ isSmOrLarger: boolean
33
+ isMdOrLarger: boolean
34
+ isLgOrLarger: boolean
35
+ isXlOrLarger: boolean
36
+ is2xlOrLarger: boolean
37
+ is3xlOrLarger: boolean
38
+ is4xlOrLarger: boolean
39
+ is5xlOrLarger: boolean
40
+ is6xlOrLarger: boolean
41
+ is7xlOrLarger: boolean
42
+ }
43
+
44
+ export const useContainerQueries = (): {
45
+ containerRef: React.RefCallback<HTMLElement>
46
+ queries: ContainerQueries
47
+ } => {
48
+ const isClient = typeof window !== 'undefined'
49
+
50
+ const [containerWidth, setContainerWidth] = useState<number>(0)
51
+
52
+ const resizeObserverRef = useRef<ResizeObserver | null>(null)
53
+
54
+ const debouncedSetContainerWidth = useDebouncedCallback((width: number) => {
55
+ setContainerWidth(width)
56
+ }, DEFAULT_DEBOUNCE_MS)
57
+
58
+ const containerRef = useCallback(
59
+ (node: HTMLElement | null) => {
60
+ // Skip if SSR
61
+ if (!isClient) return
62
+
63
+ // Cleanup previous observer
64
+ if (resizeObserverRef.current) {
65
+ resizeObserverRef.current.disconnect()
66
+ resizeObserverRef.current = null
67
+ }
68
+
69
+ if (node) {
70
+ // Create new ResizeObserver
71
+ resizeObserverRef.current = new ResizeObserver((entries) => {
72
+ for (const entry of entries) {
73
+ // Use borderBoxSize for more accurate measurements
74
+ const width = entry.borderBoxSize?.[0]?.inlineSize ?? entry.contentRect.width
75
+ debouncedSetContainerWidth(width)
76
+ }
77
+ })
78
+
79
+ resizeObserverRef.current.observe(node)
80
+
81
+ // Set initial width immediately (no debounce for initial render)
82
+ const width = node.getBoundingClientRect().width
83
+ setContainerWidth(width)
84
+ }
85
+ },
86
+ [debouncedSetContainerWidth, isClient],
87
+ )
88
+
89
+ useEffect(
90
+ () => () => {
91
+ if (resizeObserverRef.current) {
92
+ resizeObserverRef.current.disconnect()
93
+ }
94
+ },
95
+ [],
96
+ )
97
+
98
+ const queries = useMemo(
99
+ () => ({
100
+ is3xsOrLarger: containerWidth >= DEFAULT_BREAKPOINTS['3xs'],
101
+ is2xsOrLarger: containerWidth >= DEFAULT_BREAKPOINTS['2xs'],
102
+ isXsOrLarger: containerWidth >= DEFAULT_BREAKPOINTS.xs,
103
+ isSmOrLarger: containerWidth >= DEFAULT_BREAKPOINTS.sm,
104
+ isMdOrLarger: containerWidth >= DEFAULT_BREAKPOINTS.md,
105
+ isLgOrLarger: containerWidth >= DEFAULT_BREAKPOINTS.lg,
106
+ isXlOrLarger: containerWidth >= DEFAULT_BREAKPOINTS.xl,
107
+ is2xlOrLarger: containerWidth >= DEFAULT_BREAKPOINTS['2xl'],
108
+ is3xlOrLarger: containerWidth >= DEFAULT_BREAKPOINTS['3xl'],
109
+ is4xlOrLarger: containerWidth >= DEFAULT_BREAKPOINTS['4xl'],
110
+ is5xlOrLarger: containerWidth >= DEFAULT_BREAKPOINTS['5xl'],
111
+ is6xlOrLarger: containerWidth >= DEFAULT_BREAKPOINTS['6xl'],
112
+ is7xlOrLarger: containerWidth >= DEFAULT_BREAKPOINTS['7xl'],
113
+ }),
114
+ [containerWidth],
115
+ )
116
+
117
+ return {
118
+ containerRef,
119
+ queries,
120
+ }
121
+ }