diff --git a/frontend/src/ui/overlays.css b/frontend/src/ui/overlays.css index b3608c1..962992c 100644 --- a/frontend/src/ui/overlays.css +++ b/frontend/src/ui/overlays.css @@ -223,9 +223,14 @@ top: 0; width: 100%; height: 32px; + margin: 0; + padding: 0; display: grid; place-items: center; + border: 0; background: inherit; + color: inherit; + font: inherit; cursor: ns-resize; touch-action: none; user-select: none; diff --git a/frontend/src/ui/overlays/SkySheet.test.ts b/frontend/src/ui/overlays/SkySheet.test.ts index 45f81c5..baac1a6 100644 --- a/frontend/src/ui/overlays/SkySheet.test.ts +++ b/frontend/src/ui/overlays/SkySheet.test.ts @@ -30,6 +30,19 @@ describe('SkySheet', () => { expect(ordinary).not.toContain('sky-sheet__grabber') }) + it('can expose the drag handle as an accessible close button', async () => { + const clickable = await renderSheet({ + grabberClickable: true, + grabberLabel: 'Close property details', + opened: true, + swipeToClose: true, + }) + + expect(clickable).toContain(' { const source = readFileSync( new URL('./SkySheet.vue', import.meta.url), @@ -41,6 +54,7 @@ describe('SkySheet', () => { ) expect(source).toContain('swipeclose: [event: PointerEvent]') + expect(source).toContain('grabberclick: [event: MouseEvent]') expect(source).toContain('setPointerCapture(event.pointerId)') expect(source).toContain('dragOffset.value >= closeThreshold') expect(source).toContain("emit('swipeclose', event)") diff --git a/frontend/src/ui/overlays/SkySheet.vue b/frontend/src/ui/overlays/SkySheet.vue index fb55ac6..f3347b2 100644 --- a/frontend/src/ui/overlays/SkySheet.vue +++ b/frontend/src/ui/overlays/SkySheet.vue @@ -12,6 +12,8 @@ const props = withDefaults( ariaModal?: boolean | 'false' | 'true' backdrop?: boolean component?: string + grabberClickable?: boolean + grabberLabel?: string opened: boolean role?: 'alertdialog' | 'dialog' | 'none' | 'presentation' swipeToClose?: boolean @@ -21,12 +23,15 @@ const props = withDefaults( ariaModal: true, backdrop: true, component: 'div', + grabberClickable: false, + grabberLabel: '', tabindex: -1, }, ) const emit = defineEmits<{ backdropclick: [event: MouseEvent] escape: [event: KeyboardEvent] + grabberclick: [event: MouseEvent] swipeclose: [event: PointerEvent] }>() @@ -200,16 +205,20 @@ useOverlayFocusTrap({ :aria-describedby="effectiveRole ? ariaDescribedby : undefined" :tabindex="tabindex" > - + > diff --git a/frontend/src/views/apps/HouseApp.contract.test.ts b/frontend/src/views/apps/HouseApp.contract.test.ts index 02d4ad9..14c48c6 100644 --- a/frontend/src/views/apps/HouseApp.contract.test.ts +++ b/frontend/src/views/apps/HouseApp.contract.test.ts @@ -5,10 +5,13 @@ import { describe, expect, it } from 'vitest' const source = readFileSync(new URL('./HouseApp.vue', import.meta.url), 'utf8') describe('House app sheets', () => { - it('closes both sheets through their shared drag gesture', () => { + it('closes both sheets through their shared drag gesture or grabber button', () => { expect(source.match(/swipe-to-close/g)).toHaveLength(2) + expect(source.match(/grabber-clickable/g)).toHaveLength(2) expect(source).toContain('@swipeclose="selectedPropertyId = null"') expect(source).toContain('@swipeclose="candidatesOpened = false"') + expect(source).toContain('@grabberclick="selectedPropertyId = null"') + expect(source).toContain('@grabberclick="candidatesOpened = false"') }) it('sizes the panels instead of clipping the overlay roots', () => { @@ -23,4 +26,21 @@ describe('House app sheets', () => { ) expect(source).not.toContain('height: 620px;') }) + + it('keeps the header clear of the status bar and shows its full subtitle', () => { + expect(source).not.toContain('--sky-safe-area-top: 46px') + expect(source).toMatch( + /\.house-navbar :deep\(\.sky-navbar__heading\)\s*\{[^}]*grid-column:\s*1 \/ -1;/s, + ) + expect(source).toMatch( + /\.house-navbar :deep\(\.sky-navbar__subtitle\)\s*\{[^}]*text-overflow:\s*clip;[^}]*white-space:\s*normal;/s, + ) + }) + + it('uses the full popup content width for the keys list', () => { + expect(source).toContain('class="house-key-list"') + expect(source).toMatch( + /\.house-key-list\s*\{[^}]*--sky-list-outer-left:\s*0px;[^}]*--sky-list-outer-right:\s*0px;/s, + ) + }) }) diff --git a/frontend/src/views/apps/HouseApp.vue b/frontend/src/views/apps/HouseApp.vue index 343bbf0..e0ee700 100644 --- a/frontend/src/views/apps/HouseApp.vue +++ b/frontend/src/views/apps/HouseApp.vue @@ -363,9 +363,12 @@ onBeforeUnmount(() => {
@@ -505,7 +508,12 @@ onBeforeUnmount(() => { {{ phone.t('Apps.house.addKey') }} - + {
@@ -635,7 +646,6 @@ onBeforeUnmount(() => { linear-gradient(165deg, #26201d 0, #111216 48%, #171d25 100%); } .house-navbar { - --sky-safe-area-top: 46px; --sky-navbar-glass: transparent; position: absolute; z-index: 5; @@ -648,19 +658,29 @@ onBeforeUnmount(() => { opacity: 0; } .house-navbar :deep(.sky-navbar__heading) { + width: 100%; justify-content: center; + grid-column: 1 / -1; gap: 1px; + padding: 0 var(--sky-page-gutter); } .house-navbar :deep(.sky-navbar__title) { line-height: 24px; } .house-navbar :deep(.sky-navbar__subtitle) { + max-width: 100%; margin-top: 0; + overflow: visible; line-height: 15px; + text-overflow: clip; + white-space: normal; } .house-scroll { position: absolute; - inset: 104px 0 25px; + inset: calc( + var(--sky-safe-area-top) + var(--sky-navbar-height) + var(--sky-space-3) + ) + 0 25px; padding: 9px 13px 34px; overflow-x: hidden; overflow-y: auto; @@ -685,7 +705,10 @@ onBeforeUnmount(() => { } .house-state { position: absolute; - inset: 104px 0 25px; + inset: calc( + var(--sky-safe-area-top) + var(--sky-navbar-height) + var(--sky-space-3) + ) + 0 25px; padding: 30px; display: flex; flex-direction: column; @@ -1228,6 +1251,13 @@ onBeforeUnmount(() => { .house-keys { padding: 0 16px; } +.house-key-list { + --sky-list-outer-left: 0px; + --sky-list-outer-right: 0px; + width: 100%; + margin-top: 0; + margin-bottom: 0; +} .house-keys header { margin: 20px 4px 8px; }