Responsive app shell, routes and navigation #38
@@ -2,7 +2,7 @@ import { render, screen, within } from '@testing-library/react'
|
|||||||
import { MemoryRouter } from 'react-router'
|
import { MemoryRouter } from 'react-router'
|
||||||
import { afterEach, describe, expect, it } from 'vitest'
|
import { afterEach, describe, expect, it } from 'vitest'
|
||||||
import App from '../App.tsx'
|
import App from '../App.tsx'
|
||||||
import { declaredStyle } from '../test/cssRules.ts'
|
import { declaredStyle, outerSize } from '../test/cssRules.ts'
|
||||||
import { expectMinHitArea } from '../test/hitArea.ts'
|
import { expectMinHitArea } from '../test/hitArea.ts'
|
||||||
import { mockViewport } from '../test/matchMedia.ts'
|
import { mockViewport } from '../test/matchMedia.ts'
|
||||||
import bottomBarStyles from './BottomBar.module.css'
|
import bottomBarStyles from './BottomBar.module.css'
|
||||||
@@ -69,6 +69,7 @@ describe('AppShell', () => {
|
|||||||
expect(bar.right).toBe('12px')
|
expect(bar.right).toBe('12px')
|
||||||
expect(bar.bottom).toBe('calc(16px + env(safe-area-inset-bottom))')
|
expect(bar.bottom).toBe('calc(16px + env(safe-area-inset-bottom))')
|
||||||
expect(bar.height).toBe('62px')
|
expect(bar.height).toBe('62px')
|
||||||
|
expect(outerSize(nav(), 'height')).toBe(62)
|
||||||
expect(bar.background).toBe('var(--color-text)')
|
expect(bar.background).toBe('var(--color-text)')
|
||||||
expect(bar.borderRadius).toBe('var(--radius-pill)')
|
expect(bar.borderRadius).toBe('var(--radius-pill)')
|
||||||
})
|
})
|
||||||
@@ -111,9 +112,12 @@ describe('AppShell', () => {
|
|||||||
expect(screen.getAllByRole('navigation')).toHaveLength(1)
|
expect(screen.getAllByRole('navigation')).toHaveLength(1)
|
||||||
expect(nav()).toHaveClass(railStyles.rail!)
|
expect(nav()).toHaveClass(railStyles.rail!)
|
||||||
expect(declaredStyle(nav()).width).toBe('96px')
|
expect(declaredStyle(nav()).width).toBe('96px')
|
||||||
|
expect(outerSize(nav(), 'width')).toBe(96)
|
||||||
expect(declaredStyle(nav()).background).toBe('var(--color-surface)')
|
expect(declaredStyle(nav()).background).toBe('var(--color-surface)')
|
||||||
const mark = within(nav()).getByText('M')
|
const mark = within(nav()).getByText('M')
|
||||||
expect(declaredStyle(mark).width).toBe('48px')
|
expect(declaredStyle(mark).width).toBe('48px')
|
||||||
|
expect(outerSize(mark, 'width')).toBe(48)
|
||||||
|
expect(outerSize(mark, 'height')).toBe(48)
|
||||||
expect(declaredStyle(mark).background).toBe('var(--color-accent)')
|
expect(declaredStyle(mark).background).toBe('var(--color-accent)')
|
||||||
expect(screen.queryByText('MealQuest')).not.toBeInTheDocument()
|
expect(screen.queryByText('MealQuest')).not.toBeInTheDocument()
|
||||||
expect(screen.queryByText('Our kitchen')).not.toBeInTheDocument()
|
expect(screen.queryByText('Our kitchen')).not.toBeInTheDocument()
|
||||||
@@ -122,6 +126,8 @@ describe('AppShell', () => {
|
|||||||
const pill = link.firstElementChild!
|
const pill = link.firstElementChild!
|
||||||
expect(declaredStyle(pill).width).toBe('56px')
|
expect(declaredStyle(pill).width).toBe('56px')
|
||||||
expect(declaredStyle(pill).height).toBe('36px')
|
expect(declaredStyle(pill).height).toBe('36px')
|
||||||
|
expect(outerSize(pill, 'width')).toBe(56)
|
||||||
|
expect(outerSize(pill, 'height')).toBe(36)
|
||||||
expect(declaredStyle(link).fontSize).toBe('11px')
|
expect(declaredStyle(link).fontSize).toBe('11px')
|
||||||
expect(declaredStyle(link).fontWeight).toBe('700')
|
expect(declaredStyle(link).fontWeight).toBe('700')
|
||||||
expectMinHitArea(link)
|
expectMinHitArea(link)
|
||||||
@@ -138,6 +144,8 @@ describe('AppShell', () => {
|
|||||||
expect(declaredStyle(sidebar).width).toBe('208px')
|
expect(declaredStyle(sidebar).width).toBe('208px')
|
||||||
expect(declaredStyle(sidebar).background).toBe('var(--color-surface)')
|
expect(declaredStyle(sidebar).background).toBe('var(--color-surface)')
|
||||||
expect(declaredStyle(sidebar).padding).toBe('28px 16px')
|
expect(declaredStyle(sidebar).padding).toBe('28px 16px')
|
||||||
|
// The 16 px side padding sits inside the 208 px, not outside it.
|
||||||
|
expect(outerSize(sidebar, 'width')).toBe(208)
|
||||||
|
|
||||||
const wordmark = within(sidebar).getByText('MealQuest')
|
const wordmark = within(sidebar).getByText('MealQuest')
|
||||||
expect(declaredStyle(wordmark).fontFamily).toBe('var(--font-heading)')
|
expect(declaredStyle(wordmark).fontFamily).toBe('var(--font-heading)')
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
bottom: calc(16px + env(safe-area-inset-bottom));
|
bottom: calc(16px + env(safe-area-inset-bottom));
|
||||||
height: 62px;
|
height: 62px;
|
||||||
padding: 0 6px;
|
padding: 0 6px;
|
||||||
|
box-sizing: border-box;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-around;
|
justify-content: space-around;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
.rail {
|
.rail {
|
||||||
width: 96px;
|
width: 96px;
|
||||||
padding: 24px 0;
|
padding: 24px 0;
|
||||||
|
box-sizing: border-box;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
.sidebar {
|
.sidebar {
|
||||||
width: 208px;
|
width: 208px;
|
||||||
padding: 28px 16px;
|
padding: 28px 16px;
|
||||||
|
box-sizing: border-box;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
background: var(--color-surface);
|
background: var(--color-surface);
|
||||||
|
|||||||
@@ -48,3 +48,28 @@ export function animationKeyframes(element: Element): CSSKeyframesRule {
|
|||||||
if (!keyframes) throw new Error(`no @keyframes named ${JSON.stringify(name)}`)
|
if (!keyframes) throw new Error(`no @keyframes named ${JSON.stringify(name)}`)
|
||||||
return keyframes
|
return keyframes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function px(value: string, what: string): number {
|
||||||
|
if (value === '') return 0
|
||||||
|
const match = /^(\d+(?:\.\d+)?)px$/.exec(value)
|
||||||
|
if (!match) throw new Error(`${what} ${JSON.stringify(value)} is not a px length`)
|
||||||
|
return Number(match[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
// The border-box size the element's declared rules give it on one axis:
|
||||||
|
// width or height, plus padding and border unless box-sizing is border-box.
|
||||||
|
// jsdom does no layout (getBoundingClientRect is all zeros), so this is the
|
||||||
|
// way to catch padding that would push a fixed box past its specified size.
|
||||||
|
export function outerSize(element: Element, axis: 'width' | 'height'): number {
|
||||||
|
const style = declaredStyle(element)
|
||||||
|
const size = px(style.getPropertyValue(axis), axis)
|
||||||
|
if (style.boxSizing === 'border-box') return size
|
||||||
|
const sides = axis === 'width' ? ['left', 'right'] : ['top', 'bottom']
|
||||||
|
return sides.reduce(
|
||||||
|
(total, side) =>
|
||||||
|
total +
|
||||||
|
px(style.getPropertyValue(`padding-${side}`), `padding-${side}`) +
|
||||||
|
px(style.getPropertyValue(`border-${side}-width`), `border-${side}-width`),
|
||||||
|
size,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user