diff --git a/use_cases/src/App.tsx b/use_cases/src/App.tsx index a0044e7106..a52600364d 100644 --- a/use_cases/src/App.tsx +++ b/use_cases/src/App.tsx @@ -36,6 +36,7 @@ import { MutationObserverPage } from './pages/MutationObserverPage'; import { WebStoragePage } from './pages/WebStoragePage'; import { DOMBoundingRectPage } from './pages/DOMBoundingRectPage'; import { CSSShowcasePage } from './pages/CSSShowcasePage'; +import { AccessibilityPage } from './pages/AccessibilityPage'; import { BGPage } from './pages/css/BGPage'; import { BGGradientPage } from './pages/css/BGGradientPage'; import { BGImagePage } from './pages/css/BGImagePage'; @@ -85,6 +86,7 @@ function App() { } /> } /> } /> + } /> } /> {/* Advanced Web APIs */} @@ -117,4 +119,4 @@ function App() { ); } -export default App; \ No newline at end of file +export default App; diff --git a/use_cases/src/pages/AccessibilityPage.module.css b/use_cases/src/pages/AccessibilityPage.module.css new file mode 100644 index 0000000000..a12b6e07c6 --- /dev/null +++ b/use_cases/src/pages/AccessibilityPage.module.css @@ -0,0 +1,367 @@ +.pageWrapper { + position: relative; + min-height: 100vh; + background: var(--background-color, #f5f5f5); +} + +.skipLink { + position: absolute; + left: 16px; + top: -48px; + background: var(--accent-color, #007aff); + color: #ffffff; + padding: 10px 16px; + border-radius: 6px; + text-decoration: none; + font-weight: 600; + transition: top 0.2s ease, box-shadow 0.2s ease; + z-index: 10; +} + +.skipLink:focus { + top: 16px; + box-shadow: 0 6px 16px rgba(0, 0, 0, 0.25); +} + +.introHeader { + padding: 32px 20px 0; + text-align: center; + color: var(--primary-text-color, #1f1f1f); +} + +.pageTitle { + font-size: 32px; + margin: 0 0 12px; +} + +.pageSubtitle { + margin: 0 auto; + max-width: 720px; + font-size: 16px; + line-height: 1.6; + color: var(--secondary-text-color, #555555); +} + +.list { + flex: 1; + padding: 0; + margin: 0; +} + +.componentSection { + padding: 20px; + max-width: 1200px; + margin: 0 auto 60px; + box-sizing: border-box; +} + +.componentBlock { + display: flex; + flex-direction: column; + gap: 28px; +} + +.componentItem { + background: var(--card-background, #ffffff); + border-radius: 12px; + padding: 24px; + box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08); + border: 1px solid var(--border-color, #e5e5e5); +} + +.itemLabel { + font-size: 20px; + font-weight: 600; + color: var(--primary-text-color, #1f1f1f); + margin: 0 0 8px; +} + +.itemDesc { + font-size: 14px; + color: var(--secondary-text-color, #5b5b5b); + line-height: 1.6; + margin: 0 0 20px; +} + +.landmarkExample { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 16px; + background: #f8f9fb; + border-radius: 10px; + border: 1px solid #e9ecef; + padding: 20px; +} + +.landmarkExample strong { + display: block; + font-size: 14px; + color: var(--accent-color, #007aff); + margin-bottom: 6px; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.landmarkHeader, +.landmarkNav, +.landmarkMain, +.landmarkAside, +.landmarkFooter { + background: #ffffff; + border-radius: 8px; + padding: 16px; + border: 1px solid #edf1f5; + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05); +} + +.landmarkNavList { + list-style: none; + padding: 0; + margin: 8px 0 0; + display: flex; + flex-direction: column; + gap: 6px; +} + +.landmarkNavList a { + color: var(--link-color, #0b5fff); + text-decoration: none; + font-weight: 500; +} + +.landmarkNavList a:hover, +.landmarkNavList a:focus-visible { + text-decoration: underline; +} + +.menuContainer { + display: flex; + flex-direction: column; + gap: 16px; +} + +@media (min-width: 720px) { + .menuContainer { + flex-direction: row; + align-items: stretch; + } +} + +.menu { + display: inline-flex; + flex-wrap: wrap; + gap: 12px; + background: #f1f5fb; + padding: 16px; + border-radius: 10px; + border: 1px solid #d7e3ff; +} + +.menuButton { + background: #ffffff; + border: 2px solid transparent; + border-radius: 999px; + padding: 10px 18px; + font-size: 14px; + font-weight: 600; + color: var(--primary-text-color, #1f1f1f); + cursor: pointer; + transition: border-color 0.2s ease, box-shadow 0.2s ease, background 0.2s ease; +} + +.menuButton:hover { + background: #f0f4ff; +} + +.menuButton:focus-visible { + outline: none; + border-color: var(--accent-color, #007aff); + box-shadow: 0 0 0 3px rgba(0, 122, 255, 0.3); +} + +.menuButtonActive { + background: var(--accent-color, #007aff); + color: #ffffff; +} + +.menuSummary { + background: #ffffff; + border-radius: 10px; + border: 1px solid #e3e9f4; + padding: 18px; + box-shadow: 0 1px 6px rgba(0, 0, 0, 0.06); + flex: 1; +} + +.menuSummaryTitle { + margin: 0 0 8px; + font-size: 18px; +} + +.menuSummaryBody { + margin: 0 0 14px; + font-size: 14px; + line-height: 1.6; +} + +.menuHint { + margin: 0; + font-size: 12px; + color: var(--secondary-text-color, #5b5b5b); +} + +.feedbackForm { + display: flex; + flex-direction: column; + gap: 16px; +} + +.formGroup { + display: flex; + flex-direction: column; + gap: 8px; +} + +.formLabel { + font-weight: 600; + color: var(--primary-text-color, #1f1f1f); +} + +.textInput, +.textarea { + width: 100%; + box-sizing: border-box; + border-radius: 8px; + border: 1px solid #d9d9d9; + padding: 12px 14px; + font-size: 14px; + transition: border-color 0.2s ease, box-shadow 0.2s ease; + background: #ffffff; + color: var(--primary-text-color, #1f1f1f); +} + +.textInput:focus-visible, +.textarea:focus-visible { + outline: none; + border-color: var(--accent-color, #007aff); + box-shadow: 0 0 0 3px rgba(0, 122, 255, 0.25); +} + +.textarea { + resize: vertical; + min-height: 120px; +} + +.formHint { + margin: 0; + font-size: 12px; + color: var(--secondary-text-color, #5b5b5b); +} + +.errorMessage { + background: #ffe9eb; + color: #b00020; + border-radius: 8px; + padding: 12px 16px; + font-size: 14px; + border: 1px solid #ffcdd2; +} + +.successMessage { + background: #e6f4ea; + color: #146c2e; + border-radius: 8px; + padding: 12px 16px; + font-size: 14px; + border: 1px solid #b7dfc1; +} + +.submitButton { + align-self: flex-start; + background: var(--accent-color, #007aff); + color: #ffffff; + font-weight: 600; + border: none; + border-radius: 999px; + padding: 12px 24px; + cursor: pointer; + transition: background 0.2s ease, box-shadow 0.2s ease; +} + +.submitButton:hover { + background: #005fcc; +} + +.submitButton:focus-visible { + outline: none; + box-shadow: 0 0 0 3px rgba(0, 122, 255, 0.35); +} + +.faqToggle { + background: #ffffff; + border-radius: 999px; + border: 2px solid var(--accent-color, #007aff); + color: var(--accent-color, #007aff); + padding: 10px 18px; + font-weight: 600; + cursor: pointer; + transition: background 0.2s ease, color 0.2s ease, box-shadow 0.2s ease; +} + +.faqToggle:hover { + background: rgba(0, 122, 255, 0.1); +} + +.faqToggle:focus-visible { + outline: none; + box-shadow: 0 0 0 3px rgba(0, 122, 255, 0.3); +} + +.faqContent { + margin-top: 16px; + padding: 20px; + border-radius: 10px; + border: 1px solid #dfe4ec; + background: #f9fbff; + line-height: 1.6; +} + +.faqContentVisible { + animation: fadeInExpand 0.25s ease; +} + +.faqTitle { + margin: 0 0 12px; + font-size: 18px; + color: var(--primary-text-color, #1f1f1f); +} + +.faqList { + margin: 12px 0 0; + padding-left: 20px; +} + +.faqList li { + margin-bottom: 6px; +} + +.liveRegion { + margin-top: 36px; + padding: 16px 20px; + border-radius: 10px; + border: 1px solid #ccd6eb; + background: #f1f5ff; + font-size: 14px; + color: var(--primary-text-color, #1f1f1f); +} + +@keyframes fadeInExpand { + from { + opacity: 0; + transform: translateY(-4px); + } + to { + opacity: 1; + transform: translateY(0); + } +} diff --git a/use_cases/src/pages/AccessibilityPage.tsx b/use_cases/src/pages/AccessibilityPage.tsx new file mode 100644 index 0000000000..dcbec4ea40 --- /dev/null +++ b/use_cases/src/pages/AccessibilityPage.tsx @@ -0,0 +1,373 @@ +import React, { useEffect, useRef, useState } from 'react'; +import { WebFListView } from '@openwebf/react-core-ui'; +import styles from './AccessibilityPage.module.css'; + +type MenuItem = { + label: string; + description: string; +}; + +const MENU_ITEMS: MenuItem[] = [ + { + label: 'Overview', + description: 'Highlights the page purpose and reinforces orientation cues for assistive technology.', + }, + { + label: 'Keyboard Shortcuts', + description: 'Summarises keys that help power users and screen reader operators move quickly.', + }, + { + label: 'Support', + description: 'Points to human support options when automated answers fall short.', + }, +]; + +export const AccessibilityPage: React.FC = () => { + const [focusIndex, setFocusIndex] = useState(0); + const [activeMenu, setActiveMenu] = useState(MENU_ITEMS[0]); + const [announcement, setAnnouncement] = useState( + 'Interactive examples ready. Use the skip link or jump straight into the keyboard menu demo.' + ); + const [faqExpanded, setFaqExpanded] = useState(false); + const [formSubmitted, setFormSubmitted] = useState(false); + const [formError, setFormError] = useState(null); + const menuRefs = useRef>([]); + + useEffect(() => { + menuRefs.current[focusIndex]?.focus(); + }, [focusIndex]); + + const announce = (message: string) => { + setAnnouncement(message); + }; + + const handleMenuSelect = (item: MenuItem, index: number) => { + setActiveMenu(item); + setFocusIndex(index); + announce(`${item.label} menu item activated.`); + }; + + const handleMenuKeyDown = (event: React.KeyboardEvent, index: number) => { + if (event.defaultPrevented) { + return; + } + + if (event.key === 'ArrowRight' || event.key === 'ArrowDown') { + event.preventDefault(); + const nextIndex = (index + 1) % MENU_ITEMS.length; + setFocusIndex(nextIndex); + setActiveMenu(MENU_ITEMS[nextIndex]); + announce(`Moved to ${MENU_ITEMS[nextIndex].label} menu item.`); + return; + } + + if (event.key === 'ArrowLeft' || event.key === 'ArrowUp') { + event.preventDefault(); + const prevIndex = (index - 1 + MENU_ITEMS.length) % MENU_ITEMS.length; + setFocusIndex(prevIndex); + setActiveMenu(MENU_ITEMS[prevIndex]); + announce(`Moved to ${MENU_ITEMS[prevIndex].label} menu item.`); + return; + } + + if (event.key === 'Home') { + event.preventDefault(); + setFocusIndex(0); + setActiveMenu(MENU_ITEMS[0]); + announce('Moved to Overview menu item.'); + return; + } + + if (event.key === 'End') { + event.preventDefault(); + const lastIndex = MENU_ITEMS.length - 1; + setFocusIndex(lastIndex); + setActiveMenu(MENU_ITEMS[lastIndex]); + announce(`Moved to ${MENU_ITEMS[lastIndex].label} menu item.`); + return; + } + + if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault(); + handleMenuSelect(MENU_ITEMS[index], index); + } + }; + + const handleFormSubmit = (event: React.FormEvent) => { + event.preventDefault(); + setFormSubmitted(false); + + const formData = new FormData(event.currentTarget); + const name = (formData.get('name') as string | null)?.trim() ?? ''; + const email = (formData.get('email') as string | null)?.trim() ?? ''; + const message = (formData.get('message') as string | null)?.trim() ?? ''; + + if (!name) { + setFormError('Please enter your name before submitting.'); + announce('Form error: Name is missing.'); + return; + } + + const emailPattern = /^[^@\s]+@[^@\s]+\.[^@\s]+$/; + if (!emailPattern.test(email)) { + setFormError('Enter a valid email address (example: name@domain.com).'); + announce('Form error: Email address is invalid.'); + return; + } + + if (message.length < 10) { + setFormError('Tell us a little more so we can help (at least 10 characters).'); + announce('Form error: Message is too short.'); + return; + } + + setFormError(null); + setFormSubmitted(true); + event.currentTarget.reset(); + announce('Accessibility feedback form submitted successfully.'); + }; + + const toggleFaq = () => { + setFaqExpanded((previous) => { + const next = !previous; + announce(next ? 'FAQ details expanded.' : 'FAQ details collapsed.'); + return next; + }); + }; + + return ( + + + Skip to main content + + + Accessibility Use Cases + + Explore practical accessibility patterns: meaningful landmarks, keyboard interactions, live announcements, and + inclusive forms that work for everyone. + + + + + + + + + Landmarks & Skip Navigation + + + Structure pages so assistive technologies can offer shortcuts. Skip links paired with semantic landmarks + let keyboard users move directly to the content they need. + + + + + Header + Contains the brand, search box, and global navigation entry points. + + + Navigation + + + Landmark demo + + + Keyboard menu + + + Feedback form + + + + + Main + Serves the primary user goal. Landmarks make it easy to find with a single shortcut. + + + + + + + + + Keyboard-Friendly Navigation + + + Use roving tab-index patterns to keep arrow key navigation inside a widget while preserving Tab for + entering and exiting. The active item is announced as focus moves. + + + + + {MENU_ITEMS.map((item, index) => ( + { + menuRefs.current[index] = element; + }} + aria-describedby={`menu-item-desc-${index}`} + onClick={() => handleMenuSelect(item, index)} + onKeyDown={(event) => handleMenuKeyDown(event, index)} + > + {item.label} + + ))} + + + {activeMenu.label} + + {activeMenu.description} + + + Tip: Use Arrow keys to move between items, Enter or Space to activate, Home and End to jump to the + first or last item. + + + + + + + + Accessible Feedback Form + + + Labels, instructions, and inline validation errors are connected to their inputs so screen readers and + voice control software capture every requirement. + + + + + + Full name * + + + + + + + Email address * + + + + We use your address to follow up on accessibility issues. It stays private. + + + + + + Describe the issue * + + + + + + Required fields are marked with an asterisk. You can submit the form using Enter from the message field. + + + {formError && ( + + {formError} + + )} + + {formSubmitted && !formError && ( + + Thank you! We will reach out if we need more details. + + )} + + + Submit feedback + + + + + + + Discernible Expansion Controls + + + Toggle buttons expose or hide supporting context. The control announces its state so users always know + what happened. + + + + {faqExpanded ? 'Hide' : 'Show'} accessibility FAQ + + + Why does this matter? + + Accessible patterns reduce cognitive load, prevent focus traps, and ensure assistive technology conveys + everything your visual design promises. Every example on this page works with a keyboard and a screen + reader without additional configuration. + + + Focus styles remain visible for users who rely on sight and touch. + ARIA attributes are only used when semantic HTML is not enough. + Live regions announce changes without stealing focus from the user. + + + + + + {announcement} + + + + + ); +}; + +export default AccessibilityPage; diff --git a/use_cases/src/pages/HomePage.tsx b/use_cases/src/pages/HomePage.tsx index fd22924283..a349cdc346 100644 --- a/use_cases/src/pages/HomePage.tsx +++ b/use_cases/src/pages/HomePage.tsx @@ -99,6 +99,20 @@ export const HomePage: React.FC = () => { + {/* Accessibility */} + Accessibility + + navigateTo('/accessibility')}> + + Accessibility Toolkit + + Skip links, keyboard navigation, live announcements, and inclusive forms in action. + + + > + + + {/* Native UI Components Category */} Native UI Components @@ -274,4 +288,4 @@ export const HomePage: React.FC = () => { ); -}; \ No newline at end of file +};
+ Explore practical accessibility patterns: meaningful landmarks, keyboard interactions, live announcements, and + inclusive forms that work for everyone. +
+ Structure pages so assistive technologies can offer shortcuts. Skip links paired with semantic landmarks + let keyboard users move directly to the content they need. +
Contains the brand, search box, and global navigation entry points.
Serves the primary user goal. Landmarks make it easy to find with a single shortcut.
+ Use roving tab-index patterns to keep arrow key navigation inside a widget while preserving Tab for + entering and exiting. The active item is announced as focus moves. +
+ {activeMenu.description} +
+ Tip: Use Arrow keys to move between items, Enter or Space to activate, Home and End to jump to the + first or last item. +
+ Labels, instructions, and inline validation errors are connected to their inputs so screen readers and + voice control software capture every requirement. +
+ We use your address to follow up on accessibility issues. It stays private. +
+ Required fields are marked with an asterisk. You can submit the form using Enter from the message field. +
+ Toggle buttons expose or hide supporting context. The control announces its state so users always know + what happened. +
+ Accessible patterns reduce cognitive load, prevent focus traps, and ensure assistive technology conveys + everything your visual design promises. Every example on this page works with a keyboard and a screen + reader without additional configuration. +