Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 43 additions & 3 deletions src/oceans/components/common/Guide.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,37 @@ export const stopTypingSounds = () => {
};

let UnwrappedGuide = class Guide extends React.Component {
guideDialogRef = React.createRef();

componentDidUpdate() {
// Focus the dialog only when the guide changes, not on every re-render
const currentGuide = guide.getCurrentGuide();
const currentGuideId = currentGuide ? currentGuide.id : null;

if (
currentGuideId !== this.lastFocusedGuideId &&
currentGuide &&
this.guideDialogRef &&
this.guideDialogRef.current
) {
this.guideDialogRef.current.focus();
this.lastFocusedGuideId = currentGuideId;
} else if (!currentGuide) {
this.lastFocusedGuideId = null;
}
}
onTypingDone() {
clearInterval(getState().guideTypingTimer);
setState({guideShowing: true, guideTypingTimer: undefined});
}

onGuideKeyDown = (e) => {
if (e.key === ' ' || e.key === 'Enter' || e.key === 'Spacebar') {
e.preventDefault();
this.onGuideClick();
}
};

onGuideClick = () => {
const state = getState();
const currentGuide = guide.getCurrentGuide();
Expand Down Expand Up @@ -157,22 +183,36 @@ let UnwrappedGuide = class Guide extends React.Component {
key={currentGuide.id}
style={guideBgStyle}
onClick={this.onGuideClick}
onKeyDown={this.onGuideKeyDown}
tabIndex={0}
role="button"
aria-label={I18n.t('continue')}
id="uitest-dismiss-guide"
>
<div
ref={this.guideDialogRef}
aria-labelledby="guide-heading"
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

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

The guide dialog uses aria-labelledby='guide-heading' but this element only exists when currentGuide.style === 'Info'. For other guide styles, the aria-labelledby will reference a non-existent element, which could confuse screenreaders. Consider using aria-label with appropriate text for non-Info guides, or conditionally setting aria-labelledby only when the heading exists.

Suggested change
aria-labelledby="guide-heading"
aria-labelledby={currentGuide.style === 'Info' ? 'guide-heading' : undefined}

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

If we don't have a guide providing info, we don't need a label here. Plus, I tried adding a conditional aria-label and it took over precedence

tabIndex={-1}
className="guide-dialog"
style={{
...styles.guide,
...styles[`guide${currentGuide.style}`]
}}
>
<div>
{currentGuide.style === 'Info' && (
<div style={styles.guideHeading}>
<div id="guide-heading" style={styles.guideHeading}>
{I18n.t('didYouKnow')}
</div>
)}

<div style={styles.guideTypingText}>

{/* Visually hidden aria-live region for screen readers */}
<div style={{position: 'absolute', left: '-9999px', width: '1px', height: '1px', overflow: 'hidden'}} aria-live="polite">
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

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

The visually hidden div uses inline styles for screen reader-only content. While functional, this pattern is fragile and should be extracted to a reusable style constant or utility class to ensure consistency across the application and make it easier to maintain.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Understood- this is in a small repo and if I reuse it within there I can refactor to a utility class

{currentGuide.textFn(getState())}
</div>
{/* Visible Typist animation for sighted users */}
<div style={styles.guideTypingText} aria-hidden="true">
<Typist
avgTypingDelay={35}
stdTypingDelay={15}
Expand All @@ -190,7 +230,7 @@ let UnwrappedGuide = class Guide extends React.Component {
: styles.guideFinalTextContainer
}
>
<div style={styles.guideFinalText}>
<div style={styles.guideFinalText} aria-hidden="true">
{currentGuide.textFn(getState())}
</div>
</div>
Expand Down
11 changes: 11 additions & 0 deletions src/oceans/styles/fade.css
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,14 @@
opacity: 1;
}
}

/* Only show focus outline when navigating with keyboard, not on programmatic focus */
.guide-dialog:focus {
outline: none;
}

.guide-dialog:focus-visible {
outline: 2px solid white;
outline-offset: 2px;
box-shadow: 0 0 0 4px black;
}