@pgcorp/ui-kit 0.8.1 → 0.9.0
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/README.md +144 -0
- package/docs/theming.md +17 -0
- package/package.json +10 -5
- package/src/components/layout/SResizeHandle.vue +45 -5
- package/src/components/shared/_internal/SDialogSurface.css +18 -0
- package/src/components/shared/_internal/SDialogSurface.vue +6 -1
- package/src/components/shared/_internal/dialogSurface.ts +7 -1
- package/src/components/shared/containers/SDrawer.css +11 -2
- package/src/components/shared/containers/SDrawer.vue +43 -7
- package/src/components/shared/controls/STextarea.ts +13 -0
- package/src/components/shared/controls/STextarea.vue +391 -37
- package/src/components/shared/data-display/SCodeBlock.vue +4 -2
- package/src/components/shared/data-display/SVirtualList.ts +33 -0
- package/src/components/shared/data-display/SVirtualList.vue +321 -36
- package/src/styles/tokens.css +3 -0
|
@@ -6,10 +6,17 @@
|
|
|
6
6
|
:style="containerStyle"
|
|
7
7
|
:data-item-size="itemSize"
|
|
8
8
|
:data-viewport-height="height ?? undefined"
|
|
9
|
-
role="
|
|
9
|
+
:role="validatedSemanticRole"
|
|
10
10
|
:aria-label="accessibleLabel"
|
|
11
|
+
:aria-live="validatedSemanticRole === 'log' ? validatedLive : undefined"
|
|
12
|
+
:aria-busy="validatedSemanticRole === 'log' ? validatedBusy : undefined"
|
|
13
|
+
:aria-relevant="validatedSemanticRole === 'log' ? 'additions text' : undefined"
|
|
11
14
|
tabindex="0"
|
|
12
15
|
@scroll.passive="updateViewport"
|
|
16
|
+
@wheel.passive="handleWheel"
|
|
17
|
+
@keydown="handleKeydown"
|
|
18
|
+
@pointerdown="handlePointerDown"
|
|
19
|
+
@pointerup="handlePointerUp"
|
|
13
20
|
>
|
|
14
21
|
<div class="s-virtual-list__spacer" :style="spacerStyle" aria-hidden="true" />
|
|
15
22
|
<div
|
|
@@ -19,7 +26,7 @@
|
|
|
19
26
|
class="s-virtual-list__item"
|
|
20
27
|
:style="itemStyle(entry.index)"
|
|
21
28
|
:data-virtual-index="entry.index"
|
|
22
|
-
role="listitem"
|
|
29
|
+
:role="validatedSemanticRole === 'list' ? 'listitem' : 'article'"
|
|
23
30
|
:aria-label="resolveItemLabel(entry.item, entry.index)"
|
|
24
31
|
:aria-posinset="entry.index + 1"
|
|
25
32
|
:aria-setsize="items.length"
|
|
@@ -36,7 +43,12 @@
|
|
|
36
43
|
</template>
|
|
37
44
|
|
|
38
45
|
<script lang="ts">
|
|
39
|
-
import type {
|
|
46
|
+
import type {
|
|
47
|
+
SVirtualListItemKey,
|
|
48
|
+
SVirtualListLiveMode,
|
|
49
|
+
SVirtualListSemanticRole,
|
|
50
|
+
SVirtualListViewportState,
|
|
51
|
+
} from './SVirtualList'
|
|
40
52
|
|
|
41
53
|
/** Публичный контракт virtual list принадлежит source-SFC runtime. / The public virtual-list contract is owned by the source-SFC runtime. */
|
|
42
54
|
export interface SVirtualListProps<Item = unknown> {
|
|
@@ -50,6 +62,13 @@ export interface SVirtualListProps<Item = unknown> {
|
|
|
50
62
|
itemLabel: (item: Item, index: number) => string
|
|
51
63
|
accessibleLabel: string
|
|
52
64
|
externalScrollTarget?: HTMLElement | null
|
|
65
|
+
/** Controlled follow-tail intent. Use v-model:followTail when user detachment must persist in the parent. */
|
|
66
|
+
followTail?: boolean
|
|
67
|
+
nearStartThreshold?: number
|
|
68
|
+
endThreshold?: number
|
|
69
|
+
semanticRole?: SVirtualListSemanticRole
|
|
70
|
+
live?: SVirtualListLiveMode
|
|
71
|
+
busy?: boolean
|
|
53
72
|
}
|
|
54
73
|
</script>
|
|
55
74
|
|
|
@@ -65,17 +84,40 @@ import {
|
|
|
65
84
|
} from 'vue'
|
|
66
85
|
import { useOwnedAttrs } from '../../../internal/ownedAttrs'
|
|
67
86
|
import { useInteractiveLeafRegistration } from '../../../internal/passiveContentContract'
|
|
87
|
+
import {
|
|
88
|
+
validateBoolean,
|
|
89
|
+
validateBoundedNumber,
|
|
90
|
+
validateExactString,
|
|
91
|
+
} from '../../../internal/runtimeContract'
|
|
92
|
+
import type {
|
|
93
|
+
SVirtualListAlignment,
|
|
94
|
+
SVirtualListAnchor,
|
|
95
|
+
SVirtualListViewportSnapshot,
|
|
96
|
+
SVirtualListVisibleRange,
|
|
97
|
+
} from './SVirtualList'
|
|
68
98
|
defineOptions({ inheritAttrs: false })
|
|
69
99
|
|
|
70
|
-
type Alignment = 'auto' | 'start' | 'center' | 'end'
|
|
71
100
|
type StyleObject = Record<string, string>
|
|
101
|
+
type ScrollIntent = 'up' | 'down' | 'unknown' | null
|
|
72
102
|
|
|
73
103
|
const props = withDefaults(defineProps<SVirtualListProps<T>>(), {
|
|
74
104
|
overscan: 8,
|
|
75
105
|
height: null,
|
|
76
106
|
externalScrollTarget: null,
|
|
107
|
+
followTail: false,
|
|
108
|
+
nearStartThreshold: 80,
|
|
109
|
+
endThreshold: 24,
|
|
110
|
+
semanticRole: 'list',
|
|
111
|
+
live: 'off',
|
|
112
|
+
busy: false,
|
|
77
113
|
})
|
|
78
114
|
|
|
115
|
+
const emit = defineEmits<{
|
|
116
|
+
(event: 'update:followTail', value: boolean): void
|
|
117
|
+
(event: 'viewport-change', state: SVirtualListViewportState): void
|
|
118
|
+
(event: 'near-start', state: SVirtualListViewportState): void
|
|
119
|
+
}>()
|
|
120
|
+
|
|
79
121
|
defineSlots<{
|
|
80
122
|
default(props: { item: T; index: number; position: number; setSize: number }): unknown
|
|
81
123
|
}>()
|
|
@@ -91,6 +133,37 @@ const renderedItemKeysByElement = new Map<HTMLElement, SVirtualListItemKey>()
|
|
|
91
133
|
const renderedItemRefs = new Map<SVirtualListItemKey, VNodeRef>()
|
|
92
134
|
let resizeObserver: ResizeObserver | null = null
|
|
93
135
|
let boundExternalTarget: HTMLElement | null = null
|
|
136
|
+
let scrollIntent: ScrollIntent = null
|
|
137
|
+
let pointerOwnsScrollbar = false
|
|
138
|
+
let lastNearStart = false
|
|
139
|
+
let lastPublishedState = ''
|
|
140
|
+
const followTailIntent = ref(props.followTail)
|
|
141
|
+
|
|
142
|
+
const validatedSemanticRole = computed<SVirtualListSemanticRole>(() => validateExactString(
|
|
143
|
+
'SVirtualList',
|
|
144
|
+
'semanticRole',
|
|
145
|
+
props.semanticRole,
|
|
146
|
+
['list', 'log'] as const,
|
|
147
|
+
))
|
|
148
|
+
const validatedLive = computed<SVirtualListLiveMode>(() => validateExactString(
|
|
149
|
+
'SVirtualList',
|
|
150
|
+
'live',
|
|
151
|
+
props.live,
|
|
152
|
+
['off', 'polite', 'assertive'] as const,
|
|
153
|
+
))
|
|
154
|
+
const validatedBusy = computed(() => validateBoolean('SVirtualList', 'busy', props.busy))
|
|
155
|
+
const validatedNearStartThreshold = computed(() => validateBoundedNumber(
|
|
156
|
+
'SVirtualList',
|
|
157
|
+
'nearStartThreshold',
|
|
158
|
+
props.nearStartThreshold,
|
|
159
|
+
{ minimum: 0, maximum: 10_000 },
|
|
160
|
+
))
|
|
161
|
+
const validatedEndThreshold = computed(() => validateBoundedNumber(
|
|
162
|
+
'SVirtualList',
|
|
163
|
+
'endThreshold',
|
|
164
|
+
props.endThreshold,
|
|
165
|
+
{ minimum: 0, maximum: 10_000 },
|
|
166
|
+
))
|
|
94
167
|
|
|
95
168
|
const isExternal = computed(() => props.externalScrollTarget !== null)
|
|
96
169
|
|
|
@@ -199,17 +272,151 @@ function externalOffset(): number {
|
|
|
199
272
|
return root.getBoundingClientRect().top - target.getBoundingClientRect().top + target.scrollTop
|
|
200
273
|
}
|
|
201
274
|
|
|
275
|
+
function scrollOwner(): HTMLElement | null {
|
|
276
|
+
return props.externalScrollTarget ?? container.value
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function maximumScrollOffset(): number {
|
|
280
|
+
return Math.max(0, totalHeight.value - viewportHeight.value)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function currentAnchor(keys: readonly SVirtualListItemKey[] = itemKeys.value): SVirtualListAnchor | null {
|
|
284
|
+
if (keys.length === 0) return null
|
|
285
|
+
const sizes = keys.map((key) => measurements.value.get(key) ?? props.itemSize)
|
|
286
|
+
let accumulated = 0
|
|
287
|
+
let index = 0
|
|
288
|
+
const viewportOffset = Math.max(0, scrollTop.value)
|
|
289
|
+
while (index < sizes.length - 1 && accumulated + (sizes[index] ?? props.itemSize) <= viewportOffset) {
|
|
290
|
+
accumulated += sizes[index] ?? props.itemSize
|
|
291
|
+
index += 1
|
|
292
|
+
}
|
|
293
|
+
const key = keys[index]
|
|
294
|
+
if (key === undefined) return null
|
|
295
|
+
return { key, offset: viewportOffset - accumulated }
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function visibleRange(): SVirtualListVisibleRange {
|
|
299
|
+
if (props.items.length === 0) {
|
|
300
|
+
return { firstIndex: -1, lastIndex: -1, firstKey: null, lastKey: null }
|
|
301
|
+
}
|
|
302
|
+
const firstIndex = firstVisibleIndex.value
|
|
303
|
+
const lastIndex = Math.max(firstIndex, lastVisibleIndex.value)
|
|
304
|
+
return {
|
|
305
|
+
firstIndex,
|
|
306
|
+
lastIndex,
|
|
307
|
+
firstKey: itemKeys.value[firstIndex] ?? null,
|
|
308
|
+
lastKey: itemKeys.value[lastIndex] ?? null,
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function getViewportState(): SVirtualListViewportState {
|
|
313
|
+
const nearStart = scrollTop.value <= validatedNearStartThreshold.value
|
|
314
|
+
const atEnd = maximumScrollOffset() - scrollTop.value <= validatedEndThreshold.value
|
|
315
|
+
return {
|
|
316
|
+
anchor: currentAnchor(),
|
|
317
|
+
visibleRange: visibleRange(),
|
|
318
|
+
nearStart,
|
|
319
|
+
atEnd,
|
|
320
|
+
pinnedToEnd: followTailIntent.value,
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function publishViewportState(): void {
|
|
325
|
+
const state = getViewportState()
|
|
326
|
+
const signature = JSON.stringify(state)
|
|
327
|
+
if (signature !== lastPublishedState) {
|
|
328
|
+
lastPublishedState = signature
|
|
329
|
+
emit('viewport-change', state)
|
|
330
|
+
}
|
|
331
|
+
if (state.nearStart && !lastNearStart) emit('near-start', state)
|
|
332
|
+
lastNearStart = state.nearStart
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function setFollowTail(value: boolean): void {
|
|
336
|
+
const validated = validateBoolean('SVirtualList', 'setFollowTail value', value)
|
|
337
|
+
if (followTailIntent.value === validated) return
|
|
338
|
+
followTailIntent.value = validated
|
|
339
|
+
emit('update:followTail', validated)
|
|
340
|
+
publishViewportState()
|
|
341
|
+
}
|
|
342
|
+
|
|
202
343
|
function updateViewport(): void {
|
|
203
|
-
const target =
|
|
344
|
+
const target = scrollOwner()
|
|
204
345
|
if (!target) return
|
|
205
346
|
viewportHeight.value = target.clientHeight
|
|
206
347
|
scrollTop.value = Math.max(0, target.scrollTop - (isExternal.value ? externalOffset() : 0))
|
|
348
|
+
if (scrollIntent === 'up') setFollowTail(false)
|
|
349
|
+
if (scrollIntent === 'down' && maximumScrollOffset() - scrollTop.value <= validatedEndThreshold.value) {
|
|
350
|
+
setFollowTail(true)
|
|
351
|
+
}
|
|
352
|
+
scrollIntent = null
|
|
353
|
+
publishViewportState()
|
|
207
354
|
}
|
|
208
355
|
|
|
209
|
-
function
|
|
210
|
-
if (
|
|
211
|
-
|
|
212
|
-
|
|
356
|
+
function handleWheel(event: WheelEvent): void {
|
|
357
|
+
if (event.deltaY < 0) {
|
|
358
|
+
scrollIntent = 'up'
|
|
359
|
+
setFollowTail(false)
|
|
360
|
+
} else if (event.deltaY > 0) {
|
|
361
|
+
scrollIntent = 'down'
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function handleKeydown(event: KeyboardEvent): void {
|
|
366
|
+
if (['ArrowUp', 'PageUp', 'Home'].includes(event.key)) {
|
|
367
|
+
scrollIntent = 'up'
|
|
368
|
+
setFollowTail(false)
|
|
369
|
+
} else if (['ArrowDown', 'PageDown', 'End'].includes(event.key)) {
|
|
370
|
+
scrollIntent = 'down'
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function eventOwnsScrollbar(event: PointerEvent): boolean {
|
|
375
|
+
const target = scrollOwner()
|
|
376
|
+
if (!target || event.currentTarget !== target) return false
|
|
377
|
+
return event.offsetX >= target.clientWidth || event.offsetY >= target.clientHeight
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function handlePointerDown(event: PointerEvent): void {
|
|
381
|
+
pointerOwnsScrollbar = eventOwnsScrollbar(event)
|
|
382
|
+
if (!pointerOwnsScrollbar) return
|
|
383
|
+
scrollIntent = 'unknown'
|
|
384
|
+
setFollowTail(false)
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function handlePointerUp(): void {
|
|
388
|
+
if (!pointerOwnsScrollbar) return
|
|
389
|
+
pointerOwnsScrollbar = false
|
|
390
|
+
if (maximumScrollOffset() - scrollTop.value <= validatedEndThreshold.value) setFollowTail(true)
|
|
391
|
+
scrollIntent = null
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function restoreAnchor(anchor: SVirtualListAnchor, failWhenMissing: boolean): boolean {
|
|
395
|
+
const index = itemKeyIndexes.value.get(anchor.key)
|
|
396
|
+
if (index === undefined) {
|
|
397
|
+
if (failWhenMissing) {
|
|
398
|
+
throw new RangeError(`SVirtualList cannot restore missing anchor key ${describeItemKey(anchor.key)}`)
|
|
399
|
+
}
|
|
400
|
+
return false
|
|
401
|
+
}
|
|
402
|
+
const target = scrollOwner()
|
|
403
|
+
if (!target) return false
|
|
404
|
+
const rowSize = itemSizes.value[index] ?? props.itemSize
|
|
405
|
+
if (!Number.isFinite(anchor.offset) || anchor.offset < 0) {
|
|
406
|
+
throw new RangeError(
|
|
407
|
+
`SVirtualList anchor offset ${anchor.offset} is outside row ${describeItemKey(anchor.key)} size ${rowSize}`,
|
|
408
|
+
)
|
|
409
|
+
}
|
|
410
|
+
if (failWhenMissing && anchor.offset >= rowSize) {
|
|
411
|
+
throw new RangeError(
|
|
412
|
+
`SVirtualList anchor offset ${anchor.offset} is outside row ${describeItemKey(anchor.key)} size ${rowSize}`,
|
|
413
|
+
)
|
|
414
|
+
}
|
|
415
|
+
const resolvedOffset = Math.min(anchor.offset, Math.max(0, rowSize - 1))
|
|
416
|
+
const nextOffset = (offsets.value[index] ?? 0) + resolvedOffset
|
|
417
|
+
target.scrollTop = Math.max(0, nextOffset + (isExternal.value ? externalOffset() : 0))
|
|
418
|
+
updateViewport()
|
|
419
|
+
return true
|
|
213
420
|
}
|
|
214
421
|
|
|
215
422
|
function unregisterRenderedItem(key: SVirtualListItemKey): void {
|
|
@@ -264,32 +471,38 @@ function renderedItemRef(key: SVirtualListItemKey): VNodeRef {
|
|
|
264
471
|
return itemRef
|
|
265
472
|
}
|
|
266
473
|
|
|
267
|
-
function
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
return
|
|
271
|
-
}
|
|
272
|
-
if (!(entry.target instanceof HTMLElement)) {
|
|
273
|
-
throw new TypeError('SVirtualList received a ResizeObserver entry for a non-HTMLElement target')
|
|
274
|
-
}
|
|
275
|
-
const key = renderedItemKeysByElement.get(entry.target)
|
|
276
|
-
if (key === undefined || renderedItemsByKey.get(key) !== entry.target) {
|
|
277
|
-
throw new Error('SVirtualList received a ResizeObserver entry for an unregistered rendered item')
|
|
278
|
-
}
|
|
279
|
-
const index = itemKeyIndexes.value.get(key)
|
|
280
|
-
if (index === undefined) {
|
|
281
|
-
throw new Error(`SVirtualList received a ResizeObserver entry for stale key ${describeItemKey(key)}`)
|
|
282
|
-
}
|
|
283
|
-
const size = entry.borderBoxSize[0]?.blockSize ?? entry.contentRect.height
|
|
284
|
-
if (!Number.isFinite(size) || size <= 0 || measurements.value.get(key) === size) return
|
|
285
|
-
const previous = measurements.value.get(key) ?? props.itemSize
|
|
474
|
+
function measureEntries(entries: readonly ResizeObserverEntry[]): void {
|
|
475
|
+
let viewportChanged = false
|
|
476
|
+
const anchor = followTailIntent.value ? null : currentAnchor()
|
|
286
477
|
const next = new Map(measurements.value)
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
478
|
+
let measurementsChanged = false
|
|
479
|
+
for (const entry of entries) {
|
|
480
|
+
if (entry.target === container.value || entry.target === props.externalScrollTarget) {
|
|
481
|
+
viewportChanged = true
|
|
482
|
+
continue
|
|
483
|
+
}
|
|
484
|
+
if (!(entry.target instanceof HTMLElement)) {
|
|
485
|
+
throw new TypeError('SVirtualList received a ResizeObserver entry for a non-HTMLElement target')
|
|
486
|
+
}
|
|
487
|
+
const key = renderedItemKeysByElement.get(entry.target)
|
|
488
|
+
if (key === undefined || renderedItemsByKey.get(key) !== entry.target) {
|
|
489
|
+
throw new Error('SVirtualList received a ResizeObserver entry for an unregistered rendered item')
|
|
490
|
+
}
|
|
491
|
+
if (itemKeyIndexes.value.get(key) === undefined) {
|
|
492
|
+
throw new Error(`SVirtualList received a ResizeObserver entry for stale key ${describeItemKey(key)}`)
|
|
493
|
+
}
|
|
494
|
+
const size = entry.borderBoxSize[0]?.blockSize ?? entry.contentRect.height
|
|
495
|
+
if (!Number.isFinite(size) || size <= 0 || next.get(key) === size) continue
|
|
496
|
+
next.set(key, size)
|
|
497
|
+
measurementsChanged = true
|
|
498
|
+
}
|
|
499
|
+
if (measurementsChanged) measurements.value = next
|
|
500
|
+
if (followTailIntent.value && measurementsChanged) scrollToEnd({ follow: true })
|
|
501
|
+
else if (anchor && measurementsChanged) restoreAnchor(anchor, false)
|
|
502
|
+
if (viewportChanged || measurementsChanged) updateViewport()
|
|
290
503
|
}
|
|
291
504
|
|
|
292
|
-
function scrollToIndex(index: number, alignment:
|
|
505
|
+
function scrollToIndex(index: number, alignment: SVirtualListAlignment = 'auto'): void {
|
|
293
506
|
if (!Number.isInteger(index) || index < 0 || index >= props.items.length) {
|
|
294
507
|
throw new RangeError(`SVirtualList index ${index} is outside 0..${Math.max(props.items.length - 1, 0)}`)
|
|
295
508
|
}
|
|
@@ -308,7 +521,48 @@ function scrollToIndex(index: number, alignment: Alignment = 'auto'): void {
|
|
|
308
521
|
updateViewport()
|
|
309
522
|
}
|
|
310
523
|
|
|
311
|
-
|
|
524
|
+
function scrollToKey(key: SVirtualListItemKey, alignment: SVirtualListAlignment = 'auto'): void {
|
|
525
|
+
const index = itemKeyIndexes.value.get(key)
|
|
526
|
+
if (index === undefined) {
|
|
527
|
+
throw new RangeError(`SVirtualList cannot scroll to missing key ${describeItemKey(key)}`)
|
|
528
|
+
}
|
|
529
|
+
scrollToIndex(index, alignment)
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function scrollToEnd(options: { follow?: boolean } = {}): void {
|
|
533
|
+
const target = scrollOwner()
|
|
534
|
+
if (!target) return
|
|
535
|
+
if (options.follow ?? true) setFollowTail(true)
|
|
536
|
+
target.scrollTop = maximumScrollOffset() + (isExternal.value ? externalOffset() : 0)
|
|
537
|
+
updateViewport()
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function captureViewport(): SVirtualListViewportSnapshot {
|
|
541
|
+
return {
|
|
542
|
+
anchor: currentAnchor(),
|
|
543
|
+
followTail: followTailIntent.value,
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
async function restoreViewport(snapshot: SVirtualListViewportSnapshot): Promise<void> {
|
|
548
|
+
if (snapshot === null || typeof snapshot !== 'object' || Array.isArray(snapshot)) {
|
|
549
|
+
throw new TypeError('SVirtualList restoreViewport snapshot must be an object')
|
|
550
|
+
}
|
|
551
|
+
const followTail = validateBoolean('SVirtualList', 'restoreViewport.followTail', snapshot.followTail)
|
|
552
|
+
await nextTick()
|
|
553
|
+
if (followTail) {
|
|
554
|
+
scrollToEnd({ follow: true })
|
|
555
|
+
return
|
|
556
|
+
}
|
|
557
|
+
if (snapshot.anchor === null) {
|
|
558
|
+
setFollowTail(false)
|
|
559
|
+
return
|
|
560
|
+
}
|
|
561
|
+
setFollowTail(false)
|
|
562
|
+
restoreAnchor(snapshot.anchor, true)
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
async function focusItem(index: number, alignment: SVirtualListAlignment = 'auto'): Promise<void> {
|
|
312
566
|
const key = itemKeys.value[index]
|
|
313
567
|
scrollToIndex(index, alignment)
|
|
314
568
|
await nextTick()
|
|
@@ -331,29 +585,53 @@ async function focusItem(index: number, alignment: Alignment = 'auto'): Promise<
|
|
|
331
585
|
element.focus()
|
|
332
586
|
}
|
|
333
587
|
|
|
334
|
-
defineExpose({
|
|
588
|
+
defineExpose({
|
|
589
|
+
captureViewport,
|
|
590
|
+
focusItem,
|
|
591
|
+
getViewportState,
|
|
592
|
+
restoreViewport,
|
|
593
|
+
scrollToEnd,
|
|
594
|
+
scrollToIndex,
|
|
595
|
+
scrollToKey,
|
|
596
|
+
setFollowTail,
|
|
597
|
+
})
|
|
335
598
|
|
|
336
599
|
function bindExternalTarget(target: HTMLElement | null): void {
|
|
337
600
|
boundExternalTarget?.removeEventListener('scroll', updateViewport)
|
|
601
|
+
boundExternalTarget?.removeEventListener('wheel', handleWheel)
|
|
602
|
+
boundExternalTarget?.removeEventListener('keydown', handleKeydown)
|
|
603
|
+
boundExternalTarget?.removeEventListener('pointerdown', handlePointerDown)
|
|
604
|
+
boundExternalTarget?.removeEventListener('pointerup', handlePointerUp)
|
|
338
605
|
if (boundExternalTarget) resizeObserver?.unobserve(boundExternalTarget)
|
|
339
606
|
boundExternalTarget = target
|
|
340
607
|
boundExternalTarget?.addEventListener('scroll', updateViewport, { passive: true })
|
|
608
|
+
boundExternalTarget?.addEventListener('wheel', handleWheel, { passive: true })
|
|
609
|
+
boundExternalTarget?.addEventListener('keydown', handleKeydown)
|
|
610
|
+
boundExternalTarget?.addEventListener('pointerdown', handlePointerDown)
|
|
611
|
+
boundExternalTarget?.addEventListener('pointerup', handlePointerUp)
|
|
341
612
|
if (boundExternalTarget) resizeObserver?.observe(boundExternalTarget)
|
|
342
613
|
updateViewport()
|
|
343
614
|
}
|
|
344
615
|
|
|
345
616
|
onMounted(() => {
|
|
346
617
|
if (typeof ResizeObserver !== 'undefined') {
|
|
347
|
-
resizeObserver = new ResizeObserver((entries) => entries
|
|
618
|
+
resizeObserver = new ResizeObserver((entries) => measureEntries(entries))
|
|
348
619
|
if (container.value) resizeObserver.observe(container.value)
|
|
349
620
|
for (const element of renderedItemsByKey.values()) resizeObserver.observe(element)
|
|
350
621
|
}
|
|
351
622
|
bindExternalTarget(props.externalScrollTarget)
|
|
352
623
|
updateViewport()
|
|
624
|
+
if (followTailIntent.value) void nextTick(() => scrollToEnd({ follow: true }))
|
|
353
625
|
})
|
|
354
626
|
|
|
355
627
|
watch(() => props.externalScrollTarget, bindExternalTarget)
|
|
356
|
-
watch(() =>
|
|
628
|
+
watch(() => props.followTail, (value) => {
|
|
629
|
+
followTailIntent.value = validateBoolean('SVirtualList', 'followTail', value)
|
|
630
|
+
if (followTailIntent.value) void nextTick(() => scrollToEnd({ follow: false }))
|
|
631
|
+
else publishViewportState()
|
|
632
|
+
})
|
|
633
|
+
watch(() => itemKeys.value, async (keys, previousKeys) => {
|
|
634
|
+
const anchor = followTailIntent.value ? null : currentAnchor(previousKeys)
|
|
357
635
|
const activeKeys = new Set(keys)
|
|
358
636
|
measurements.value = new Map([...measurements.value].filter(([key]) => activeKeys.has(key)))
|
|
359
637
|
for (const key of renderedItemsByKey.keys()) {
|
|
@@ -362,11 +640,18 @@ watch(() => itemKeys.value, (keys) => {
|
|
|
362
640
|
for (const key of renderedItemRefs.keys()) {
|
|
363
641
|
if (!activeKeys.has(key)) renderedItemRefs.delete(key)
|
|
364
642
|
}
|
|
643
|
+
await nextTick()
|
|
644
|
+
if (followTailIntent.value) scrollToEnd({ follow: true })
|
|
645
|
+
else if (anchor) restoreAnchor(anchor, false)
|
|
365
646
|
updateViewport()
|
|
366
647
|
})
|
|
367
648
|
|
|
368
649
|
onBeforeUnmount(() => {
|
|
369
650
|
boundExternalTarget?.removeEventListener('scroll', updateViewport)
|
|
651
|
+
boundExternalTarget?.removeEventListener('wheel', handleWheel)
|
|
652
|
+
boundExternalTarget?.removeEventListener('keydown', handleKeydown)
|
|
653
|
+
boundExternalTarget?.removeEventListener('pointerdown', handlePointerDown)
|
|
654
|
+
boundExternalTarget?.removeEventListener('pointerup', handlePointerUp)
|
|
370
655
|
resizeObserver?.disconnect()
|
|
371
656
|
renderedItemsByKey.clear()
|
|
372
657
|
renderedItemKeysByElement.clear()
|
package/src/styles/tokens.css
CHANGED
|
@@ -116,6 +116,9 @@
|
|
|
116
116
|
--s-control-hitbox-md: 2.5rem;
|
|
117
117
|
--s-control-hitbox-lg: 3rem;
|
|
118
118
|
--s-control-indicator-size: 1.25rem;
|
|
119
|
+
--s-resize-handle-hitbox-compact: 0.375rem;
|
|
120
|
+
--s-resize-handle-hitbox-standard: 0.75rem;
|
|
121
|
+
--s-resize-handle-hitbox-spacious: 2rem;
|
|
119
122
|
--s-icon-size-2xs: 0.75rem;
|
|
120
123
|
--s-icon-size-xs: 1rem;
|
|
121
124
|
--s-icon-size-sm: 1.25rem;
|