@kaizen/components 0.0.0-canary-useContainerQueries-20251121043854 → 0.0.0-canary-useContainerQueries-20251125215952
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 +0 -2
- package/dist/cjs/src/TitleBlock/TitleBlock.module.scss.cjs +1 -0
- package/dist/cjs/src/TitleBlock/utils.cjs +1 -1
- package/dist/esm/index.mjs +0 -1
- package/dist/esm/src/TitleBlock/TitleBlock.module.scss.mjs +1 -0
- package/dist/esm/src/TitleBlock/utils.mjs +1 -1
- package/dist/styles.css +25 -5
- package/dist/types/TitleBlock/types.d.ts +1 -1
- package/dist/types/utils/index.d.ts +0 -1
- package/package.json +1 -1
- 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 +0 -1
- package/dist/cjs/src/utils/useContainerQueries.cjs +0 -334
- package/dist/esm/src/utils/useContainerQueries.mjs +0 -326
- package/dist/types/utils/useContainerQueries.d.ts +0 -91
- package/src/utils/useContainerQueries.spec.tsx +0 -242
- package/src/utils/useContainerQueries.tsx +0 -333
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
import React, { type ReactNode } from 'react';
|
|
2
|
-
type Props = Record<string, string>;
|
|
3
|
-
type GenericChildrenType = {
|
|
4
|
-
children?: ReactNode;
|
|
5
|
-
};
|
|
6
|
-
type ContainerQueries = {
|
|
7
|
-
isXs: boolean;
|
|
8
|
-
isSm: boolean;
|
|
9
|
-
isMd: boolean;
|
|
10
|
-
isLg: boolean;
|
|
11
|
-
isXl: boolean;
|
|
12
|
-
is2xl: boolean;
|
|
13
|
-
is3xl: boolean;
|
|
14
|
-
is4xl: boolean;
|
|
15
|
-
is5xl: boolean;
|
|
16
|
-
is6xl: boolean;
|
|
17
|
-
is7xl: boolean;
|
|
18
|
-
[key: string]: boolean;
|
|
19
|
-
};
|
|
20
|
-
type ContainerComponents = {
|
|
21
|
-
'XsOnly': (props: GenericChildrenType) => JSX.Element;
|
|
22
|
-
'SmOnly': (props: GenericChildrenType) => JSX.Element;
|
|
23
|
-
'MdOnly': (props: GenericChildrenType) => JSX.Element;
|
|
24
|
-
'LgOnly': (props: GenericChildrenType) => JSX.Element;
|
|
25
|
-
'XlOnly': (props: GenericChildrenType) => JSX.Element;
|
|
26
|
-
'2xlOnly': (props: GenericChildrenType) => JSX.Element;
|
|
27
|
-
'3xlOnly': (props: GenericChildrenType) => JSX.Element;
|
|
28
|
-
'4xlOnly': (props: GenericChildrenType) => JSX.Element;
|
|
29
|
-
'5xlOnly': (props: GenericChildrenType) => JSX.Element;
|
|
30
|
-
'6xlOnly': (props: GenericChildrenType) => JSX.Element;
|
|
31
|
-
'7xlOnly': (props: GenericChildrenType) => JSX.Element;
|
|
32
|
-
'XsOrLarger': (props: GenericChildrenType) => JSX.Element;
|
|
33
|
-
'SmOrLarger': (props: GenericChildrenType) => JSX.Element;
|
|
34
|
-
'MdOrLarger': (props: GenericChildrenType) => JSX.Element;
|
|
35
|
-
'LgOrLarger': (props: GenericChildrenType) => JSX.Element;
|
|
36
|
-
'XlOrLarger': (props: GenericChildrenType) => JSX.Element;
|
|
37
|
-
[key: string]: (props: GenericChildrenType) => JSX.Element;
|
|
38
|
-
};
|
|
39
|
-
/**
|
|
40
|
-
* A React hook for responding to container size changes using Tailwind CSS container query breakpoints.
|
|
41
|
-
*
|
|
42
|
-
* This hook uses ResizeObserver to detect when a container element crosses breakpoint thresholds,
|
|
43
|
-
* enabling component-level responsive behavior independent of viewport size.
|
|
44
|
-
*
|
|
45
|
-
* @param propQueries - Optional custom container size queries in the format { queryName: 'minWidth' }
|
|
46
|
-
* Example: { large: '600px', extraLarge: '48rem' }
|
|
47
|
-
*
|
|
48
|
-
* @returns An object containing:
|
|
49
|
-
* - containerRef: A ref to attach to your container element
|
|
50
|
-
* - queries: Boolean flags for each breakpoint (isXs, isSm, isMd, etc.) and custom queries
|
|
51
|
-
* - components: React components for conditional rendering (XsOnly, SmOrLarger, etc.)
|
|
52
|
-
*
|
|
53
|
-
* @example
|
|
54
|
-
* ```tsx
|
|
55
|
-
* const MyComponent = () => {
|
|
56
|
-
* const { containerRef, queries, components } = useContainerQueries()
|
|
57
|
-
* const { MdOrLarger } = components
|
|
58
|
-
*
|
|
59
|
-
* return (
|
|
60
|
-
* <div ref={containerRef}>
|
|
61
|
-
* {queries.isSm && <p>Small container</p>}
|
|
62
|
-
* <MdOrLarger>
|
|
63
|
-
* <p>Medium or larger container</p>
|
|
64
|
-
* </MdOrLarger>
|
|
65
|
-
* </div>
|
|
66
|
-
* )
|
|
67
|
-
* }
|
|
68
|
-
* ```
|
|
69
|
-
*
|
|
70
|
-
* @example With custom queries
|
|
71
|
-
* ```tsx
|
|
72
|
-
* const { containerRef, queries, components } = useContainerQueries({
|
|
73
|
-
* compact: '400px',
|
|
74
|
-
* spacious: '800px',
|
|
75
|
-
* })
|
|
76
|
-
* const { Compact, Spacious } = components
|
|
77
|
-
*
|
|
78
|
-
* return (
|
|
79
|
-
* <div ref={containerRef}>
|
|
80
|
-
* <Compact><p>Compact view</p></Compact>
|
|
81
|
-
* <Spacious><p>Spacious view</p></Spacious>
|
|
82
|
-
* </div>
|
|
83
|
-
* )
|
|
84
|
-
* ```
|
|
85
|
-
*/
|
|
86
|
-
export declare const useContainerQueries: (propQueries?: Props) => {
|
|
87
|
-
containerRef: React.RefCallback<HTMLElement>;
|
|
88
|
-
queries: ContainerQueries;
|
|
89
|
-
components: ContainerComponents;
|
|
90
|
-
};
|
|
91
|
-
export {};
|
|
@@ -1,242 +0,0 @@
|
|
|
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, components } = useContainerQueries({
|
|
8
|
-
compact: '400px',
|
|
9
|
-
wide: '800px',
|
|
10
|
-
})
|
|
11
|
-
const { MdOrLarger, Compact } = components
|
|
12
|
-
|
|
13
|
-
return (
|
|
14
|
-
<div ref={containerRef} data-testid="container">
|
|
15
|
-
{queries.isSm && <button type="button">Small query boolean</button>}
|
|
16
|
-
<MdOrLarger>
|
|
17
|
-
<button type="button">Medium or larger component</button>
|
|
18
|
-
</MdOrLarger>
|
|
19
|
-
{queries.compact && <button type="button">Compact query boolean</button>}
|
|
20
|
-
<Compact>
|
|
21
|
-
<button type="button">Compact component</button>
|
|
22
|
-
</Compact>
|
|
23
|
-
</div>
|
|
24
|
-
)
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// Mock ResizeObserver
|
|
28
|
-
class ResizeObserverMock {
|
|
29
|
-
callback: ResizeObserverCallback
|
|
30
|
-
elements: Set<Element>
|
|
31
|
-
|
|
32
|
-
constructor(callback: ResizeObserverCallback) {
|
|
33
|
-
this.callback = callback
|
|
34
|
-
this.elements = new Set()
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
observe(target: Element): void {
|
|
38
|
-
this.elements.add(target)
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
unobserve(target: Element): void {
|
|
42
|
-
this.elements.delete(target)
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
disconnect(): void {
|
|
46
|
-
this.elements.clear()
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// Helper method to trigger resize
|
|
50
|
-
trigger(width: number): void {
|
|
51
|
-
const entries: ResizeObserverEntry[] = Array.from(this.elements).map((element) => ({
|
|
52
|
-
target: element,
|
|
53
|
-
contentRect: {
|
|
54
|
-
width,
|
|
55
|
-
height: 100,
|
|
56
|
-
top: 0,
|
|
57
|
-
left: 0,
|
|
58
|
-
bottom: 100,
|
|
59
|
-
right: width,
|
|
60
|
-
x: 0,
|
|
61
|
-
y: 0,
|
|
62
|
-
} as DOMRectReadOnly,
|
|
63
|
-
borderBoxSize: [
|
|
64
|
-
{
|
|
65
|
-
inlineSize: width,
|
|
66
|
-
blockSize: 100,
|
|
67
|
-
},
|
|
68
|
-
],
|
|
69
|
-
contentBoxSize: [
|
|
70
|
-
{
|
|
71
|
-
inlineSize: width,
|
|
72
|
-
blockSize: 100,
|
|
73
|
-
},
|
|
74
|
-
],
|
|
75
|
-
devicePixelContentBoxSize: [
|
|
76
|
-
{
|
|
77
|
-
inlineSize: width,
|
|
78
|
-
blockSize: 100,
|
|
79
|
-
},
|
|
80
|
-
],
|
|
81
|
-
})) as ResizeObserverEntry[]
|
|
82
|
-
|
|
83
|
-
this.callback(entries, this as unknown as ResizeObserver)
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
let resizeObserverInstance: ResizeObserverMock | null = null
|
|
88
|
-
|
|
89
|
-
const setupResizeObserver = (): ResizeObserverMock => {
|
|
90
|
-
const mockObserver = vi.fn((callback: ResizeObserverCallback) => {
|
|
91
|
-
resizeObserverInstance = new ResizeObserverMock(callback)
|
|
92
|
-
return resizeObserverInstance
|
|
93
|
-
})
|
|
94
|
-
|
|
95
|
-
global.ResizeObserver = mockObserver as unknown as typeof ResizeObserver
|
|
96
|
-
|
|
97
|
-
return resizeObserverInstance!
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
describe('useContainerQueries()', () => {
|
|
101
|
-
beforeEach(() => {
|
|
102
|
-
resizeObserverInstance = null
|
|
103
|
-
})
|
|
104
|
-
|
|
105
|
-
afterEach(() => {
|
|
106
|
-
vi.restoreAllMocks()
|
|
107
|
-
})
|
|
108
|
-
|
|
109
|
-
it('shows and hides content based on Tailwind container breakpoints', async () => {
|
|
110
|
-
setupResizeObserver()
|
|
111
|
-
|
|
112
|
-
// Mock getBoundingClientRect to return a small width initially
|
|
113
|
-
const mockGetBoundingClientRect = vi.fn(() => ({
|
|
114
|
-
width: 300,
|
|
115
|
-
height: 100,
|
|
116
|
-
top: 0,
|
|
117
|
-
left: 0,
|
|
118
|
-
bottom: 100,
|
|
119
|
-
right: 300,
|
|
120
|
-
x: 0,
|
|
121
|
-
y: 0,
|
|
122
|
-
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
123
|
-
toJSON: () => {},
|
|
124
|
-
}))
|
|
125
|
-
Element.prototype.getBoundingClientRect = mockGetBoundingClientRect
|
|
126
|
-
|
|
127
|
-
render(<ExampleComponent />)
|
|
128
|
-
|
|
129
|
-
// Initially at 300px, should not show sm (384px) or md (448px) content
|
|
130
|
-
expect(screen.queryByRole('button', { name: /Small query boolean/i })).not.toBeInTheDocument()
|
|
131
|
-
expect(
|
|
132
|
-
screen.queryByRole('button', { name: /Medium or larger component/i }),
|
|
133
|
-
).not.toBeInTheDocument()
|
|
134
|
-
|
|
135
|
-
// Trigger resize to 400px (sm breakpoint is 384px)
|
|
136
|
-
await act(async () => {
|
|
137
|
-
resizeObserverInstance?.trigger(400)
|
|
138
|
-
})
|
|
139
|
-
|
|
140
|
-
expect(screen.queryByRole('button', { name: /Small query boolean/i })).toBeInTheDocument()
|
|
141
|
-
expect(
|
|
142
|
-
screen.queryByRole('button', { name: /Medium or larger component/i }),
|
|
143
|
-
).not.toBeInTheDocument()
|
|
144
|
-
|
|
145
|
-
// Trigger resize to 500px (md breakpoint is 448px)
|
|
146
|
-
await act(async () => {
|
|
147
|
-
resizeObserverInstance?.trigger(500)
|
|
148
|
-
})
|
|
149
|
-
|
|
150
|
-
expect(screen.queryByRole('button', { name: /Small query boolean/i })).toBeInTheDocument()
|
|
151
|
-
expect(
|
|
152
|
-
screen.queryByRole('button', { name: /Medium or larger component/i }),
|
|
153
|
-
).toBeInTheDocument()
|
|
154
|
-
})
|
|
155
|
-
|
|
156
|
-
it('shows and hides content based on custom queries', async () => {
|
|
157
|
-
setupResizeObserver()
|
|
158
|
-
|
|
159
|
-
const mockGetBoundingClientRect = vi.fn(() => ({
|
|
160
|
-
width: 300,
|
|
161
|
-
height: 100,
|
|
162
|
-
top: 0,
|
|
163
|
-
left: 0,
|
|
164
|
-
bottom: 100,
|
|
165
|
-
right: 300,
|
|
166
|
-
x: 0,
|
|
167
|
-
y: 0,
|
|
168
|
-
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
169
|
-
toJSON: () => {},
|
|
170
|
-
}))
|
|
171
|
-
Element.prototype.getBoundingClientRect = mockGetBoundingClientRect
|
|
172
|
-
|
|
173
|
-
render(<ExampleComponent />)
|
|
174
|
-
|
|
175
|
-
// Initially at 300px, custom 'compact' query (400px) should not match
|
|
176
|
-
expect(screen.queryByRole('button', { name: /Compact query boolean/i })).not.toBeInTheDocument()
|
|
177
|
-
expect(screen.queryByRole('button', { name: /Compact component/i })).not.toBeInTheDocument()
|
|
178
|
-
|
|
179
|
-
// Trigger resize to 450px (compact is 400px)
|
|
180
|
-
await act(async () => {
|
|
181
|
-
resizeObserverInstance?.trigger(450)
|
|
182
|
-
})
|
|
183
|
-
|
|
184
|
-
expect(screen.queryByRole('button', { name: /Compact query boolean/i })).toBeInTheDocument()
|
|
185
|
-
expect(screen.queryByRole('button', { name: /Compact component/i })).toBeInTheDocument()
|
|
186
|
-
})
|
|
187
|
-
|
|
188
|
-
it('returns SSR-safe defaults when window is undefined', () => {
|
|
189
|
-
// This test verifies the SSR code path exists
|
|
190
|
-
// In actual SSR environment, the hook returns safe defaults
|
|
191
|
-
expect(useContainerQueries).toBeDefined()
|
|
192
|
-
})
|
|
193
|
-
|
|
194
|
-
it('cleans up ResizeObserver on unmount', () => {
|
|
195
|
-
setupResizeObserver()
|
|
196
|
-
|
|
197
|
-
const mockGetBoundingClientRect = vi.fn(() => ({
|
|
198
|
-
width: 500,
|
|
199
|
-
height: 100,
|
|
200
|
-
top: 0,
|
|
201
|
-
left: 0,
|
|
202
|
-
bottom: 100,
|
|
203
|
-
right: 500,
|
|
204
|
-
x: 0,
|
|
205
|
-
y: 0,
|
|
206
|
-
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
207
|
-
toJSON: () => {},
|
|
208
|
-
}))
|
|
209
|
-
Element.prototype.getBoundingClientRect = mockGetBoundingClientRect
|
|
210
|
-
|
|
211
|
-
const { unmount } = render(<ExampleComponent />)
|
|
212
|
-
|
|
213
|
-
const disconnectSpy = vi.spyOn(resizeObserverInstance!, 'disconnect')
|
|
214
|
-
|
|
215
|
-
unmount()
|
|
216
|
-
|
|
217
|
-
expect(disconnectSpy).toHaveBeenCalled()
|
|
218
|
-
})
|
|
219
|
-
|
|
220
|
-
it('observes the container element', () => {
|
|
221
|
-
setupResizeObserver()
|
|
222
|
-
|
|
223
|
-
const mockGetBoundingClientRect = vi.fn(() => ({
|
|
224
|
-
width: 500,
|
|
225
|
-
height: 100,
|
|
226
|
-
top: 0,
|
|
227
|
-
left: 0,
|
|
228
|
-
bottom: 100,
|
|
229
|
-
right: 500,
|
|
230
|
-
x: 0,
|
|
231
|
-
y: 0,
|
|
232
|
-
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
233
|
-
toJSON: () => {},
|
|
234
|
-
}))
|
|
235
|
-
Element.prototype.getBoundingClientRect = mockGetBoundingClientRect
|
|
236
|
-
|
|
237
|
-
render(<ExampleComponent />)
|
|
238
|
-
|
|
239
|
-
// Verify that the ResizeObserver is observing the container
|
|
240
|
-
expect(resizeObserverInstance?.elements.size).toBe(1)
|
|
241
|
-
})
|
|
242
|
-
})
|
|
@@ -1,333 +0,0 @@
|
|
|
1
|
-
/* eslint-disable react-hooks/rules-of-hooks */
|
|
2
|
-
import React, { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from 'react'
|
|
3
|
-
|
|
4
|
-
type Props = Record<string, string>
|
|
5
|
-
type GenericChildrenType = { children?: ReactNode }
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Tailwind CSS default container query breakpoints
|
|
9
|
-
* These match the default values from @tailwindcss/container-queries plugin
|
|
10
|
-
*/
|
|
11
|
-
const DEFAULT_BREAKPOINTS = {
|
|
12
|
-
'xs': '20rem', // 320px
|
|
13
|
-
'sm': '24rem', // 384px
|
|
14
|
-
'md': '28rem', // 448px
|
|
15
|
-
'lg': '32rem', // 512px
|
|
16
|
-
'xl': '36rem', // 576px
|
|
17
|
-
'2xl': '42rem', // 672px
|
|
18
|
-
'3xl': '48rem', // 768px
|
|
19
|
-
'4xl': '56rem', // 896px
|
|
20
|
-
'5xl': '64rem', // 1024px
|
|
21
|
-
'6xl': '72rem', // 1152px
|
|
22
|
-
'7xl': '80rem', // 1280px
|
|
23
|
-
} as const
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Convert rem/px values to pixels for comparison
|
|
27
|
-
*/
|
|
28
|
-
const parseBreakpointValue = (value: string): number => {
|
|
29
|
-
if (value.endsWith('rem')) {
|
|
30
|
-
return parseFloat(value) * 16 // Assuming 1rem = 16px
|
|
31
|
-
}
|
|
32
|
-
if (value.endsWith('px')) {
|
|
33
|
-
return parseFloat(value)
|
|
34
|
-
}
|
|
35
|
-
return parseFloat(value)
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
type HelperComponentProps = {
|
|
39
|
-
children?: ReactNode
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
type ContainerQueries = {
|
|
43
|
-
isXs: boolean
|
|
44
|
-
isSm: boolean
|
|
45
|
-
isMd: boolean
|
|
46
|
-
isLg: boolean
|
|
47
|
-
isXl: boolean
|
|
48
|
-
is2xl: boolean
|
|
49
|
-
is3xl: boolean
|
|
50
|
-
is4xl: boolean
|
|
51
|
-
is5xl: boolean
|
|
52
|
-
is6xl: boolean
|
|
53
|
-
is7xl: boolean
|
|
54
|
-
[key: string]: boolean
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
type ContainerComponents = {
|
|
58
|
-
'XsOnly': (props: GenericChildrenType) => JSX.Element
|
|
59
|
-
'SmOnly': (props: GenericChildrenType) => JSX.Element
|
|
60
|
-
'MdOnly': (props: GenericChildrenType) => JSX.Element
|
|
61
|
-
'LgOnly': (props: GenericChildrenType) => JSX.Element
|
|
62
|
-
'XlOnly': (props: GenericChildrenType) => JSX.Element
|
|
63
|
-
'2xlOnly': (props: GenericChildrenType) => JSX.Element
|
|
64
|
-
'3xlOnly': (props: GenericChildrenType) => JSX.Element
|
|
65
|
-
'4xlOnly': (props: GenericChildrenType) => JSX.Element
|
|
66
|
-
'5xlOnly': (props: GenericChildrenType) => JSX.Element
|
|
67
|
-
'6xlOnly': (props: GenericChildrenType) => JSX.Element
|
|
68
|
-
'7xlOnly': (props: GenericChildrenType) => JSX.Element
|
|
69
|
-
'XsOrLarger': (props: GenericChildrenType) => JSX.Element
|
|
70
|
-
'SmOrLarger': (props: GenericChildrenType) => JSX.Element
|
|
71
|
-
'MdOrLarger': (props: GenericChildrenType) => JSX.Element
|
|
72
|
-
'LgOrLarger': (props: GenericChildrenType) => JSX.Element
|
|
73
|
-
'XlOrLarger': (props: GenericChildrenType) => JSX.Element
|
|
74
|
-
[key: string]: (props: GenericChildrenType) => JSX.Element
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* A React hook for responding to container size changes using Tailwind CSS container query breakpoints.
|
|
79
|
-
*
|
|
80
|
-
* This hook uses ResizeObserver to detect when a container element crosses breakpoint thresholds,
|
|
81
|
-
* enabling component-level responsive behavior independent of viewport size.
|
|
82
|
-
*
|
|
83
|
-
* @param propQueries - Optional custom container size queries in the format { queryName: 'minWidth' }
|
|
84
|
-
* Example: { large: '600px', extraLarge: '48rem' }
|
|
85
|
-
*
|
|
86
|
-
* @returns An object containing:
|
|
87
|
-
* - containerRef: A ref to attach to your container element
|
|
88
|
-
* - queries: Boolean flags for each breakpoint (isXs, isSm, isMd, etc.) and custom queries
|
|
89
|
-
* - components: React components for conditional rendering (XsOnly, SmOrLarger, etc.)
|
|
90
|
-
*
|
|
91
|
-
* @example
|
|
92
|
-
* ```tsx
|
|
93
|
-
* const MyComponent = () => {
|
|
94
|
-
* const { containerRef, queries, components } = useContainerQueries()
|
|
95
|
-
* const { MdOrLarger } = components
|
|
96
|
-
*
|
|
97
|
-
* return (
|
|
98
|
-
* <div ref={containerRef}>
|
|
99
|
-
* {queries.isSm && <p>Small container</p>}
|
|
100
|
-
* <MdOrLarger>
|
|
101
|
-
* <p>Medium or larger container</p>
|
|
102
|
-
* </MdOrLarger>
|
|
103
|
-
* </div>
|
|
104
|
-
* )
|
|
105
|
-
* }
|
|
106
|
-
* ```
|
|
107
|
-
*
|
|
108
|
-
* @example With custom queries
|
|
109
|
-
* ```tsx
|
|
110
|
-
* const { containerRef, queries, components } = useContainerQueries({
|
|
111
|
-
* compact: '400px',
|
|
112
|
-
* spacious: '800px',
|
|
113
|
-
* })
|
|
114
|
-
* const { Compact, Spacious } = components
|
|
115
|
-
*
|
|
116
|
-
* return (
|
|
117
|
-
* <div ref={containerRef}>
|
|
118
|
-
* <Compact><p>Compact view</p></Compact>
|
|
119
|
-
* <Spacious><p>Spacious view</p></Spacious>
|
|
120
|
-
* </div>
|
|
121
|
-
* )
|
|
122
|
-
* ```
|
|
123
|
-
*/
|
|
124
|
-
export const useContainerQueries = (
|
|
125
|
-
propQueries: Props = {},
|
|
126
|
-
): {
|
|
127
|
-
containerRef: React.RefCallback<HTMLElement>
|
|
128
|
-
queries: ContainerQueries
|
|
129
|
-
components: ContainerComponents
|
|
130
|
-
} => {
|
|
131
|
-
// SSR support - return safe defaults when window is undefined
|
|
132
|
-
if (typeof window === 'undefined') {
|
|
133
|
-
return {
|
|
134
|
-
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
135
|
-
containerRef: () => {},
|
|
136
|
-
queries: {
|
|
137
|
-
isXs: false,
|
|
138
|
-
isSm: false,
|
|
139
|
-
isMd: false,
|
|
140
|
-
isLg: false,
|
|
141
|
-
isXl: false,
|
|
142
|
-
is2xl: false,
|
|
143
|
-
is3xl: false,
|
|
144
|
-
is4xl: false,
|
|
145
|
-
is5xl: false,
|
|
146
|
-
is6xl: false,
|
|
147
|
-
is7xl: true, // Default to largest for SSR
|
|
148
|
-
},
|
|
149
|
-
components: {
|
|
150
|
-
'XsOnly': () => <></>,
|
|
151
|
-
'SmOnly': () => <></>,
|
|
152
|
-
'MdOnly': () => <></>,
|
|
153
|
-
'LgOnly': () => <></>,
|
|
154
|
-
'XlOnly': () => <></>,
|
|
155
|
-
'2xlOnly': () => <></>,
|
|
156
|
-
'3xlOnly': () => <></>,
|
|
157
|
-
'4xlOnly': () => <></>,
|
|
158
|
-
'5xlOnly': () => <></>,
|
|
159
|
-
'6xlOnly': () => <></>,
|
|
160
|
-
'7xlOnly': (props: HelperComponentProps) => <>{props.children}</>,
|
|
161
|
-
'XsOrLarger': (props: HelperComponentProps) => <>{props.children}</>,
|
|
162
|
-
'SmOrLarger': (props: HelperComponentProps) => <>{props.children}</>,
|
|
163
|
-
'MdOrLarger': (props: HelperComponentProps) => <>{props.children}</>,
|
|
164
|
-
'LgOrLarger': (props: HelperComponentProps) => <>{props.children}</>,
|
|
165
|
-
'XlOrLarger': (props: HelperComponentProps) => <>{props.children}</>,
|
|
166
|
-
},
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// Parse all breakpoints to pixel values for comparison
|
|
171
|
-
const breakpointsPx = useMemo(
|
|
172
|
-
() =>
|
|
173
|
-
Object.entries(DEFAULT_BREAKPOINTS).reduce(
|
|
174
|
-
(acc, [key, value]) => {
|
|
175
|
-
acc[key] = parseBreakpointValue(value)
|
|
176
|
-
return acc
|
|
177
|
-
},
|
|
178
|
-
{} as Record<string, number>,
|
|
179
|
-
),
|
|
180
|
-
[],
|
|
181
|
-
)
|
|
182
|
-
|
|
183
|
-
// Parse custom queries
|
|
184
|
-
const customQueriesPx = useMemo(
|
|
185
|
-
() =>
|
|
186
|
-
Object.entries(propQueries).reduce(
|
|
187
|
-
(acc, [key, value]) => {
|
|
188
|
-
acc[key] = parseBreakpointValue(value)
|
|
189
|
-
return acc
|
|
190
|
-
},
|
|
191
|
-
{} as Record<string, number>,
|
|
192
|
-
),
|
|
193
|
-
[propQueries],
|
|
194
|
-
)
|
|
195
|
-
|
|
196
|
-
// State to track container width
|
|
197
|
-
const [containerWidth, setContainerWidth] = useState<number>(0)
|
|
198
|
-
|
|
199
|
-
// ResizeObserver ref
|
|
200
|
-
const resizeObserverRef = useRef<ResizeObserver | null>(null)
|
|
201
|
-
|
|
202
|
-
// Callback ref for the container element
|
|
203
|
-
const containerRef = useCallback((node: HTMLElement | null) => {
|
|
204
|
-
// Cleanup previous observer
|
|
205
|
-
if (resizeObserverRef.current) {
|
|
206
|
-
resizeObserverRef.current.disconnect()
|
|
207
|
-
resizeObserverRef.current = null
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
if (node) {
|
|
211
|
-
// Create new ResizeObserver
|
|
212
|
-
resizeObserverRef.current = new ResizeObserver((entries) => {
|
|
213
|
-
for (const entry of entries) {
|
|
214
|
-
// Use borderBoxSize for more accurate measurements
|
|
215
|
-
const width = entry.borderBoxSize?.[0]?.inlineSize ?? entry.contentRect.width
|
|
216
|
-
setContainerWidth(width)
|
|
217
|
-
}
|
|
218
|
-
})
|
|
219
|
-
|
|
220
|
-
resizeObserverRef.current.observe(node)
|
|
221
|
-
|
|
222
|
-
// Set initial width
|
|
223
|
-
const width = node.getBoundingClientRect().width
|
|
224
|
-
setContainerWidth(width)
|
|
225
|
-
}
|
|
226
|
-
}, [])
|
|
227
|
-
|
|
228
|
-
// Cleanup on unmount
|
|
229
|
-
useEffect(
|
|
230
|
-
() => () => {
|
|
231
|
-
if (resizeObserverRef.current) {
|
|
232
|
-
resizeObserverRef.current.disconnect()
|
|
233
|
-
}
|
|
234
|
-
},
|
|
235
|
-
[],
|
|
236
|
-
)
|
|
237
|
-
|
|
238
|
-
// Calculate breakpoint matches based on container width
|
|
239
|
-
const breakpointMatches = useMemo(
|
|
240
|
-
() => ({
|
|
241
|
-
isXs: containerWidth >= breakpointsPx.xs,
|
|
242
|
-
isSm: containerWidth >= breakpointsPx.sm,
|
|
243
|
-
isMd: containerWidth >= breakpointsPx.md,
|
|
244
|
-
isLg: containerWidth >= breakpointsPx.lg,
|
|
245
|
-
isXl: containerWidth >= breakpointsPx.xl,
|
|
246
|
-
is2xl: containerWidth >= breakpointsPx['2xl'],
|
|
247
|
-
is3xl: containerWidth >= breakpointsPx['3xl'],
|
|
248
|
-
is4xl: containerWidth >= breakpointsPx['4xl'],
|
|
249
|
-
is5xl: containerWidth >= breakpointsPx['5xl'],
|
|
250
|
-
is6xl: containerWidth >= breakpointsPx['6xl'],
|
|
251
|
-
is7xl: containerWidth >= breakpointsPx['7xl'],
|
|
252
|
-
}),
|
|
253
|
-
[containerWidth, breakpointsPx],
|
|
254
|
-
)
|
|
255
|
-
|
|
256
|
-
// Calculate custom query matches
|
|
257
|
-
const customMatches = useMemo(
|
|
258
|
-
() =>
|
|
259
|
-
Object.entries(customQueriesPx).reduce(
|
|
260
|
-
(acc, [key, value]) => {
|
|
261
|
-
acc[key] = containerWidth >= value
|
|
262
|
-
return acc
|
|
263
|
-
},
|
|
264
|
-
{} as Record<string, boolean>,
|
|
265
|
-
),
|
|
266
|
-
[containerWidth, customQueriesPx],
|
|
267
|
-
)
|
|
268
|
-
|
|
269
|
-
// Helper function to check if container is at exact breakpoint (not larger)
|
|
270
|
-
const isExactBreakpoint = useCallback(
|
|
271
|
-
(breakpoint: keyof typeof breakpointsPx): boolean => {
|
|
272
|
-
const sortedBreakpoints = Object.entries(breakpointsPx).sort(([, a], [, b]) => a - b)
|
|
273
|
-
const currentIndex = sortedBreakpoints.findIndex(([key]) => key === breakpoint)
|
|
274
|
-
const nextBreakpoint = sortedBreakpoints[currentIndex + 1]
|
|
275
|
-
|
|
276
|
-
const minWidth = breakpointsPx[breakpoint]
|
|
277
|
-
const maxWidth = nextBreakpoint ? nextBreakpoint[1] : Infinity
|
|
278
|
-
|
|
279
|
-
return containerWidth >= minWidth && containerWidth < maxWidth
|
|
280
|
-
},
|
|
281
|
-
[containerWidth, breakpointsPx],
|
|
282
|
-
)
|
|
283
|
-
|
|
284
|
-
// Create helper components for Tailwind breakpoints
|
|
285
|
-
const components = useMemo(
|
|
286
|
-
() => ({
|
|
287
|
-
'XsOnly': (props: HelperComponentProps) => <>{isExactBreakpoint('xs') && props.children}</>,
|
|
288
|
-
'SmOnly': (props: HelperComponentProps) => <>{isExactBreakpoint('sm') && props.children}</>,
|
|
289
|
-
'MdOnly': (props: HelperComponentProps) => <>{isExactBreakpoint('md') && props.children}</>,
|
|
290
|
-
'LgOnly': (props: HelperComponentProps) => <>{isExactBreakpoint('lg') && props.children}</>,
|
|
291
|
-
'XlOnly': (props: HelperComponentProps) => <>{isExactBreakpoint('xl') && props.children}</>,
|
|
292
|
-
'2xlOnly': (props: HelperComponentProps) => <>{isExactBreakpoint('2xl') && props.children}</>,
|
|
293
|
-
'3xlOnly': (props: HelperComponentProps) => <>{isExactBreakpoint('3xl') && props.children}</>,
|
|
294
|
-
'4xlOnly': (props: HelperComponentProps) => <>{isExactBreakpoint('4xl') && props.children}</>,
|
|
295
|
-
'5xlOnly': (props: HelperComponentProps) => <>{isExactBreakpoint('5xl') && props.children}</>,
|
|
296
|
-
'6xlOnly': (props: HelperComponentProps) => <>{isExactBreakpoint('6xl') && props.children}</>,
|
|
297
|
-
'7xlOnly': (props: HelperComponentProps) => <>{isExactBreakpoint('7xl') && props.children}</>,
|
|
298
|
-
'XsOrLarger': (props: HelperComponentProps) => (
|
|
299
|
-
<>{breakpointMatches.isXs && props.children}</>
|
|
300
|
-
),
|
|
301
|
-
'SmOrLarger': (props: HelperComponentProps) => (
|
|
302
|
-
<>{breakpointMatches.isSm && props.children}</>
|
|
303
|
-
),
|
|
304
|
-
'MdOrLarger': (props: HelperComponentProps) => (
|
|
305
|
-
<>{breakpointMatches.isMd && props.children}</>
|
|
306
|
-
),
|
|
307
|
-
'LgOrLarger': (props: HelperComponentProps) => (
|
|
308
|
-
<>{breakpointMatches.isLg && props.children}</>
|
|
309
|
-
),
|
|
310
|
-
'XlOrLarger': (props: HelperComponentProps) => (
|
|
311
|
-
<>{breakpointMatches.isXl && props.children}</>
|
|
312
|
-
),
|
|
313
|
-
// Custom query components
|
|
314
|
-
...Object.keys(customQueriesPx).reduce(
|
|
315
|
-
(acc, key) => {
|
|
316
|
-
const componentName = key.charAt(0).toUpperCase() + key.slice(1)
|
|
317
|
-
acc[componentName] = (props: HelperComponentProps): JSX.Element => (
|
|
318
|
-
<>{customMatches[key] && props.children}</>
|
|
319
|
-
)
|
|
320
|
-
return acc
|
|
321
|
-
},
|
|
322
|
-
{} as Record<string, (props: GenericChildrenType) => JSX.Element>,
|
|
323
|
-
),
|
|
324
|
-
}),
|
|
325
|
-
[breakpointMatches, customMatches, isExactBreakpoint, customQueriesPx],
|
|
326
|
-
)
|
|
327
|
-
|
|
328
|
-
return {
|
|
329
|
-
containerRef,
|
|
330
|
-
queries: { ...breakpointMatches, ...customMatches },
|
|
331
|
-
components,
|
|
332
|
-
}
|
|
333
|
-
}
|