Hacktoberfest 2026: những issue maintainer đã đánh dấu cho tháng Mười, đang mở và phù hợp người mới. Xem issue Hacktoberfest

ARIA-IMPLEMENTATION-EXAMPLES.md

Đang mở
#659 0 bình luận 0 reaction 0 người được giao Xem trên GitHub

Chưa có ai nhận issue này.

Đánh giá

Độ khó
3/5
Thời gian dự kiến
1-2 ngày
Mức phù hợp với người mới
45/100
Loại issue
Tài liệu
Độ rõ ràng
Khá rõ ràng
Mức độ hoạt động
Đình trệ
Công nghệ
javascript

Hướng nghiên cứu

Tài liệu được đề xuất là ARIA-IMPLEMENTATION-EXAMPLES.md; trước tiên hãy so sánh các ví dụ trong đó với ACCESSIBILITY-CHECKLIST.md và ANALYSIS-solid-chat-styles-for-solid-ecosystem.md. Công việc được xem là hoàn tất khi các ví dụ triển khai ARIA được mô tả trong issue đã được ghi lại trong tài liệu và phù hợp với hướng dẫn về khả năng truy cập của dự án.

Do mô hình lập chỉ mục viết ra từ nội dung của issue.

Mô tả

ARIA Implementation Examples for Solid-UI

Date: January 15, 2026
Related: Accessibility Checklist, Theme System Analysis


Overview

This document provides complete ARIA implementation examples for the Solid-UI theme system and common components. Copy and adapt these patterns for accessibility-compliant widgets.


1. Theme Switcher Component

Basic Select Implementation
/**
 * Creates an accessible theme switcher select element
 * @param {Document} dom - DOM context
 * @param {Object} options - Configuration options
 * @returns {HTMLElement} Theme switcher widget
 */
export function createThemeSwitcher(dom, options = {}) {
  const container = dom.createElement('div')
  container.className = 'theme-switcher-container'
  
  // Label
  const label = dom.createElement('label')
  label.htmlFor = 'solid-ui-theme-select'
  label.textContent = options.label || 'Color Theme'
  
  // Select element with ARIA
  const select = dom.createElement('select')
  select.id = 'solid-ui-theme-select'
  select.className = 'theme-switcher'
  select.setAttribute('aria-label', 'Choose application color theme')
  select.setAttribute('aria-describedby', 'theme-help')
  
  // Add themes as options
  Object.entries(themeLoader.themes).forEach(([name, path]) => {
    const option = dom.createElement('option')
    option.value = name
    option.textContent = name.charAt(0).toUpperCase() + name.slice(1)
    if (name === themeLoader.currentTheme) {
      option.selected = true
    }
    select.appendChild(option)
  })
  
  // Help text (visible)
  const helpText = dom.createElement('span')
  helpText.id = 'theme-help'
  helpText.className = 'help-text'
  helpText.textContent = 'Changes the color scheme of the application'
  
  // Change handler with announcement
  select.addEventListener('change', async (e) => {
    const themeName = e.target.value
    const themeLabel = e.target.options[e.target.selectedIndex].textContent
    
    // Update theme
    await themeLoader.loadTheme(themeName)
    
    // Announce change to screen readers
    announceThemeChange(dom, themeLabel)
  })
  
  // Assemble
  container.appendChild(label)
  container.appendChild(select)
  container.appendChild(helpText)
  
  return container
}

/**
 * Announces theme changes to screen readers
 * @param {Document} dom - DOM context
 * @param {string} themeName - Name of the theme
 */
function announceThemeChange(dom, themeName) {
  // Create or reuse announcement region
  let announcement = dom.getElementById('theme-announcement')
  if (!announcement) {
    announcement = dom.createElement('div')
    announcement.id = 'theme-announcement'
    announcement.setAttribute('role', 'status')
    announcement.setAttribute('aria-live', 'polite')
    announcement.setAttribute('aria-atomic', 'true')
    announcement.className = 'sr-only'
    dom.body.appendChild(announcement)
  }
  
  // Update announcement
  announcement.textContent = `Theme changed to ${themeName}`
  
  // Clear after announcement (optional)
  setTimeout(() => {
    announcement.textContent = ''
  }, 1000)
}
Advanced Custom Dropdown Implementation
/**
 * Creates an accessible custom theme switcher with dropdown
 * Follows ARIA combobox pattern
 */
export function createCustomThemeSwitcher(dom, options = {}) {
  const container = dom.createElement('div')
  container.className = 'custom-theme-switcher'
  
  // Button trigger
  const button = dom.createElement('button')
  button.type = 'button'
  button.id = 'theme-button'
  button.className = 'theme-button'
  button.setAttribute('aria-haspopup', 'listbox')
  button.setAttribute('aria-expanded', 'false')
  button.setAttribute('aria-labelledby', 'theme-button-label')
  
  const buttonLabel = dom.createElement('span')
  buttonLabel.id = 'theme-button-label'
  buttonLabel.textContent = 'Choose Theme: '
  
  const currentTheme = dom.createElement('span')
  currentTheme.textContent = themeLoader.currentTheme
  currentTheme.setAttribute('aria-live', 'polite')
  
  button.appendChild(buttonLabel)
  button.appendChild(currentTheme)
  
  // Listbox for options
  const listbox = dom.createElement('ul')
  listbox.id = 'theme-listbox'
  listbox.className = 'theme-listbox'
  listbox.setAttribute('role', 'listbox')
  listbox.setAttribute('aria-labelledby', 'theme-button-label')
  listbox.style.display = 'none'
  
  let selectedIndex = 0
  const themes = Object.entries(themeLoader.themes)
  
  themes.forEach(([name, path], index) => {
    const option = dom.createElement('li')
    option.setAttribute('role', 'option')
    option.id = `theme-option-${name}`
    option.textContent = name.charAt(0).toUpperCase() + name.slice(1)
    option.dataset.value = name
    
    if (name === themeLoader.currentTheme) {
      option.setAttribute('aria-selected', 'true')
      option.className = 'theme-option selected'
      selectedIndex = index
    } else {
      option.setAttribute('aria-selected', 'false')
      option.className = 'theme-option'
    }
    
    // Click handler
    option.addEventListener('click', () => {
      selectTheme(name, option)
    })
    
    listbox.appendChild(option)
  })
  
  // Toggle dropdown
  function toggleDropdown(show) {
    const isExpanded = show !== undefined ? show : button.getAttribute('aria-expanded') === 'false'
    button.setAttribute('aria-expanded', String(isExpanded))
    listbox.style.display = isExpanded ? 'block' : 'none'
    
    if (isExpanded) {
      // Focus first or selected option
      const selectedOption = listbox.querySelector('[aria-selected="true"]')
      if (selectedOption) {
        selectedOption.focus()
      }
    }
  }
  
  // Select theme
  function selectTheme(themeName, option) {
    // Update UI
    listbox.querySelectorAll('[role="option"]').forEach(opt => {
      opt.setAttribute('aria-selected', 'false')
      opt.classList.remove('selected')
    })
    option.setAttribute('aria-selected', 'true')
    option.classList.add('selected')
    
    currentTheme.textContent = option.textContent
    
    // Load theme
    themeLoader.loadTheme(themeName)
    
    // Close dropdown
    toggleDropdown(false)
    button.focus()
    
    // Announce
    announceThemeChange(dom, option.textContent)
  }
  
  // Keyboard navigation
  listbox.addEventListener('keydown', (e) => {
    const options = Array.from(listbox.querySelectorAll('[role="option"]'))
    const currentIndex = options.findIndex(opt => opt === dom.activeElement)
    
    switch (e.key) {
      case 'ArrowDown':
        e.preventDefault()
        if (currentIndex < options.length - 1) {
          options[currentIndex + 1].focus()
        }
        break
        
      case 'ArrowUp':
        e.preventDefault()
        if (currentIndex > 0) {
          options[currentIndex - 1].focus()
        }
        break
        
      case 'Home':
        e.preventDefault()
        options[0].focus()
        break
        
      case 'End':
        e.preventDefault()
        options[options.length - 1].focus()
        break
        
      case 'Enter':
      case ' ':
        e.preventDefault()
        if (dom.activeElement.hasAttribute('data-value')) {
          selectTheme(dom.activeElement.dataset.value, dom.activeElement)
        }
        break
        
      case 'Escape':
        e.preventDefault()
        toggleDropdown(false)
        button.focus()
        break
    }
  })
  
  // Button click
  button.addEventListener('click', () => {
    toggleDropdown()
  })
  
  // Click outside to close
  dom.addEventListener('click', (e) => {
    if (!container.contains(e.target)) {
      toggleDropdown(false)
    }
  })
  
  container.appendChild(button)
  container.appendChild(listbox)
  
  return container
}

2. Button Components

Standard Button with Icon
/**
 * Creates an accessible button with icon
 */
export function createIconButton(dom, options) {
  const button = dom.createElement('button')
  button.type = options.type || 'button'
  button.className = options.className || 'icon-button'
  
  // Accessible label (required for icon-only buttons)
  button.setAttribute('aria-label', options.label)
  
  // Optional tooltip
  if (options.tooltip) {
    button.setAttribute('title', options.tooltip)
  }
  
  // Icon (decorative, hidden from screen readers)
  const icon = dom.createElement('span')
  icon.className = 'icon'
  icon.setAttribute('aria-hidden', 'true')
  icon.textContent = options.icon || '⚙️'
  
  button.appendChild(icon)
  
  // Click handler
  if (options.onClick) {
    button.addEventListener('click', options.onClick)
  }
  
  return button
}

// Usage
const settingsButton = createIconButton(dom, {
  label: 'Open settings',
  tooltip: 'Settings',
  icon: '⚙️',
  onClick: () => openSettings()
})
Toggle Button (Switch)
/**
 * Creates an accessible toggle/switch button
 */
export function createToggleButton(dom, options) {
  const button = dom.createElement('button')
  button.type = 'button'
  button.className = 'toggle-button'
  button.setAttribute('role', 'switch')
  button.setAttribute('aria-checked', String(options.checked || false))
  button.setAttribute('aria-label', options.label)
  
  // Visual indicator
  const indicator = dom.createElement('span')
  indicator.className = 'toggle-indicator'
  indicator.setAttribute('aria-hidden', 'true')
  
  const labelSpan = dom.createElement('span')
  labelSpan.textContent = options.label
  
  button.appendChild(indicator)
  button.appendChild(labelSpan)
  
  // Toggle handler
  button.addEventListener('click', () => {
    const isChecked = button.getAttribute('aria-checked') === 'true'
    button.setAttribute('aria-checked', String(!isChecked))
    
    if (options.onChange) {
      options.onChange(!isChecked)
    }
  })
  
  return button
}

// Usage
const darkModeToggle = createToggleButton(dom, {
  label: 'Dark mode',
  checked: false,
  onChange: (enabled) => {
    if (enabled) {
      themeLoader.loadTheme('dark')
    } else {
      themeLoader.loadTheme('light')
    }
  }
})
Button with Loading State
/**
 * Creates a button that can show loading state
 */
export function createLoadingButton(dom, options) {
  const button = dom.createElement('button')
  button.type = options.type || 'button'
  button.className = 'loading-button'
  
  const text = dom.createElement('span')
  text.className = 'button-text'
  text.textContent = options.text
  
  const spinner = dom.createElement('span')
  spinner.className = 'button-spinner'
  spinner.setAttribute('aria-hidden', 'true')
  spinner.textContent = '⏳'
  spinner.style.display = 'none'
  
  button.appendChild(text)
  button.appendChild(spinner)
  
  // Set loading state
  button.setLoading = (isLoading) => {
    button.disabled = isLoading
    button.setAttribute('aria-busy', String(isLoading))
    
    if (isLoading) {
      text.textContent = options.loadingText || 'Loading...'
      spinner.style.display = 'inline-block'
    } else {
      text.textContent = options.text
      spinner.style.display = 'none'
    }
  }
  
  // Click handler
  if (options.onClick) {
    button.addEventListener('click', async () => {
      button.setLoading(true)
      try {
        await options.onClick()
      } finally {
        button.setLoading(false)
      }
    })
  }
  
  return button
}

// Usage
const saveButton = createLoadingButton(dom, {
  text: 'Save Changes',
  loadingText: 'Saving...',
  onClick: async () => {
    await saveData()
  }
})

3. Form Components

Accessible Text Input
/**
 * Creates an accessible text input field
 */
export function createTextInput(dom, options) {
  const container = dom.createElement('div')
  container.className = 'form-field'
  
  // Label (required)
  const label = dom.createElement('label')
  label.htmlFor = options.id
  label.textContent = options.label
  if (options.required) {
    label.innerHTML += ' <span aria-label="required">*</span>'
  }
  
  // Input
  const input = dom.createElement('input')
  input.type = options.type || 'text'
  input.id = options.id
  input.name = options.name || options.id
  input.className = 'form-input'
  
  if (options.required) {
    input.setAttribute('aria-required', 'true')
    input.required = true
  }
  
  if (options.placeholder) {
    input.placeholder = options.placeholder
  }
  
  // Help text
  const helpId = `${options.id}-help`
  const helpText = dom.createElement('span')
  helpText.id = helpId
  helpText.className = 'help-text'
  helpText.textContent = options.helpText || ''
  
  // Error text
  const errorId = `${options.id}-error`
  const errorText = dom.createElement('span')
  errorText.id = errorId
  errorText.className = 'error-text'
  errorText.setAttribute('role', 'alert')
  errorText.style.display = 'none'
  
  // Link descriptions
  const describedBy = [helpId]
  if (options.showError) {
    describedBy.push(errorId)
  }
  input.setAttribute('aria-describedby', describedBy.join(' '))
  
  // Validation
  input.setError = (errorMessage) => {
    if (errorMessage) {
      input.setAttribute('aria-invalid', 'true')
      input.classList.add('error')
      errorText.textContent = errorMessage
      errorText.style.display = 'block'
    } else {
      input.setAttribute('aria-invalid', 'false')
      input.classList.remove('error')
      errorText.textContent = ''
      errorText.style.display = 'none'
    }
  }
  
  // Assemble
  container.appendChild(label)
  container.appendChild(input)
  if (options.helpText) {
    container.appendChild(helpText)
  }
  container.appendChild(errorText)
  
  return { container, input }
}

// Usage
const { container, input } = createTextInput(dom, {
  id: 'username',
  label: 'Username',
  required: true,
  helpText: 'Must be 3-20 characters',
  placeholder: 'Enter username'
})

// Validation example
input.addEventListener('blur', () => {
  if (input.value.length < 3) {
    input.setError('Username must be at least 3 characters')
  } else {
    input.setError(null)
  }
})

4. Chat Components

Chat Message with ARIA
/**
 * Creates an accessible chat message
 */
export function createChatMessage(dom, options) {
  const message = dom.createElement('li')
  message.className = 'chat-message'
  message.setAttribute('role', 'article')
  
  // Author
  const author = dom.createElement('span')
  author.className = 'message-author'
  author.textContent = options.author
  
  // Timestamp
  const timestamp = dom.createElement('time')
  timestamp.className = 'message-time'
  timestamp.setAttribute('datetime', options.timestamp)
  timestamp.textContent = formatTime(options.timestamp)
  
  // Content
  const content = dom.createElement('p')
  content.className = 'message-content'
  content.textContent = options.content
  
  message.appendChild(author)
  message.appendChild(timestamp)
  message.appendChild(content)
  
  return message
}

### Chat Container with Live Region

```javascript
/**
 * Creates an accessible chat container
 */
export function createChatContainer(dom, options) {
  const container = dom.createElement('div')
  container.className = 'chat-container'
  container.setAttribute('role', 'log')
  container.setAttribute('aria-label', options.label || 'Chat messages')
  
  // Message list
  const messageList = dom.createElement('ul')
  messageList.className = 'message-list'
  messageList.setAttribute('aria-live', 'polite')
  messageList.setAttribute('aria-atomic', 'false')
  messageList.setAttribute('aria-relevant', 'additions')
  
  // Screen reader announcement region (separate from visual)
  const srAnnouncement = dom.createElement('div')
  srAnnouncement.className = 'sr-only'
  srAnnouncement.setAttribute('role', 'status')
  srAnnouncement.setAttribute('aria-live', 'polite')
  srAnnouncement.setAttribute('aria-atomic', 'true')
  
  // Add message function
  container.addMessage = (messageData) => {
    // Add to visual list
    const message = createChatMessage(dom, messageData)
    messageList.appendChild(message)
    
    // Announce to screen readers
    srAnnouncement.textContent = `New message from ${messageData.author}: ${messageData.content}`
    
    // Clear announcement after it's been read
    setTimeout(() => {
      srAnnouncement.textContent = ''
    }, 1000)
    
    // Scroll to bottom
    messageList.scrollTop = messageList.scrollHeight
  }
  
  container.appendChild(messageList)
  container.appendChild(srAnnouncement)
  
  return container
}

5. Dialog/Modal Components

/**
 * Creates an accessible modal dialog
 */
export function createDialog(dom, options) {
  // Backdrop
  const backdrop = dom.createElement('div')
  backdrop.className = 'dialog-backdrop'
  backdrop.setAttribute('aria-hidden', 'true')
  
  // Dialog container
  const dialog = dom.createElement('div')
  dialog.className = 'dialog'
  dialog.setAttribute('role', 'dialog')
  dialog.setAttribute('aria-modal', 'true')
  dialog.setAttribute('aria-labelledby', 'dialog-title')
  dialog.setAttribute('aria-describedby', 'dialog-description')
  
  // Title
  const title = dom.createElement('h2')
  title.id = 'dialog-title'
  title.textContent = options.title
  
  // Description
  const description = dom.createElement('p')
  description.id = 'dialog-description'
  description.textContent = options.description
  
  // Content
  const content = dom.createElement('div')
  content.className = 'dialog-content'
  if (options.content) {
    content.appendChild(options.content)
  }
  
  // Actions
  const actions = dom.createElement('div')
  actions.className = 'dialog-actions'
  
  const cancelButton = dom.createElement('button')
  cancelButton.type = 'button'
  cancelButton.textContent = options.cancelText || 'Cancel'
  cancelButton.addEventListener('click', () => dialog.close())
  
  const confirmButton = dom.createElement('button')
  confirmButton.type = 'button'
  confirmButton.textContent = options.confirmText || 'Confirm'
  confirmButton.addEventListener('click', () => {
    if (options.onConfirm) {
      options.onConfirm()
    }
    dialog.close()
  })
  
  actions.appendChild(cancelButton)
  actions.appendChild(confirmButton)
  
  // Assemble
  dialog.appendChild(title)
  dialog.appendChild(description)
  dialog.appendChild(content)
  dialog.appendChild(actions)
  
  backdrop.appendChild(dialog)
  
  // Focus trap
  const focusableElements = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
  let firstFocusable, lastFocusable
  
  function updateFocusableElements() {
    const focusables = dialog.querySelectorAll(focusableElements)
    firstFocusable = focusables[0]
    lastFocusable = focusables[focusables.length - 1]
  }
  
  dialog.addEventListener('keydown', (e) => {
    if (e.key === 'Escape') {
      dialog.close()
      return
    }
    
    if (e.key === 'Tab') {
      if (e.shiftKey) {
        if (dom.activeElement === firstFocusable) {
          e.preventDefault()
          lastFocusable.focus()
        }
      } else {
        if (dom.activeElement === lastFocusable) {
          e.preventDefault()
          firstFocusable.focus()
        }
      }
    }
  })
  
  // Open/close methods
  let previousFocus
  
  dialog.open = () => {
    previousFocus = dom.activeElement
    dom.body.appendChild(backdrop)
    updateFocusableElements()
    firstFocusable.focus()
    dom.body.style.overflow = 'hidden'
  }
  
  dialog.close = () => {
    backdrop.remove()
    dom.body.style.overflow = ''
    if (previousFocus) {
      previousFocus.focus()
    }
  }
  
  // Close on backdrop click
  backdrop.addEventListener('click', (e) => {
    if (e.target === backdrop) {
      dialog.close()
    }
  })
  
  return dialog
}

// Usage
const confirmDialog = createDialog(dom, {
  title: 'Confirm Action',
  description: 'Are you sure you want to delete this item?',
  confirmText: 'Delete',
  cancelText: 'Cancel',
  onConfirm: () => {
    deleteItem()
  }
})

confirmDialog.open()

6. Utility Functions

Focus Management
/**
 * Manages focus for dynamic content
 */
export const focusManager = {
  /**
   * Stores current focus element
   */
  storeFocus() {
    this._previousFocus = document.activeElement
  },
  
  /**
   * Restores previously focused element
   */
  restoreFocus() {
    if (this._previousFocus && this._previousFocus.focus) {
      this._previousFocus.focus()
    }
  },
  
  /**
   * Moves focus to element
   */
  moveFocusTo(element) {
    if (element && element.focus) {
      element.focus()
    }
  },
  
  /**
   * Gets all focusable elements in container
   */
  getFocusableElements(container) {
    const selector = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
    return Array.from(container.querySelectorAll(selector))
      .filter(el => !el.disabled && el.offsetParent !== null)
  }
}
Announcement Helpers
/**
 * Announces message to screen readers
 */
export function announce(dom, message, priority = 'polite') {
  const announcement = dom.createElement('div')
  announcement.setAttribute('role', priority === 'assertive' ? 'alert' : 'status')
  announcement.setAttribute('aria-live', priority)
  announcement.setAttribute('aria-atomic', 'true')
  announcement.className = 'sr-only'
  announcement.textContent = message
  
  dom.body.appendChild(announcement)
  
  setTimeout(() => {
    announcement.remove()
  }, 1000)
}

// Usage
announce(dom, 'Theme changed successfully')
announce(dom, 'Error: Could not save changes', 'assertive')

Complete CSS for Accessibility

/* accessibility.css - Core accessibility styles */

/* Screen reader only content */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border-width: 0;
}

.sr-only-focusable:focus {
  position: static;
  width: auto;
  height: auto;
  overflow: visible;
  clip: auto;
  white-space: normal;
}

/* Focus indicators */
:focus-visible {
  outline: 2px solid var(--sui-focus-color, #667eea);
  outline-offset: 2px;
}

/* Skip links */
.skip-link {
  position: absolute;
  top: -40px;
  left: 0;
  background: var(--sui-primary);
  color: white;
  padding: 0.5em 1em;
  text-decoration: none;
  z-index: 100;
}

.skip-link:focus {
  top: 0;
}

/* High contrast mode */
@media (prefers-contrast: high) {
  :root {
    --sui-border: #000;
    --sui-text: #000;
    --sui-bg: #fff;
  }
  
  button,
  input,
  select {
    border: 2px solid currentColor;
  }
}

/* Reduced motion */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

/* Touch target size */
button,
a,
input,
select,
textarea {
  min-height: 44px;
  min-width: 44px;
}

/* Visible focus for keyboard users only */
:focus:not(:focus-visible) {
  outline: none;
}

Status: Reference Implementation
Last Updated: January 15, 2026
Maintainer: Solid-UI Team

Ngôn ngữ chính
TypeScript
Star
153
Fork
46
Merge trung bình
1 ngày 8 giờ
Pull request đã merge (30 ngày)
7

Hướng dẫn đóng góp

Chưa lập chỉ mục được hướng dẫn đóng góp cho kho mã nguồn này

Bắt đầu từ đâu

  1. Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
  2. Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
  3. Fork repository và làm thay đổi trên một nhánh.
  4. Mở pull request có tham chiếu số hiệu của issue.

Issue khác của SolidOS/solid-ui

Tất cả issue của SolidOS/solid-ui

Issue tương tự

Thêm issue về TypeScript

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.