@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.
- package/dist/cjs/index.cjs +2 -0
- package/dist/cjs/src/Button/Button.cjs +1 -0
- package/dist/cjs/src/Calendar/CalendarRange/CalendarRange.cjs +4 -2
- package/dist/cjs/src/Calendar/CalendarRange/CalendarRange.module.scss.cjs +2 -1
- package/dist/cjs/src/Calendar/CalendarSingle/CalendarSingle.cjs +9 -5
- package/dist/cjs/src/Calendar/CalendarSingle/CalendarSingle.module.scss.cjs +2 -1
- package/dist/cjs/src/Calendar/LegacyCalendarRange/LegacyCalendarRange.cjs +4 -2
- package/dist/cjs/src/Calendar/LegacyCalendarRange/LegacyCalendarRange.module.scss.cjs +2 -1
- package/dist/cjs/src/LinkButton/LinkButton.cjs +1 -0
- package/dist/cjs/src/Pagination/Pagination.cjs +1 -0
- package/dist/cjs/src/TitleBlock/TitleBlock.module.scss.cjs +1 -0
- package/dist/cjs/src/TitleBlock/utils.cjs +1 -1
- package/dist/cjs/src/Tooltip/OverlayArrow.cjs +1 -0
- package/dist/cjs/src/Tooltip/Tooltip.cjs +1 -0
- package/dist/cjs/src/utils/useContainerQueries.cjs +89 -0
- package/dist/esm/index.mjs +1 -0
- package/dist/esm/src/Button/Button.mjs +1 -0
- package/dist/esm/src/Calendar/CalendarRange/CalendarRange.mjs +4 -2
- package/dist/esm/src/Calendar/CalendarRange/CalendarRange.module.scss.mjs +2 -1
- package/dist/esm/src/Calendar/CalendarSingle/CalendarSingle.mjs +9 -5
- package/dist/esm/src/Calendar/CalendarSingle/CalendarSingle.module.scss.mjs +2 -1
- package/dist/esm/src/Calendar/LegacyCalendarRange/LegacyCalendarRange.mjs +4 -2
- package/dist/esm/src/Calendar/LegacyCalendarRange/LegacyCalendarRange.module.scss.mjs +2 -1
- package/dist/esm/src/LinkButton/LinkButton.mjs +1 -0
- package/dist/esm/src/Pagination/Pagination.mjs +1 -0
- package/dist/esm/src/TitleBlock/TitleBlock.module.scss.mjs +1 -0
- package/dist/esm/src/TitleBlock/utils.mjs +1 -1
- package/dist/esm/src/Tooltip/OverlayArrow.mjs +1 -0
- package/dist/esm/src/Tooltip/Tooltip.mjs +1 -0
- package/dist/esm/src/utils/useContainerQueries.mjs +87 -0
- package/dist/styles.css +34 -5
- package/dist/types/Calendar/CalendarSingle/CalendarSingle.d.ts +3 -1
- package/dist/types/TitleBlock/types.d.ts +1 -1
- package/dist/types/utils/index.d.ts +1 -0
- package/dist/types/utils/useContainerQueries.d.ts +21 -0
- package/package.json +1 -1
- package/src/Calendar/CalendarRange/CalendarRange.module.scss +4 -0
- package/src/Calendar/CalendarRange/CalendarRange.tsx +16 -2
- package/src/Calendar/CalendarSingle/CalendarSingle.module.scss +4 -0
- package/src/Calendar/CalendarSingle/CalendarSingle.tsx +20 -2
- package/src/Calendar/LegacyCalendarRange/LegacyCalendarRange.module.scss +4 -0
- package/src/Calendar/LegacyCalendarRange/LegacyCalendarRange.tsx +16 -2
- package/src/TitleBlock/TitleBlock.module.scss +29 -0
- package/src/TitleBlock/_docs/TitleBlock--usage-guidelines.mdx +4 -0
- package/src/TitleBlock/_docs/TitleBlock.stories.tsx +108 -0
- package/src/TitleBlock/_variables.scss +1 -0
- package/src/TitleBlock/types.ts +1 -1
- package/src/TitleBlock/utils.ts +1 -1
- package/src/utils/index.ts +1 -0
- package/src/utils/useContainerQueries.spec.tsx +209 -0
- 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
|
+
}
|