Repository: SU-SWS/ood-giving-campaign
Pull request #411: A11y | DS-1510 | Add role="button" to clickable video preview wrapper
Author: login: yvonnetangsu
Created: 2025-04-29T00:36:58Z | Merged: 2025-04-30T00:59:07Z

# READY FOR REVIEW

# Summary
- Add `role="button"` to the clickable div when EmbedMedia preview mode is on (I can't change the HTML element so this is a compromise to make it accessible)
- Better handling when space key is pressed while focused on the preview (so it doesn't scroll down the page - should behave like enter key)
- Update `getNumBloks`

# Review By (Date)
- Soon

# Criticality
- 5

# Review Tasks

## Setup tasks and/or behavior to test

1. Go to https://deploy-preview-411--giving-campaign.netlify.app/test/yvonne-test-video-preview-a11y
2. There are 3 embed media on the page. Only the first 2 are using the preview mode. The 2nd one has a custom preview image while the 1st one uses the default. Inspect the first two players using the preview mode. Check that the `div` has `role="button"`  as an attribute.
![Screenshot 2025-04-29 at 4 09 14 PM](https://github.com/user-attachments/assets/72a09511-1e0d-4b52-be4e-97759b5b4bd1)

3. Press the space bar while you're focused on the preview, check that it plays the video and doesn't scroll the page.
4. Inspect the last embed player on the page. It shouldn't have any role="button" class because it's not using the preview mode.
5. Look at code

# Associated Issues and/or People
- JIRA ticket(s) - DS-1510



Diff:
diff --git a/components/EmbedMedia/EmbedMedia.tsx b/components/EmbedMedia/EmbedMedia.tsx
index 44068fdb..359273d4 100644
--- a/components/EmbedMedia/EmbedMedia.tsx
+++ b/components/EmbedMedia/EmbedMedia.tsx
@@ -1,6 +1,7 @@
-import React, { useEffect, useState } from 'react';
+import React, { useEffect, useRef, useState } from 'react';
 import { cnb } from 'cnbuilder';
 import ReactPlayer from 'react-player/lazy';
+import { useEventListener } from 'usehooks-ts';
 import { Caption, type CaptionProps } from '@/components/Media/Caption';
 import { FlexBox } from '@/components/FlexBox';
 import { PreviewImage } from './PreviewImage';
@@ -49,13 +50,48 @@ export const EmbedMedia = ({
    * https://github.com/cookpete/react-player/issues/1428
    */
   const [hasWindow, setHasWindow] = useState(false);
+  const playerWrapperRef = useRef<HTMLDivElement>(null);
+  const PreviewImg = previewImageSrc ? <PreviewImage previewImageSrc={previewImageSrc} /> : null;
+
   useEffect(() => {
-    if (typeof window !== 'undefined') {
-      setHasWindow(true);
-    }
+    setHasWindow(typeof window !== 'undefined');
   }, []);
 
-  const PreviewImg = previewImageSrc ? <PreviewImage previewImageSrc={previewImageSrc} /> : null;
+  // If the space key is pressed, do not scroll the page and click the element
+  const handleSpaceKeyDown = (event: KeyboardEvent) => {
+    if (event.code === 'Space' || event.key === ' ') {
+      event.preventDefault();
+      (event.target as HTMLElement).click();
+    }
+  };
+
+  useEffect(() => {
+    if (!isPreview || !playerWrapperRef.current) return;
+
+    const wrapper = playerWrapperRef.current;
+
+    /**
+     * Observe for an element with class .react-player__preview inside the wrapper
+     * and set its role to button when it is added to the DOM
+     */
+    const observer = new MutationObserver(() => {
+      const preview = wrapper.querySelector('.react-player__preview');
+      if (preview) {
+        preview.setAttribute('role', 'button');
+        observer.disconnect();
+      }
+    });
+
+    observer.observe(wrapper, {
+      childList: true, // Look for added/removed child elements
+      subtree: true, // Look for added/removed elements in all descendants inside wrapper
+    });
+
+    // Cleanup on unmount
+    return () => observer.disconnect();
+  }, [isPreview]);
+
+  useEventListener('keydown', handleSpaceKeyDown, playerWrapperRef);
 
   return (
     <WidthBox
@@ -68,7 +104,7 @@ export const EmbedMedia = ({
     >
       <figure>
         {/* Extra classnames passed into wrapper for Vimeo responsive bug */}
-        <div className={cnb(mediaAspectRatios[aspectRatio], styles.mediaWrapper)}>
+        <div className={cnb(mediaAspectRatios[aspectRatio], styles.mediaWrapper)} ref={playerWrapperRef}>
           {hasWindow && (
             <ReactPlayer
               width="100%"
diff --git a/utilities/getNumBloks.ts b/utilities/getNumBloks.ts
index 322bb60d..d8cd99e8 100644
--- a/utilities/getNumBloks.ts
+++ b/utilities/getNumBloks.ts
@@ -2,13 +2,13 @@ import { type SbBlokData } from '@storyblok/react/rsc';
 /**
  * Returns the number of nested bloks when we use the CreateBloks utility.
  *
- * @param {object} section - The section to count bloks in.
- * @returns {number} The number of bloks in the section.
+ * @param sbField - The Storyblok field to count bloks in.
+ * @returns The number of bloks added to this Storyblok field.
  */
 
-export const getNumBloks = (section: SbBlokData[]) => {
-  if (section) {
-    return Object.keys(section).length;
+export const getNumBloks = (sbField: SbBlokData[]) => {
+  if (!!sbField?.length) {
+    return sbField.length;
   }
 
   return 0;


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This pull request aims to enhance the accessibility of video previews by adding role="button" to elements in preview mode and refining the space key handling to trigger video playback without page scrolling.  
- Updated utility function getNumBloks for clearer parameter naming and simplified logic.  
- Adjusted EmbedMedia to set role="button" dynamically using a MutationObserver and to handle space key events via an event listener.

### Reviewed Changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

| File                                   | Description                                                      |
| -------------------------------------- | ---------------------------------------------------------------- |
| utilities/getNumBloks.ts               | Improved parameter naming and streamlined the array length check |
| components/EmbedMedia/EmbedMedia.tsx   | Added accessibility improvements by setting role="button" on preview elements and handling space key events appropriately  |




state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-29T23:11:41Z
user: login: yvonnetangsu
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-29T23:21:41Z
user: login: sherakama
body: Looks and functions well. 

It's not every day you get to use the MutationObserver. Fun workaround. 

![Screenshot 2025-04-29 at 17 07 02](https://github.com/user-attachments/assets/3e633493-5cf8-4052-8952-141df129a72d)
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-30T00:07:52Z

Review comments:
diff_hunk: @@ -49,13 +50,48 @@ export const EmbedMedia = ({
    * https://github.com/cookpete/react-player/issues/1428
    */
   const [hasWindow, setHasWindow] = useState(false);
+  const playerWrapperRef = useRef<HTMLDivElement>(null);
+  const PreviewImg = previewImageSrc ? <PreviewImage previewImageSrc={previewImageSrc} /> : null;
+
   useEffect(() => {
-    if (typeof window !== 'undefined') {
-      setHasWindow(true);
-    }
+    setHasWindow(typeof window !== 'undefined');
   }, []);
 
-  const PreviewImg = previewImageSrc ? <PreviewImage previewImageSrc={previewImageSrc} /> : null;
+  // If the space key is pressed, do not scroll the page and click the element
+  const handleSpaceKeyDown = (event: KeyboardEvent) => {
+    if (event.code === 'Space' || event.key === ' ') {
+      event.preventDefault();
path: components/EmbedMedia/EmbedMedia.tsx
user: login: yvonnetangsu
body: This prevents scrolling down the page when the spacebar is clicked
created_at: 2025-04-29T23:21:41Z
author_association: MEMBER

PR comments:
user: login: netlify[bot]
created_at: 2025-04-29T00:37:14Z
body: ### <span aria-hidden="true">✅</span> Deploy Preview for *giving-campaign* ready!


|  Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | be7e79dcb1d73ef1ca44b2ac182efff5b765cbee |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/sites/giving-campaign/deploys/68115daa94c3480009dd7199 |
|<span aria-hidden="true">😎</span> Deploy Preview | [https://deploy-preview-411--giving-campaign.netlify.app](https://deploy-preview-411--giving-campaign.netlify.app) |
|<span aria-hidden="true">📱</span> Preview on mobile | <details><summary> Toggle QR Code... </summary><br /><br />![QR Code](https://app.netlify.com/qr-code/eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1cmwiOiJodHRwczovL2RlcGxveS1wcmV2aWV3LTQxMS0tZ2l2aW5nLWNhbXBhaWduLm5ldGxpZnkuYXBwIn0.85-UT2pie89KSu9OpgNA627_-IjyzBMpiiwuz5sIKEQ)<br /><br />_Use your smartphone camera to open QR code link._</details> |
---
<!-- [giving-campaign Preview](https://deploy-preview-411--giving-campaign.netlify.app) -->
_To edit notification comments on pull requests, go to your [Netlify site configuration](https://app.netlify.com/sites/giving-campaign/configuration/notifications#deploy-webhooks)._
author_association: NONE
user: login: yvonnetangsu
created_at: 2025-04-30T00:12:45Z
body: > Looks and functions well.
> 
> It's not every day you get to use the MutationObserver. Fun workaround.
> 

Thanks for testing @sherakama ! Yeah I've never used it before. I tried some other approaches but this is the only one that works reliably 😄 
author_association: MEMBER
user: login: yvonnetangsu
created_at: 2025-04-30T00:15:13Z
body: Asking Caryl to test in JIRA so will leave this up in case she has anything to add.
author_association: MEMBER
user: login: yvonnetangsu
created_at: 2025-04-30T00:59:02Z
body: Caryl said no more siteimprove issues. Merging in and going to do the same on Open Minds 😄 
author_association: MEMBER