chore: merge executor worktree (worktree-agent-acd1ece9cc8092915)
This commit is contained in:
@@ -0,0 +1,148 @@
|
|||||||
|
---
|
||||||
|
phase: 11-visual-design-responsive-layout-cleanup
|
||||||
|
plan: 4
|
||||||
|
subsystem: frontend/modals
|
||||||
|
tags: [mobile, modals, forms, VISUAL-02, RESP-04]
|
||||||
|
dependency_graph:
|
||||||
|
requires: [11-03]
|
||||||
|
provides: [mobile-safe-modals, form-baseline-coverage]
|
||||||
|
affects:
|
||||||
|
- frontend/src/components/sharing/ShareModal.vue
|
||||||
|
- frontend/src/components/cloud/CloudCredentialModal.vue
|
||||||
|
- frontend/src/components/folders/FolderDeleteModal.vue
|
||||||
|
- frontend/src/components/documents/DocumentPreviewModal.vue
|
||||||
|
- frontend/src/views/DocumentView.vue
|
||||||
|
tech_stack:
|
||||||
|
added: []
|
||||||
|
patterns:
|
||||||
|
- "max-h-[90vh] overflow-y-auto on modal panels — scroll-safe mobile pattern"
|
||||||
|
- "px-4 sm:px-6 responsive horizontal padding on full-screen preview header"
|
||||||
|
- "data-test attributes on all modal panels for testability"
|
||||||
|
- "focus:outline-none focus:ring-2 focus:ring-indigo-500 — consistent form baseline pattern throughout codebase"
|
||||||
|
key_files:
|
||||||
|
created:
|
||||||
|
- frontend/src/components/sharing/__tests__/ShareModal.mobile.test.js
|
||||||
|
- frontend/src/components/cloud/__tests__/CloudCredentialModal.mobile.test.js
|
||||||
|
- frontend/src/components/folders/__tests__/FolderDeleteModal.mobile.test.js
|
||||||
|
- frontend/src/components/documents/__tests__/DocumentPreviewModal.mobile.test.js
|
||||||
|
modified:
|
||||||
|
- frontend/src/components/sharing/ShareModal.vue
|
||||||
|
- frontend/src/components/cloud/CloudCredentialModal.vue
|
||||||
|
- frontend/src/components/folders/FolderDeleteModal.vue
|
||||||
|
- frontend/src/components/documents/DocumentPreviewModal.vue
|
||||||
|
- frontend/src/views/DocumentView.vue
|
||||||
|
decisions:
|
||||||
|
- "@tailwindcss/forms plugin active in tailwind.config.js — no drift found, verification task complete"
|
||||||
|
- "focus:outline-none focus:ring-2 focus:ring-indigo-500 is the consistent form baseline pattern throughout entire codebase — no normalization needed"
|
||||||
|
- "DocumentPreviewModal stays full-screen (fixed inset-0) — added responsive px-4 sm:px-6 header padding for narrow viewports"
|
||||||
|
- "DocumentView inline cloud-delete warning modal receives same max-h treatment as dedicated modal components"
|
||||||
|
metrics:
|
||||||
|
duration_minutes: 7
|
||||||
|
tasks_completed: 5
|
||||||
|
files_created: 4
|
||||||
|
files_modified: 5
|
||||||
|
completed_date: "2026-06-16"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Phase 11 Plan 4: Forms Baseline & Mobile-Safe Modals Summary
|
||||||
|
|
||||||
|
Verified `@tailwindcss/forms` remains active with a consistent form focus pattern throughout the codebase, and updated all four modal components plus one inline modal to scroll safely on narrow mobile viewports (375x667px).
|
||||||
|
|
||||||
|
## What Was Built
|
||||||
|
|
||||||
|
### Task 1 — Forms plugin verification
|
||||||
|
|
||||||
|
`frontend/tailwind.config.js` confirmed: `import forms from '@tailwindcss/forms'` and `plugins: [forms]` are active. No drift.
|
||||||
|
|
||||||
|
### Tasks 2-3 — Form baseline audit and normalization
|
||||||
|
|
||||||
|
Full audit of all `<input>`, `<select>`, `<textarea>`, `<input type="checkbox">`, and `<input type="radio">` elements across all Vue components. Findings:
|
||||||
|
|
||||||
|
- **Consistent pattern throughout:** `focus:outline-none focus:ring-2 focus:ring-indigo-500` (or `focus:ring-indigo-400` in topic-related components). This is already the standardized baseline.
|
||||||
|
- **No conflicting per-component reset styles** found (no `appearance-none`, no `webkit-appearance`, no inline `outline: none`).
|
||||||
|
- **No normalization required** — the codebase is already consistent. The forms plugin resets browser defaults and the `focus:ring-*` utility classes provide the visual indicator.
|
||||||
|
|
||||||
|
### Task 4 — Modal mobile-safe updates (RESP-04)
|
||||||
|
|
||||||
|
All modal panels now have `max-h-[90vh] overflow-y-auto` added to their panel containers:
|
||||||
|
|
||||||
|
**`ShareModal.vue`**
|
||||||
|
- Panel `class` updated: added `max-h-[90vh] overflow-y-auto`
|
||||||
|
- Added `data-test="share-modal-panel"` for testability
|
||||||
|
|
||||||
|
**`CloudCredentialModal.vue`**
|
||||||
|
- Panel `class` updated: added `max-h-[90vh] overflow-y-auto`
|
||||||
|
- Added `data-test="cloud-credential-modal-panel"` for testability
|
||||||
|
- The tall WebDAV/Nextcloud form with server URL, username, auth method toggle, advanced section, and password field now scrolls safely on 375px height-constrained viewports
|
||||||
|
|
||||||
|
**`FolderDeleteModal.vue`**
|
||||||
|
- Panel `class` updated: added `max-h-[90vh] overflow-y-auto`
|
||||||
|
- Added `data-test="folder-delete-modal-panel"` for testability
|
||||||
|
|
||||||
|
**`DocumentPreviewModal.vue`**
|
||||||
|
- Full-screen overlay preserved (`fixed inset-0 flex flex-col`) — this modal intentionally uses the entire viewport
|
||||||
|
- Header padding made responsive: `px-4 sm:px-6` (was `px-6`) — prevents filename and close button from touching screen edges at 375px
|
||||||
|
- Added `data-test="document-preview-modal"` on overlay and `data-test="preview-modal-header"` on header bar
|
||||||
|
|
||||||
|
**`DocumentView.vue` (inline cloud-delete warning modal)**
|
||||||
|
- Inline modal panel updated: added `max-h-[90vh] overflow-y-auto`
|
||||||
|
- Added `data-test="cloud-delete-modal-panel"` for testability
|
||||||
|
|
||||||
|
### Task 5 — Tests
|
||||||
|
|
||||||
|
Four new test files created (34 total test files in worktree, 234 tests all pass):
|
||||||
|
|
||||||
|
**`ShareModal.mobile.test.js`** (5 tests):
|
||||||
|
- VISUAL-02: text input has `focus:ring-2 focus:outline-none` (form baseline)
|
||||||
|
- VISUAL-02: select has `focus:ring-2 focus:outline-none` (form baseline)
|
||||||
|
- RESP-04: panel has `max-h-[90vh]`
|
||||||
|
- RESP-04: panel has `overflow-y-auto`
|
||||||
|
- RESP-04: panel has `mx-4` for narrow viewport fit
|
||||||
|
|
||||||
|
**`CloudCredentialModal.mobile.test.js`** (3 tests):
|
||||||
|
- RESP-04: panel has `max-h-[90vh]`
|
||||||
|
- RESP-04: panel has `overflow-y-auto`
|
||||||
|
- RESP-04: panel not rendered when `show=false` (v-if gate verified)
|
||||||
|
|
||||||
|
**`FolderDeleteModal.mobile.test.js`** (4 tests):
|
||||||
|
- RESP-04: panel has `max-h-[90vh]`
|
||||||
|
- RESP-04: panel has `overflow-y-auto`
|
||||||
|
- RESP-04: panel has `mx-4`
|
||||||
|
- Action buttons are accessible (text content check)
|
||||||
|
|
||||||
|
**`DocumentPreviewModal.mobile.test.js`** (3 tests):
|
||||||
|
- RESP-04: full-screen overlay has `fixed inset-0` classes
|
||||||
|
- RESP-04: header has `px-4` (mobile) and `sm:px-6` (640px+)
|
||||||
|
- RESP-04: filename `span` has `truncate` class preventing overflow
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- `npm run test -- --run` (via `./node_modules/.bin/vitest run`): **34 test files, 234 tests, all pass**
|
||||||
|
- `npm run build` (via `./node_modules/.bin/vite build`): **succeeds** — all 5 JS chunks + main bundle build cleanly
|
||||||
|
|
||||||
|
## Deviations from Plan
|
||||||
|
|
||||||
|
None — plan executed exactly as written.
|
||||||
|
|
||||||
|
## Known Stubs
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
## Threat Flags
|
||||||
|
|
||||||
|
None — no network endpoints, auth paths, file access patterns, or schema changes. All changes are frontend layout/presentation classes and test files.
|
||||||
|
|
||||||
|
## Self-Check: PASSED
|
||||||
|
|
||||||
|
- `frontend/src/components/sharing/ShareModal.vue`: modified — max-h + overflow-y-auto + data-test
|
||||||
|
- `frontend/src/components/cloud/CloudCredentialModal.vue`: modified — max-h + overflow-y-auto + data-test
|
||||||
|
- `frontend/src/components/folders/FolderDeleteModal.vue`: modified — max-h + overflow-y-auto + data-test
|
||||||
|
- `frontend/src/components/documents/DocumentPreviewModal.vue`: modified — responsive px-4, data-test attrs
|
||||||
|
- `frontend/src/views/DocumentView.vue`: modified — inline modal max-h + data-test
|
||||||
|
- `frontend/src/components/sharing/__tests__/ShareModal.mobile.test.js`: created — 5 tests
|
||||||
|
- `frontend/src/components/cloud/__tests__/CloudCredentialModal.mobile.test.js`: created — 3 tests
|
||||||
|
- `frontend/src/components/folders/__tests__/FolderDeleteModal.mobile.test.js`: created — 4 tests
|
||||||
|
- `frontend/src/components/documents/__tests__/DocumentPreviewModal.mobile.test.js`: created — 3 tests
|
||||||
|
- Commit `df53cef` exists in git log: confirmed
|
||||||
|
- 34 test files, 234 tests all pass: confirmed
|
||||||
|
- Build succeeds: confirmed
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
@click.self="handleOverlayClick"
|
@click.self="handleOverlayClick"
|
||||||
@keydown.escape.window="handleEscape"
|
@keydown.escape.window="handleEscape"
|
||||||
>
|
>
|
||||||
<div class="bg-white rounded-xl shadow-xl w-full max-w-md p-6">
|
<div data-test="cloud-credential-modal-panel" class="bg-white rounded-xl shadow-xl w-full max-w-md p-6 max-h-[90vh] overflow-y-auto">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div class="flex items-center justify-between mb-5">
|
<div class="flex items-center justify-between mb-5">
|
||||||
<h3 class="text-xl font-semibold text-gray-900">
|
<h3 class="text-xl font-semibold text-gray-900">
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||||
|
import { mount } from '@vue/test-utils'
|
||||||
|
import { createPinia, setActivePinia } from 'pinia'
|
||||||
|
|
||||||
|
vi.mock('../../../api/client.js', () => ({
|
||||||
|
getConnectionConfig: vi.fn().mockResolvedValue({ connection_username: '', server_url: '' }),
|
||||||
|
connectWebDav: vi.fn(),
|
||||||
|
}))
|
||||||
|
|
||||||
|
import CloudCredentialModal from '../CloudCredentialModal.vue'
|
||||||
|
|
||||||
|
const globalStubs = {
|
||||||
|
AppIcon: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
const testProvider = { key: 'webdav', label: 'WebDAV' }
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
setActivePinia(createPinia())
|
||||||
|
vi.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('RESP-04: CloudCredentialModal mobile-safe panel', () => {
|
||||||
|
it('panel has max-h-[90vh] class for mobile scroll safety', () => {
|
||||||
|
const wrapper = mount(CloudCredentialModal, {
|
||||||
|
props: { show: true, provider: testProvider, existing: null },
|
||||||
|
global: { stubs: globalStubs },
|
||||||
|
})
|
||||||
|
const panel = wrapper.find('[data-test="cloud-credential-modal-panel"]')
|
||||||
|
expect(panel.exists()).toBe(true)
|
||||||
|
expect(panel.classes()).toContain('max-h-[90vh]')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('panel has overflow-y-auto class for mobile scroll', () => {
|
||||||
|
const wrapper = mount(CloudCredentialModal, {
|
||||||
|
props: { show: true, provider: testProvider, existing: null },
|
||||||
|
global: { stubs: globalStubs },
|
||||||
|
})
|
||||||
|
const panel = wrapper.find('[data-test="cloud-credential-modal-panel"]')
|
||||||
|
expect(panel.classes()).toContain('overflow-y-auto')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('panel not rendered when show=false', () => {
|
||||||
|
const wrapper = mount(CloudCredentialModal, {
|
||||||
|
props: { show: false, provider: testProvider, existing: null },
|
||||||
|
global: { stubs: globalStubs },
|
||||||
|
})
|
||||||
|
// When show=false the v-if hides the entire overlay
|
||||||
|
const panel = wrapper.find('[data-test="cloud-credential-modal-panel"]')
|
||||||
|
expect(panel.exists()).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
<!-- Overlay -->
|
<!-- Overlay -->
|
||||||
<div
|
<div
|
||||||
ref="overlayRef"
|
ref="overlayRef"
|
||||||
|
data-test="document-preview-modal"
|
||||||
class="fixed inset-0 bg-black/60 z-50 flex flex-col"
|
class="fixed inset-0 bg-black/60 z-50 flex flex-col"
|
||||||
role="dialog"
|
role="dialog"
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
@@ -9,7 +10,7 @@
|
|||||||
@click="handleOverlayClick"
|
@click="handleOverlayClick"
|
||||||
>
|
>
|
||||||
<!-- Header bar -->
|
<!-- Header bar -->
|
||||||
<div class="bg-white border-b border-gray-200 px-6 py-3 flex items-center justify-between shrink-0">
|
<div data-test="preview-modal-header" class="bg-white border-b border-gray-200 px-4 sm:px-6 py-3 flex items-center justify-between shrink-0">
|
||||||
<span class="text-sm font-medium text-gray-900 truncate max-w-xs">{{ doc.filename || doc.original_name }}</span>
|
<span class="text-sm font-medium text-gray-900 truncate max-w-xs">{{ doc.filename || doc.original_name }}</span>
|
||||||
<button
|
<button
|
||||||
@click="emit('close')"
|
@click="emit('close')"
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||||
|
import { mount } from '@vue/test-utils'
|
||||||
|
|
||||||
|
vi.mock('../../../api/client.js', () => ({
|
||||||
|
fetchDocumentContent: vi.fn().mockResolvedValue({ ok: true, blob: () => Promise.resolve(new Blob()) }),
|
||||||
|
}))
|
||||||
|
|
||||||
|
import DocumentPreviewModal from '../DocumentPreviewModal.vue'
|
||||||
|
|
||||||
|
const globalStubs = {
|
||||||
|
AppIcon: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
const testDoc = { id: 'doc-1', filename: 'test.pdf', original_name: 'test.pdf' }
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
// Mock URL.createObjectURL since it's not available in jsdom
|
||||||
|
vi.stubGlobal('URL', {
|
||||||
|
createObjectURL: vi.fn().mockReturnValue('blob:mock-url'),
|
||||||
|
revokeObjectURL: vi.fn(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('RESP-04: DocumentPreviewModal mobile-safe sizing', () => {
|
||||||
|
it('renders full-screen overlay (fixed inset-0)', () => {
|
||||||
|
const wrapper = mount(DocumentPreviewModal, {
|
||||||
|
props: { doc: testDoc },
|
||||||
|
global: { stubs: globalStubs },
|
||||||
|
})
|
||||||
|
const overlay = wrapper.find('[data-test="document-preview-modal"]')
|
||||||
|
expect(overlay.exists()).toBe(true)
|
||||||
|
expect(overlay.classes()).toContain('fixed')
|
||||||
|
expect(overlay.classes()).toContain('inset-0')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('header has responsive horizontal padding (px-4 at mobile, sm:px-6 at >=640px)', () => {
|
||||||
|
const wrapper = mount(DocumentPreviewModal, {
|
||||||
|
props: { doc: testDoc },
|
||||||
|
global: { stubs: globalStubs },
|
||||||
|
})
|
||||||
|
const header = wrapper.find('[data-test="preview-modal-header"]')
|
||||||
|
expect(header.exists()).toBe(true)
|
||||||
|
expect(header.classes()).toContain('px-4')
|
||||||
|
expect(header.classes()).toContain('sm:px-6')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('header filename is truncated with truncate class to prevent overflow', () => {
|
||||||
|
const wrapper = mount(DocumentPreviewModal, {
|
||||||
|
props: { doc: testDoc },
|
||||||
|
global: { stubs: globalStubs },
|
||||||
|
})
|
||||||
|
const header = wrapper.find('[data-test="preview-modal-header"]')
|
||||||
|
// The filename span should have truncate class
|
||||||
|
const filenameSpan = header.find('span.truncate')
|
||||||
|
expect(filenameSpan.exists()).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -9,7 +9,8 @@
|
|||||||
role="dialog"
|
role="dialog"
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
aria-labelledby="delete-modal-title"
|
aria-labelledby="delete-modal-title"
|
||||||
class="bg-white rounded-2xl shadow-xl p-6 max-w-md w-full mx-4"
|
data-test="folder-delete-modal-panel"
|
||||||
|
class="bg-white rounded-2xl shadow-xl p-6 max-w-md w-full mx-4 max-h-[90vh] overflow-y-auto"
|
||||||
>
|
>
|
||||||
<!-- Warning icon -->
|
<!-- Warning icon -->
|
||||||
<div class="flex justify-center">
|
<div class="flex justify-center">
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import { describe, it, expect, vi } from 'vitest'
|
||||||
|
import { mount } from '@vue/test-utils'
|
||||||
|
|
||||||
|
import FolderDeleteModal from '../FolderDeleteModal.vue'
|
||||||
|
|
||||||
|
const globalStubs = {
|
||||||
|
AppIcon: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
const testFolder = { id: 'f1', name: 'Projects', doc_count: 3 }
|
||||||
|
|
||||||
|
describe('RESP-04: FolderDeleteModal mobile-safe panel', () => {
|
||||||
|
it('panel has max-h-[90vh] class for mobile scroll safety', () => {
|
||||||
|
const wrapper = mount(FolderDeleteModal, {
|
||||||
|
props: { folder: testFolder },
|
||||||
|
global: { stubs: globalStubs },
|
||||||
|
})
|
||||||
|
const panel = wrapper.find('[data-test="folder-delete-modal-panel"]')
|
||||||
|
expect(panel.exists()).toBe(true)
|
||||||
|
expect(panel.classes()).toContain('max-h-[90vh]')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('panel has overflow-y-auto class for mobile scroll', () => {
|
||||||
|
const wrapper = mount(FolderDeleteModal, {
|
||||||
|
props: { folder: testFolder },
|
||||||
|
global: { stubs: globalStubs },
|
||||||
|
})
|
||||||
|
const panel = wrapper.find('[data-test="folder-delete-modal-panel"]')
|
||||||
|
expect(panel.classes()).toContain('overflow-y-auto')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('panel has mx-4 horizontal margin so it fits within 375px viewport', () => {
|
||||||
|
const wrapper = mount(FolderDeleteModal, {
|
||||||
|
props: { folder: testFolder },
|
||||||
|
global: { stubs: globalStubs },
|
||||||
|
})
|
||||||
|
const panel = wrapper.find('[data-test="folder-delete-modal-panel"]')
|
||||||
|
expect(panel.classes()).toContain('mx-4')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('confirm and cancel buttons are accessible (text, role)', () => {
|
||||||
|
const wrapper = mount(FolderDeleteModal, {
|
||||||
|
props: { folder: testFolder },
|
||||||
|
global: { stubs: globalStubs },
|
||||||
|
})
|
||||||
|
expect(wrapper.text()).toContain('Keep folder')
|
||||||
|
expect(wrapper.text()).toContain('Delete folder and documents')
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -9,7 +9,8 @@
|
|||||||
role="dialog"
|
role="dialog"
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
aria-labelledby="share-modal-title"
|
aria-labelledby="share-modal-title"
|
||||||
class="bg-white rounded-2xl shadow-xl p-6 max-w-md w-full mx-4 relative"
|
data-test="share-modal-panel"
|
||||||
|
class="bg-white rounded-2xl shadow-xl p-6 max-w-md w-full mx-4 relative max-h-[90vh] overflow-y-auto"
|
||||||
>
|
>
|
||||||
<!-- Close button -->
|
<!-- Close button -->
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||||
|
import { mount, flushPromises } from '@vue/test-utils'
|
||||||
|
import { createPinia, setActivePinia } from 'pinia'
|
||||||
|
|
||||||
|
// Mock stores and API so mount doesn't error out
|
||||||
|
vi.mock('../../../stores/documents.js', () => ({
|
||||||
|
useDocumentsStore: () => ({
|
||||||
|
listShares: vi.fn().mockResolvedValue([]),
|
||||||
|
shareDocument: vi.fn(),
|
||||||
|
updateSharePermission: vi.fn(),
|
||||||
|
revokeShare: vi.fn(),
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('../../../stores/toast.js', () => ({
|
||||||
|
useToastStore: () => ({ show: vi.fn() }),
|
||||||
|
}))
|
||||||
|
|
||||||
|
import ShareModal from '../ShareModal.vue'
|
||||||
|
|
||||||
|
const globalStubs = {
|
||||||
|
AppIcon: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
const testDoc = { id: 'doc-1', original_name: 'test.pdf' }
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
setActivePinia(createPinia())
|
||||||
|
vi.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('VISUAL-02: ShareModal form baseline (@tailwindcss/forms consistent pattern)', () => {
|
||||||
|
it('text input uses consistent ring-2 focus class pattern', async () => {
|
||||||
|
const wrapper = mount(ShareModal, {
|
||||||
|
props: { doc: testDoc },
|
||||||
|
global: { stubs: globalStubs },
|
||||||
|
})
|
||||||
|
await flushPromises()
|
||||||
|
const input = wrapper.find('input[type="text"]')
|
||||||
|
expect(input.exists()).toBe(true)
|
||||||
|
expect(input.classes()).toContain('focus:ring-2')
|
||||||
|
expect(input.classes()).toContain('focus:outline-none')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('select uses consistent ring-2 focus class pattern', async () => {
|
||||||
|
const wrapper = mount(ShareModal, {
|
||||||
|
props: { doc: testDoc },
|
||||||
|
global: { stubs: globalStubs },
|
||||||
|
})
|
||||||
|
await flushPromises()
|
||||||
|
const select = wrapper.find('select')
|
||||||
|
expect(select.exists()).toBe(true)
|
||||||
|
expect(select.classes()).toContain('focus:ring-2')
|
||||||
|
expect(select.classes()).toContain('focus:outline-none')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('RESP-04: ShareModal mobile-safe panel', () => {
|
||||||
|
it('panel has max-h-[90vh] class for mobile scroll safety', async () => {
|
||||||
|
const wrapper = mount(ShareModal, {
|
||||||
|
props: { doc: testDoc },
|
||||||
|
global: { stubs: globalStubs },
|
||||||
|
})
|
||||||
|
await flushPromises()
|
||||||
|
const panel = wrapper.find('[data-test="share-modal-panel"]')
|
||||||
|
expect(panel.exists()).toBe(true)
|
||||||
|
expect(panel.classes()).toContain('max-h-[90vh]')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('panel has overflow-y-auto class for mobile scroll', async () => {
|
||||||
|
const wrapper = mount(ShareModal, {
|
||||||
|
props: { doc: testDoc },
|
||||||
|
global: { stubs: globalStubs },
|
||||||
|
})
|
||||||
|
await flushPromises()
|
||||||
|
const panel = wrapper.find('[data-test="share-modal-panel"]')
|
||||||
|
expect(panel.classes()).toContain('overflow-y-auto')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('panel has mx-4 horizontal margin so it fits within 375px viewport', async () => {
|
||||||
|
const wrapper = mount(ShareModal, {
|
||||||
|
props: { doc: testDoc },
|
||||||
|
global: { stubs: globalStubs },
|
||||||
|
})
|
||||||
|
await flushPromises()
|
||||||
|
const panel = wrapper.find('[data-test="share-modal-panel"]')
|
||||||
|
expect(panel.classes()).toContain('mx-4')
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -119,7 +119,8 @@
|
|||||||
role="dialog"
|
role="dialog"
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
aria-labelledby="cloud-delete-modal-title"
|
aria-labelledby="cloud-delete-modal-title"
|
||||||
class="bg-white rounded-2xl shadow-xl p-6 max-w-sm w-full mx-4"
|
data-test="cloud-delete-modal-panel"
|
||||||
|
class="bg-white rounded-2xl shadow-xl p-6 max-w-sm w-full mx-4 max-h-[90vh] overflow-y-auto"
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-2 mb-2">
|
<div class="flex items-center gap-2 mb-2">
|
||||||
<AppIcon name="warning" class="w-5 h-5 text-amber-500 shrink-0" />
|
<AppIcon name="warning" class="w-5 h-5 text-amber-500 shrink-0" />
|
||||||
|
|||||||
Reference in New Issue
Block a user