Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: auto scroll to the selected week in sidebar and closing sidebar on ESC #1531

Merged
merged 3 commits into from
Dec 3, 2024
Merged
Changes from 2 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
55 changes: 52 additions & 3 deletions src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,19 @@ export function Sidebar({
}
};

// listen for ESC key and close the sidebar
const handleEscape = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
closeSidebar();
}
};

document.addEventListener('mousedown', handleClickOutside);
document.addEventListener('keydown', handleEscape);

return () => {
document.removeEventListener('mousedown', handleClickOutside);
document.removeEventListener('keydown', handleEscape);
};
}, [sidebarRef]);

Expand Down Expand Up @@ -129,6 +138,30 @@ export function Sidebar({
[courseId, findPathToContent, fullCourseContent],
);

const activeItemRef = useRef<HTMLDivElement | HTMLAnchorElement | null>(null);

useEffect(() => {
if (sidebarOpen && activeItemRef.current) {
activeItemRef.current.scrollIntoView({
behavior: 'smooth',
block: 'center',
});
// focus on the active item
if (activeItemRef.current instanceof HTMLAnchorElement) {
activeItemRef.current.focus();
} else if (activeItemRef.current instanceof HTMLDivElement) {
// check for the first focusable element and focus on it
const firstFocusableElement =
activeItemRef.current.querySelector('button, a');
if (firstFocusableElement) {
(
firstFocusableElement as HTMLButtonElement | HTMLAnchorElement
).focus();
}
}
}
}, [sidebarOpen]);

const renderContent = useCallback(
(contents: FullCourseContent[]) => {
return contents.map((content) => {
Expand All @@ -140,6 +173,11 @@ export function Sidebar({
key={content.id}
value={`item-${content.id}`}
className={`rounded-md border-none ${isActiveContent ? 'bg-primary/5' : ''}`}
ref={
isActiveContent
? (activeItemRef as React.RefObject<HTMLDivElement>)
: null
}
>
<AccordionTrigger className="rounded-md px-4 text-lg font-medium capitalize">
{content.title}
Expand All @@ -156,6 +194,11 @@ export function Sidebar({
key={content.id}
href={navigateToContent(content.id) || '#'}
className={`flex w-full cursor-pointer items-center rounded-md p-4 tracking-tight hover:bg-primary/10 ${isActiveContent ? 'bg-primary/10' : ''}`}
ref={
isActiveContent
? (activeItemRef as React.RefObject<HTMLAnchorElement>)
: null
}
>
<div className="flex w-full items-center justify-between gap-2">
<div className="flex items-center gap-2">
Expand Down Expand Up @@ -186,7 +229,7 @@ export function Sidebar({
);

return (
<div className='sticky top-[72px] z-20 bg-background py-2'>
<div className="sticky top-[72px] z-20 bg-background py-2">
<Button
ref={buttonRef}
onClick={() => setSidebarOpen((s) => !s)}
Expand All @@ -206,7 +249,9 @@ export function Sidebar({
variants={sidebarVariants}
className="fixed right-0 top-0 z-[99999] flex h-screen w-full flex-col gap-4 overflow-y-auto rounded-r-lg border-l border-primary/10 bg-neutral-50 dark:bg-neutral-900 md:max-w-[30vw]"
>
<div className="sticky top-0 z-10 flex items-center justify-between border-b border-primary/10 bg-neutral-50 p-5 dark:bg-neutral-900"> <h4 className="text-xl font-bold tracking-tighter text-primary lg:text-2xl">
<div className="sticky top-0 z-10 flex items-center justify-between border-b border-primary/10 bg-neutral-50 p-5 dark:bg-neutral-900">
{' '}
<h4 className="text-xl font-bold tracking-tighter text-primary lg:text-2xl">
Course Content
</h4>
<Button
Expand All @@ -217,7 +262,11 @@ export function Sidebar({
<X className="size-5" />
</Button>
</div>
<Accordion type="multiple" className="w-full px-4 capitalize">
<Accordion
type="multiple"
defaultValue={currentActiveContentIds.map((num) => `item-${num}`)}
className="w-full px-4 capitalize"
>
{memoizedContent}
</Accordion>
</motion.div>
Expand Down
Loading