0

I need to focus overlay/dialog when using screen reader. How to focus only Overlay on next.js using aria

so I added aria-hidden="true" on main area when open dialog , and added role="dialog" aria-modal="true"

But on screen reader, when I tapped, focus / read contents on main area.

<Overlay render={render}>
  <div 
    className={previewOverlay} 
    role="dialog" 
    aria-modal="true"
    aria-labelledby="preview-dialog-title"
  >
    <AppHeader config={headerConfig} />

    {media?.mediaType === MEDIA_TYPE.PHOTO ? (
      <img 
        className="media" 
        src={media?.url} 
        alt="media-preview"
        aria-hidden="true"
      />
    ) : (
      <video
        className="media"
        src={media?.url}
        playsInline
        autoPlay
        muted
        loop
        aria-hidden="true"
      />
    )}
    
    {/* Screen reader accessible watermark description */}
    <div 
      className="sr-only" 
      aria-live="polite"
      style={{
        position: 'absolute',
        width: '1px',
        height: '1px',
        padding: '0',
        margin: '-1px',
        overflow: 'hidden',
        clip: 'rect(0, 0, 0, 0)',
        whiteSpace: 'nowrap',
        border: '0'
      }}
    >
      {localizationService.t('common:accessibility.watermarkDescription')}
    </div>

    <div className="buttons-wrapper">
      <Button
        ref={noButtonRef}
        className="uppercase"
        dataType="secondary"
        aria-label={localizationService.t('common:buttons.no') + ' - ' + localizationService.t('common:accessibility.thisIsNotMyContent')}
        onClick={() => {
          if (isUrlAssociationFlow) {
            handleUrlAssociationEvent(
              ANALYTICS_EVENT.MEDIA_BY_URL_ASSOCIATION_CANCELED
            );
          }

          close();
        }}
      >
        {localizationService.t('common:buttons.no')}
      </Button>

      <Button
        className="uppercase"
        dataType="primary"
        disabled={isLoading}
        aria-label={isLoading ? 
          localizationService.t('common:buttons.loading') : 
          localizationService.t('common:buttons.yes') + ' - ' + localizationService.t('common:accessibility.thisIsMyContent')
        }
        onClick={handleAddMedia}
      >
        {localizationService.t(
          isLoading ? 'common:buttons.loading' : 'common:buttons.yes'
        )}
      </Button>
    </div>
  </div>
</Overlay>

import * as React from 'react';
import * as ReactDOM from 'react-dom';

/* Hooks */
import { useMountTransition } from 'hooks';

/* Styles */
import { overlayComponent } from './style.module.css';

type Props = React.HTMLAttributes<HTMLDivElement> &
  Partial<{
    render: boolean;
    className: string;
    portalId: string;
    children: string | JSX.Element | (string | JSX.Element)[];
  }>;

const Overlay: React.FC<Props> = React.memo(
  ({
    render = true,
    className = '',
    portalId = 'overlay',
    children,
    ...restHtmlAttributes
  }: Props) => {
    const hasTransitionedIn = useMountTransition(render, 300);

    const isRender = render || hasTransitionedIn;

    const animationClassName = `${hasTransitionedIn ? 'transitioned-in' : ''} ${
      render ? 'visible' : ''
    }`;

/* Prevent body scroll and hide main content from screen readers */
React.useEffect(() => {
  document.body.style.overflow = isRender ? 'hidden' : 'unset';
  
  // Hide main content from screen readers when overlay is open
  const mainContent = document.getElementById('app');
  if (mainContent) {
    const ariaHiddenValue = isRender ? 'true' : 'false';
    mainContent.setAttribute('aria-hidden', ariaHiddenValue);
    console.log('Overlay isRender:', isRender, 'aria-hidden set to:', ariaHiddenValue);
  }

  return () => {
    document.body.style.overflow = 'unset';
    if (mainContent) {
      mainContent.setAttribute('aria-hidden', 'false');
      console.log('Overlay cleanup: aria-hidden set to false');
    }
  };
}, [isRender]);

return ReactDOM.createPortal(
  <>
    {isRender && (
      <div
        // eslint-disable-next-line max-len
        className={`${overlayComponent} ${animationClassName} ${className} `}
        {...restHtmlAttributes}
      >
        {children}
      </div>
    )}
  </>,
  document.getElementById(portalId) as HTMLElement
    );
  }
);

Overlay.displayName = 'Overlay';

export type OverlayProps = Props;
export { Overlay };

1 Answer 1

0

It‘s your job to set focus, ARIA is not controlling that, only helping to adhere to standards.

overlay.header.focus() or something alike.

aria-hidden only hides the element itself from assistive technology. It doesn’t block the user from interacting with elements inside it, like links or buttons, with the mouse, touch screen or keyboard.

inert does prevent interaction inside, and also hides contents from assistive technology.

It’s important that the dialog element is outside the hidden or inert element.

Since focus can still be set inside a hidden element, screen readers will expose focused contents regardless of any parent aria-hidden state.

Did you consider using the native <dialog> element? It comes with built-in methods show() and close() which cover inert and setting focus already.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.