Skip to content

Commit

Permalink
Merge branch 'main' into fix/diacom
Browse files Browse the repository at this point in the history
  • Loading branch information
mfshao authored Dec 11, 2024
2 parents a146915 + 8fd7d5b commit d64e164
Show file tree
Hide file tree
Showing 17 changed files with 375 additions and 18 deletions.
15 changes: 15 additions & 0 deletions config/heal/home/SlideData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import IconHdd from '../../../src/lib/Home/Assets/Icons/Icon-Hdd.svg';
import IconAnalyses from '../../../src/lib/Home/Assets/Icons/Icon-Analyses.svg';

export const slideData = [
{
href: 'https://www.askjeeves.com',
icon: IconHdd,
text: 'View the latest studies who have shared their data!',
},
{
href: 'https://www.spacejam.com/1996/',
icon: IconAnalyses,
text: 'Explore example analyses!',
},
];
13 changes: 7 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@
"postcss": "^8.4.29",
"postcss-loader": "^7.3.2",
"postcss-preset-env": "^8.4.2",
"prettier": "^2.7.1",
"prettier": "^3.4.1",
"react": "^18.2.0",
"react-dom": "18.2.0",
"tailwindcss": "^3.4.10",
Expand Down
2 changes: 1 addition & 1 deletion src/lib/Home/Assets/Icons/Icon-Analyses.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion src/lib/Home/Assets/Icons/Icon-Hdd.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed src/lib/Home/Assets/Images/1-1.webp
Binary file not shown.
Binary file removed src/lib/Home/Assets/Images/2-1.webp
Binary file not shown.
Binary file removed src/lib/Home/Assets/Images/3-1.webp
Binary file not shown.
Binary file removed src/lib/Home/Assets/Images/4-1.webp
Binary file not shown.
Binary file removed src/lib/Home/Assets/Images/5-1.webp
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/* Keyframes for Slide Movement */
@keyframes slideFromCenter {
0% {
transform: translateX(0); /* Start at the center */
}
100% {
transform: translateX(-100vw); /* Move to the left off-screen */
display: none;
}
}

@keyframes slideFromRight {
0% {
transform: translateX(100vw); /* Start from the right off-screen */
}
100% {
transform: translateX(0); /* Move to the center */
}
}

/* CSS Classes for Animations */
.current-slide {
animation: slideFromRight .75s ease-in-out forwards;
}

.previous-slide {
animation: slideFromCenter .75s ease-in-out forwards;
}

.hidden-slide {
display: none;
}
47 changes: 41 additions & 6 deletions src/lib/Home/Components/CarouselBanner/CarouselBanner.test.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,47 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom';
import CarouselBanner from './CarouselBanner';
import Slide from './Slide';

describe('CarouselBanner Component', () => {
test('renders the CarouselBanner component', () => {
// Mock the Slide and CarouselControls components
jest.mock('./Slide', () => {
return jest.fn(() => <div>Slide Component</div>);
});

jest.mock('./CarouselControls', () => {
return jest.fn(() => <div>Carousel Controls</div>);
});

// Mock the slideData import
jest.mock('../../../../../config/heal/home/SlideData', () => ({
slideData: [
{ href: '#slide1', icon: () => <div />, text: 'Slide 1' },
{ href: '#slide2', icon: () => <div />, text: 'Slide 2' },
],
}));

describe('CarouselBanner', () => {
// Clear mocks before each test
beforeEach(() => {
jest.clearAllMocks();
});

test('renders the component with slides', () => {
render(<CarouselBanner />);

// Ensure the CarouselBanner is rendered
expect(screen.getByTestId('carousel-banner')).toBeInTheDocument();

// Ensure that Slide components are rendered based on slideData mock
expect(Slide).toHaveBeenCalledTimes(2); // Should call Slide twice based on mock
});

test('does not render CarouselControls when there is only one slide', () => {
// Mock the slideData to simulate a single slide
jest.mock('../../../../../config/heal/home/SlideData', () => ({
slideData: [{ href: '#slide1', icon: () => <div />, text: 'Slide 1' }],
}));
render(<CarouselBanner />);
const element = screen.getByTestId('carousel-banner');
expect(element).toBeInTheDocument();
// CarouselControls should not be rendered when there's only one slide
expect(screen.queryByTestId('carousel-controls')).toBeNull();
});
});
62 changes: 59 additions & 3 deletions src/lib/Home/Components/CarouselBanner/CarouselBanner.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,68 @@
import React from 'react';
import React, { useState, useEffect } from 'react';
import { slideData } from '../../../../../config/heal/home/SlideData';
import Slide from './Slide';
import CarouselControls from './CarouselControls';

const CarouselBanner: React.FC = () => {
const [currentSlide, setCurrentSlide] = useState(0);
const [isPlaying, setIsPlaying] = useState(true);
const slideDisplayTime = 5000;
const bannerOnly = slideData.length === 1; // used to render carousel as a banner if only one slide

const advanceToNextSlide = () =>
setCurrentSlide((prevIndex) => (prevIndex + 1) % slideData.length);

useEffect(() => {
// If autoplay is off or there is only one slide, don't run the interval
if (!isPlaying || bannerOnly) return;
const interval = setInterval(() => {
advanceToNextSlide();
}, slideDisplayTime);
return () => clearInterval(interval);
}, [isPlaying, bannerOnly]);

return (
<div
data-testid="carousel-banner"
className="carousel-banner text-center p-4"
className="
carousel-banner
left-1/2
transform
-translate-x-1/2
flex
p-4
bg-carousel-gradient
h-36 lg:h-28
overflow-hidden
px-[10%] md:px-[33%]
relative"
aria-live="polite"
aria-label="Carousel banner displaying slides"
>
<h1 className="text-2xl font-bold">Carousel Banner</h1>
<div className="slide-container w-[100%] relative flex justify-center">
{slideData.map((obj, i) => (
<Slide
key={i}
currentSlide={currentSlide}
numberOfSlides={slideData.length}
iterator={i}
href={obj.href}
Icon={obj.icon}
text={obj.text}
/>
))}
</div>

{!bannerOnly && (
<CarouselControls
isPlaying={isPlaying}
setIsPlaying={setIsPlaying}
slideData={slideData}
advanceToNextSlide={advanceToNextSlide}
currentSlide={currentSlide}
setCurrentSlide={setCurrentSlide}
/>
)}
</div>
);
};
Expand Down
83 changes: 83 additions & 0 deletions src/lib/Home/Components/CarouselBanner/CarouselControls.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { render, screen, fireEvent } from '@testing-library/react';
import CarouselControls from './CarouselControls';

describe('CarouselControls', () => {
const mockSetIsPlaying = jest.fn();
const mockSetCurrentSlide = jest.fn();
const mockAdvanceToNextSlide = jest.fn();

const defaultProps = {
isPlaying: false,
setIsPlaying: mockSetIsPlaying,
slideData: [
{ href: '#slide1', icon: () => <div />, text: 'Slide 1' },
{ href: '#slide2', icon: () => <div />, text: 'Slide 2' },
{ href: '#slide3', icon: () => <div />, text: 'Slide 3' },
],
advanceToNextSlide: mockAdvanceToNextSlide,
currentSlide: 0,
setCurrentSlide: mockSetCurrentSlide,
};

afterEach(() => {
jest.clearAllMocks();
});

test('renders Play/Pause button and toggles state when clicked', () => {
// Render component with initial state (paused)
render(<CarouselControls {...defaultProps} />);

// Verify button is showing "▶" for play
const playPauseButton = screen.getByLabelText('Play carousel');
expect(playPauseButton).toHaveTextContent('▶');

// Click play button
fireEvent.click(playPauseButton);
expect(mockSetIsPlaying).toHaveBeenCalledWith(true);
expect(mockAdvanceToNextSlide).toHaveBeenCalled(); // Advance to next slide when starting

// Render again with "isPlaying" set to true
render(<CarouselControls {...defaultProps} isPlaying={true} />);

// Verify button is showing "⏸" for pause
const pauseButton = screen.getByLabelText('Pause carousel');
expect(pauseButton).toHaveTextContent('⏸');

// Click pause button
fireEvent.click(pauseButton);
expect(mockSetIsPlaying).toHaveBeenCalledWith(false);
});

test('clicking a slide indicator sets the current slide and stops the carousel', () => {
render(<CarouselControls {...defaultProps} />);

const slideIndicator = screen.getByLabelText('Go to slide 2');
fireEvent.click(slideIndicator);

// Verify setCurrentSlide was called with correct index
expect(mockSetCurrentSlide).toHaveBeenCalledWith(1);

// Verify setIsPlaying was called with false
expect(mockSetIsPlaying).toHaveBeenCalledWith(false);
});

test('does not call advanceToNextSlide if carousel is paused', () => {
render(<CarouselControls {...defaultProps} />);

const playPauseButton = screen.getByLabelText('Play carousel');
fireEvent.click(playPauseButton); // Starts the carousel and calls advanceToNextSlide

expect(mockAdvanceToNextSlide).toHaveBeenCalled();
});

test('clicking on a slide indicator stops the carousel', () => {
const playingProps = { ...defaultProps, isPlaying: true };
render(<CarouselControls {...playingProps} />);

const slideIndicator = screen.getByLabelText('Go to slide 2');
fireEvent.click(slideIndicator);

// Verify that the carousel stops when the indicator is clicked
expect(mockSetIsPlaying).toHaveBeenCalledWith(false);
});
});
75 changes: 75 additions & 0 deletions src/lib/Home/Components/CarouselBanner/CarouselControls.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
type CarouselControlsProps = {
isPlaying: boolean;
setIsPlaying: (isPlaying: boolean) => void;
slideData: {
href: string;
icon: React.ElementType<{ className: string }>;
text: string;
}[];
advanceToNextSlide: () => void;
currentSlide: number;
setCurrentSlide: (index: number) => void;
};

const CarouselControls = ({
isPlaying,
setIsPlaying,
slideData,
advanceToNextSlide,
currentSlide,
setCurrentSlide,
}: CarouselControlsProps) => {
const togglePlayPause = () => {
setIsPlaying(isPlaying ? false : true);
};

return (
<div
data-testid="carousel-controls"
className="
carousel-controls
absolute bottom-4 left-1/2
transform -translate-x-1/2
flex items-center space-x-4
"
>
{/* Play/Pause Button */}
<button
className={`
${!isPlaying && 'pb-[1px]'}
hover:opacity-90 h-[12px] w-[12px]
border-white rounded-full flex
items-center justify-center bg-white
text-xs text-heal-carousel_button
`}
aria-label={isPlaying ? 'Pause carousel' : 'Play carousel'}
onClick={() => {
togglePlayPause();
if (!isPlaying) advanceToNextSlide();
}}
>
{isPlaying ? '⏸' : '▶'}
</button>

{/* Slide Indicators */}
<div className="flex space-x-2">
{slideData.map((_: object, i: number) => (
<button
key={i}
aria-label={`Go to slide ${i + 1}`}
className={`
w-3 h-3 rounded-full
hover:opacity-90 border border-white
${i === currentSlide ? 'bg-white' : 'bg-transparent'}
`}
onClick={() => {
setCurrentSlide(i);
setIsPlaying(false);
}}
/>
))}
</div>
</div>
);
};
export default CarouselControls;
Loading

0 comments on commit d64e164

Please sign in to comment.