FIX - polish House header and sheets

Respect the shared phone safe area, keep the House subtitle visible, and align the keys list with the sheet content width. Make the shared sheet grabber optionally clickable while preserving swipe-to-close behavior and add focused contract coverage.
This commit is contained in:
Leon.Schmidt
2026-08-16 20:11:40 +02:00
parent d75f2c38b4
commit f7b0072853
5 changed files with 86 additions and 8 deletions
+5
View File
@@ -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;
+14
View File
@@ -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('<button')
expect(clickable).toContain('class="sky-sheet__grabber"')
expect(clickable).toContain('aria-label="Close property details"')
})
it('owns pointer capture, close thresholds, and settling motion', () => {
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)")
+12 -3
View File
@@ -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"
>
<div
<component
:is="grabberClickable ? 'button' : 'div'"
v-if="swipeToClose"
class="sky-sheet__grabber"
aria-hidden="true"
:type="grabberClickable ? 'button' : undefined"
:aria-hidden="grabberClickable ? undefined : true"
:aria-label="grabberClickable ? grabberLabel : undefined"
@click="grabberClickable && emit('grabberclick', $event)"
@lostpointercapture="finishDrag($event, true)"
@pointercancel="finishDrag($event, true)"
@pointerdown="startDrag"
@pointermove="moveDrag"
@pointerup="finishDrag($event)"
></div>
></component>
<slot />
</component>
</div>
@@ -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,
)
})
})
+34 -4
View File
@@ -363,9 +363,12 @@ onBeforeUnmount(() => {
<sky-sheet
:opened="Boolean(selectedProperty)"
class="house-detail-sheet"
grabber-clickable
:grabber-label="phone.t('Common.close')"
swipe-to-close
@backdropclick="selectedPropertyId = null"
@escape="selectedPropertyId = null"
@grabberclick="selectedPropertyId = null"
@swipeclose="selectedPropertyId = null"
>
<section v-if="selectedProperty" class="house-detail">
@@ -505,7 +508,12 @@ onBeforeUnmount(() => {
<Plus :size="15" />{{ phone.t('Apps.house.addKey') }}
</sky-button>
</header>
<sky-list v-if="selectedProperty.keys?.length" inset strong>
<sky-list
v-if="selectedProperty.keys?.length"
inset
strong
class="house-key-list"
>
<sky-list-item
v-for="key in selectedProperty.keys"
:key="key.identifier"
@@ -542,9 +550,12 @@ onBeforeUnmount(() => {
<sky-sheet
:opened="candidatesOpened"
class="house-candidates-sheet"
grabber-clickable
:grabber-label="phone.t('Common.close')"
swipe-to-close
@backdropclick="candidatesOpened = false"
@escape="candidatesOpened = false"
@grabberclick="candidatesOpened = false"
@swipeclose="candidatesOpened = false"
>
<section class="house-candidates">
@@ -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;
}