|
277 | 277 | margin-bottom: 4px; |
278 | 278 | } |
279 | 279 |
|
280 | | - .tooltip:hover::after { |
| 280 | + .tooltip:hover::after, |
| 281 | + .tooltip:focus::after, |
| 282 | + .tooltip:focus-visible::after { |
281 | 283 | opacity: 1; |
282 | 284 | visibility: visible; |
283 | 285 | } |
284 | 286 |
|
285 | | - .help-icon { |
286 | | - display: inline-block; |
287 | | - width: 14px; |
288 | | - height: 14px; |
289 | | - line-height: 14px; |
290 | | - text-align: center; |
291 | | - font-size: 0.65rem; |
| 287 | + .help-btn { |
| 288 | + display: inline-flex; |
| 289 | + align-items: center; |
| 290 | + justify-content: center; |
| 291 | + width: 18px; |
| 292 | + height: 18px; |
| 293 | + font-size: 0.7rem; |
292 | 294 | background: var(--bg-secondary); |
| 295 | + border: 1px solid var(--text-secondary); |
293 | 296 | border-radius: 50%; |
294 | 297 | color: var(--text-secondary); |
295 | 298 | margin-left: 4px; |
296 | 299 | cursor: help; |
| 300 | + padding: 0; |
| 301 | + font-family: inherit; |
| 302 | + } |
| 303 | + |
| 304 | + .help-btn:focus-visible { |
| 305 | + outline: 2px solid var(--accent-cyan); |
| 306 | + outline-offset: 2px; |
| 307 | + } |
| 308 | + |
| 309 | + .skip-link { |
| 310 | + position: absolute; |
| 311 | + top: -40px; |
| 312 | + left: 0; |
| 313 | + background: var(--accent-cyan); |
| 314 | + color: var(--bg-primary); |
| 315 | + padding: 8px 16px; |
| 316 | + z-index: 1000; |
| 317 | + text-decoration: none; |
| 318 | + border-radius: 0 0 8px 0; |
| 319 | + } |
| 320 | + |
| 321 | + .skip-link:focus { |
| 322 | + top: 0; |
| 323 | + } |
| 324 | + |
| 325 | + .sr-only { |
| 326 | + position: absolute; |
| 327 | + width: 1px; |
| 328 | + height: 1px; |
| 329 | + padding: 0; |
| 330 | + margin: -1px; |
| 331 | + overflow: hidden; |
| 332 | + clip: rect(0, 0, 0, 0); |
| 333 | + white-space: nowrap; |
| 334 | + border: 0; |
297 | 335 | } |
298 | 336 |
|
299 | 337 | .warning-box { |
|
369 | 407 | </style> |
370 | 408 | </head> |
371 | 409 | <body> |
| 410 | + <a href="#main" class="skip-link">Skip to main content</a> |
372 | 411 | <div class="container"> |
373 | 412 | <header> |
374 | | - <div class="logo">LIQ Flash Loan</div> |
| 413 | + <h1 class="logo">LIQ Flash Loan</h1> |
375 | 414 | <button id="connectBtn" class="btn btn-connect">Connect Wallet</button> |
376 | 415 | </header> |
| 416 | + <main id="main"> |
377 | 417 |
|
378 | 418 | <!-- Status Panel --> |
379 | | - <div class="panel"> |
380 | | - <div class="panel-title"> |
381 | | - <span class="status-dot loading" id="statusDot"></span> |
| 419 | + <section class="panel" aria-labelledby="status-heading"> |
| 420 | + <h2 class="panel-title" id="status-heading"> |
| 421 | + <span class="status-dot loading" id="statusDot" aria-hidden="true"></span> |
382 | 422 | Pool Status |
383 | | - </div> |
| 423 | + <span class="sr-only" id="statusAnnounce" aria-live="polite"></span> |
| 424 | + </h2> |
384 | 425 | <div class="stats-grid"> |
385 | 426 | <div class="stat"> |
386 | | - <div class="stat-label">Pool Balance (tracked)</div> |
387 | | - <div class="stat-value" id="poolBalance">-</div> |
| 427 | + <div class="stat-label" id="poolBalanceLabel">Pool Balance (tracked)</div> |
| 428 | + <div class="stat-value" id="poolBalance" aria-labelledby="poolBalanceLabel">-</div> |
388 | 429 | </div> |
389 | 430 | <div class="stat"> |
390 | | - <div class="stat-label">Actual USDC Balance</div> |
391 | | - <div class="stat-value" id="actualBalance">-</div> |
| 431 | + <div class="stat-label" id="actualBalanceLabel">Actual USDC Balance</div> |
| 432 | + <div class="stat-value" id="actualBalance" aria-labelledby="actualBalanceLabel">-</div> |
392 | 433 | </div> |
393 | 434 | <div class="stat"> |
394 | | - <div class="stat-label"> |
| 435 | + <div class="stat-label" id="syncStatusLabel"> |
395 | 436 | Sync Status |
396 | | - <span class="help-icon tooltip" data-tooltip="Synced = poolBalance matches actual USDC balance">?</span> |
| 437 | + <button type="button" class="help-btn tooltip" data-tooltip="Synced = poolBalance matches actual USDC balance" aria-label="Help: Synced means poolBalance matches actual USDC balance">?</button> |
397 | 438 | </div> |
398 | | - <div class="stat-value" id="syncStatus">-</div> |
| 439 | + <div class="stat-value" id="syncStatus" aria-labelledby="syncStatusLabel">-</div> |
399 | 440 | </div> |
400 | 441 | <div class="stat"> |
401 | | - <div class="stat-label">Owner</div> |
402 | | - <div class="stat-value" id="ownerAddress">-</div> |
| 442 | + <div class="stat-label" id="ownerLabel">Owner</div> |
| 443 | + <div class="stat-value" id="ownerAddress" aria-labelledby="ownerLabel">-</div> |
403 | 444 | </div> |
404 | 445 | </div> |
405 | 446 | <div class="contract-info"> |
406 | | - Contract: <a href="https://etherscan.io/address/0xe9eb8a0f6328e243086fe6efee0857e14fa2cb87" target="_blank" id="contractLink">0xe9eb8a0f6328e243086fe6efee0857e14fa2cb87</a> |
| 447 | + Contract: <a href="https://etherscan.io/address/0xe9eb8a0f6328e243086fe6efee0857e14fa2cb87" target="_blank" rel="noopener noreferrer" id="contractLink">0xe9eb8a0f6328e243086fe6efee0857e14fa2cb87 <span class="sr-only">(opens in new tab)</span></a> |
407 | 448 | </div> |
408 | | - </div> |
| 449 | + </section> |
409 | 450 |
|
410 | 451 | <!-- Warning Box (shown when unsynced) --> |
411 | | - <div class="warning-box hidden" id="warningBox"> |
412 | | - [!] Excess USDC detected. Call sync() to protect it from being drained. |
| 452 | + <div class="warning-box hidden" id="warningBox" role="alert" aria-live="assertive"> |
| 453 | + Warning: Excess USDC detected. Call sync() to protect it from being drained. |
413 | 454 | </div> |
414 | 455 |
|
415 | 456 | <!-- Actions Panel --> |
416 | | - <div class="panel"> |
417 | | - <div class="panel-title">Actions</div> |
418 | | - <div class="actions-row"> |
419 | | - <button class="action-btn" id="syncAction" data-action="sync"> |
420 | | - <div class="action-icon">[S]</div> |
| 457 | + <section class="panel" aria-labelledby="actions-heading"> |
| 458 | + <h2 class="panel-title" id="actions-heading">Actions</h2> |
| 459 | + <div class="actions-row" role="tablist" aria-label="Action selection"> |
| 460 | + <button class="action-btn" id="syncAction" data-action="sync" role="tab" aria-selected="false" aria-controls="syncPanel"> |
| 461 | + <div class="action-icon" aria-hidden="true">[S]</div> |
421 | 462 | <div>Sync</div> |
422 | 463 | </button> |
423 | | - <button class="action-btn" id="depositAction" data-action="deposit"> |
424 | | - <div class="action-icon">[+]</div> |
| 464 | + <button class="action-btn" id="depositAction" data-action="deposit" role="tab" aria-selected="false" aria-controls="depositPanel"> |
| 465 | + <div class="action-icon" aria-hidden="true">[+]</div> |
425 | 466 | <div>Deposit</div> |
426 | 467 | </button> |
427 | | - <button class="action-btn" id="withdrawAction" data-action="withdraw"> |
428 | | - <div class="action-icon">[-]</div> |
| 468 | + <button class="action-btn" id="withdrawAction" data-action="withdraw" role="tab" aria-selected="false" aria-controls="withdrawPanel"> |
| 469 | + <div class="action-icon" aria-hidden="true">[-]</div> |
429 | 470 | <div>Withdraw</div> |
430 | 471 | </button> |
431 | 472 | </div> |
432 | | - </div> |
| 473 | + </section> |
433 | 474 |
|
434 | 475 | <!-- Action Forms --> |
435 | | - <div class="panel hidden" id="syncPanel"> |
436 | | - <div class="panel-title">Sync Pool Balance</div> |
| 476 | + <section class="panel hidden" id="syncPanel" role="tabpanel" aria-labelledby="syncAction"> |
| 477 | + <h2 class="panel-title">Sync Pool Balance</h2> |
437 | 478 | <p style="color: var(--text-secondary); margin-bottom: 1rem; font-size: 0.9rem;"> |
438 | 479 | Sets poolBalance = USDC.balanceOf(this). Call after any direct USDC transfers. |
439 | 480 | </p> |
440 | 481 | <button class="btn btn-primary" id="syncBtn">Sync Now</button> |
441 | | - </div> |
| 482 | + </section> |
442 | 483 |
|
443 | | - <div class="panel hidden" id="depositPanel"> |
444 | | - <div class="panel-title">Deposit USDC</div> |
| 484 | + <section class="panel hidden" id="depositPanel" role="tabpanel" aria-labelledby="depositAction"> |
| 485 | + <h2 class="panel-title">Deposit USDC</h2> |
445 | 486 | <div class="input-group"> |
446 | | - <label>Amount (USDC)</label> |
447 | | - <input type="number" id="depositAmount" placeholder="1000.00" step="0.01"> |
| 487 | + <label for="depositAmount">Amount (USDC)</label> |
| 488 | + <input type="number" id="depositAmount" placeholder="1000.00" step="0.01" aria-describedby="depositHelp"> |
| 489 | + <span id="depositHelp" class="sr-only">Enter the amount of USDC you want to deposit</span> |
448 | 490 | </div> |
449 | 491 | <button class="btn btn-primary" id="depositBtn">Approve & Deposit</button> |
450 | | - </div> |
| 492 | + </section> |
451 | 493 |
|
452 | | - <div class="panel hidden" id="withdrawPanel"> |
453 | | - <div class="panel-title">Withdraw USDC</div> |
| 494 | + <section class="panel hidden" id="withdrawPanel" role="tabpanel" aria-labelledby="withdrawAction"> |
| 495 | + <h2 class="panel-title">Withdraw USDC</h2> |
454 | 496 | <div class="input-group"> |
455 | | - <label>Amount (USDC)</label> |
456 | | - <input type="number" id="withdrawAmount" placeholder="1000.00" step="0.01"> |
| 497 | + <label for="withdrawAmount">Amount (USDC)</label> |
| 498 | + <input type="number" id="withdrawAmount" placeholder="1000.00" step="0.01" aria-describedby="withdrawHelp"> |
| 499 | + <span id="withdrawHelp" class="sr-only">Enter the amount of USDC you want to withdraw</span> |
457 | 500 | </div> |
458 | 501 | <button class="btn btn-primary" id="withdrawBtn">Withdraw</button> |
459 | | - </div> |
| 502 | + </section> |
460 | 503 |
|
461 | 504 | <!-- Activity Log --> |
462 | | - <div class="panel"> |
463 | | - <div class="panel-title">Activity Log</div> |
464 | | - <div class="log" id="log"> |
| 505 | + <section class="panel" aria-labelledby="log-heading"> |
| 506 | + <h2 class="panel-title" id="log-heading">Activity Log</h2> |
| 507 | + <div class="log" id="log" role="log" aria-live="polite" aria-relevant="additions"> |
465 | 508 | <div class="log-entry log-info">Welcome to LIQ Flash Loan Dashboard</div> |
466 | 509 | </div> |
467 | | - </div> |
| 510 | + </section> |
| 511 | + |
| 512 | + <!-- Screen reader announcements --> |
| 513 | + <div id="srAnnounce" class="sr-only" aria-live="assertive" aria-atomic="true"></div> |
| 514 | + </main> |
468 | 515 | </div> |
469 | 516 |
|
470 | 517 | <script type="module"> |
|
709 | 756 |
|
710 | 757 | actionBtns.forEach(btn => { |
711 | 758 | btn.addEventListener('click', () => { |
712 | | - actionBtns.forEach(b => b.classList.remove('active')); |
| 759 | + // Update visual state |
| 760 | + actionBtns.forEach(b => { |
| 761 | + b.classList.remove('active'); |
| 762 | + b.setAttribute('aria-selected', 'false'); |
| 763 | + }); |
713 | 764 | btn.classList.add('active'); |
| 765 | + btn.setAttribute('aria-selected', 'true'); |
| 766 | + |
| 767 | + // Show/hide panels |
714 | 768 | const action = btn.dataset.action; |
715 | 769 | syncPanel.classList.toggle('hidden', action !== 'sync'); |
716 | 770 | depositPanel.classList.toggle('hidden', action !== 'deposit'); |
717 | 771 | withdrawPanel.classList.toggle('hidden', action !== 'withdraw'); |
| 772 | + |
| 773 | + // Focus the first focusable element in the panel |
| 774 | + const activePanel = document.getElementById(action + 'Panel'); |
| 775 | + if (activePanel) { |
| 776 | + const focusable = activePanel.querySelector('input, button'); |
| 777 | + if (focusable) focusable.focus(); |
| 778 | + } |
718 | 779 | }); |
719 | 780 | }); |
720 | 781 |
|
|
0 commit comments