@pyreon/elements 0.24.4 → 0.24.6
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/package.json +10 -12
- package/src/Element/component.tsx +0 -315
- package/src/Element/constants.ts +0 -96
- package/src/Element/index.ts +0 -6
- package/src/Element/types.ts +0 -168
- package/src/Element/utils.ts +0 -15
- package/src/List/component.tsx +0 -105
- package/src/List/index.ts +0 -5
- package/src/Overlay/component.tsx +0 -140
- package/src/Overlay/context.tsx +0 -36
- package/src/Overlay/index.ts +0 -7
- package/src/Overlay/positioning.ts +0 -191
- package/src/Overlay/useOverlay.tsx +0 -461
- package/src/Portal/component.tsx +0 -54
- package/src/Portal/index.ts +0 -5
- package/src/Text/component.tsx +0 -67
- package/src/Text/index.ts +0 -5
- package/src/Text/styled.ts +0 -30
- package/src/Util/component.tsx +0 -43
- package/src/Util/index.ts +0 -5
- package/src/__tests__/Content.test.tsx +0 -123
- package/src/__tests__/Element-slot-reactivity.browser.test.tsx +0 -152
- package/src/__tests__/Element.test.ts +0 -819
- package/src/__tests__/Iterator.test.ts +0 -492
- package/src/__tests__/Iterator.types.test.ts +0 -237
- package/src/__tests__/List.test.ts +0 -199
- package/src/__tests__/Overlay.test.ts +0 -492
- package/src/__tests__/Portal.test.ts +0 -156
- package/src/__tests__/Text.test.ts +0 -274
- package/src/__tests__/Util.test.ts +0 -63
- package/src/__tests__/Wrapper-innerhtml.test.tsx +0 -178
- package/src/__tests__/Wrapper.test.tsx +0 -196
- package/src/__tests__/elements.browser.test.tsx +0 -132
- package/src/__tests__/equalBeforeAfter.test.ts +0 -122
- package/src/__tests__/helpers.test.ts +0 -65
- package/src/__tests__/integration.test.tsx +0 -118
- package/src/__tests__/internElementBundle.test.ts +0 -102
- package/src/__tests__/iterator-function-children.test.tsx +0 -120
- package/src/__tests__/native-markers.test.ts +0 -13
- package/src/__tests__/overlayContext.test.tsx +0 -78
- package/src/__tests__/perf-stress.browser.test.tsx +0 -119
- package/src/__tests__/positioning.test.ts +0 -90
- package/src/__tests__/responsiveProps.test.ts +0 -328
- package/src/__tests__/slot-component-reference.test.tsx +0 -157
- package/src/__tests__/useOverlay.test.ts +0 -1336
- package/src/__tests__/utils.test.ts +0 -69
- package/src/__tests__/wrapper-block-cascade.test.ts +0 -121
- package/src/constants.ts +0 -1
- package/src/env.d.ts +0 -6
- package/src/helpers/Content/component.tsx +0 -75
- package/src/helpers/Content/index.ts +0 -3
- package/src/helpers/Content/styled.ts +0 -105
- package/src/helpers/Content/types.ts +0 -49
- package/src/helpers/Iterator/component.tsx +0 -316
- package/src/helpers/Iterator/index.ts +0 -30
- package/src/helpers/Iterator/types.ts +0 -138
- package/src/helpers/Wrapper/component.tsx +0 -180
- package/src/helpers/Wrapper/constants.ts +0 -10
- package/src/helpers/Wrapper/index.ts +0 -3
- package/src/helpers/Wrapper/styled.ts +0 -64
- package/src/helpers/Wrapper/types.ts +0 -56
- package/src/helpers/Wrapper/utils.ts +0 -7
- package/src/helpers/index.ts +0 -4
- package/src/helpers/internElementBundle.ts +0 -37
- package/src/helpers/isPyreonComponent.ts +0 -46
- package/src/index.ts +0 -42
- package/src/manifest.ts +0 -190
- package/src/tests/manifest-snapshot.test.ts +0 -45
- package/src/types.ts +0 -112
- package/src/utils.ts +0 -5
|
@@ -1,461 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Core hook powering the Overlay component. Manages open/close state, DOM
|
|
3
|
-
* event listeners (click, hover, scroll, resize, ESC key), and dynamic
|
|
4
|
-
* positioning of overlay content relative to its trigger. Supports dropdown,
|
|
5
|
-
* tooltip, popover, and modal types with automatic edge-of-viewport flipping.
|
|
6
|
-
* Event handlers are throttled for performance, and nested overlay blocking
|
|
7
|
-
* is coordinated through the overlay context.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { signal } from '@pyreon/reactivity'
|
|
11
|
-
import { throttle } from '@pyreon/ui-core'
|
|
12
|
-
import { value } from '@pyreon/unistyle'
|
|
13
|
-
import { IS_DEVELOPMENT } from '../utils'
|
|
14
|
-
import Provider, { useOverlayContext } from './context'
|
|
15
|
-
import {
|
|
16
|
-
adjustForAncestor,
|
|
17
|
-
calcDropdownHorizontal,
|
|
18
|
-
calcDropdownVertical,
|
|
19
|
-
calcModalPos,
|
|
20
|
-
type Align,
|
|
21
|
-
type AlignX,
|
|
22
|
-
type AlignY,
|
|
23
|
-
type OverlayPosition,
|
|
24
|
-
} from './positioning'
|
|
25
|
-
|
|
26
|
-
export type UseOverlayProps = Partial<{
|
|
27
|
-
isOpen: boolean
|
|
28
|
-
openOn: 'click' | 'hover' | 'manual'
|
|
29
|
-
closeOn: 'click' | 'clickOnTrigger' | 'clickOutsideContent' | 'hover' | 'manual'
|
|
30
|
-
type: 'dropdown' | 'tooltip' | 'popover' | 'modal' | 'custom'
|
|
31
|
-
position: 'absolute' | 'fixed' | 'relative' | 'static'
|
|
32
|
-
align: Align
|
|
33
|
-
alignX: AlignX
|
|
34
|
-
alignY: AlignY
|
|
35
|
-
offsetX: number
|
|
36
|
-
offsetY: number
|
|
37
|
-
throttleDelay: number
|
|
38
|
-
parentContainer: HTMLElement | null
|
|
39
|
-
closeOnEsc: boolean
|
|
40
|
-
hoverDelay: number
|
|
41
|
-
disabled: boolean
|
|
42
|
-
onOpen: () => void
|
|
43
|
-
onClose: () => void
|
|
44
|
-
}>
|
|
45
|
-
|
|
46
|
-
// Reference counter for nested modals sharing document.body overflow lock.
|
|
47
|
-
let modalOverflowCount = 0
|
|
48
|
-
|
|
49
|
-
// Hoisted: closeOn values that count as "click-driven close". Inlined
|
|
50
|
-
// previously, allocating a fresh 3-element array on each click-listener
|
|
51
|
-
// setupListeners re-run. Ported from vitus-labs `804dd0e2`.
|
|
52
|
-
const CLICK_CLOSE_KINDS: ReadonlySet<string> = new Set([
|
|
53
|
-
'click',
|
|
54
|
-
'clickOnTrigger',
|
|
55
|
-
'clickOutsideContent',
|
|
56
|
-
])
|
|
57
|
-
|
|
58
|
-
const devWarn = (msg: string) => {
|
|
59
|
-
if (!IS_DEVELOPMENT) return
|
|
60
|
-
// oxlint-disable-next-line no-console
|
|
61
|
-
console.warn(msg)
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
type ComputeResult = {
|
|
66
|
-
pos: OverlayPosition
|
|
67
|
-
resolvedAlignX?: AlignX
|
|
68
|
-
resolvedAlignY?: AlignY
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const computePosition = (
|
|
72
|
-
type: string,
|
|
73
|
-
align: Align,
|
|
74
|
-
alignX: AlignX,
|
|
75
|
-
alignY: AlignY,
|
|
76
|
-
offsetX: number,
|
|
77
|
-
offsetY: number,
|
|
78
|
-
triggerEl: HTMLElement | null,
|
|
79
|
-
contentEl: HTMLElement | null,
|
|
80
|
-
ancestorOffset: { top: number; left: number },
|
|
81
|
-
): ComputeResult => {
|
|
82
|
-
const isDropdown = ['dropdown', 'tooltip', 'popover'].includes(type)
|
|
83
|
-
|
|
84
|
-
if (isDropdown && (!triggerEl || !contentEl)) {
|
|
85
|
-
devWarn(
|
|
86
|
-
`[@pyreon/elements] Overlay (${type}): ` +
|
|
87
|
-
`${triggerEl ? 'contentRef' : 'triggerRef'} is not attached. ` +
|
|
88
|
-
'Position cannot be calculated without both refs.',
|
|
89
|
-
)
|
|
90
|
-
return { pos: {} }
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
if (isDropdown && triggerEl && contentEl) {
|
|
94
|
-
const c = contentEl.getBoundingClientRect()
|
|
95
|
-
const t = triggerEl.getBoundingClientRect()
|
|
96
|
-
const result =
|
|
97
|
-
align === 'top' || align === 'bottom'
|
|
98
|
-
? calcDropdownVertical(c, t, align, alignX, offsetX, offsetY)
|
|
99
|
-
: calcDropdownHorizontal(c, t, align as 'left' | 'right', alignY, offsetX, offsetY)
|
|
100
|
-
|
|
101
|
-
return {
|
|
102
|
-
pos: adjustForAncestor(result.pos, ancestorOffset),
|
|
103
|
-
resolvedAlignX: result.resolvedAlignX,
|
|
104
|
-
resolvedAlignY: result.resolvedAlignY,
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
if (type === 'modal') {
|
|
109
|
-
if (!contentEl) {
|
|
110
|
-
devWarn(
|
|
111
|
-
'[@pyreon/elements] Overlay (modal): contentRef is not attached. ' +
|
|
112
|
-
'Modal position cannot be calculated without a content element.',
|
|
113
|
-
)
|
|
114
|
-
return { pos: {} }
|
|
115
|
-
}
|
|
116
|
-
const c = contentEl.getBoundingClientRect()
|
|
117
|
-
return {
|
|
118
|
-
pos: adjustForAncestor(calcModalPos(c, alignX, alignY, offsetX, offsetY), ancestorOffset),
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
return { pos: {} }
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
const processVisibilityEvent = (
|
|
126
|
-
e: Event,
|
|
127
|
-
active: boolean,
|
|
128
|
-
openOn: string,
|
|
129
|
-
closeOn: string,
|
|
130
|
-
isTrigger: (evt: Event) => boolean,
|
|
131
|
-
isContent: (evt: Event) => boolean,
|
|
132
|
-
showContent: () => void,
|
|
133
|
-
hideContent: () => void,
|
|
134
|
-
) => {
|
|
135
|
-
if (!active && openOn === 'click' && e.type === 'click' && isTrigger(e)) {
|
|
136
|
-
showContent()
|
|
137
|
-
return
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
if (!active) return
|
|
141
|
-
|
|
142
|
-
if (closeOn === 'hover' && e.type === 'scroll') {
|
|
143
|
-
hideContent()
|
|
144
|
-
return
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
if (e.type !== 'click') return
|
|
148
|
-
|
|
149
|
-
if (closeOn === 'click') {
|
|
150
|
-
hideContent()
|
|
151
|
-
} else if (closeOn === 'clickOnTrigger' && isTrigger(e)) {
|
|
152
|
-
hideContent()
|
|
153
|
-
} else if (closeOn === 'clickOutsideContent' && !isContent(e)) {
|
|
154
|
-
hideContent()
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
const useOverlay = ({
|
|
159
|
-
isOpen = false,
|
|
160
|
-
openOn = 'click',
|
|
161
|
-
closeOn = 'click',
|
|
162
|
-
type = 'dropdown',
|
|
163
|
-
position = 'fixed',
|
|
164
|
-
align = 'bottom',
|
|
165
|
-
alignX: propAlignX = 'left',
|
|
166
|
-
alignY: propAlignY = 'bottom',
|
|
167
|
-
offsetX = 0,
|
|
168
|
-
offsetY = 0,
|
|
169
|
-
throttleDelay = 200,
|
|
170
|
-
parentContainer,
|
|
171
|
-
closeOnEsc = true,
|
|
172
|
-
hoverDelay = 100,
|
|
173
|
-
disabled,
|
|
174
|
-
onOpen,
|
|
175
|
-
onClose,
|
|
176
|
-
}: Partial<UseOverlayProps> = {}) => {
|
|
177
|
-
const ctx = useOverlayContext()
|
|
178
|
-
|
|
179
|
-
// Signal-based state
|
|
180
|
-
const active = signal(isOpen)
|
|
181
|
-
const isContentLoaded = signal(false)
|
|
182
|
-
const innerAlignX = signal(propAlignX)
|
|
183
|
-
const innerAlignY = signal(propAlignY)
|
|
184
|
-
const blockedCount = signal(0)
|
|
185
|
-
|
|
186
|
-
const blocked = () => blockedCount() > 0
|
|
187
|
-
|
|
188
|
-
// DOM refs (plain variables, component runs once)
|
|
189
|
-
let triggerEl: HTMLElement | null = null
|
|
190
|
-
let contentEl: HTMLElement | null = null
|
|
191
|
-
const _prevFocusEl: HTMLElement | null = null
|
|
192
|
-
let hoverTimeout: ReturnType<typeof setTimeout> | null = null
|
|
193
|
-
|
|
194
|
-
const triggerRef = (node: HTMLElement | null) => {
|
|
195
|
-
triggerEl = node
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
const contentRefCallback = (node: HTMLElement | null) => {
|
|
199
|
-
contentEl = node
|
|
200
|
-
isContentLoaded.set(!!node)
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
const setBlocked = () => blockedCount.update((c) => c + 1)
|
|
204
|
-
const setUnblocked = () => blockedCount.update((c) => Math.max(0, c - 1))
|
|
205
|
-
|
|
206
|
-
const showContent = () => {
|
|
207
|
-
active.set(true)
|
|
208
|
-
onOpen?.()
|
|
209
|
-
ctx.setBlocked?.()
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
const hideContent = () => {
|
|
213
|
-
active.set(false)
|
|
214
|
-
isContentLoaded.set(false)
|
|
215
|
-
onClose?.()
|
|
216
|
-
ctx.setUnblocked?.()
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// Position calculation helpers
|
|
220
|
-
const getAncestorOffset = () => {
|
|
221
|
-
if (typeof document === 'undefined') return { top: 0, left: 0 }
|
|
222
|
-
if (position !== 'absolute' || !contentEl) {
|
|
223
|
-
return { top: 0, left: 0 }
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
const offsetParent = contentEl.offsetParent as HTMLElement | null
|
|
227
|
-
if (!offsetParent || offsetParent === document.body) {
|
|
228
|
-
return { top: 0, left: 0 }
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
const rect = offsetParent.getBoundingClientRect()
|
|
232
|
-
return { top: rect.top, left: rect.left }
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
const calculateContentPosition = () => {
|
|
236
|
-
if (!active() || !isContentLoaded()) return {}
|
|
237
|
-
|
|
238
|
-
const result = computePosition(
|
|
239
|
-
type,
|
|
240
|
-
align,
|
|
241
|
-
propAlignX,
|
|
242
|
-
propAlignY,
|
|
243
|
-
offsetX,
|
|
244
|
-
offsetY,
|
|
245
|
-
triggerEl,
|
|
246
|
-
contentEl,
|
|
247
|
-
getAncestorOffset(),
|
|
248
|
-
)
|
|
249
|
-
|
|
250
|
-
if (result.resolvedAlignX) innerAlignX.set(result.resolvedAlignX)
|
|
251
|
-
if (result.resolvedAlignY) innerAlignY.set(result.resolvedAlignY)
|
|
252
|
-
|
|
253
|
-
return result.pos
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
const assignContentPosition = (values: OverlayPosition = {}) => {
|
|
257
|
-
if (!contentEl) return
|
|
258
|
-
|
|
259
|
-
const el = contentEl
|
|
260
|
-
const setValue = (param?: string | number) => value(param, 16) as string
|
|
261
|
-
|
|
262
|
-
el.style.position = position
|
|
263
|
-
|
|
264
|
-
el.style.top = values.top != null ? setValue(values.top) : ''
|
|
265
|
-
el.style.bottom = values.bottom != null ? setValue(values.bottom) : ''
|
|
266
|
-
el.style.left = values.left != null ? setValue(values.left) : ''
|
|
267
|
-
el.style.right = values.right != null ? setValue(values.right) : ''
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
const setContentPosition = () => {
|
|
271
|
-
const currentPosition = calculateContentPosition()
|
|
272
|
-
assignContentPosition(currentPosition)
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
const isNodeOrChild = (getRef: () => HTMLElement | null) => (e: Event) => {
|
|
276
|
-
const ref = getRef()
|
|
277
|
-
if (e?.target && ref) {
|
|
278
|
-
return ref.contains(e.target as Element) || e.target === ref
|
|
279
|
-
}
|
|
280
|
-
return false
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
const handleVisibilityByEventType = (e: Event) => {
|
|
284
|
-
if (blocked() || disabled) return
|
|
285
|
-
|
|
286
|
-
processVisibilityEvent(
|
|
287
|
-
e,
|
|
288
|
-
active(),
|
|
289
|
-
openOn,
|
|
290
|
-
closeOn,
|
|
291
|
-
isNodeOrChild(() => triggerEl),
|
|
292
|
-
isNodeOrChild(() => contentEl),
|
|
293
|
-
showContent,
|
|
294
|
-
hideContent,
|
|
295
|
-
)
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
const handleContentPosition = throttle(() => setContentPosition(), throttleDelay)
|
|
299
|
-
|
|
300
|
-
const handleClick = (e: Event) => handleVisibilityByEventType(e)
|
|
301
|
-
|
|
302
|
-
const handleVisibility = throttle((e: Event) => handleVisibilityByEventType(e), throttleDelay)
|
|
303
|
-
|
|
304
|
-
// --------------------------------------------------------------------------
|
|
305
|
-
// Set up all event listeners on mount, clean up on unmount
|
|
306
|
-
// --------------------------------------------------------------------------
|
|
307
|
-
const setupListeners = () => {
|
|
308
|
-
if (typeof window === 'undefined') return () => {}
|
|
309
|
-
const cleanups: (() => void)[] = []
|
|
310
|
-
|
|
311
|
-
// Click-based open/close
|
|
312
|
-
const enabledClick = openOn === 'click' || CLICK_CLOSE_KINDS.has(closeOn)
|
|
313
|
-
|
|
314
|
-
if (enabledClick) {
|
|
315
|
-
window.addEventListener('click', handleClick)
|
|
316
|
-
cleanups.push(() => window.removeEventListener('click', handleClick))
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
// ESC key
|
|
320
|
-
if (closeOnEsc) {
|
|
321
|
-
const handleEscKey = (e: KeyboardEvent) => {
|
|
322
|
-
if (e.key === 'Escape' && active() && !blocked()) {
|
|
323
|
-
hideContent()
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
window.addEventListener('keydown', handleEscKey)
|
|
327
|
-
cleanups.push(() => window.removeEventListener('keydown', handleEscKey))
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
// Hover-based open/close
|
|
331
|
-
const enabledHover = openOn === 'hover' || closeOn === 'hover'
|
|
332
|
-
if (enabledHover) {
|
|
333
|
-
const clearHoverTimeout = () => {
|
|
334
|
-
if (hoverTimeout != null) {
|
|
335
|
-
clearTimeout(hoverTimeout)
|
|
336
|
-
hoverTimeout = null
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
const scheduleHide = () => {
|
|
341
|
-
clearHoverTimeout()
|
|
342
|
-
hoverTimeout = setTimeout(hideContent, hoverDelay)
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
const onTriggerEnter = () => {
|
|
346
|
-
clearHoverTimeout()
|
|
347
|
-
if (openOn === 'hover' && !active()) showContent()
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
const onTriggerLeave = () => {
|
|
351
|
-
if (closeOn === 'hover' && active()) scheduleHide()
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
const onContentEnter = () => {
|
|
355
|
-
clearHoverTimeout()
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
const onContentLeave = () => {
|
|
359
|
-
if (closeOn === 'hover' && active()) scheduleHide()
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
// We need to defer listener attachment until refs are available
|
|
363
|
-
const attachHoverListeners = () => {
|
|
364
|
-
if (triggerEl) {
|
|
365
|
-
triggerEl.addEventListener('mouseenter', onTriggerEnter)
|
|
366
|
-
triggerEl.addEventListener('mouseleave', onTriggerLeave)
|
|
367
|
-
}
|
|
368
|
-
if (contentEl) {
|
|
369
|
-
contentEl.addEventListener('mouseenter', onContentEnter)
|
|
370
|
-
contentEl.addEventListener('mouseleave', onContentLeave)
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
attachHoverListeners()
|
|
375
|
-
|
|
376
|
-
cleanups.push(() => {
|
|
377
|
-
clearHoverTimeout()
|
|
378
|
-
if (triggerEl) {
|
|
379
|
-
triggerEl.removeEventListener('mouseenter', onTriggerEnter)
|
|
380
|
-
triggerEl.removeEventListener('mouseleave', onTriggerLeave)
|
|
381
|
-
}
|
|
382
|
-
if (contentEl) {
|
|
383
|
-
contentEl.removeEventListener('mouseenter', onContentEnter)
|
|
384
|
-
contentEl.removeEventListener('mouseleave', onContentLeave)
|
|
385
|
-
}
|
|
386
|
-
})
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
// Resize/scroll repositioning
|
|
390
|
-
const shouldSetOverflow = type === 'modal'
|
|
391
|
-
|
|
392
|
-
const onScroll = (e: Event) => {
|
|
393
|
-
handleContentPosition()
|
|
394
|
-
handleVisibility(e)
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
if (shouldSetOverflow) {
|
|
398
|
-
modalOverflowCount++
|
|
399
|
-
if (modalOverflowCount === 1) document.body.style.overflow = 'hidden'
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
window.addEventListener('resize', handleContentPosition)
|
|
403
|
-
window.addEventListener('scroll', onScroll, { passive: true })
|
|
404
|
-
cleanups.push(() => {
|
|
405
|
-
handleContentPosition.cancel()
|
|
406
|
-
handleVisibility.cancel()
|
|
407
|
-
if (shouldSetOverflow) {
|
|
408
|
-
modalOverflowCount--
|
|
409
|
-
if (modalOverflowCount === 0) document.body.style.overflow = ''
|
|
410
|
-
}
|
|
411
|
-
window.removeEventListener('resize', handleContentPosition)
|
|
412
|
-
window.removeEventListener('scroll', onScroll)
|
|
413
|
-
})
|
|
414
|
-
|
|
415
|
-
// Parent container scroll
|
|
416
|
-
if (parentContainer) {
|
|
417
|
-
if (closeOn !== 'hover') parentContainer.style.overflow = 'hidden'
|
|
418
|
-
|
|
419
|
-
const onParentScroll = (e: Event) => {
|
|
420
|
-
handleContentPosition()
|
|
421
|
-
handleVisibility(e)
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
parentContainer.addEventListener('scroll', onParentScroll, {
|
|
425
|
-
passive: true,
|
|
426
|
-
})
|
|
427
|
-
cleanups.push(() => {
|
|
428
|
-
parentContainer.style.overflow = ''
|
|
429
|
-
parentContainer.removeEventListener('scroll', onParentScroll)
|
|
430
|
-
})
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
// Cleanup function
|
|
434
|
-
return () => {
|
|
435
|
-
for (const cleanup of cleanups) cleanup()
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
// Handle disabled state
|
|
440
|
-
if (disabled) {
|
|
441
|
-
active.set(false)
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
return {
|
|
445
|
-
triggerRef,
|
|
446
|
-
contentRef: contentRefCallback,
|
|
447
|
-
active,
|
|
448
|
-
align,
|
|
449
|
-
alignX: innerAlignX,
|
|
450
|
-
alignY: innerAlignY,
|
|
451
|
-
showContent,
|
|
452
|
-
hideContent,
|
|
453
|
-
blocked,
|
|
454
|
-
setBlocked,
|
|
455
|
-
setUnblocked,
|
|
456
|
-
setupListeners,
|
|
457
|
-
Provider,
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
export default useOverlay
|
package/src/Portal/component.tsx
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Portal — renders children into a per-instance wrapper element appended to
|
|
3
|
-
* `DOMLocation` (defaults to `document.body`). Mirrors vitus-labs's Portal:
|
|
4
|
-
* a fresh wrapper is created per Portal mount, children render INSIDE it
|
|
5
|
-
* (not directly into DOMLocation), and the wrapper is removed on unmount.
|
|
6
|
-
*
|
|
7
|
-
* Per-instance wrapper isolation matters when multiple Portals share a
|
|
8
|
-
* DOMLocation (e.g. several modals on `document.body`) — without the wrapper
|
|
9
|
-
* their children would intermingle, defeating CSS scoping and making
|
|
10
|
-
* cleanup brittle.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import type { VNodeChild } from '@pyreon/core'
|
|
14
|
-
import { Portal as CorePortal, onUnmount } from '@pyreon/core'
|
|
15
|
-
import { PKG_NAME } from '../constants'
|
|
16
|
-
import type { PyreonComponent } from '../types'
|
|
17
|
-
|
|
18
|
-
export interface Props {
|
|
19
|
-
/**
|
|
20
|
-
* DOM element to mount the wrapper into. Defaults to `document.body`.
|
|
21
|
-
*/
|
|
22
|
-
DOMLocation?: HTMLElement
|
|
23
|
-
/**
|
|
24
|
-
* Children rendered inside the wrapper.
|
|
25
|
-
*/
|
|
26
|
-
children: VNodeChild
|
|
27
|
-
/**
|
|
28
|
-
* HTML tag for the per-instance wrapper element. Defaults to `'div'`.
|
|
29
|
-
*/
|
|
30
|
-
tag?: string
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const Component: PyreonComponent<Props> = (props) => {
|
|
34
|
-
if (typeof document === 'undefined') return null
|
|
35
|
-
|
|
36
|
-
const tag = props.tag ?? 'div'
|
|
37
|
-
const target = props.DOMLocation ?? document.body
|
|
38
|
-
const wrapper = document.createElement(tag)
|
|
39
|
-
target.appendChild(wrapper)
|
|
40
|
-
|
|
41
|
-
onUnmount(() => {
|
|
42
|
-
wrapper.remove()
|
|
43
|
-
})
|
|
44
|
-
|
|
45
|
-
return <CorePortal target={wrapper}>{props.children}</CorePortal>
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const name = `${PKG_NAME}/Portal` as const
|
|
49
|
-
|
|
50
|
-
Component.displayName = name
|
|
51
|
-
Component.pkgName = PKG_NAME
|
|
52
|
-
Component.PYREON__COMPONENT = name
|
|
53
|
-
|
|
54
|
-
export default Component
|
package/src/Portal/index.ts
DELETED
package/src/Text/component.tsx
DELETED
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Text component for rendering inline or block-level text. Supports a
|
|
3
|
-
* `paragraph` shorthand that automatically renders as a `<p>` tag, or
|
|
4
|
-
* a custom `tag` for semantic HTML (h1-h6, span, etc.). Marked with
|
|
5
|
-
* a static `isText` flag so other components can detect text children.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { splitProps } from '@pyreon/core'
|
|
9
|
-
import type { PyreonHTMLAttributes, VNodeChild } from '@pyreon/core'
|
|
10
|
-
import type { HTMLTextTags } from '@pyreon/ui-core'
|
|
11
|
-
import { PKG_NAME } from '../constants'
|
|
12
|
-
import type { ExtendCss, PyreonComponent } from '../types'
|
|
13
|
-
import Styled from './styled'
|
|
14
|
-
|
|
15
|
-
export type Props = Partial<{
|
|
16
|
-
/**
|
|
17
|
-
* Label can be used instead of children for inline syntax. But **children** prop takes a precedence
|
|
18
|
-
*/
|
|
19
|
-
label: VNodeChild
|
|
20
|
-
/**
|
|
21
|
-
* Children to be rendered within **Text** component.
|
|
22
|
-
*/
|
|
23
|
-
children: VNodeChild
|
|
24
|
-
/**
|
|
25
|
-
* Defines whether should behave as a block text element. Automatically adds **p** HTML tag
|
|
26
|
-
*/
|
|
27
|
-
paragraph: boolean
|
|
28
|
-
/**
|
|
29
|
-
* Defines what kind of HTML tag should be rendered
|
|
30
|
-
*/
|
|
31
|
-
tag: HTMLTextTags
|
|
32
|
-
/**
|
|
33
|
-
* If an additional styling needs to be added, it can be do so via injecting styles using this property.
|
|
34
|
-
*/
|
|
35
|
-
css: ExtendCss
|
|
36
|
-
}> &
|
|
37
|
-
PyreonHTMLAttributes
|
|
38
|
-
|
|
39
|
-
const Component: PyreonComponent<Props> & {
|
|
40
|
-
isText?: true
|
|
41
|
-
} = (props) => {
|
|
42
|
-
const [own, rest] = splitProps(props, ['paragraph', 'label', 'children', 'tag', 'css', 'ref'])
|
|
43
|
-
|
|
44
|
-
// `paragraph` shorthand maps to <p>; otherwise pass through `tag`. Ternary
|
|
45
|
-
// form replaces the prior `let finalTag` + if/else block — V8 prefers the
|
|
46
|
-
// single-assignment shape for inline-cache stability. Ported from
|
|
47
|
-
// vitus-labs `804dd0e2`.
|
|
48
|
-
const finalTag = own.paragraph ? 'p' : own.tag
|
|
49
|
-
|
|
50
|
-
return (
|
|
51
|
-
<Styled ref={own.ref} as={finalTag} $text={{ extraStyles: own.css }} {...rest}>
|
|
52
|
-
{own.children ?? own.label}
|
|
53
|
-
</Styled>
|
|
54
|
-
)
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// ----------------------------------------------
|
|
58
|
-
// DEFINE STATICS
|
|
59
|
-
// ----------------------------------------------
|
|
60
|
-
const name = `${PKG_NAME}/Text` as const
|
|
61
|
-
|
|
62
|
-
Component.displayName = name
|
|
63
|
-
Component.pkgName = PKG_NAME
|
|
64
|
-
Component.PYREON__COMPONENT = name
|
|
65
|
-
Component.isText = true
|
|
66
|
-
|
|
67
|
-
export default Component
|
package/src/Text/index.ts
DELETED
package/src/Text/styled.ts
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Styled text primitive that inherits color, font-weight, and line-height
|
|
3
|
-
* from its parent so it blends seamlessly into any context. Additional
|
|
4
|
-
* styles can be injected via the responsive `extraStyles` prop processed
|
|
5
|
-
* through makeItResponsive.
|
|
6
|
-
*/
|
|
7
|
-
import { config } from '@pyreon/ui-core'
|
|
8
|
-
import { extendCss, makeItResponsive } from '@pyreon/unistyle'
|
|
9
|
-
import type { ResponsiveStylesCallback } from '../types'
|
|
10
|
-
|
|
11
|
-
const { styled, css, textComponent } = config
|
|
12
|
-
|
|
13
|
-
const styles: ResponsiveStylesCallback = ({ css: cssFn, theme: t }) => cssFn`
|
|
14
|
-
${t.extraStyles && extendCss(t.extraStyles)};
|
|
15
|
-
`
|
|
16
|
-
|
|
17
|
-
export default styled(textComponent, { layer: 'elements' })`
|
|
18
|
-
${css`
|
|
19
|
-
color: inherit;
|
|
20
|
-
font-weight: inherit;
|
|
21
|
-
line-height: 1;
|
|
22
|
-
`};
|
|
23
|
-
|
|
24
|
-
${makeItResponsive({
|
|
25
|
-
key: '$text',
|
|
26
|
-
styles,
|
|
27
|
-
css,
|
|
28
|
-
normalize: false,
|
|
29
|
-
})};
|
|
30
|
-
`
|
package/src/Util/component.tsx
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Utility wrapper that injects className and/or style props into its
|
|
3
|
-
* children without adding any DOM nodes of its own. Uses the core `render`
|
|
4
|
-
* helper to clone children with the merged props.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import type { VNode, VNodeChild } from '@pyreon/core'
|
|
8
|
-
import { render } from '@pyreon/ui-core'
|
|
9
|
-
import { PKG_NAME } from '../constants'
|
|
10
|
-
import type { PyreonComponent } from '../types'
|
|
11
|
-
|
|
12
|
-
export interface Props {
|
|
13
|
-
/**
|
|
14
|
-
* Children to be rendered within **Util** component.
|
|
15
|
-
*/
|
|
16
|
-
children: VNodeChild
|
|
17
|
-
/**
|
|
18
|
-
* Class name(s) to be added to children component.
|
|
19
|
-
*/
|
|
20
|
-
className?: string | string[] | undefined
|
|
21
|
-
/**
|
|
22
|
-
* Style property to extend children component inline styles
|
|
23
|
-
*/
|
|
24
|
-
style?: Record<string, unknown> | undefined
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const Component: PyreonComponent<Props> = (({ children, className, style }: Props) => {
|
|
28
|
-
const mergedClasses = Array.isArray(className) ? className.join(' ') : className
|
|
29
|
-
|
|
30
|
-
const finalProps: Record<string, any> = {}
|
|
31
|
-
if (style) finalProps.style = style
|
|
32
|
-
if (mergedClasses) finalProps.className = mergedClasses
|
|
33
|
-
|
|
34
|
-
return render(children, finalProps) as VNode | null
|
|
35
|
-
}) as PyreonComponent<Props>
|
|
36
|
-
|
|
37
|
-
const name = `${PKG_NAME}/Util` as const
|
|
38
|
-
|
|
39
|
-
Component.displayName = name
|
|
40
|
-
Component.pkgName = PKG_NAME
|
|
41
|
-
Component.PYREON__COMPONENT = name
|
|
42
|
-
|
|
43
|
-
export default Component
|