You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
596 lines
19 KiB
596 lines
19 KiB
import { context, type WithRefOpts } from '../../provider';
|
|
import { boolToEmptyStrOrUndef } from '$adom';
|
|
import { readableActive, state, type Active, type ActiveProps } from '$libs/reactive';
|
|
import type { Direction } from '../../types';
|
|
import { Soma } from '../../core/soma.svelte';
|
|
import { SCROLL_AREA_LANGS } from './langs';
|
|
import { isBrowser } from '$adom';
|
|
|
|
import { scrollAreaMorfo } from '../../../morfo/components/scroll-area';
|
|
import type { SomaRuntime, SomaRuntimePart } from '../../runtime.svelte';
|
|
import type { TimerHandle } from '$timer';
|
|
|
|
export type ScrollAreaType = 'hover' | 'scroll' | 'auto' | 'always';
|
|
|
|
// ── Root ─────────────────────────────────────────────────────────────────────
|
|
|
|
interface ScrollAreaOpts
|
|
extends
|
|
WithRefOpts,
|
|
ActiveProps<{
|
|
type: ScrollAreaType;
|
|
scrollHideDelay: number;
|
|
dir: Direction;
|
|
}> {}
|
|
|
|
export class ScrollAreaProvider {
|
|
readonly opts: ScrollAreaOpts;
|
|
readonly runtimePart: SomaRuntimePart;
|
|
readonly soma: Soma;
|
|
readonly runtime: SomaRuntime;
|
|
static readonly ctx = context<ScrollAreaProvider>('ScrollArea');
|
|
static get(): ScrollAreaProvider | undefined {
|
|
return this.ctx.getOr(undefined) as ScrollAreaProvider | undefined;
|
|
}
|
|
static require(): ScrollAreaProvider {
|
|
return this.ctx.get();
|
|
}
|
|
|
|
static create(opts: ScrollAreaOpts) {
|
|
return new ScrollAreaProvider(opts);
|
|
}
|
|
|
|
// Viewport tracking
|
|
viewportRef = state<HTMLElement | null>(null);
|
|
contentWidth = $state(0);
|
|
contentHeight = $state(0);
|
|
viewportWidth = $state(0);
|
|
viewportHeight = $state(0);
|
|
scrollTop = $state(0);
|
|
scrollLeft = $state(0);
|
|
|
|
private resizeObserver: ResizeObserver | null = null;
|
|
|
|
private constructor(opts: ScrollAreaOpts) {
|
|
this.opts = opts;
|
|
this.soma = Soma.require();
|
|
this.runtime = this.soma.runtime(scrollAreaMorfo, {});
|
|
this.runtimePart = this.runtime.part('provider', {
|
|
id: opts.id,
|
|
ref: opts.ref,
|
|
owner: this,
|
|
context: ScrollAreaProvider.ctx
|
|
});
|
|
|
|
// Cleanup resize observer on unmount
|
|
$effect(() => {
|
|
return () => this.cleanupResize();
|
|
});
|
|
}
|
|
|
|
// ── Overflow detection ───────────────────────────────────────────────────
|
|
|
|
readonly hasOverflowX = $derived.by(() => this.contentWidth > this.viewportWidth);
|
|
readonly hasOverflowY = $derived.by(() => this.contentHeight > this.viewportHeight);
|
|
|
|
// ── Position tracking ────────────────────────────────────────────────────
|
|
|
|
readonly isAtTop = $derived.by(() => this.scrollTop <= 0);
|
|
readonly isAtBottom = $derived.by(
|
|
() => this.scrollTop >= this.contentHeight - this.viewportHeight - 1
|
|
);
|
|
readonly isAtLeft = $derived.by(() => this.scrollLeft <= 0);
|
|
readonly isAtRight = $derived.by(
|
|
() => this.scrollLeft >= this.contentWidth - this.viewportWidth - 1
|
|
);
|
|
|
|
// ── Scroll progress (0..1) ───────────────────────────────────────────────
|
|
|
|
getScrollProgressY(): number {
|
|
const max = this.contentHeight - this.viewportHeight;
|
|
return max <= 0 ? 0 : this.scrollTop / max;
|
|
}
|
|
|
|
getScrollProgressX(): number {
|
|
const max = this.contentWidth - this.viewportWidth;
|
|
return max <= 0 ? 0 : this.scrollLeft / max;
|
|
}
|
|
|
|
// ── Viewport setup ───────────────────────────────────────────────────────
|
|
|
|
setupViewport(el: HTMLElement | null) {
|
|
this.viewportRef.current = el;
|
|
this.cleanupResize();
|
|
|
|
if (!el || !isBrowser) return;
|
|
|
|
const update = () => {
|
|
this.viewportWidth = el.clientWidth;
|
|
this.viewportHeight = el.clientHeight;
|
|
this.contentWidth = el.scrollWidth;
|
|
this.contentHeight = el.scrollHeight;
|
|
};
|
|
|
|
update();
|
|
|
|
this.resizeObserver = new ResizeObserver(update);
|
|
this.resizeObserver.observe(el);
|
|
if (el.firstElementChild) {
|
|
this.resizeObserver.observe(el.firstElementChild);
|
|
}
|
|
}
|
|
|
|
handleScroll(el: HTMLElement) {
|
|
this.scrollTop = el.scrollTop;
|
|
this.scrollLeft = el.scrollLeft;
|
|
// Re-measure in case content changed
|
|
this.contentWidth = el.scrollWidth;
|
|
this.contentHeight = el.scrollHeight;
|
|
}
|
|
|
|
private cleanupResize() {
|
|
this.resizeObserver?.disconnect();
|
|
this.resizeObserver = null;
|
|
}
|
|
|
|
readonly props = $derived.by(() =>
|
|
this.runtimePart.assert({
|
|
...this.runtimePart.props,
|
|
dir: this.opts.dir.current,
|
|
style: {
|
|
position: 'relative',
|
|
'--soma-scroll-area-corner-width':
|
|
this.hasOverflowX && this.hasOverflowY
|
|
? 'var(--soma-scroll-area-scrollbar-size, 8px)'
|
|
: '0px',
|
|
'--soma-scroll-area-corner-height':
|
|
this.hasOverflowX && this.hasOverflowY
|
|
? 'var(--soma-scroll-area-scrollbar-size, 8px)'
|
|
: '0px'
|
|
},
|
|
'data-overflow-x': boolToEmptyStrOrUndef(this.hasOverflowX),
|
|
'data-overflow-y': boolToEmptyStrOrUndef(this.hasOverflowY)
|
|
} as const)
|
|
);
|
|
}
|
|
|
|
// ── Viewport ────────────────────────────────────────────────────────────────
|
|
|
|
interface ScrollAreaViewportOpts extends WithRefOpts {}
|
|
|
|
export class ScrollAreaViewportProvider {
|
|
readonly opts: ScrollAreaViewportOpts;
|
|
readonly runtimePart: SomaRuntimePart;
|
|
static create(opts: ScrollAreaViewportOpts) {
|
|
return new ScrollAreaViewportProvider(opts);
|
|
}
|
|
|
|
readonly provider: ScrollAreaProvider;
|
|
|
|
private constructor(opts: ScrollAreaViewportOpts) {
|
|
this.opts = opts;
|
|
this.provider = ScrollAreaProvider.require();
|
|
this.runtimePart = this.provider.runtime.part('viewport', {
|
|
id: opts.id,
|
|
ref: opts.ref,
|
|
owner: this,
|
|
onRefChange: (el) => {
|
|
this.provider.setupViewport(el);
|
|
}
|
|
});
|
|
}
|
|
|
|
readonly onscroll = (e: Event) => {
|
|
this.provider.handleScroll(e.currentTarget as HTMLElement);
|
|
};
|
|
|
|
readonly props = $derived.by(() =>
|
|
this.runtimePart.assert({
|
|
...this.runtimePart.props,
|
|
style: {
|
|
overflow: 'scroll',
|
|
'scrollbar-width': 'none'
|
|
},
|
|
'data-at-top': boolToEmptyStrOrUndef(this.provider.isAtTop),
|
|
'data-at-bottom': boolToEmptyStrOrUndef(this.provider.isAtBottom),
|
|
'data-at-left': boolToEmptyStrOrUndef(this.provider.isAtLeft),
|
|
'data-at-right': boolToEmptyStrOrUndef(this.provider.isAtRight),
|
|
'data-overflow-x': boolToEmptyStrOrUndef(this.provider.hasOverflowX),
|
|
'data-overflow-y': boolToEmptyStrOrUndef(this.provider.hasOverflowY),
|
|
onscroll: this.onscroll
|
|
} as const)
|
|
);
|
|
}
|
|
|
|
// ── Scrollbar ───────────────────────────────────────────────────────────────
|
|
|
|
type Orientation = 'horizontal' | 'vertical';
|
|
|
|
interface ScrollAreaScrollbarOpts
|
|
extends
|
|
WithRefOpts,
|
|
ActiveProps<{
|
|
orientation: Orientation;
|
|
}> {}
|
|
|
|
export class ScrollAreaScrollbarProvider {
|
|
readonly opts: ScrollAreaScrollbarOpts;
|
|
readonly runtimePart: SomaRuntimePart;
|
|
static readonly ctx = context<ScrollAreaScrollbarProvider>('ScrollAreaScrollbar');
|
|
static get(): ScrollAreaScrollbarProvider | undefined {
|
|
return this.ctx.getOr(undefined) as ScrollAreaScrollbarProvider | undefined;
|
|
}
|
|
static require(): ScrollAreaScrollbarProvider {
|
|
return this.ctx.get();
|
|
}
|
|
|
|
static create(opts: ScrollAreaScrollbarOpts) {
|
|
return new ScrollAreaScrollbarProvider(opts);
|
|
}
|
|
|
|
readonly provider: ScrollAreaProvider;
|
|
|
|
// Visibility state
|
|
visible = $state(false);
|
|
private hideTimer: TimerHandle | null = null;
|
|
private mounted = false;
|
|
hovering = $state(false);
|
|
dragging = $state(false);
|
|
|
|
private constructor(opts: ScrollAreaScrollbarOpts) {
|
|
this.opts = opts;
|
|
this.provider = ScrollAreaProvider.require();
|
|
this.runtimePart = this.provider.runtime.part('scrollbar', {
|
|
id: opts.id,
|
|
ref: opts.ref,
|
|
owner: this,
|
|
context: ScrollAreaScrollbarProvider.ctx
|
|
});
|
|
|
|
// Skip initial-mount flash — defer mounted flag to next frame
|
|
const mountedFrame = this.provider.soma.dom.requestFrame(() => {
|
|
this.mounted = true;
|
|
}, opts.ref.current);
|
|
|
|
// Cleanup on unmount
|
|
$effect(() => {
|
|
return () => {
|
|
this.provider.soma.dom.cancelFrame(mountedFrame, opts.ref.current);
|
|
this.clearHideTimer();
|
|
};
|
|
});
|
|
|
|
// Reveal the scrollbar on parent scroll for 'scroll' and 'hover' types.
|
|
// Previously lived in the wrapper as an ad-hoc effect.
|
|
$effect(() => {
|
|
void this.provider.scrollTop;
|
|
void this.provider.scrollLeft;
|
|
const type = this.provider.opts.type.current;
|
|
if (type === 'scroll' || type === 'hover') {
|
|
this.show();
|
|
this.scheduleHide();
|
|
}
|
|
});
|
|
|
|
// Reveal on root hover for 'hover' type. Previously the wrapper attached
|
|
// pointerenter/pointerleave listeners to the root element — move that
|
|
// behavior to the provider so the wrapper stays thin.
|
|
$effect(() => {
|
|
const rootEl = this.provider.opts.ref?.current;
|
|
if (!rootEl || this.provider.opts.type.current !== 'hover') return;
|
|
|
|
const onenter = () => this.show();
|
|
const onleave = () => {
|
|
if (!this.dragging) this.scheduleHide();
|
|
};
|
|
|
|
const cleanupEnter = this.provider.soma.dom.listen(rootEl, 'pointerenter', onenter);
|
|
const cleanupLeave = this.provider.soma.dom.listen(rootEl, 'pointerleave', onleave);
|
|
return () => {
|
|
cleanupEnter();
|
|
cleanupLeave();
|
|
};
|
|
});
|
|
}
|
|
|
|
private get isVertical(): boolean {
|
|
return this.opts.orientation.current === 'vertical';
|
|
}
|
|
|
|
private get hasOverflow(): boolean {
|
|
return this.isVertical ? this.provider.hasOverflowY : this.provider.hasOverflowX;
|
|
}
|
|
|
|
/** Whether the scrollbar should be displayed based on type and state. */
|
|
readonly shouldShow = $derived.by(() => {
|
|
const type = this.provider.opts.type.current;
|
|
if (type === 'always') return true;
|
|
if (!this.hasOverflow) return false;
|
|
// Skip initial mount to prevent flash
|
|
if (!this.mounted && (type === 'scroll' || type === 'hover')) return false;
|
|
if (type === 'auto') return true;
|
|
if (type === 'hover') return this.visible;
|
|
if (type === 'scroll') return this.visible;
|
|
return false;
|
|
});
|
|
|
|
/** Show the scrollbar (for hover/scroll modes). */
|
|
show() {
|
|
this.clearHideTimer();
|
|
this.visible = true;
|
|
}
|
|
|
|
/** Schedule hiding the scrollbar after delay. */
|
|
scheduleHide() {
|
|
this.clearHideTimer();
|
|
const delay = this.provider.opts.scrollHideDelay.current;
|
|
this.hideTimer = this.provider.soma.uix.timers.schedule(
|
|
this.hideTimerKey,
|
|
delay,
|
|
() => {
|
|
this.hideTimer = null;
|
|
if (!this.hovering && !this.dragging) {
|
|
this.visible = false;
|
|
}
|
|
},
|
|
{ replace: true, meta: { component: 'scroll-area', action: 'hide-scrollbar' } }
|
|
);
|
|
}
|
|
|
|
private clearHideTimer() {
|
|
if (this.hideTimer) {
|
|
this.hideTimer.cancel();
|
|
this.hideTimer = null;
|
|
}
|
|
}
|
|
|
|
private get hideTimerKey() {
|
|
return `soma:scroll-area:${this.opts.id.current}:hide:${this.opts.orientation.current}`;
|
|
}
|
|
|
|
readonly onpointerenter = () => {
|
|
this.hovering = true;
|
|
this.show();
|
|
};
|
|
|
|
readonly onpointerleave = () => {
|
|
this.hovering = false;
|
|
if (!this.dragging) this.scheduleHide();
|
|
};
|
|
|
|
/** Handle click on the track (not thumb) to scroll to that position. */
|
|
readonly onclick = (e: MouseEvent) => {
|
|
const el = this.opts.ref.current;
|
|
const viewport = this.provider.viewportRef.current;
|
|
if (!el || !viewport) return;
|
|
|
|
const rect = el.getBoundingClientRect();
|
|
|
|
if (this.isVertical) {
|
|
const clickRatio = (e.clientY - rect.top) / rect.height;
|
|
const maxScroll = this.provider.contentHeight - this.provider.viewportHeight;
|
|
viewport.scrollTop = clickRatio * maxScroll;
|
|
} else {
|
|
const clickRatio = (e.clientX - rect.left) / rect.width;
|
|
const maxScroll = this.provider.contentWidth - this.provider.viewportWidth;
|
|
viewport.scrollLeft = clickRatio * maxScroll;
|
|
}
|
|
};
|
|
|
|
// ── Scroll value for ARIA ────────────────────────────────────────────────
|
|
|
|
private get scrollPercent(): number {
|
|
return Math.round(
|
|
(this.isVertical ? this.provider.getScrollProgressY() : this.provider.getScrollProgressX()) *
|
|
100
|
|
);
|
|
}
|
|
|
|
readonly props = $derived.by(() =>
|
|
this.runtimePart.assert({
|
|
...this.runtimePart.props,
|
|
role: 'scrollbar' as const,
|
|
'aria-controls': this.provider.viewportRef.current?.id || undefined,
|
|
'aria-valuenow': this.scrollPercent,
|
|
'aria-valuemin': 0,
|
|
'aria-valuemax': 100,
|
|
'aria-orientation': this.opts.orientation.current,
|
|
'aria-label': this.isVertical
|
|
? this.provider.soma.langs.ts(SCROLL_AREA_LANGS.SCROLLBAR_VERTICAL)
|
|
: this.provider.soma.langs.ts(SCROLL_AREA_LANGS.SCROLLBAR_HORIZONTAL),
|
|
tabindex: -1,
|
|
'data-state': this.shouldShow ? 'visible' : 'hidden',
|
|
'data-orientation': this.opts.orientation.current,
|
|
'data-hover': boolToEmptyStrOrUndef(this.hovering),
|
|
'data-dragging': boolToEmptyStrOrUndef(this.dragging),
|
|
'data-overflow-x': boolToEmptyStrOrUndef(this.provider.hasOverflowX),
|
|
'data-overflow-y': boolToEmptyStrOrUndef(this.provider.hasOverflowY),
|
|
onpointerenter: this.onpointerenter,
|
|
onpointerleave: this.onpointerleave,
|
|
onclick: this.onclick
|
|
} as const)
|
|
);
|
|
}
|
|
|
|
// ── Thumb ────────────────────────────────────────────────────────────────────
|
|
|
|
interface ScrollAreaThumbOpts extends WithRefOpts {}
|
|
|
|
export class ScrollAreaThumbProvider {
|
|
readonly opts: ScrollAreaThumbOpts;
|
|
readonly runtimePart: SomaRuntimePart;
|
|
static create(opts: ScrollAreaThumbOpts) {
|
|
return new ScrollAreaThumbProvider(opts);
|
|
}
|
|
|
|
readonly scrollbar: ScrollAreaScrollbarProvider;
|
|
readonly provider: ScrollAreaProvider;
|
|
|
|
private dragStartY = 0;
|
|
private dragStartX = 0;
|
|
private dragStartScrollTop = 0;
|
|
private dragStartScrollLeft = 0;
|
|
|
|
private constructor(opts: ScrollAreaThumbOpts) {
|
|
this.opts = opts;
|
|
this.scrollbar = ScrollAreaScrollbarProvider.require();
|
|
this.provider = this.scrollbar.provider;
|
|
this.runtimePart = this.provider.runtime.part('thumb', {
|
|
id: opts.id,
|
|
ref: opts.ref,
|
|
owner: this
|
|
});
|
|
}
|
|
|
|
private get isVertical(): boolean {
|
|
return this.scrollbar.opts.orientation.current === 'vertical';
|
|
}
|
|
|
|
// ── Size calculation ─────────────────────────────────────────────────────
|
|
|
|
private get thumbSize(): number {
|
|
if (this.isVertical) {
|
|
if (this.provider.contentHeight <= 0) return 0;
|
|
const ratio = this.provider.viewportHeight / this.provider.contentHeight;
|
|
const trackHeight = this.scrollbar.opts.ref.current?.clientHeight ?? 0;
|
|
return Math.max(ratio * trackHeight, 20);
|
|
} else {
|
|
if (this.provider.contentWidth <= 0) return 0;
|
|
const ratio = this.provider.viewportWidth / this.provider.contentWidth;
|
|
const trackWidth = this.scrollbar.opts.ref.current?.clientWidth ?? 0;
|
|
return Math.max(ratio * trackWidth, 20);
|
|
}
|
|
}
|
|
|
|
private get thumbOffset(): number {
|
|
const scrollbarEl = this.scrollbar.opts.ref.current;
|
|
if (!scrollbarEl) return 0;
|
|
|
|
if (this.isVertical) {
|
|
const trackHeight = scrollbarEl.clientHeight;
|
|
const maxOffset = trackHeight - this.thumbSize;
|
|
return this.provider.getScrollProgressY() * maxOffset;
|
|
} else {
|
|
const trackWidth = scrollbarEl.clientWidth;
|
|
const maxOffset = trackWidth - this.thumbSize;
|
|
return this.provider.getScrollProgressX() * maxOffset;
|
|
}
|
|
}
|
|
|
|
// ── Drag handling ────────────────────────────────────────────────────────
|
|
|
|
readonly onpointerdown = (e: PointerEvent) => {
|
|
if (e.button !== 0) return;
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
|
|
const viewport = this.provider.viewportRef.current;
|
|
if (!viewport) return;
|
|
|
|
this.scrollbar.dragging = true;
|
|
this.dragStartY = e.clientY;
|
|
this.dragStartX = e.clientX;
|
|
this.dragStartScrollTop = viewport.scrollTop;
|
|
this.dragStartScrollLeft = viewport.scrollLeft;
|
|
|
|
(e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
|
|
};
|
|
|
|
readonly onpointermove = (e: PointerEvent) => {
|
|
if (!this.scrollbar.dragging) return;
|
|
e.preventDefault();
|
|
|
|
const viewport = this.provider.viewportRef.current;
|
|
const scrollbarEl = this.scrollbar.opts.ref.current;
|
|
if (!viewport || !scrollbarEl) return;
|
|
|
|
if (this.isVertical) {
|
|
const deltaY = e.clientY - this.dragStartY;
|
|
const trackHeight = scrollbarEl.clientHeight;
|
|
const scrollRatio = deltaY / (trackHeight - this.thumbSize);
|
|
const maxScroll = this.provider.contentHeight - this.provider.viewportHeight;
|
|
viewport.scrollTop = this.dragStartScrollTop + scrollRatio * maxScroll;
|
|
} else {
|
|
const deltaX = e.clientX - this.dragStartX;
|
|
const trackWidth = scrollbarEl.clientWidth;
|
|
const scrollRatio = deltaX / (trackWidth - this.thumbSize);
|
|
const maxScroll = this.provider.contentWidth - this.provider.viewportWidth;
|
|
viewport.scrollLeft = this.dragStartScrollLeft + scrollRatio * maxScroll;
|
|
}
|
|
};
|
|
|
|
readonly onpointerup = (_e: PointerEvent) => {
|
|
this.scrollbar.dragging = false;
|
|
this.scrollbar.scheduleHide();
|
|
};
|
|
|
|
readonly onlostpointercapture = () => {
|
|
this.scrollbar.dragging = false;
|
|
};
|
|
|
|
readonly onclick = (e: MouseEvent) => {
|
|
e.stopPropagation();
|
|
};
|
|
|
|
readonly props = $derived.by(() =>
|
|
this.runtimePart.assert({
|
|
...this.runtimePart.props,
|
|
'data-state': this.scrollbar.shouldShow ? 'visible' : 'hidden',
|
|
'data-orientation': this.scrollbar.opts.orientation.current,
|
|
'data-hover': boolToEmptyStrOrUndef(this.scrollbar.hovering),
|
|
'data-dragging': boolToEmptyStrOrUndef(this.scrollbar.dragging),
|
|
style: {
|
|
position: 'absolute',
|
|
[this.isVertical ? 'top' : 'left']: `${this.thumbOffset}px`,
|
|
[this.isVertical ? 'height' : 'width']: `${this.thumbSize}px`,
|
|
[this.isVertical ? 'width' : 'height']: '100%',
|
|
'border-radius': 'inherit',
|
|
'--soma-scroll-area-thumb-width': `${this.isVertical ? '100%' : `${this.thumbSize}px`}`,
|
|
'--soma-scroll-area-thumb-height': `${this.isVertical ? `${this.thumbSize}px` : '100%'}`
|
|
},
|
|
onpointerdown: this.onpointerdown,
|
|
onpointermove: this.onpointermove,
|
|
onpointerup: this.onpointerup,
|
|
onlostpointercapture: this.onlostpointercapture,
|
|
onclick: this.onclick
|
|
} as const)
|
|
);
|
|
}
|
|
|
|
// ── Corner ──────────────────────────────────────────────────────────────────
|
|
|
|
interface ScrollAreaCornerOpts extends WithRefOpts {}
|
|
|
|
export class ScrollAreaCornerProvider {
|
|
readonly opts: ScrollAreaCornerOpts;
|
|
readonly runtimePart: SomaRuntimePart;
|
|
static create(opts: ScrollAreaCornerOpts) {
|
|
return new ScrollAreaCornerProvider(opts);
|
|
}
|
|
|
|
readonly provider: ScrollAreaProvider;
|
|
|
|
private constructor(opts: ScrollAreaCornerOpts) {
|
|
this.opts = opts;
|
|
this.provider = ScrollAreaProvider.require();
|
|
this.runtimePart = this.provider.runtime.part('corner', {
|
|
id: opts.id,
|
|
ref: opts.ref,
|
|
owner: this
|
|
});
|
|
}
|
|
|
|
readonly shouldShow = $derived.by(() => this.provider.hasOverflowX && this.provider.hasOverflowY);
|
|
|
|
readonly props = $derived.by(() =>
|
|
this.runtimePart.assert({
|
|
...this.runtimePart.props,
|
|
'data-state': this.shouldShow ? 'visible' : 'hidden',
|
|
style: {
|
|
width: 'var(--soma-scroll-area-corner-width)',
|
|
height: 'var(--soma-scroll-area-corner-height)'
|
|
}
|
|
} as const)
|
|
);
|
|
}
|