Skip to content
Closed
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
3 changes: 3 additions & 0 deletions .jules/palette.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,3 +115,6 @@
## $(date +%Y-%m-%d) - Prevent accidental data loss in inline editors
**Learning:** Forms that take a long time to fill out (like a WBS editor) are prone to accidental closure by users pressing `Escape` or clicking cancel. This causes immediate data loss without any warning, resulting in frustration.
**Action:** When working on editors that can be dismissed, track whether the user has modified any fields compared to their initial state. If there are changes, intercept the close action and present a confirmation dialog (`window.confirm`) to ensure they really want to discard their edits. Bypass this for intentional saves or explicit data overrides.
## 2026-08-27 - μš”μ•½ μΉ΄λ“œ 툴팁 ν‚€λ³΄λ“œ μ ‘κ·Όμ„± κ°œμ„ 
**Learning:** div와 같은 λΉ„λŒ€ν™”ν˜• μš”μ†Œμ— title(툴팁)이 μžˆμ„ 경우 슀크린 리더와 ν‚€λ³΄λ“œ μ‚¬μš©μžμ—κ²Œ λ…ΈμΆœλ˜μ§€ μ•ŠλŠ” μ ‘κ·Όμ„± λ¬Έμ œκ°€ 있음.
**Action:** λΉ„λŒ€ν™”ν˜• μš”μ†Œμ— νˆ΄νŒμ΄λ‚˜ μ€‘μš”ν•œ 정보가 ν¬ν•¨λœ 경우 tabindex="0"을 μΆ”κ°€ν•˜μ—¬ 포컀슀λ₯Ό 받을 수 있게 ν•˜κ³ , role="note"(λ˜λŠ” 상황에 λ§žλŠ” μ μ ˆν•œ ARIA μ—­ν• )λ₯Ό μΆ”κ°€ν•˜μ—¬ μ‹œλ§¨ν‹±ν•œ 의미λ₯Ό λΆ€μ—¬ν•˜λ©°, :focus-visible μŠ€νƒ€μΌμ„ μΆ”κ°€ν•˜μ—¬ ν‚€λ³΄λ“œ λ‚΄λΉ„κ²Œμ΄μ…˜ μ‹œ 포컀슀 μƒνƒœλ₯Ό λͺ…ν™•νžˆ 보여주어야 함.
10 changes: 7 additions & 3 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
<link rel="preload" href="styles.css" as="style" />
<link rel="modulepreload" href="app.js" />
<link rel="stylesheet" href="styles.css" />
<link rel="stylesheet" href="summary-card-accessibility.css" />
<link rel="stylesheet" href="toast-state.css" />
</head>
<body>
Expand All @@ -29,20 +30,23 @@ <h1>ScopeWeave Planner</h1>
<span>기쀀일</span>
<input data-testid="base-date-input" id="base-date" type="date" />
</label>
<div class="meta-value-card" title="ν”„λ‘œμ νŠΈμ˜ μž‘μ—… κΈ°κ°„(일수) ν•©κ³„μž…λ‹ˆλ‹€.">
<div class="meta-value-card" tabindex="0" role="note" aria-describedby="summary-total-days-description">
<span class="meta-label">μ „μ²΄μΌμˆ˜</span>
<strong data-testid="summary-total-days" id="summary-total-days">0일</strong>
<span class="meta-card-description" id="summary-total-days-description">ν”„λ‘œμ νŠΈμ˜ μž‘μ—… κΈ°κ°„(일수) ν•©κ³„μž…λ‹ˆλ‹€.</span>
</div>
Comment on lines +33 to 37

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ” Description may be announced twice by screen readers

Each card carries role="note" and contains the .meta-card-description span as a child while also pointing to it through aria-describedby. On focus a screen reader can announce that text both as note content and as the description. The span is visibility: hidden by default, which limits the effect, but this is worth checking with an actual screen reader.

Devin Review

Was this helpful? React with πŸ‘ or πŸ‘Ž to provide feedback.

</div>

<div class="meta-grid meta-grid-secondary">
<div class="meta-value-card accent-card plan-card" title="κΈ°κ°„(일수) κ°€μ€‘μΉ˜κ°€ 반영된 ν”„λ‘œμ νŠΈ 전체 κ³„νš μ§„μ²™λ₯ μž…λ‹ˆλ‹€.">
<div class="meta-value-card accent-card plan-card" tabindex="0" role="note" aria-describedby="summary-planned-progress-description">
<span class="meta-label">κ³„νšμ§„μ²™λ₯ (λˆ„μ )</span>
<strong data-testid="summary-planned-progress" id="summary-planned-progress">0.00%</strong>
<span class="meta-card-description" id="summary-planned-progress-description">κΈ°κ°„(일수) κ°€μ€‘μΉ˜κ°€ 반영된 ν”„λ‘œμ νŠΈ 전체 κ³„νš μ§„μ²™λ₯ μž…λ‹ˆλ‹€.</span>
</div>
<div class="meta-value-card accent-card actual-card" title="κΈ°κ°„(일수) κ°€μ€‘μΉ˜κ°€ 반영된 ν”„λ‘œμ νŠΈ 전체 싀적 μ§„μ²™λ₯ μž…λ‹ˆλ‹€.">
<div class="meta-value-card accent-card actual-card" tabindex="0" role="note" aria-describedby="summary-actual-progress-description">
<span class="meta-label">싀적진척λ₯ (λˆ„μ )</span>
<strong data-testid="summary-actual-progress" id="summary-actual-progress">0.00%</strong>
<span class="meta-card-description" id="summary-actual-progress-description">κΈ°κ°„(일수) κ°€μ€‘μΉ˜κ°€ 반영된 ν”„λ‘œμ νŠΈ 전체 싀적 μ§„μ²™λ₯ μž…λ‹ˆλ‹€.</span>
</div>
<div class="sync-panel">
<span id="sync-status" role="status" aria-live="polite" aria-atomic="true">λΈŒλΌμš°μ € 둜컬 μžλ™μ €μž₯ μ‚¬μš© 쀑</span>
Expand Down
3 changes: 2 additions & 1 deletion styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -360,7 +360,8 @@ button {
.secondary-button:focus-visible,
input:focus-visible,
select:focus-visible,
.gantt-bar:focus-visible {
.gantt-bar:focus-visible,
.meta-value-card:focus-visible {
outline: 3px solid rgba(37, 99, 235, 0.5);
outline-offset: 2px;
}
Expand Down
32 changes: 32 additions & 0 deletions summary-card-accessibility.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
.meta-value-card {
position: relative;
}
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.

.meta-card-description {
position: absolute;
top: calc(100% + 8px);
left: 0;
z-index: 20;
width: max-content;
max-width: min(320px, calc(100vw - 48px));
padding: 8px 10px;
border-radius: 8px;
background: #0f172a;
color: #ffffff;
box-shadow: var(--shadow-md);
font-size: 0.8125rem;
font-weight: 500;
line-height: 1.4;
opacity: 0;
visibility: hidden;
transform: translateY(-4px);
transition: opacity 120ms ease, transform 120ms ease, visibility 120ms ease;
pointer-events: none;
}

.meta-value-card:hover .meta-card-description,
.meta-value-card:focus .meta-card-description {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
33 changes: 33 additions & 0 deletions tests/e2e/toast-accessibility.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,36 @@ test('cloud status feedback is visibly rendered as a non-focus-taking live statu
{ message: 'advisory status must not capture keyboard focus' },
).toBe(false);
});

test('summary cards expose keyboard-focusable descriptions without relying on title alone', async ({ page }) => {
await page.goto('/');

const cases = [
{
card: page.locator('.meta-value-card').first(),
descriptionId: 'summary-total-days-description',
description: 'ν”„λ‘œμ νŠΈμ˜ μž‘μ—… κΈ°κ°„(일수) ν•©κ³„μž…λ‹ˆλ‹€.',
},
{
card: page.locator('.plan-card'),
descriptionId: 'summary-planned-progress-description',
description: 'κΈ°κ°„(일수) κ°€μ€‘μΉ˜κ°€ 반영된 ν”„λ‘œμ νŠΈ 전체 κ³„νš μ§„μ²™λ₯ μž…λ‹ˆλ‹€.',
},
{
card: page.locator('.actual-card'),
descriptionId: 'summary-actual-progress-description',
description: 'κΈ°κ°„(일수) κ°€μ€‘μΉ˜κ°€ 반영된 ν”„λ‘œμ νŠΈ 전체 싀적 μ§„μ²™λ₯ μž…λ‹ˆλ‹€.',
},
];

for (const { card, descriptionId, description } of cases) {
await expect(card).toHaveAttribute('aria-describedby', descriptionId);
const descriptionNode = page.locator(`#${descriptionId}`);
await expect(descriptionNode).toHaveText(description);
await expect(descriptionNode).toBeHidden();

await card.focus();
await expect(card).toBeFocused();
await expect(descriptionNode).toBeVisible();
}
});
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Loading