From e6f5142e483c93388d7269c0ac4273d09ee116db Mon Sep 17 00:00:00 2001 From: Will Miao Date: Fri, 4 Sep 2026 19:02:31 +0800 Subject: [PATCH] fix(ui): re-measure autocomplete scrollbar inset on programmatic value changes The --lm-vscrollbar-width inset from 634ea7f2 was only refreshed on input events, mount and mode changes. Programmatic value updates (widget.setValue from "send lora to workflow", external value-change events) change the textarea content without an input event, leaving the corner clear (x) button overlapping a freshly appeared classic scrollbar until the next keystroke. Mount-time pending value replay was already covered. - onExternalValueChange and widget.onSetValue now call updateVScrollbarWidth() alongside the hasText update - tests: cover both paths by overriding textarea metrics to an overflowing state and asserting the 15px gutter lands in the CSS var --- .../src/components/AutocompleteTextWidget.vue | 6 ++ .../components/AutocompleteTextWidget.test.ts | 63 ++++++++++++++++++- 2 files changed, 68 insertions(+), 1 deletion(-) diff --git a/vue-widgets/src/components/AutocompleteTextWidget.vue b/vue-widgets/src/components/AutocompleteTextWidget.vue index a9010188..d1f6947c 100644 --- a/vue-widgets/src/components/AutocompleteTextWidget.vue +++ b/vue-widgets/src/components/AutocompleteTextWidget.vue @@ -200,6 +200,9 @@ const onWheel = (event: WheelEvent) => { // Handle external value changes (e.g., from "send lora to workflow") const onExternalValueChange = () => { + // The DOM value was set synchronously by the caller; the new content may + // have toggled the vertical scrollbar on/off. + updateVScrollbarWidth() updateHasTextState() } @@ -209,6 +212,9 @@ const setupWidgetOnSetValue = () => { props.widget.onSetValue = (value: string) => { // The DOM value is already set by setValue, just update our state hasText.value = value.length > 0 + // Programmatic sets can toggle the scrollbar; re-measure so the + // corner clear button stays clear of it. + updateVScrollbarWidth() } } } diff --git a/vue-widgets/tests/components/AutocompleteTextWidget.test.ts b/vue-widgets/tests/components/AutocompleteTextWidget.test.ts index 941cda0a..84aaee47 100644 --- a/vue-widgets/tests/components/AutocompleteTextWidget.test.ts +++ b/vue-widgets/tests/components/AutocompleteTextWidget.test.ts @@ -16,7 +16,7 @@ import AutocompleteTextWidget from '@/components/AutocompleteTextWidget.vue' function createMockWidget() { return { callback: vi.fn(), - onSetValue: undefined, + onSetValue: undefined as ((v: string) => void) | undefined, inputEl: undefined, metadataWidget: undefined, name: 'text', @@ -217,4 +217,65 @@ describe('AutocompleteTextWidget vertical scrollbar inset', () => { expect(wrapperEl.style.getPropertyValue('--lm-vscrollbar-width')).toBe('0px') }) + it('re-measures the inset when the value is set programmatically via onSetValue', async () => { + const { wrapper, widget } = mountWidget() + const textarea = wrapper.find('textarea').element as HTMLTextAreaElement + + // Start with no overflow + overrideTextareaMetrics(textarea, { + scrollHeight: 100, + clientHeight: 100, + offsetWidth: 320, + clientWidth: 320, + }) + await nextTick() + + // Simulate an external setValue (e.g. "send lora to workflow"): the DOM + // value is set by the caller and widget.onSetValue fires without an + // input event. Content now overflows → inset must be re-measured. + overrideTextareaMetrics(textarea, { + scrollHeight: 200, + clientHeight: 100, + offsetWidth: 320, + clientWidth: 305, // 15px scrollbar gutter + }) + if (!widget.onSetValue) throw new Error('onSetValue not installed by component') + widget.onSetValue('long content') + await nextTick() + + const wrapperEl = wrapper.find('.input-wrapper').element as HTMLElement + expect(wrapperEl.style.getPropertyValue('--lm-vscrollbar-width')).toBe('15px') + }) + + it('re-measures the inset on external value-change events', async () => { + const { wrapper } = mountWidget() + const textarea = wrapper.find('textarea').element as HTMLTextAreaElement + + // Start with no overflow + overrideTextareaMetrics(textarea, { + scrollHeight: 100, + clientHeight: 100, + offsetWidth: 320, + clientWidth: 320, + }) + await nextTick() + + // The lora-manager:autocomplete-value-changed event fires when the + // widget value is set externally; content now overflows → re-measure. + overrideTextareaMetrics(textarea, { + scrollHeight: 200, + clientHeight: 100, + offsetWidth: 320, + clientWidth: 305, // 15px scrollbar gutter + }) + textarea.dispatchEvent( + new CustomEvent('lora-manager:autocomplete-value-changed', { + detail: { value: 'long content' }, + }) + ) + await nextTick() + + const wrapperEl = wrapper.find('.input-wrapper').element as HTMLElement + expect(wrapperEl.style.getPropertyValue('--lm-vscrollbar-width')).toBe('15px') + }) })