Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .Jules/palette.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 2024-06-22 - Overview Stats ν† κΈ€ λ²„νŠΌ ν‚€λ³΄λ“œ μ ‘κ·Όμ„± κ°œμ„ 
**Learning:** `overview-stats` μ»΄ν¬λ„ŒνŠΈμ˜ μ„€λͺ… ν…μŠ€νŠΈλ₯Ό νŽΌμΉ˜κ±°λ‚˜ μ ‘λŠ” `<button>` μš”μ†Œμ— ν‚€λ³΄λ“œ 포컀슀 μŠ€νƒ€μΌμ΄ λˆ„λ½λ˜μ–΄ μžˆμ–΄, ν‚€λ³΄λ“œ λ‚΄λΉ„κ²Œμ΄μ…˜ μ‚¬μš©μžμ—κ²Œ ν˜„μž¬ 포컀슀 μœ„μΉ˜λ₯Ό λͺ…ν™•νžˆ 보여주지 λͺ»ν–ˆμŠ΅λ‹ˆλ‹€. `hover` μŠ€νƒ€μΌμ€ μžˆμ—ˆμœΌλ‚˜ `focus-visible` μ²˜λ¦¬κ°€ μ—†μ–΄ μ ‘κ·Όμ„± 결함이 μžˆμ—ˆμŠ΅λ‹ˆλ‹€.
**Action:** Tailwind CSS의 `focus-visible` κ΄€λ ¨ μœ ν‹Έλ¦¬ν‹° 클래슀(`focus-visible:outline-none`, `focus-visible:ring-2`, `focus-visible:ring-ring`, `rounded-sm`)λ₯Ό μΆ”κ°€ν•˜μ—¬ νƒ­(Tab) ν‚€ 이동 μ‹œ 포컀슀 링이 보이도둝 μˆ˜μ •ν–ˆμŠ΅λ‹ˆλ‹€. μ•žμœΌλ‘œ λŒ€ν™”ν˜• μ»΄ν¬λ„ŒνŠΈλ₯Ό 섀계할 λ•ŒλŠ” 항상 ν‚€λ³΄λ“œ μ ‘κ·Όμ„±(focus states)을 염두에 두고 μž‘μ—…ν•΄μ•Ό ν•©λ‹ˆλ‹€.
**Action:** Tailwind CSS의 `focus-visible` κ΄€λ ¨ μœ ν‹Έλ¦¬ν‹° 클래슀(`focus-visible:outline-none`, `focus-visible:ring-2`, `focus-visible:ring-ring`, `rounded-sm`)λ₯Ό μΆ”κ°€ν•˜μ—¬ νƒ­(Tab) ν‚€ 이동 μ‹œ 포컀슀 링이 보이도둝 μˆ˜μ •ν–ˆμŠ΅λ‹ˆλ‹€. μ•žμœΌλ‘œ λŒ€ν™”ν˜• μ»΄ν¬λ„ŒνŠΈλ₯Ό 섀계할 λ•ŒλŠ” 항상 ν‚€λ³΄λ“œ μ ‘κ·Όμ„±(focus states)을 염두에 두고 μž‘μ—…ν•΄μ•Ό ν•©λ‹ˆλ‹€.

## 2024-07-10 - 접을 수 μžˆλŠ” μ˜μ—­(Collapsible Region) μ ‘κ·Όμ„± κ°œμ„ 
**Learning:** `ContextSection` μ»΄ν¬λ„ŒνŠΈμ™€ 같이 μ•„μ½”λ””μ–Έ ν˜•νƒœμ˜ 접을 수 μžˆλŠ” μ˜μ—­μ€ 슀크린 리더가 μ»¨ν…μΈ μ˜ μƒνƒœλ₯Ό μ˜¬λ°”λ₯΄κ²Œ μΈμ‹ν•˜κ³  읽어주기 μœ„ν•΄ ν† κΈ€ λ²„νŠΌκ³Ό 컨텐츠 μ»¨ν…Œμ΄λ„ˆ κ°„μ˜ λͺ…ν™•ν•œ ARIA 연결이 ν•„μš”ν•©λ‹ˆλ‹€. React의 `useId()` 훅을 μ‚¬μš©ν•˜μ—¬ `aria-controls`, `id`, `aria-labelledby`λ₯Ό λ™μ μœΌλ‘œ 생성해 μ„œλ‘œ μ—°κ²°ν•˜λ©΄ μ•ˆμ •μ μž…λ‹ˆλ‹€. λ˜ν•œ, 컨텐츠 μ˜μ—­μ—λŠ” `role="region"`이 λ°˜λ“œμ‹œ ν•„μš”ν•˜λ‹€λŠ” 점을 ν™•μΈν–ˆμŠ΅λ‹ˆλ‹€.
**Action:** ν–₯ν›„ 접을 수 μžˆλŠ” μ»΄ν¬λ„ŒνŠΈ(Collapsible Region)λ₯Ό λ§Œλ“€κ±°λ‚˜ μˆ˜μ •ν•  λ•ŒλŠ” 항상 `useId()`λ₯Ό μ‚¬μš©ν•˜μ—¬ ν† κΈ€ λ²„νŠΌ(`aria-controls`)κ³Ό 컨텐츠 μ»¨ν…Œμ΄λ„ˆ(`id`, `role="region"`, `aria-labelledby`)λ₯Ό λ™μ μœΌλ‘œ μ—°κ²°ν•˜λ„λ‘ ν•©λ‹ˆλ‹€. μΆ”κ°€λ‘œ ν‚€λ³΄λ“œ λ„€λΉ„κ²Œμ΄μ…˜ μ‚¬μš©μžλ₯Ό μœ„ν•œ λͺ…ν™•ν•œ 포컀슀 링(`focus-visible` κ΄€λ ¨ 클래슀 적용) μ²˜λ¦¬λ„ μžŠμ§€ 말아야 ν•©λ‹ˆλ‹€.
2 changes: 2 additions & 0 deletions packages/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,9 @@
"devDependencies": {
"@tailwindcss/postcss": "^4.2.2",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^14.6.1",
"@types/bcryptjs": "^2",
"@types/node": "^20",
"@types/react": "^19",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/** @vitest-environment jsdom */
import React from 'react'
import { afterEach, describe, expect, it } from 'vitest'
import { render, screen, cleanup } from '@testing-library/react'
import '@testing-library/jest-dom/vitest'
import userEvent from '@testing-library/user-event'
import { ContextSection } from './context-section'

describe('ContextSection', () => {
afterEach(() => {
cleanup()
})

it('renders title and toggles content', async () => {
const user = userEvent.setup()
render(
<ContextSection title="Test Title">
<div data-testid="test-content">Content</div>
</ContextSection>
)

// Initially closed
expect(screen.getByRole('button', { name: 'Test Title' })).toHaveAttribute('aria-expanded', 'false')
expect(screen.queryByTestId('test-content')).not.toBeInTheDocument()

// Click to open
await user.click(screen.getByRole('button', { name: 'Test Title' }))
expect(screen.getByRole('button', { name: 'Test Title' })).toHaveAttribute('aria-expanded', 'true')

const region = screen.getByRole('region', { name: 'Test Title' })
expect(region).toBeInTheDocument()
expect(screen.getByTestId('test-content')).toBeInTheDocument()

// Click to close
await user.click(screen.getByRole('button', { name: 'Test Title' }))
expect(screen.getByRole('button', { name: 'Test Title' })).toHaveAttribute('aria-expanded', 'false')
expect(screen.queryByTestId('test-content')).not.toBeInTheDocument()
})

it('renders open by default if defaultOpen is true', () => {
render(
<ContextSection title="Test Title" defaultOpen>
<div data-testid="test-content">Content</div>
</ContextSection>
)

expect(screen.getByRole('button', { name: 'Test Title' })).toHaveAttribute('aria-expanded', 'true')
expect(screen.getByTestId('test-content')).toBeInTheDocument()
expect(screen.getByRole('region', { name: 'Test Title' })).toBeInTheDocument()
})
})
16 changes: 13 additions & 3 deletions packages/web/src/components/dashboard/reports/context-section.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client'

import { useState, type ReactNode } from 'react'
import React, { useState, useId, type ReactNode } from 'react'
import { ChevronDown, ChevronUp } from 'lucide-react'
import { cn } from '@/lib/utils'

Expand All @@ -12,17 +12,22 @@ interface ContextSectionProps {

export function ContextSection({ title, children, defaultOpen = false }: ContextSectionProps) {
const [open, setOpen] = useState(defaultOpen)
const id = useId()
const buttonId = `context-section-btn-${id}`
const contentId = `context-section-content-${id}`

return (
<div className="rounded-xl bg-card ring-1 ring-foreground/10 overflow-hidden">
<button
type="button"
id={buttonId}
onClick={() => setOpen((v) => !v)}
className={cn(
'w-full flex items-center justify-between px-4 py-3 text-left',
'hover:bg-card-elevated transition-colors',
'hover:bg-card-elevated transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
)}
aria-expanded={open}
aria-controls={contentId}
>
<h2 className="text-base font-medium">{title}</h2>
{open ? (
Expand All @@ -32,7 +37,12 @@ export function ContextSection({ title, children, defaultOpen = false }: Context
)}
</button>
{open && (
<div className="px-4 pb-4 pt-1">
<div
id={contentId}
role="region"
aria-labelledby={buttonId}
className="px-4 pb-4 pt-1"
>
{children}
</div>
)}
Expand Down
73 changes: 73 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading