fix(frontend): 修复分组描述换行和下拉框溢出
This commit is contained in:
@@ -16,7 +16,7 @@
|
||||
<!-- Row 2: description with top spacing -->
|
||||
<span
|
||||
v-if="description"
|
||||
class="mt-1.5 w-full text-left text-xs leading-relaxed text-gray-500 dark:text-gray-400 line-clamp-2"
|
||||
class="mt-1.5 w-full whitespace-pre-line [overflow-wrap:anywhere] text-left text-xs leading-relaxed text-gray-500 dark:text-gray-400 line-clamp-3"
|
||||
>
|
||||
{{ description }}
|
||||
</span>
|
||||
|
||||
@@ -184,6 +184,7 @@ const dropdownRef = ref<HTMLElement | null>(null)
|
||||
const optionsListRef = ref<HTMLElement | null>(null)
|
||||
const dropdownPosition = ref<'bottom' | 'top'>('bottom')
|
||||
const triggerRect = ref<DOMRect | null>(null)
|
||||
const dropdownViewportPadding = 8
|
||||
|
||||
// i18n placeholders
|
||||
const placeholderText = computed(() => props.placeholder ?? t('common.selectOption'))
|
||||
@@ -200,10 +201,14 @@ const dropdownStyle = computed(() => {
|
||||
if (!triggerRect.value) return {}
|
||||
|
||||
const rect = triggerRect.value
|
||||
const left = Math.max(dropdownViewportPadding, rect.left)
|
||||
const availableWidth = Math.max(0, window.innerWidth - left - dropdownViewportPadding)
|
||||
const minWidth = Math.min(rect.width, availableWidth)
|
||||
const style: Record<string, string> = {
|
||||
position: 'fixed',
|
||||
left: `${rect.left}px`,
|
||||
minWidth: `${rect.width}px`,
|
||||
left: `${left}px`,
|
||||
minWidth: `${minWidth}px`,
|
||||
maxWidth: `${availableWidth}px`,
|
||||
zIndex: '100000020'
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import GroupOptionItem from '../GroupOptionItem.vue'
|
||||
|
||||
vi.mock('vue-i18n', async () => {
|
||||
const actual = await vi.importActual<typeof import('vue-i18n')>('vue-i18n')
|
||||
return {
|
||||
...actual,
|
||||
useI18n: () => ({ t: (key: string) => key }),
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/stores/app', () => ({
|
||||
useAppStore: () => ({ cachedPublicSettings: null }),
|
||||
}))
|
||||
|
||||
describe('GroupOptionItem description layout', () => {
|
||||
it('applies multiline and overflow-safe text styles', () => {
|
||||
const description = 'First section\nvery-long-unbroken-description-value-that-must-not-overflow'
|
||||
const wrapper = mount(GroupOptionItem, {
|
||||
props: {
|
||||
name: 'Example group',
|
||||
platform: 'openai',
|
||||
description,
|
||||
},
|
||||
global: {
|
||||
stubs: {
|
||||
GroupBadge: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const descriptionElement = wrapper
|
||||
.findAll('span')
|
||||
.find((element) => element.text() === description)
|
||||
|
||||
expect(descriptionElement).toBeDefined()
|
||||
expect(descriptionElement?.classes()).toContain('whitespace-pre-line')
|
||||
expect(descriptionElement?.classes()).toContain('[overflow-wrap:anywhere]')
|
||||
expect(descriptionElement?.classes()).toContain('line-clamp-3')
|
||||
expect(wrapper.find('[title]').attributes('title')).toBe(description)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,63 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { nextTick } from 'vue'
|
||||
|
||||
import Select from '../Select.vue'
|
||||
|
||||
vi.mock('vue-i18n', async () => {
|
||||
const actual = await vi.importActual<typeof import('vue-i18n')>('vue-i18n')
|
||||
return {
|
||||
...actual,
|
||||
useI18n: () => ({ t: (key: string) => key }),
|
||||
}
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
document.body.innerHTML = ''
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
describe('Select dropdown viewport constraints', () => {
|
||||
it('limits the teleported dropdown to the available viewport width', async () => {
|
||||
Object.defineProperty(window, 'innerWidth', {
|
||||
configurable: true,
|
||||
value: 320,
|
||||
})
|
||||
|
||||
vi.spyOn(HTMLElement.prototype, 'getBoundingClientRect').mockReturnValue({
|
||||
x: 220,
|
||||
y: 20,
|
||||
top: 20,
|
||||
right: 300,
|
||||
bottom: 60,
|
||||
left: 220,
|
||||
width: 80,
|
||||
height: 40,
|
||||
toJSON: () => ({}),
|
||||
})
|
||||
|
||||
const wrapper = mount(Select, {
|
||||
props: {
|
||||
modelValue: null,
|
||||
options: [
|
||||
{
|
||||
value: 'example',
|
||||
label: 'very-long-unbroken-option-value-that-must-not-overflow',
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
await wrapper.get('button').trigger('click')
|
||||
await nextTick()
|
||||
|
||||
const dropdown = document.body.querySelector<HTMLElement>('.select-dropdown-portal')
|
||||
|
||||
expect(dropdown).not.toBeNull()
|
||||
expect(dropdown?.style.left).toBe('220px')
|
||||
expect(dropdown?.style.minWidth).toBe('80px')
|
||||
expect(dropdown?.style.maxWidth).toBe('92px')
|
||||
|
||||
wrapper.unmount()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user