streamline TestSlide, move messages to main props, and add inert attribute support

pull/36425/head
ChaosExAnima 3 days ago
parent 1fd1a0d29c
commit 6b4c20a3f9
No known key found for this signature in database
GPG Key ID: 8F2B333100FB6117

@ -1,4 +1,4 @@
import type { CSSProperties, FC } from 'react';
import type { FC } from 'react';
import type { Meta, StoryObj } from '@storybook/react-vite';
import { fn, userEvent, expect } from 'storybook/test';
@ -10,34 +10,22 @@ interface TestSlideProps {
id: number;
text: string;
color: string;
styles?: CSSProperties;
}
const TestSlide: FC<TestSlideProps & { active: boolean }> = ({
active,
text,
color,
styles = {},
}) => {
return (
<div
style={{
...styles,
backgroundColor: active ? color : 'black',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: 'white',
fontSize: 24,
fontWeight: 'bold',
minHeight: 100,
transition: 'background-color 0.3s',
}}
>
{text}
</div>
);
};
}) => (
<div
className='test-slide'
style={{
backgroundColor: active ? color : undefined,
}}
>
{text}
</div>
);
const slides: TestSlideProps[] = [
{
@ -73,7 +61,24 @@ const meta = {
emptyFallback: 'No slides available',
},
render(args) {
return <Carousel {...args} />;
return (
<>
<Carousel {...args} />
<style>
{`.test-slide {
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 24px;
font-weight: bold;
min-height: 100px;
transition: background-color 0.3s;
background-color: black;
}`}
</style>
</>
);
},
argTypes: {
emptyFallback: {
@ -90,13 +95,18 @@ type Story = StoryObj<typeof meta>;
export const Default: Story = {
async play({ args, canvas }) {
const nextButton = await canvas.findByRole('button', { name: /next/i });
const slides = await canvas.findAllByRole('group');
await expect(slides).toHaveLength(slides.length);
await userEvent.click(nextButton);
await expect(args.onChangeSlide).toHaveBeenCalledWith(1);
await expect(args.onChangeSlide).toHaveBeenCalledWith(1, slides[1]);
await userEvent.click(nextButton);
await expect(args.onChangeSlide).toHaveBeenCalledWith(2);
await expect(args.onChangeSlide).toHaveBeenCalledWith(2, slides[2]);
// Wrap around
await userEvent.click(nextButton);
await expect(args.onChangeSlide).toHaveBeenCalledWith(0);
await expect(args.onChangeSlide).toHaveBeenCalledWith(0, slides[0]);
},
};

@ -1,11 +1,4 @@
import {
useCallback,
useEffect,
useLayoutEffect,
useMemo,
useRef,
useState,
} from 'react';
import { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react';
import type {
ComponentPropsWithoutRef,
ComponentType,
@ -13,6 +6,9 @@ import type {
ReactNode,
} from 'react';
import type { MessageDescriptor } from 'react-intl';
import { defineMessages } from 'react-intl';
import classNames from 'classnames';
import { usePrevious } from '@dnd-kit/utilities';
@ -24,6 +20,17 @@ import { CarouselPagination } from './pagination';
import './styles.scss';
const defaultMessages = defineMessages({
previous: { id: 'lightbox.previous', defaultMessage: 'Previous' },
next: { id: 'lightbox.next', defaultMessage: 'Next' },
current: {
id: 'carousel.current',
defaultMessage: '<sr>Slide</sr> {current, number} / {max, number}',
},
});
export type MessageKeys = keyof typeof defaultMessages;
export interface CarouselSlideProps {
id: string | number;
}
@ -37,9 +44,10 @@ export interface CarouselProps<
> {
items: SlideProps[];
renderItem: RenderSlideFn<SlideProps>;
onChangeSlide?: (index: number) => void;
onChangeSlide?: (index: number, ref: Element) => void;
paginationComponent?: ComponentType<CarouselPaginationProps> | null;
paginationProps?: Partial<CarouselPaginationProps>;
messages?: Record<MessageKeys, MessageDescriptor>;
emptyFallback?: ReactNode;
classNamePrefix?: string;
slideClassName?: string;
@ -53,6 +61,7 @@ export const Carousel = <
onChangeSlide,
paginationComponent: Pagination = CarouselPagination,
paginationProps = {},
messages = defaultMessages,
children,
emptyFallback = null,
className,
@ -73,11 +82,13 @@ export const Carousel = <
} else if (newIndex > max) {
newIndex = 0;
}
const slide = wrapperRef.current?.children[newIndex];
if (slide) {
setCurrentSlideHeight(slide.scrollHeight);
onChangeSlide?.(newIndex, slide);
}
onChangeSlide?.(newIndex);
return newIndex;
});
},
@ -142,6 +153,7 @@ export const Carousel = <
onNext={handleNext}
onPrev={handlePrev}
className={`${classNamePrefix}__pagination`}
messages={messages}
{...paginationProps}
/>
)}
@ -198,13 +210,7 @@ const CarouselSlideWrapper = <SlideProps extends CarouselSlideProps>({
[observer],
);
useEffect(() => {
if (slideRef.current && active) {
slideRef.current.focus();
}
}, [active]);
const slide = useMemo(
const children = useMemo(
() => renderItem(item, active, index),
[renderItem, item, active, index],
);
@ -215,10 +221,11 @@ const CarouselSlideWrapper = <SlideProps extends CarouselSlideProps>({
className={className}
role='group'
aria-roledescription='slide'
// @ts-expect-error inert in not in this version of React
inert={active ? 'true' : undefined}
inert={active ? '' : undefined}
tabIndex={-1}
data-index={index}
>
{slide}
{children}
</div>
);
};

@ -1,40 +1,31 @@
import type { FC, MouseEventHandler } from 'react';
import type { MessageDescriptor } from 'react-intl';
import { defineMessages, useIntl } from 'react-intl';
import { useIntl } from 'react-intl';
import ChevronLeftIcon from '@/material-icons/400-24px/chevron_left.svg?react';
import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react';
import { IconButton } from '../icon_button';
import type { MessageKeys } from './index';
export interface CarouselPaginationProps {
onNext: MouseEventHandler;
onPrev: MouseEventHandler;
current: number;
max: number;
className?: string;
messages?: Record<MessageKeys, MessageDescriptor>;
messages: Record<MessageKeys, MessageDescriptor>;
}
export const defaultMessages = defineMessages({
previous: { id: 'lightbox.previous', defaultMessage: 'Previous' },
next: { id: 'lightbox.next', defaultMessage: 'Next' },
current: {
id: 'carousel.current',
defaultMessage: '<sr>Slide</sr> {current, number} / {max, number}',
},
});
type MessageKeys = keyof typeof defaultMessages;
export const CarouselPagination: FC<CarouselPaginationProps> = ({
onNext,
onPrev,
current,
max,
className = '',
messages = defaultMessages,
messages,
}) => {
const intl = useIntl();
return (

@ -71,7 +71,7 @@ export const FeaturedCarousel: React.FC<{
renderItem={renderSlide}
aria-labelledby={`${accessibilityId}-title`}
classNamePrefix='featured-carousel'
paginationProps={{ messages }}
messages={messages}
>
<h4 className='featured-carousel__title' id={`${accessibilityId}-title`}>
<Icon id='thumb-tack' icon={PushPinIcon} />

@ -0,0 +1,6 @@
declare namespace React {
interface HTMLAttributes<T> extends AriaAttributes, DOMAttributes<T> {
// Add inert attribute support, which is only in React 19.2. See: https://github.com/facebook/react/pull/24730
inert?: '';
}
}
Loading…
Cancel
Save