From 21d7426db1ec772147adbc4ce2dffb4f3935b6c5 Mon Sep 17 00:00:00 2001 From: James-9696 Date: Thu, 25 Dec 2025 01:30:48 -0800 Subject: [PATCH 1/2] fix(modal): add aria-* attribute --- .../demos/pc/app/modal/basic-usage.spec.ts | 6 +-- .../modal/modal-footer-composition-api.vue | 6 +-- .../demos/pc/app/modal/modal-footer.spec.ts | 13 +++++-- .../sites/demos/pc/app/modal/modal-footer.vue | 6 +-- .../demos/pc/app/modal/modal-header.spec.ts | 4 +- .../demos/pc/app/modal/modal-size.spec.ts | 4 +- .../sites/demos/pc/app/modal/status.spec.ts | 37 +++++++++++-------- packages/vue/src/modal/src/pc.vue | 15 +++++++- 8 files changed, 57 insertions(+), 34 deletions(-) diff --git a/examples/sites/demos/pc/app/modal/basic-usage.spec.ts b/examples/sites/demos/pc/app/modal/basic-usage.spec.ts index 32c3178063..f861cce6c0 100644 --- a/examples/sites/demos/pc/app/modal/basic-usage.spec.ts +++ b/examples/sites/demos/pc/app/modal/basic-usage.spec.ts @@ -5,7 +5,7 @@ test('基本用法', async ({ page }) => { await page.goto('modal#basic-usage') const modal = page.locator('.tiny-modal__status-wrapper svg').first() - const content = page.locator('.tiny-modal__content') + const content = page.locator('.tiny-modal.type__alert.is__visible') // 基本提示框 await page.getByRole('button', { name: '基本提示框' }).click() @@ -24,11 +24,11 @@ test('基本用法', async ({ page }) => { // 打开弹窗 1 await page.getByRole('button', { name: '打开弹窗 1' }).click() - await expect(content.nth(1)).toHaveText(/窗口内容1/) + await expect(content).toHaveText(/窗口内容1/) await page.getByRole('button', { name: '确定' }).click() // 打开弹窗 2 await page.getByRole('button', { name: '打开弹窗 2' }).click() - await expect(content.nth(2)).toHaveText(/窗口内容2/) + await expect(content).toHaveText(/窗口内容2/) await page.getByRole('button', { name: '确定' }).click() }) diff --git a/examples/sites/demos/pc/app/modal/modal-footer-composition-api.vue b/examples/sites/demos/pc/app/modal/modal-footer-composition-api.vue index 9c0aa35812..80b2991338 100644 --- a/examples/sites/demos/pc/app/modal/modal-footer-composition-api.vue +++ b/examples/sites/demos/pc/app/modal/modal-footer-composition-api.vue @@ -2,11 +2,11 @@

函数式调用

- 自定义弹窗底部 + 函数式自定义弹窗底部

标签式调用

- 自定义弹窗底部 + 标签式自定义弹窗底部

#foot 插槽

- 自定义弹窗底部 + 插槽自定义弹窗底部 { page.on('pageerror', (exception) => expect(exception).toBeNull()) await page.goto('modal#modal-footer') - const footer = page.locator('.tiny-modal__footer') + const footer = page.locator('.tiny-modal__footer > .tiny-button--info') + const footerLink = page.locator('.tiny-link--primary') // 自定义弹窗底部 - await page.getByRole('button', { name: '自定义弹窗底部' }).first().click() + await page.getByRole('button', { name: '函数式自定义弹窗底部' }).click() await expect(footer.first()).toHaveText(/Okk/) await page.getByRole('button', { name: 'Okk~~' }).click() - await page.getByRole('button', { name: '自定义弹窗底部' }).nth(2).click() - await expect(footer.nth(1)).toHaveText(/返回/) + await page.getByRole('button', { name: '标签式自定义弹窗底部' }).click() + await expect(footer.first()).toHaveText(/Okk/) + await page.getByRole('button', { name: 'Okk~~' }).click() + + await page.getByRole('button', { name: '插槽自定义弹窗底部' }).click() + await expect(footerLink.first()).toHaveText(/返回/) }) diff --git a/examples/sites/demos/pc/app/modal/modal-footer.vue b/examples/sites/demos/pc/app/modal/modal-footer.vue index 1cd630d0e0..6fc0d44a90 100644 --- a/examples/sites/demos/pc/app/modal/modal-footer.vue +++ b/examples/sites/demos/pc/app/modal/modal-footer.vue @@ -2,11 +2,11 @@

函数式调用

- 自定义弹窗底部 + 函数式自定义弹窗底部

标签式调用

- 自定义弹窗底部 + 标签式自定义弹窗底部

#foot 插槽

- 自定义弹窗底部 + 插槽自定义弹窗底部 { page.on('pageerror', (exception) => expect(exception).toBeNull()) await page.goto('modal#modal-header') const demo = page.locator('#modal-header') - const header = page.locator('.tiny-modal__title') + const header = page.locator('.tiny-modal.is__visible') // 自定义弹窗标题 await demo.getByRole('button', { name: '自定义弹窗标题' }).first().click() - await expect(header.first()).toHaveText(/自定义弹窗标题/) + await expect(header).toHaveText(/自定义弹窗标题/) await page.getByRole('button', { name: '确定' }).click() }) diff --git a/examples/sites/demos/pc/app/modal/modal-size.spec.ts b/examples/sites/demos/pc/app/modal/modal-size.spec.ts index 9419f9d178..e75acb0231 100644 --- a/examples/sites/demos/pc/app/modal/modal-size.spec.ts +++ b/examples/sites/demos/pc/app/modal/modal-size.spec.ts @@ -3,13 +3,13 @@ import { test, expect } from '@playwright/test' test('弹窗大小全屏', async ({ page }) => { page.on('pageerror', (exception) => expect(exception).toBeNull()) await page.goto('modal#modal-size') - const modal = page.locator('.tiny-modal') + const modal = page.locator('.status__question') const box = page.locator('.type__confirm > .tiny-modal__box') await page.getByRole('button', { name: '自定义弹窗大小' }).first().click() await expect(box.first()).toHaveCSS('width', '800px') await page.getByRole('button', { name: '确定' }).click() await page.getByRole('button', { name: '弹窗全屏' }).first().click() - await expect(modal.nth(2)).toHaveClass(/is__maximize/) + await expect(modal).toHaveClass(/is__maximize/) await page.getByRole('button', { name: '确定' }).click() }) diff --git a/examples/sites/demos/pc/app/modal/status.spec.ts b/examples/sites/demos/pc/app/modal/status.spec.ts index 16bae6d4e4..14c139bcb0 100644 --- a/examples/sites/demos/pc/app/modal/status.spec.ts +++ b/examples/sites/demos/pc/app/modal/status.spec.ts @@ -3,46 +3,53 @@ import { test, expect } from '@playwright/test' test('状态和图标', async ({ page }) => { page.on('pageerror', (exception) => expect(exception).toBeNull()) await page.goto('modal#status') - const modal = page.locator('.tiny-modal__status-wrapper svg').first() - const message = page.locator('.type__message') + const modal = page.locator( + '.tiny-modal__wrapper.is__visible > .tiny-modal__box > .tiny-modal__header > .tiny-modal__status-wrapper > svg' + ) + const message = page.locator('.type__message.is__visible') // 信息提示图标 await page.getByRole('button', { name: '信息提示图标' }).first().click() await expect(modal).toHaveClass(/tiny-modal-svg__info/) await page.getByRole('button', { name: '确定' }).click() - // ----消息状态示例---- - await page.getByRole('button', { name: '信息提示图标' }).nth(1).click() - await expect(message).toHaveClass(/status__info/) // 成功提示图标 await page.getByRole('button', { name: '成功提示图标' }).first().click() await expect(modal).toHaveClass(/tiny-modal-svg__success/) await page.getByRole('button', { name: '确定' }).click() - // ----消息状态示例---- - await page.getByRole('button', { name: '成功提示图标' }).nth(1).click() - await expect(message).toHaveClass(/status__success/) // 警告提示图标 await page.getByRole('button', { name: '警告提示图标' }).first().click() await expect(modal).toHaveClass(/tiny-modal-svg__warning/) await page.getByRole('button', { name: '确定' }).click() - // ----消息状态示例---- - await page.getByRole('button', { name: '警告提示图标' }).nth(1).click() - await expect(message).toHaveClass(/status__warning/) // 错误提示图标 await page.getByRole('button', { name: '错误提示图标' }).first().click() await expect(modal).toHaveClass(/tiny-svg tiny-modal-svg__error/) await page.getByRole('button', { name: '确定' }).click() - // ----消息状态示例---- - await page.getByRole('button', { name: '错误提示图标' }).nth(1).click() - await expect(message).toHaveClass(/status__error/) // 加载提示图标 await page.getByRole('button', { name: '加载提示图标' }).first().click() await expect(modal).toHaveClass(/tiny-modal-svg__refresh/) await page.getByRole('button', { name: '确定' }).click() + + // ----消息状态示例---- + await page.getByRole('button', { name: '信息提示图标' }).nth(1).click() + await expect(message).toHaveClass(/status__info/) + + // ----消息状态示例---- + await page.getByRole('button', { name: '成功提示图标' }).nth(1).click() + await expect(message.nth(1)).toHaveClass(/status__success/) + + // ----消息状态示例---- + await page.getByRole('button', { name: '警告提示图标' }).nth(1).click() + await expect(message.nth(2)).toHaveClass(/status__warning/) + + // ----消息状态示例---- + await page.getByRole('button', { name: '错误提示图标' }).nth(1).click() + await expect(message.nth(3)).toHaveClass(/status__error/) + // ----消息状态示例---- await page.getByRole('button', { name: '加载提示图标' }).nth(1).click() - await expect(message).toHaveClass(/status__loading/) + await expect(message.nth(4)).toHaveClass(/status__loading/) }) diff --git a/packages/vue/src/modal/src/pc.vue b/packages/vue/src/modal/src/pc.vue index 4b32f2463f..25cffb6633 100644 --- a/packages/vue/src/modal/src/pc.vue +++ b/packages/vue/src/modal/src/pc.vue @@ -160,6 +160,10 @@ export default defineComponent({ h( 'div', { + role: 'dialog', + 'aria-modal': 'true', + 'aria-labelledby': 'modal_unique_0', + 'aria-describedby': 'modal_unique_1', class: 'tiny-modal__box', style: state.boxStyle, ref: 'modalBox', @@ -187,6 +191,7 @@ export default defineComponent({ [ typeof status === 'string' ? h(STATUS_MAPPING_COMPINENT[status.toUpperCase()], { + 'aria-label': `${constants.STATUS_MAPPING_CLASSS[status.toUpperCase()] || 'default'}-circle`, class: [constants.STATUS_MAPPING_CLASSS[status.toUpperCase()]] }) : h(status, { @@ -199,13 +204,15 @@ export default defineComponent({ ? h( 'span', { - class: 'tiny-modal__title' + class: 'tiny-modal__title', + id: 'modal_unique_0' }, title || t('ui.alert.title') ) : null, resize ? h(zoomLocat ? iconMinscreenRight() : iconFullscreenRight(), { + 'aria-label': 'Zoom', class: ['tiny-modal__zoom-btn', 'trigger__btn'], on: { click: this.toggleZoomEvent @@ -214,6 +221,7 @@ export default defineComponent({ : null, showClose ? h(iconClose(), { + 'aria-label': 'Close', class: ['tiny-modal__close-btn', 'trigger__btn'], on: { click: this.closeEvent @@ -238,6 +246,7 @@ export default defineComponent({ [ typeof status === 'string' ? h(STATUS_MAPPING_COMPINENT[status.toUpperCase()], { + 'aria-label': `${constants.STATUS_MAPPING_CLASSS[status.toUpperCase()] || 'default'}-message-circle`, class: [constants.STATUS_MAPPING_CLASSS[status.toUpperCase()]] }) : h(status, { @@ -249,7 +258,8 @@ export default defineComponent({ h( 'div', { - class: 'tiny-modal__content' + class: 'tiny-modal__content', + id: 'modal_unique_1' }, defaultSlot ? [defaultSlot.call(this, { $modal: this }, h)] @@ -269,6 +279,7 @@ export default defineComponent({ }, [ h(iconClose(), { + 'aria-label': 'Close', class: ['tiny-modal__close-btn'], on: { click: this.closeEvent From fa9bc672fc08381ef12ee41a302ea4fcbdd7c3ce Mon Sep 17 00:00:00 2001 From: James-9696 Date: Tue, 6 Jan 2026 00:25:51 -0800 Subject: [PATCH 2/2] fix: modify review comments --- packages/renderless/src/modal/vue.ts | 3 +++ packages/renderless/types/modal.type.ts | 2 ++ packages/vue/src/modal/src/pc.vue | 8 ++++---- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/renderless/src/modal/vue.ts b/packages/renderless/src/modal/vue.ts index bc84b1ae81..d7b67099bc 100644 --- a/packages/renderless/src/modal/vue.ts +++ b/packages/renderless/src/modal/vue.ts @@ -44,6 +44,7 @@ import { hideScrollbar, watchVisible } from './index' +import { nanoid } from '@opentiny/utils' import type { IModalApi, IModalProps, IModalRenderlessParamUtils, ISharedRenderlessParamHooks } from '@/types' export const api = [ @@ -78,6 +79,8 @@ export const renderless = ( const api = {} as IModalApi const lockScrollClass = constants.SCROLL_LOCK_CLASS(mode) const state = reactive({ + titleId: 'tiny-modal-title-' + nanoid.api.nanoid(8), + contentId: 'tiny-modal-content-' + nanoid.api.nanoid(8), emitter: emitter(), visible: false, contentVisible: false, diff --git a/packages/renderless/types/modal.type.ts b/packages/renderless/types/modal.type.ts index 04fac1cba0..dc2761ccb4 100644 --- a/packages/renderless/types/modal.type.ts +++ b/packages/renderless/types/modal.type.ts @@ -22,6 +22,8 @@ export interface IModalState { prevEvent: null | Event options: any[] theme: string | undefined + titleId: string + contentId: string } export type IModalProps = ExtractPropTypes diff --git a/packages/vue/src/modal/src/pc.vue b/packages/vue/src/modal/src/pc.vue index 25cffb6633..522bf3c77a 100644 --- a/packages/vue/src/modal/src/pc.vue +++ b/packages/vue/src/modal/src/pc.vue @@ -162,8 +162,8 @@ export default defineComponent({ { role: 'dialog', 'aria-modal': 'true', - 'aria-labelledby': 'modal_unique_0', - 'aria-describedby': 'modal_unique_1', + 'aria-labelledby': state.titleId, + 'aria-describedby': state.contentId, class: 'tiny-modal__box', style: state.boxStyle, ref: 'modalBox', @@ -205,7 +205,7 @@ export default defineComponent({ 'span', { class: 'tiny-modal__title', - id: 'modal_unique_0' + id: state.titleId }, title || t('ui.alert.title') ) @@ -259,7 +259,7 @@ export default defineComponent({ 'div', { class: 'tiny-modal__content', - id: 'modal_unique_1' + id: state.contentId }, defaultSlot ? [defaultSlot.call(this, { $modal: this }, h)]