Skip to content

Enhance accessibility features across the application#27

Open
fk0u wants to merge 1 commit intoOurCreativity:mainfrom
fk0u:fix/accessibility-improvements
Open

Enhance accessibility features across the application#27
fk0u wants to merge 1 commit intoOurCreativity:mainfrom
fk0u:fix/accessibility-improvements

Conversation

@fk0u
Copy link
Copy Markdown

@fk0u fk0u commented Dec 24, 2025

📋 Deskripsi

PR ini mengimplementasikan perbaikan aksesibilitas (Accessibility/a11y) untuk memenuhi standar WCAG 2.1 Level AA, serta meningkatkan pengalaman pengguna screen reader dan keyboard.

Perubahan mencakup:

  • Navigasi: Menambahkan Skip Link untuk melewati navigasi utama.
  • Modal: Mengimplementasikan Focus Trap agar navigasi keyboard tidak keluar dari modal yang aktif.
  • Screen Reader: Menambahkan aria-label, aria-expanded, dan atribut aksesibilitas lainnya pada elemen interaktif (tombol icon, accordion).
  • Linting: Menambahkan konfigurasi .hintrc (jika ada) untuk menjaga standar aksesibilitas ke depannya.

🔗 Issue Terkait

Fixes #6 (Accessibility Improvements)

🔄 Tipe Perubahan

  • 🐛 Bug fix (perbaikan isu aksesibilitas yang hilang)
  • ✨ Fitur baru (Skip Link, Focus Trap)
  • 📝 Dokumentasi (perubahan pada dokumentasi)
  • 🎨 Style (formatting, tidak ada perubahan kode)
  • ♻️ Refactor (perubahan kode tanpa menambah fitur/fix)
  • 🔧 Chore (maintenance, dependencies)

📸 Screenshot

Fitur Deskripsi
Skip Link Muncul saat menekan Tab pertama kali di halaman.
Focus Trap Fokus keyboard terkunci di dalam modal saat terbuka.

✅ Checklist

  • Kode saya mengikuti standar proyek
  • Saya sudah melakukan self-review
  • Saya sudah menambahkan komentar di bagian yang kompleks (logic Focus Trap)
  • Saya sudah update dokumentasi yang terkait
  • Perubahan saya tidak menghasilkan warning baru (Build sukses)
  • Saya sudah test perubahan saya secara lokal (Keyboard navigation & Build check)

📝 Catatan untuk Reviewer

Mohon perhatikan implementasi useEffect pada ContributorModal dan CreationStudio untuk logic focus trap. Pastikan tidak ada memory leak pada event listener keydown.


Open with Devin

- Added skip link to main content in App component for better navigation
- Implemented focus trap in ContributorModal and CreationStudio components to manage keyboard navigation
- Added aria-labels and titles to various elements for improved screen reader support
- Updated Footer and Navbar components with appropriate aria-labels for social media links and menu buttons
- Improved Info page with aria-expanded attribute for FAQ buttons
- Introduced .hintrc file for accessibility linting configuration
@fk0u fk0u requested a review from Ardelyo as a code owner December 24, 2025 07:12
Copilot AI review requested due to automatic review settings December 24, 2025 07:12
@vercel
Copy link
Copy Markdown

vercel bot commented Dec 24, 2025

@fk0u is attempting to deploy a commit to the drthought's projects Team on Vercel.

A member of the Team first needs to authorize it.

@gemini-code-assist
Copy link
Copy Markdown

Summary of Changes

Hello @fk0u, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

Pull request ini bertujuan untuk secara signifikan meningkatkan aksesibilitas aplikasi, menjadikannya lebih mudah digunakan bagi individu dengan disabilitas, terutama mereka yang mengandalkan pembaca layar dan navigasi keyboard. Perubahan ini memastikan kepatuhan terhadap standar WCAG 2.1 Level AA, yang mengarah pada pengalaman pengguna yang lebih inklusif dan lancar di seluruh platform.

Highlights

  • Peningkatan Aksesibilitas WCAG 2.1 AA: Memastikan aplikasi memenuhi standar aksesibilitas WCAG 2.1 Level AA.
  • Navigasi Skip Link: Menambahkan tautan 'Lewati ke konten utama' yang tersembunyi secara visual tetapi dapat difokuskan, memungkinkan pengguna keyboard untuk melewati navigasi utama.
  • Focus Trap pada Modal: Mengimplementasikan perangkap fokus untuk modal ContributorModal dan CreationStudio agar navigasi keyboard tetap di dalam modal, meningkatkan pengalaman pengguna keyboard.
  • Atribut ARIA yang Ditingkatkan: Menambahkan aria-label, aria-expanded, dan atribut relevan lainnya pada elemen interaktif untuk pengalaman pembaca layar yang lebih baik.
  • Konfigurasi Linting Aksesibilitas: Menambahkan file .hintrc untuk menegakkan standar aksesibilitas di masa mendatang.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

Secara keseluruhan, PR ini melakukan pekerjaan yang sangat baik dalam meningkatkan aksesibilitas aplikasi, sejalan dengan deskripsi. Penambahan skip link, focus trap, dan atribut ARIA yang relevan adalah langkah-langkah penting.

Saya menemukan beberapa area yang memerlukan perbaikan:

  • Terdapat bug kritis pada implementasi focus trap di ContributorModal yang menyebabkannya tidak berfungsi.
  • Logika focus trap diduplikasi di dua komponen, yang dapat diekstraksi menjadi custom hook untuk meningkatkan maintainability.
  • Ada penggunaan atribut title yang tidak efektif pada input file yang tersembunyi.

Setelah perbaikan ini, fitur aksesibilitas akan jauh lebih kuat dan andal. Terima kasih atas kontribusinya!

Comment on lines +52 to +54
const focusableElements = modalRef.current?.parentElement?.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

critical

Implementasi focus trap saat ini memiliki bug kritis. Query modalRef.current?.parentElement?.querySelectorAll(...) menargetkan elemen yang salah dan tidak akan menemukan elemen yang dapat difokuskan seperti tombol tutup atau tautan eksternal, karena elemen-elemen tersebut tidak berada di dalam parentElement dari modalRef.current. Akibatnya, focus trap tidak berfungsi seperti yang diharapkan.

Untuk memperbaikinya, Anda perlu menargetkan container modal yang benar, yaitu dua level di atas modalRef.current (parentElement.parentElement).

Suggested change
const focusableElements = modalRef.current?.parentElement?.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const focusableElements = modalRef.current?.parentElement?.parentElement?.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);

Comment on lines +61 to +101
React.useEffect(() => {
if (!isOpen) return;

const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
onClose();
return;
}

if (e.key === 'Tab') {
const focusableElements = modalRef.current?.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);

if (!focusableElements || focusableElements.length === 0) return;

const firstElement = focusableElements[0] as HTMLElement;
const lastElement = focusableElements[focusableElements.length - 1] as HTMLElement;

if (e.shiftKey) {
if (document.activeElement === firstElement) {
e.preventDefault();
lastElement.focus();
}
} else {
if (document.activeElement === lastElement) {
e.preventDefault();
firstElement.focus();
}
}
}
};

document.addEventListener('keydown', handleKeyDown);
// Focus the close button initially
setTimeout(() => closeButtonRef.current?.focus(), 100);

return () => {
document.removeEventListener('keydown', handleKeyDown);
};
}, [isOpen, onClose]);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Logika untuk focus trap di dalam useEffect ini diduplikasi di dua komponen: CreationStudio.tsx dan ContributorModal.tsx. Ini membuat kode lebih sulit untuk dipelihara.

Saya sarankan untuk mengekstrak logika ini ke dalam custom hook yang dapat digunakan kembali, misalnya useFocusTrap. Ini akan membuat kode lebih bersih, mengurangi duplikasi, dan mempermudah pemeliharaan di masa mendatang.

Contoh struktur hook:

function useFocusTrap(containerRef, initialFocusRef, isOpen, onClose) {
  useEffect(() => {
    if (!isOpen) return;

    // ... all the logic from handleKeyDown ...

    document.addEventListener('keydown', handleKeyDown);
    setTimeout(() => initialFocusRef.current?.focus(), 100);

    return () => {
      document.removeEventListener('keydown', handleKeyDown);
    };
  }, [isOpen, onClose, containerRef, initialFocusRef]);
}

onChange={handleImageUpload}
accept="image/*"
className="hidden"
title="Upload Image"
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Atribut title pada elemen <input> yang tersembunyi (className="hidden") tidak memberikan manfaat aksesibilitas karena elemen ini tidak dapat difokuskan atau diakses oleh screen reader.

Untuk meningkatkan aksesibilitas, area yang dapat diklik (div yang membungkus input ini) harus dibuat dapat diakses. Anda dapat melakukannya dengan menambahkan atribut berikut ke div tersebut:

  • role="button"
  • tabIndex="0"
  • aria-label="Upload Image"
  • Handler onKeyDown untuk memicu klik saat tombol Enter atau Spasi ditekan.

Karena title ini tidak efektif, saya sarankan untuk menghapusnya. Hal yang sama berlaku untuk input unggah video di baris 352.

@vercel
Copy link
Copy Markdown

vercel bot commented Dec 24, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
ourcreativity Ready Ready Preview, Comment Dec 24, 2025 7:16am

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR implements accessibility improvements to meet WCAG 2.1 Level AA standards, including skip links, focus trap modals, and ARIA attributes for better screen reader and keyboard navigation support.

Key Changes:

  • Added skip link functionality to bypass navigation and jump directly to main content
  • Implemented focus trap logic in modals (CreationStudio and ContributorModal) to contain keyboard navigation
  • Enhanced ARIA attributes across interactive elements (buttons, accordions, links) for screen reader accessibility

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
App.tsx Added skip link with sr-only utility and assigned id="main-content" to main element
components/Navbar.tsx Added aria-label to logo link and mobile menu toggle button
components/Footer.tsx Added aria-label attributes to social media icon links
pages/Info.tsx Added aria-expanded to FAQ accordion buttons; refactored inline SVG style to Tailwind class
components/CreationStudio/index.tsx Implemented focus trap with refs, added aria-labels to icon buttons, associated form labels with inputs via htmlFor/id
components/ContributorModal.tsx Implemented focus trap, added aria-label to close button and GitHub link, refactored inline style to Tailwind class
.hintrc Added webhint configuration with aria-valid-attr-value check disabled

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread components/Footer.tsx
<Instagram size={20} />
</a>
<a href="https://www.tiktok.com/@ourcreativity.ofc" target="_blank" rel="noopener noreferrer" className="text-gray-500 hover:text-white transition-colors" title="TikTok">
<a href="https://www.tiktok.com/@ourcreativity.ofc" target="_blank" rel="noopener noreferrer" className="text-gray-500 hover:text-white transition-colors" title="TikTok" aria-label="TikTok">
Copy link

Copilot AI Dec 24, 2025

Choose a reason for hiding this comment

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

Adding both title and aria-label with identical values is redundant. Screen readers will announce the aria-label, and sighted users will see the title as a tooltip. For icon-only links, aria-label alone is sufficient for screen reader users. The title attribute can be removed to reduce duplication, or kept if you specifically want a visual tooltip for sighted users (though the redundancy is acceptable in this case).

Copilot uses AI. Check for mistakes.
Comment on lines +77 to +80
setTimeout(() => closeButtonRef.current?.focus(), 100);

return () => {
document.removeEventListener('keydown', handleKeyDown);
Copy link

Copilot AI Dec 24, 2025

Choose a reason for hiding this comment

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

The setTimeout call is not cleaned up if the component unmounts or isOpen changes before the timeout executes. This could cause a focus attempt on an unmounted component, resulting in errors or memory leaks. Store the timeout ID and clear it in the cleanup function.

Suggested change
setTimeout(() => closeButtonRef.current?.focus(), 100);
return () => {
document.removeEventListener('keydown', handleKeyDown);
const timeoutId = window.setTimeout(() => closeButtonRef.current?.focus(), 100);
return () => {
document.removeEventListener('keydown', handleKeyDown);
clearTimeout(timeoutId);

Copilot uses AI. Check for mistakes.
Comment on lines +96 to +99
setTimeout(() => closeButtonRef.current?.focus(), 100);

return () => {
document.removeEventListener('keydown', handleKeyDown);
Copy link

Copilot AI Dec 24, 2025

Choose a reason for hiding this comment

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

The setTimeout call is not cleaned up if the component unmounts or isOpen changes before the timeout executes. This could cause a focus attempt on an unmounted component, resulting in errors or memory leaks. Store the timeout ID and clear it in the cleanup function.

Suggested change
setTimeout(() => closeButtonRef.current?.focus(), 100);
return () => {
document.removeEventListener('keydown', handleKeyDown);
const timeoutId = window.setTimeout(() => closeButtonRef.current?.focus(), 100);
return () => {
document.removeEventListener('keydown', handleKeyDown);
clearTimeout(timeoutId);

Copilot uses AI. Check for mistakes.
target="_blank"
rel="noopener noreferrer"
className="p-3 rounded-full bg-white/5 text-white hover:bg-white hover:text-black transition-all duration-300"
title="Lihat profil GitHub"
Copy link

Copilot AI Dec 24, 2025

Choose a reason for hiding this comment

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

Adding both title and aria-label with identical values is redundant. Screen readers will announce the aria-label, and sighted users will see the title as a tooltip. For icon-only links, aria-label alone is sufficient for screen reader users. The title attribute can be removed to reduce duplication, or kept if you specifically want a visual tooltip for sighted users (though the redundancy is acceptable in this case).

Suggested change
title="Lihat profil GitHub"

Copilot uses AI. Check for mistakes.
}

if (e.key === 'Tab') {
const focusableElements = modalRef.current?.parentElement?.querySelectorAll(
Copy link

Copilot AI Dec 24, 2025

Choose a reason for hiding this comment

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

The focus trap query selector is using modalRef.current?.parentElement?.querySelectorAll() which searches beyond the modal content. Looking at the DOM structure, modalRef is assigned to the inner scrollable content div, and parentElement would be the motion.div containing the entire modal window. However, this inconsistency with CreationStudio (which uses modalRef.current directly) could lead to including elements outside the intended modal scope. For proper focus containment, the query should be scoped to the actual modal container, not its parent element.

Suggested change
const focusableElements = modalRef.current?.parentElement?.querySelectorAll(
const focusableElements = modalRef.current?.querySelectorAll(

Copilot uses AI. Check for mistakes.
onChange={handleImageUpload}
accept="image/*"
className="hidden"
title="Upload Image"
Copy link

Copilot AI Dec 24, 2025

Choose a reason for hiding this comment

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

The title attribute on hidden file inputs is not meaningful for accessibility. Screen readers won't announce these hidden inputs, and sighted users won't see them. The title is typically used for tooltips on visible, interactive elements. Consider removing these title attributes or ensuring they serve a clear purpose in your accessibility strategy.

Suggested change
title="Upload Image"

Copilot uses AI. Check for mistakes.
onChange={handleImageUpload}
accept="video/*"
className="hidden"
title="Upload Video"
Copy link

Copilot AI Dec 24, 2025

Choose a reason for hiding this comment

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

The title attribute on hidden file inputs is not meaningful for accessibility. Screen readers won't announce these hidden inputs, and sighted users won't see them. The title is typically used for tooltips on visible, interactive elements. Consider removing these title attributes or ensuring they serve a clear purpose in your accessibility strategy.

Suggested change
title="Upload Video"

Copilot uses AI. Check for mistakes.
value={formData.division}
onChange={(e) => setFormData(prev => ({ ...prev, division: e.target.value as any }))}
className="w-full bg-[#111] border border-white/10 rounded-xl px-4 py-3 text-white focus:outline-none"
title="Select Division"
Copy link

Copilot AI Dec 24, 2025

Choose a reason for hiding this comment

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

Adding a title attribute to a select element that already has an associated label (via htmlFor/id) is redundant. The label provides the accessible name, and the title would only create a tooltip on hover. For accessibility, the label association is sufficient and preferred. Consider removing the title attribute to avoid duplication.

Suggested change
title="Select Division"

Copilot uses AI. Check for mistakes.
Comment thread .hintrc
Comment on lines +7 to +10
"default",
{
"aria-valid-attr-value": "off"
}
Copy link

Copilot AI Dec 24, 2025

Choose a reason for hiding this comment

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

Disabling the 'aria-valid-attr-value' rule in the hint configuration removes an important accessibility check that validates ARIA attribute values. This rule helps catch invalid ARIA attribute values that could break screen reader functionality. Unless there's a specific false positive you're addressing, consider removing this override to maintain accessibility validation. If there is a specific issue, document it and use more targeted exclusions.

Suggested change
"default",
{
"aria-valid-attr-value": "off"
}
"default"

Copilot uses AI. Check for mistakes.
Comment thread components/Footer.tsx

<div className="flex items-center gap-8">
<a href="https://www.instagram.com/ourcreativity.ofc/" target="_blank" rel="noopener noreferrer" className="text-gray-500 hover:text-white transition-colors" title="Instagram">
<a href="https://www.instagram.com/ourcreativity.ofc/" target="_blank" rel="noopener noreferrer" className="text-gray-500 hover:text-white transition-colors" title="Instagram" aria-label="Instagram">
Copy link

Copilot AI Dec 24, 2025

Choose a reason for hiding this comment

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

Adding both title and aria-label with identical values is redundant. Screen readers will announce the aria-label, and sighted users will see the title as a tooltip. For icon-only links, aria-label alone is sufficient for screen reader users. The title attribute can be removed to reduce duplication, or kept if you specifically want a visual tooltip for sighted users (though the redundancy is acceptable in this case).

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 1 potential issue.

View issue and 5 additional flags in Devin Review.

Open in Devin Review

Comment on lines +52 to +54
const focusableElements = modalRef.current?.parentElement?.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Focus trap in ContributorModal excludes close button due to incorrect DOM traversal

The focus trap implementation in ContributorModal queries focusable elements using modalRef.current?.parentElement?.querySelectorAll(...) (line 52). However, modalRef is attached to an inner content div (line 145), making parentElement the scrollable container div (line 144). The close button (lines 134-141) is a sibling of this scrollable container, not a child of it.

DOM structure:

<motion.div> (modal window - line 129)
  <button ref={closeButtonRef}> (close button - line 134) ← NOT included in query
  <div> (scrollable container - line 144) ← This is modalRef.current.parentElement
    <div ref={modalRef}> (content - line 145)

When Tab is pressed, the focus trap calculates firstElement and lastElement from elements inside the scrollable container only. The close button is excluded from this list. This means:

  1. If the user tabs to the close button and then tabs again, the focus trap logic won't work correctly because document.activeElement (the close button) won't match lastElement (which is inside the scrollable container)
  2. The focus trap doesn't properly contain focus within all interactive elements of the modal

This undermines the accessibility goal of the focus trap implementation.

Recommendation: Query from the modal window container instead. Either move modalRef to the modal window div (line 129's motion.div), or use a separate ref for the focus trap that encompasses both the close button and the scrollable content. For example: const focusableElements = modalRef.current?.parentElement?.parentElement?.querySelectorAll(...) to get the modal window, or better yet, add a new ref to the motion.div at line 129.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@@ -54,6 +54,51 @@ export const CreationStudio: React.FC<Props> = ({ isOpen, onClose, onPublish })

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

(Refers to lines 52-53)

🚩 consoleOutput state is accumulated but never displayed

The consoleOutput state variable at components/CreationStudio/index.tsx:52 is set via setConsoleOutput callbacks from PyodideSandbox (lines 368-369), but the accumulated output is never rendered or displayed anywhere in the component. This appears to be intentional infrastructure for future Python console output display, but currently the output is lost. The state keeps accumulating with prev + out which could lead to memory growth if many Python scripts are run in a session.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Refactor] Migrasi ke BrowserRouter untuk Routing yang Lebih Baik

2 participants