Skip to content

Commit

Permalink
remove focus on card when overlay is up and add autoFocus for first i…
Browse files Browse the repository at this point in the history
…nteractive element on overlay
  • Loading branch information
jomcarvajal committed Jan 24, 2025
1 parent 9155b76 commit d7bb31c
Show file tree
Hide file tree
Showing 6 changed files with 1,228 additions and 1,146 deletions.
87 changes: 63 additions & 24 deletions src/components/Card.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ReactNode, useState, useRef } from "react";
import { ReactNode, useState, useRef, useEffect, useCallback } from "react";
import { breakpoints, colors, layouts, mixins } from "../theme";
import { AvailablePoints, StepBase, StepWithData } from "../types";
import styled from "styled-components";
Expand Down Expand Up @@ -205,8 +205,7 @@ export const StyledOverlay = styled.div`
transform: translate(-50%, -50%);
width: 100%;
height: 100%;
background-color: #FFFFFF;
opacity: 0.8;
background-color: #FFFFFF80;
z-index: 2;
`;

Expand Down Expand Up @@ -249,6 +248,8 @@ const StepCard = ({
overlayChildren,
...otherProps }: StepCardProps) => {

// Helps to stop focusing first child when is already focused
const [previousFocusedElement, setPreviousFocusedElement] = useState<HTMLElement | null>(null);
const overlayRef = useRef<HTMLDivElement>(null);
const [showOverlay, setShowOverlay] = useState<boolean>(false);

Expand All @@ -262,6 +263,42 @@ const StepCard = ({
}
};

const handleOverlayFocus = useCallback((event: FocusEvent) => {
setShowOverlay(true);
const firstOverlayFocusableElement = document.getElementById('overlay-element')?.querySelector(
'button, [href], input, select, textarea'
) as HTMLElement;

if (
(firstOverlayFocusableElement !== previousFocusedElement) &&
(event.target === overlayRef.current)
) {
setPreviousFocusedElement(firstOverlayFocusableElement);
firstOverlayFocusableElement.focus();
}
}, [overlayRef, previousFocusedElement]);

const hideFocusableElements = useCallback(() => {
const focusableElements = Array.from(document.getElementById("step-card")?.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
) || []);

focusableElements.forEach((el) => {
(el as HTMLElement).setAttribute('tabindex', '-1');
});
}, []);

useEffect(() => {
const currentOverlayRef = overlayRef.current;
if (currentOverlayRef && overlayChildren) {
currentOverlayRef.addEventListener('focus', handleOverlayFocus);
hideFocusableElements();
}
return () => {
currentOverlayRef?.removeEventListener('focus', handleOverlayFocus);
};
}, [overlayChildren, overlayRef, handleOverlayFocus, hideFocusableElements]);

return (
<OuterStepCard {...otherProps}>
{multipartBadge}
Expand All @@ -273,36 +310,38 @@ const StepCard = ({
? {
onMouseOver: () => setShowOverlay(true),
onMouseLeave: () => setShowOverlay(false),
onFocus: () => setShowOverlay(true),
onBlur: handleOverlayBlur,
tabIndex: 0,
}
: {})
}
>
{(overlayChildren && showOverlay) &&
<StyledOverlay>
<StyledOverlay id="overlay-element">
{overlayChildren}
</StyledOverlay>}
{questionNumber && isHomework && stepType === 'exercise' &&
<StepCardHeader className="step-card-header">
<div>
{leftHeaderChildren}
<h2 className="question-info">
{headerTitleChildren}
<span>{formattedQuestionNumber}</span>
{showTotalQuestions ? <span className="num-questions">&nbsp;/ {numberOfQuestions}</span> : null}
<span className="separator">|</span>
<span className="question-id">ID: {questionId}</span>
</h2>
</div>
{availablePoints || rightHeaderChildren ? <div>
{availablePoints && <div className="points">{availablePoints} Points</div>}
{rightHeaderChildren}
</div> : null}
</StepCardHeader>
</StyledOverlay>
}
<StepCardQuestion unpadded={unpadded}>{children}</StepCardQuestion>
<div id="step-card">
{questionNumber && isHomework && stepType === 'exercise' &&
<StepCardHeader className="step-card-header">
<div>
{leftHeaderChildren}
<h2 className="question-info">
{headerTitleChildren}
<span>{formattedQuestionNumber}</span>
{showTotalQuestions ? <span className="num-questions">&nbsp;/ {numberOfQuestions}</span> : null}
<span className="separator">|</span>
<span className="question-id">ID: {questionId}</span>
</h2>
</div>
{availablePoints || rightHeaderChildren ? <div>
{availablePoints && <div className="points">{availablePoints} Points</div>}
{rightHeaderChildren}
</div> : null}
</StepCardHeader>
}
<StepCardQuestion unpadded={unpadded}>{children}</StepCardQuestion>
</div>
</div>
</InnerStepCard>
</OuterStepCard>
Expand Down
2 changes: 1 addition & 1 deletion src/components/Exercise.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -951,7 +951,7 @@ export const OverlayCard = () => {
return (
<TextResizerProvider>
<Exercise {...props1} className='preview-card' />
<Exercise {...props2} />
<Exercise {...props2} className='preview-card' />
</TextResizerProvider>
);
};
4 changes: 2 additions & 2 deletions src/components/IncludeRemoveQuestion/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,11 @@ export const IncludeRemoveQuestion = ({buttonVariant, onIncludeHandler, onRemove

return (
<StyledContainer>
<StyledButton className={buttonVariant} onClick={() => onClickHandler(buttonVariant)} aria-label="details">
<StyledButton className={buttonVariant} onClick={() => onClickHandler(buttonVariant)} aria-label={buttonVariant}>
<StyledIcon className={buttonVariant} icon={buttonIcon} aria-label={buttonVariant + ' question'} border size="lg" />
<span>{generateButtonText(buttonVariant)}</span>
</StyledButton>
<StyledButton className="details" aria-label="details button">
<StyledButton className="details" aria-label="details">
<StyledIcon className="details" icon={faEllipsisH} border size="lg"/>
<span>Details</span>
</StyledButton>
Expand Down
Loading

0 comments on commit d7bb31c

Please sign in to comment.