mirror of
https://github.com/dream-num/univer.git
synced 2026-08-28 23:01:30 +08:00
fix(design): improve gallery and pager accessibility (#7569)
This commit is contained in:
@@ -14,10 +14,11 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { OneToOneIcon, ZoomInIcon, ZoomOutIcon } from '@univerjs/icons';
|
||||
import { CloseIcon, OneToOneIcon, ZoomInIcon, ZoomOutIcon } from '@univerjs/icons';
|
||||
import { useContext, useEffect, useRef, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { clsx } from '../../helper/clsx';
|
||||
import { Button } from '../button/Button';
|
||||
import { ConfigContext } from '../config-provider/ConfigProvider';
|
||||
import { Pager } from '../pager/Pager';
|
||||
|
||||
@@ -28,53 +29,107 @@ export interface IGalleryProps {
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
}
|
||||
|
||||
const buttonClassName = `
|
||||
univer-flex univer-cursor-pointer univer-items-center univer-justify-center univer-border-none
|
||||
univer-bg-transparent univer-p-0 univer-text-current
|
||||
hover:univer-text-gray-0
|
||||
const toolbarButtonClassName = `
|
||||
!univer-border-transparent !univer-bg-transparent !univer-text-gray-300
|
||||
hover:!univer-bg-gray-600 hover:!univer-text-gray-0
|
||||
focus-visible:!univer-outline-none focus-visible:!univer-ring-2 focus-visible:!univer-ring-gray-0
|
||||
`;
|
||||
|
||||
const focusableElementSelector = `
|
||||
button:not([disabled]), [href], input:not([disabled]), select:not([disabled]),
|
||||
textarea:not([disabled]), [tabindex]:not([tabindex="-1"])
|
||||
`;
|
||||
|
||||
export function Gallery(props: IGalleryProps) {
|
||||
const { className, images, open, onOpenChange } = props;
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
const [isVisible, setIsVisible] = useState(Boolean(open));
|
||||
const [activeImageIndex, setActiveImageIndex] = useState(0);
|
||||
const [zoomLevel, setZoomLevel] = useState(1);
|
||||
const { direction, locale } = useContext(ConfigContext);
|
||||
|
||||
const dialogRef = useRef<HTMLDivElement>(null);
|
||||
const closeButtonRef = useRef<HTMLButtonElement>(null);
|
||||
const previouslyFocusedElementRef = useRef<HTMLElement | null>(null);
|
||||
|
||||
const activeImage = images[activeImageIndex];
|
||||
const hasPagination = images.length > 1;
|
||||
const imageLabel = locale?.Accessibility.image
|
||||
?.replace('{0}', String(activeImageIndex + 1))
|
||||
.replace('{1}', String(images.length)) ?? `Image ${activeImageIndex + 1} of ${images.length}`;
|
||||
|
||||
// Focus management
|
||||
useEffect(() => {
|
||||
if (open && dialogRef.current) {
|
||||
dialogRef.current.focus();
|
||||
}
|
||||
const timer = setTimeout(() => {
|
||||
setIsVisible(Boolean(open));
|
||||
|
||||
if (!open && previouslyFocusedElementRef.current?.isConnected) {
|
||||
previouslyFocusedElementRef.current.focus();
|
||||
}
|
||||
if (!open) {
|
||||
previouslyFocusedElementRef.current = null;
|
||||
}
|
||||
}, open ? 0 : 150);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [open]);
|
||||
|
||||
// ESC close support
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const handler = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
onOpenChange?.(false);
|
||||
}
|
||||
};
|
||||
window.addEventListener('keydown', handler);
|
||||
return () => window.removeEventListener('keydown', handler);
|
||||
}, [open, onOpenChange]);
|
||||
|
||||
previouslyFocusedElementRef.current = document.activeElement instanceof HTMLElement
|
||||
? document.activeElement
|
||||
: null;
|
||||
closeButtonRef.current?.focus();
|
||||
}, [open]);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setIsVisible(true);
|
||||
} else {
|
||||
const timer = setTimeout(() => {
|
||||
setIsVisible(false);
|
||||
}, 150);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [open]);
|
||||
if (!open && !isVisible) return;
|
||||
|
||||
const handler = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape' && open) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
onOpenChange?.(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key !== 'Tab' || !dialogRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const focusableElements = Array.from(
|
||||
dialogRef.current.querySelectorAll<HTMLElement>(focusableElementSelector)
|
||||
);
|
||||
|
||||
if (focusableElements.length === 0) {
|
||||
event.preventDefault();
|
||||
dialogRef.current.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
const firstElement = focusableElements[0];
|
||||
const lastElement = focusableElements[focusableElements.length - 1];
|
||||
const activeElement = document.activeElement;
|
||||
|
||||
if (event.shiftKey && (activeElement === firstElement || !dialogRef.current.contains(activeElement))) {
|
||||
event.preventDefault();
|
||||
lastElement.focus();
|
||||
} else if (!event.shiftKey && (activeElement === lastElement || !dialogRef.current.contains(activeElement))) {
|
||||
event.preventDefault();
|
||||
firstElement.focus();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', handler);
|
||||
return () => window.removeEventListener('keydown', handler);
|
||||
}, [isVisible, onOpenChange, open]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (previouslyFocusedElementRef.current?.isConnected) {
|
||||
previouslyFocusedElementRef.current.focus();
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
// wheel
|
||||
useEffect(() => {
|
||||
@@ -99,10 +154,12 @@ export function Gallery(props: IGalleryProps) {
|
||||
setZoomLevel(1);
|
||||
return;
|
||||
}
|
||||
const newZoomLevel = zoomLevel + ratio;
|
||||
if (newZoomLevel < 0.5) return;
|
||||
if (newZoomLevel > 2) return;
|
||||
setZoomLevel(newZoomLevel);
|
||||
|
||||
setZoomLevel((previousZoomLevel) => Math.min(Math.max(0.5, previousZoomLevel + ratio), 2));
|
||||
}
|
||||
|
||||
function handleClose() {
|
||||
onOpenChange?.(false);
|
||||
}
|
||||
|
||||
return createPortal(
|
||||
@@ -111,7 +168,7 @@ export function Gallery(props: IGalleryProps) {
|
||||
dir={direction}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label={locale?.Accessibility.imageGallery}
|
||||
aria-label={locale?.Accessibility.imageGallery ?? 'Image gallery'}
|
||||
tabIndex={-1}
|
||||
ref={dialogRef}
|
||||
className={clsx(
|
||||
@@ -129,9 +186,28 @@ export function Gallery(props: IGalleryProps) {
|
||||
<div
|
||||
className="univer-absolute univer-inset-0 univer-size-full univer-bg-gray-900 univer-opacity-80"
|
||||
aria-hidden="true"
|
||||
onClick={() => onOpenChange?.(false)}
|
||||
onClick={handleClose}
|
||||
/>
|
||||
|
||||
<Button
|
||||
ref={closeButtonRef}
|
||||
data-u-comp="gallery-close"
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
aria-label={locale?.Accessibility.close ?? 'Close'}
|
||||
className={`
|
||||
univer-absolute univer-right-4 univer-top-4 univer-z-10 univer-size-10 univer-rounded-full
|
||||
!univer-border-gray-500 !univer-bg-gray-800 !univer-text-gray-0
|
||||
hover:!univer-bg-gray-700
|
||||
focus-visible:!univer-outline-none focus-visible:!univer-ring-2 focus-visible:!univer-ring-gray-0
|
||||
rtl:univer-left-4 rtl:univer-right-auto
|
||||
`}
|
||||
onClick={handleClose}
|
||||
>
|
||||
<CloseIcon aria-hidden="true" />
|
||||
</Button>
|
||||
|
||||
{/* Content */}
|
||||
<div
|
||||
className="univer-relative univer-flex univer-w-fit univer-items-center univer-justify-center"
|
||||
@@ -146,12 +222,16 @@ export function Gallery(props: IGalleryProps) {
|
||||
transform: `scale(${zoomLevel})`,
|
||||
}}
|
||||
src={activeImage}
|
||||
alt={locale?.Accessibility.image.replace('{0}', String(activeImageIndex + 1)).replace('{1}', String(images.length))}
|
||||
alt={imageLabel}
|
||||
draggable={false}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<span className="univer-sr-only" aria-live="polite" aria-atomic="true">
|
||||
{imageLabel}
|
||||
</span>
|
||||
|
||||
{/* Toolbar */}
|
||||
<footer
|
||||
className={`
|
||||
@@ -170,33 +250,44 @@ export function Gallery(props: IGalleryProps) {
|
||||
`}
|
||||
value={activeImageIndex + 1}
|
||||
total={images.length}
|
||||
previousButtonAriaLabel={locale?.Accessibility.previous ?? 'Previous'}
|
||||
nextButtonAriaLabel={locale?.Accessibility.next ?? 'Next'}
|
||||
onChange={(value) => setActiveImageIndex(value - 1)}
|
||||
/>
|
||||
)}
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
aria-label={locale?.Accessibility.zoomIn}
|
||||
className={buttonClassName}
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
aria-label={locale?.Accessibility.zoomIn ?? 'Zoom in'}
|
||||
className={toolbarButtonClassName}
|
||||
disabled={zoomLevel >= 2}
|
||||
onClick={() => handleToggleZoom(0.25)}
|
||||
>
|
||||
<ZoomInIcon aria-hidden="true" />
|
||||
</button>
|
||||
<button
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
aria-label={locale?.Accessibility.zoomOut}
|
||||
className={buttonClassName}
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
aria-label={locale?.Accessibility.zoomOut ?? 'Zoom out'}
|
||||
className={toolbarButtonClassName}
|
||||
disabled={zoomLevel <= 0.5}
|
||||
onClick={() => handleToggleZoom(-0.25)}
|
||||
>
|
||||
<ZoomOutIcon aria-hidden="true" />
|
||||
</button>
|
||||
<button
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
aria-label={locale?.Accessibility.resetZoom}
|
||||
className={buttonClassName}
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
aria-label={locale?.Accessibility.resetZoom ?? 'Reset zoom'}
|
||||
className={toolbarButtonClassName}
|
||||
disabled={zoomLevel === 1}
|
||||
onClick={() => handleToggleZoom('reset')}
|
||||
>
|
||||
<OneToOneIcon aria-hidden="true" />
|
||||
</button>
|
||||
</Button>
|
||||
</footer>
|
||||
</div>,
|
||||
document.body
|
||||
|
||||
@@ -15,8 +15,9 @@
|
||||
*/
|
||||
|
||||
import type { ComponentProps, PropsWithChildren } from 'react';
|
||||
import { cleanup, fireEvent, render, screen } from '@testing-library/react';
|
||||
import { act, cleanup, fireEvent, render, screen } from '@testing-library/react';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import enUS from '../../../locale/en-US';
|
||||
import { ConfigProvider } from '../../config-provider/ConfigProvider';
|
||||
import { Gallery } from '../Gallery';
|
||||
@@ -36,8 +37,18 @@ function renderGallery(props: ComponentProps<typeof Gallery>) {
|
||||
return render(<Gallery {...props} />, { wrapper: LocaleProvider });
|
||||
}
|
||||
|
||||
function FocusHarness({ open }: { open: boolean }) {
|
||||
return (
|
||||
<>
|
||||
<button type="button">Open gallery trigger</button>
|
||||
<Gallery images={images} open={open} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
describe('Gallery', () => {
|
||||
@@ -77,13 +88,48 @@ describe('Gallery', () => {
|
||||
expect(onOpenChange).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
it('toolbar buttons have correct aria-labels', () => {
|
||||
it('provides named close, pagination, and zoom controls', () => {
|
||||
renderGallery({ images, open: true });
|
||||
expect(screen.getByRole('button', { name: 'Close' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Previous' })).toBeDisabled();
|
||||
expect(screen.getByRole('button', { name: 'Next' })).toBeEnabled();
|
||||
expect(screen.getByRole('button', { name: /zoom in/i })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: /zoom out/i })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: /reset zoom/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls onOpenChange(false) when clicking the close button', () => {
|
||||
const onOpenChange = vi.fn();
|
||||
renderGallery({ images, open: true, onOpenChange });
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Close' }));
|
||||
|
||||
expect(onOpenChange).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
it('contains focus while open and restores it after closing', () => {
|
||||
vi.useFakeTimers();
|
||||
const { rerender } = render(<FocusHarness open={false} />, { wrapper: LocaleProvider });
|
||||
const trigger = screen.getByRole('button', { name: 'Open gallery trigger' });
|
||||
trigger.focus();
|
||||
|
||||
rerender(<FocusHarness open />);
|
||||
|
||||
const closeButton = screen.getByRole('button', { name: 'Close' });
|
||||
const zoomOutButton = screen.getByRole('button', { name: /zoom out/i });
|
||||
expect(closeButton).toHaveFocus();
|
||||
|
||||
fireEvent.keyDown(window, { key: 'Tab', shiftKey: true });
|
||||
expect(zoomOutButton).toHaveFocus();
|
||||
|
||||
fireEvent.keyDown(window, { key: 'Tab' });
|
||||
expect(closeButton).toHaveFocus();
|
||||
|
||||
rerender(<FocusHarness open={false} />);
|
||||
act(() => vi.advanceTimersByTime(150));
|
||||
expect(trigger).toHaveFocus();
|
||||
});
|
||||
|
||||
it('keeps the toolbar controls in logical order in RTL', () => {
|
||||
render(
|
||||
<ConfigProvider locale={enUS.design} direction="rtl" mountContainer={document.body}>
|
||||
|
||||
@@ -15,7 +15,10 @@
|
||||
*/
|
||||
|
||||
import { MoreLeftIcon, MoreRightIcon } from '@univerjs/icons';
|
||||
import { useContext } from 'react';
|
||||
|
||||
import { clsx } from '../../helper/clsx';
|
||||
import { ConfigContext } from '../config-provider/ConfigProvider';
|
||||
|
||||
export interface IPagerProps {
|
||||
className?: string;
|
||||
@@ -23,14 +26,28 @@ export interface IPagerProps {
|
||||
value: number;
|
||||
total: number;
|
||||
loop?: boolean;
|
||||
previousButtonAriaLabel?: string;
|
||||
nextButtonAriaLabel?: string;
|
||||
onChange?(value: number): void;
|
||||
}
|
||||
|
||||
export function Pager(props: IPagerProps) {
|
||||
const { className, value: current = 0, total: count = 0, loop, text: propText, onChange } = props;
|
||||
const {
|
||||
className,
|
||||
value: current = 0,
|
||||
total: count = 0,
|
||||
loop,
|
||||
text: propText,
|
||||
previousButtonAriaLabel,
|
||||
nextButtonAriaLabel,
|
||||
onChange,
|
||||
} = props;
|
||||
const { locale } = useContext(ConfigContext);
|
||||
|
||||
const text = propText ?? `${current}/${count}`;
|
||||
const hasValue = count > 0;
|
||||
const previousButtonDisabled = !loop && current <= 1;
|
||||
const nextButtonDisabled = !loop && current >= count;
|
||||
|
||||
const onClickLeftArrow = () => {
|
||||
if (current === 1) {
|
||||
@@ -66,31 +83,39 @@ export function Pager(props: IPagerProps) {
|
||||
<button
|
||||
data-u-comp="pager-left-arrow"
|
||||
className={`
|
||||
univer-inline-flex univer-size-4 univer-cursor-pointer univer-items-center univer-rounded
|
||||
univer-inline-flex univer-size-6 univer-cursor-pointer univer-items-center univer-rounded
|
||||
univer-border-none univer-bg-transparent univer-p-0 univer-text-current
|
||||
hover:univer-bg-gray-50
|
||||
focus-visible:univer-outline-none focus-visible:univer-ring-2
|
||||
focus-visible:univer-ring-primary-500
|
||||
disabled:univer-cursor-default disabled:univer-opacity-40
|
||||
dark:hover:!univer-bg-gray-600
|
||||
`}
|
||||
type="button"
|
||||
role="button"
|
||||
aria-label={previousButtonAriaLabel ?? locale?.Accessibility.previous ?? 'Previous'}
|
||||
disabled={previousButtonDisabled}
|
||||
onClick={onClickLeftArrow}
|
||||
>
|
||||
<MoreLeftIcon className="rtl:univer-rotate-180" />
|
||||
<MoreLeftIcon className="rtl:univer-rotate-180" aria-hidden="true" />
|
||||
</button>
|
||||
<span className="univer-mx-1">{text}</span>
|
||||
<span className="univer-mx-1" aria-live="polite" aria-atomic="true">{text}</span>
|
||||
<button
|
||||
data-u-comp="pager-right-arrow"
|
||||
className={`
|
||||
univer-inline-flex univer-size-4 univer-cursor-pointer univer-items-center univer-rounded
|
||||
univer-inline-flex univer-size-6 univer-cursor-pointer univer-items-center univer-rounded
|
||||
univer-border-none univer-bg-transparent univer-p-0 univer-text-current
|
||||
hover:univer-bg-gray-50
|
||||
focus-visible:univer-outline-none focus-visible:univer-ring-2
|
||||
focus-visible:univer-ring-primary-500
|
||||
disabled:univer-cursor-default disabled:univer-opacity-40
|
||||
dark:hover:!univer-bg-gray-600
|
||||
`}
|
||||
type="button"
|
||||
role="button"
|
||||
aria-label={nextButtonAriaLabel ?? locale?.Accessibility.next ?? 'Next'}
|
||||
disabled={nextButtonDisabled}
|
||||
onClick={onClickRightArrow}
|
||||
>
|
||||
<MoreRightIcon className="rtl:univer-rotate-180" />
|
||||
<MoreRightIcon className="rtl:univer-rotate-180" aria-hidden="true" />
|
||||
</button>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -14,23 +14,25 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { IPagerProps } from '../Pager';
|
||||
import { cleanup, fireEvent, render } from '@testing-library/react';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import type { ComponentProps } from 'react';
|
||||
import { cleanup, fireEvent, render, screen } from '@testing-library/react';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { Pager } from '../Pager';
|
||||
import '@testing-library/jest-dom/vitest';
|
||||
|
||||
afterEach(cleanup);
|
||||
|
||||
describe('Pager', () => {
|
||||
const defaultProps: IPagerProps = {
|
||||
value: 1,
|
||||
total: 5,
|
||||
onChange: vi.fn(),
|
||||
};
|
||||
let defaultProps: ComponentProps<typeof Pager>;
|
||||
|
||||
// cleanup after each test
|
||||
afterEach(cleanup);
|
||||
beforeEach(() => {
|
||||
defaultProps = {
|
||||
value: 1,
|
||||
total: 5,
|
||||
onChange: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
it('renders correctly with default props', () => {
|
||||
const { getByText } = render(<Pager {...defaultProps} />);
|
||||
@@ -38,32 +40,33 @@ describe('Pager', () => {
|
||||
});
|
||||
|
||||
it('renders correctly with custom text', () => {
|
||||
const props: IPagerProps = { ...defaultProps, text: 'Custom Text' };
|
||||
const props: ComponentProps<typeof Pager> = { ...defaultProps, text: 'Custom Text' };
|
||||
const { getByText } = render(<Pager {...props} />);
|
||||
expect(getByText('Custom Text')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('renders only text when total is 0', () => {
|
||||
const props: IPagerProps = { ...defaultProps, total: 0 };
|
||||
const props: ComponentProps<typeof Pager> = { ...defaultProps, total: 0 };
|
||||
const { getByText, queryByRole } = render(<Pager {...props} />);
|
||||
expect(getByText('1/0')).toBeTruthy();
|
||||
expect(queryByRole('button')).not.toBeTruthy();
|
||||
});
|
||||
|
||||
it('calls onChange with correct value when arrow is clicked', () => {
|
||||
const { container } = render(<Pager {...defaultProps} />);
|
||||
const rightArrow = container.querySelector('[data-u-comp="pager-right-arrow"]') as HTMLButtonElement;
|
||||
const { rerender } = render(<Pager {...defaultProps} />);
|
||||
const rightArrow = screen.getByRole('button', { name: 'Next' });
|
||||
|
||||
fireEvent.click(rightArrow);
|
||||
expect(defaultProps.onChange).toBeCalled();
|
||||
expect(defaultProps.onChange).toHaveBeenLastCalledWith(2);
|
||||
|
||||
const leftArrow = container.querySelector('[data-u-comp="pager-left-arrow"]') as HTMLButtonElement;
|
||||
rerender(<Pager {...defaultProps} value={2} />);
|
||||
const leftArrow = screen.getByRole('button', { name: 'Previous' });
|
||||
fireEvent.click(leftArrow);
|
||||
expect(defaultProps.onChange).toBeCalled();
|
||||
expect(defaultProps.onChange).toHaveBeenLastCalledWith(1);
|
||||
});
|
||||
|
||||
it('loops to last page when clicking left arrow on first page', () => {
|
||||
const props: IPagerProps = { ...defaultProps, loop: true };
|
||||
const props: ComponentProps<typeof Pager> = { ...defaultProps, loop: true };
|
||||
const { container } = render(<Pager {...props} />);
|
||||
const leftArrow = container.querySelector('[data-u-comp="pager-left-arrow"]') as HTMLButtonElement;
|
||||
|
||||
@@ -72,7 +75,7 @@ describe('Pager', () => {
|
||||
});
|
||||
|
||||
it('loops to first page when clicking right arrow on last page', () => {
|
||||
const props: IPagerProps = { ...defaultProps, value: 5, loop: true };
|
||||
const props: ComponentProps<typeof Pager> = { ...defaultProps, value: 5, loop: true };
|
||||
const { container } = render(<Pager {...props} />);
|
||||
const rightArrow = container.querySelector('[data-u-comp="pager-right-arrow"]') as HTMLButtonElement;
|
||||
|
||||
@@ -81,11 +84,12 @@ describe('Pager', () => {
|
||||
});
|
||||
|
||||
it('does not loop when loop prop is false', () => {
|
||||
const props: IPagerProps = { ...defaultProps, value: 5, loop: false };
|
||||
const { container } = render(<Pager {...props} />);
|
||||
const rightArrow = container.querySelector('[data-u-comp="pager-right-arrow"]') as HTMLButtonElement;
|
||||
const props: ComponentProps<typeof Pager> = { ...defaultProps, value: 5, loop: false };
|
||||
render(<Pager {...props} />);
|
||||
const rightArrow = screen.getByRole('button', { name: 'Next' });
|
||||
|
||||
expect(rightArrow).toBeDisabled();
|
||||
fireEvent.click(rightArrow);
|
||||
expect(props.onChange).toBeCalledWith(5);
|
||||
expect(props.onChange).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -20,6 +20,9 @@ const locale: typeof enUS = {
|
||||
design: {
|
||||
Accessibility: {
|
||||
closeBadge: 'إغلاق الشارة',
|
||||
close: 'إغلاق',
|
||||
previous: 'السابق',
|
||||
next: 'التالي',
|
||||
imageGallery: 'معرض الصور',
|
||||
image: 'الصورة {0} من {1}',
|
||||
zoomIn: 'تكبير',
|
||||
|
||||
@@ -20,6 +20,9 @@ const locale: typeof enUS = {
|
||||
design: {
|
||||
Accessibility: {
|
||||
closeBadge: 'Tanca la insígnia',
|
||||
close: 'Tanca',
|
||||
previous: 'Anterior',
|
||||
next: 'Següent',
|
||||
imageGallery: 'Galeria d’imatges',
|
||||
image: 'Imatge {0} de {1}',
|
||||
zoomIn: 'Apropa',
|
||||
|
||||
@@ -20,6 +20,9 @@ const locale: typeof enUS = {
|
||||
design: {
|
||||
Accessibility: {
|
||||
closeBadge: 'Badge schließen',
|
||||
close: 'Schließen',
|
||||
previous: 'Zurück',
|
||||
next: 'Weiter',
|
||||
imageGallery: 'Bildergalerie',
|
||||
image: 'Bild {0} von {1}',
|
||||
zoomIn: 'Vergrößern',
|
||||
|
||||
@@ -18,6 +18,9 @@ const locale = {
|
||||
design: {
|
||||
Accessibility: {
|
||||
closeBadge: 'Close badge',
|
||||
close: 'Close',
|
||||
previous: 'Previous',
|
||||
next: 'Next',
|
||||
imageGallery: 'Image gallery',
|
||||
image: 'Image {0} of {1}',
|
||||
zoomIn: 'Zoom in',
|
||||
|
||||
@@ -20,6 +20,9 @@ const locale: typeof enUS = {
|
||||
design: {
|
||||
Accessibility: {
|
||||
closeBadge: 'Cerrar insignia',
|
||||
close: 'Cerrar',
|
||||
previous: 'Anterior',
|
||||
next: 'Siguiente',
|
||||
imageGallery: 'Galería de imágenes',
|
||||
image: 'Imagen {0} de {1}',
|
||||
zoomIn: 'Acercar',
|
||||
|
||||
@@ -20,6 +20,9 @@ const locale: typeof enUS = {
|
||||
design: {
|
||||
Accessibility: {
|
||||
closeBadge: 'بستن نشان',
|
||||
close: 'بستن',
|
||||
previous: 'قبلی',
|
||||
next: 'بعدی',
|
||||
imageGallery: 'گالری تصاویر',
|
||||
image: 'تصویر {0} از {1}',
|
||||
zoomIn: 'بزرگنمایی',
|
||||
|
||||
@@ -20,6 +20,9 @@ const locale: typeof enUS = {
|
||||
design: {
|
||||
Accessibility: {
|
||||
closeBadge: 'Fermer le badge',
|
||||
close: 'Fermer',
|
||||
previous: 'Précédent',
|
||||
next: 'Suivant',
|
||||
imageGallery: 'Galerie d’images',
|
||||
image: 'Image {0} sur {1}',
|
||||
zoomIn: 'Zoom avant',
|
||||
|
||||
@@ -20,6 +20,9 @@ const locale: typeof enUS = {
|
||||
design: {
|
||||
Accessibility: {
|
||||
closeBadge: 'Tutup lencana',
|
||||
close: 'Tutup',
|
||||
previous: 'Sebelumnya',
|
||||
next: 'Berikutnya',
|
||||
imageGallery: 'Galeri gambar',
|
||||
image: 'Gambar {0} dari {1}',
|
||||
zoomIn: 'Perbesar',
|
||||
|
||||
@@ -20,6 +20,9 @@ const locale: typeof enUS = {
|
||||
design: {
|
||||
Accessibility: {
|
||||
closeBadge: 'Chiudi badge',
|
||||
close: 'Chiudi',
|
||||
previous: 'Precedente',
|
||||
next: 'Successivo',
|
||||
imageGallery: 'Galleria immagini',
|
||||
image: 'Immagine {0} di {1}',
|
||||
zoomIn: 'Ingrandisci',
|
||||
|
||||
@@ -20,6 +20,9 @@ const locale: typeof enUS = {
|
||||
design: {
|
||||
Accessibility: {
|
||||
closeBadge: 'バッジを閉じる',
|
||||
close: '閉じる',
|
||||
previous: '前へ',
|
||||
next: '次へ',
|
||||
imageGallery: '画像ギャラリー',
|
||||
image: '画像 {0}/{1}',
|
||||
zoomIn: '拡大',
|
||||
|
||||
@@ -20,6 +20,9 @@ const locale: typeof enUS = {
|
||||
design: {
|
||||
Accessibility: {
|
||||
closeBadge: '배지 닫기',
|
||||
close: '닫기',
|
||||
previous: '이전',
|
||||
next: '다음',
|
||||
imageGallery: '이미지 갤러리',
|
||||
image: '이미지 {0}/{1}',
|
||||
zoomIn: '확대',
|
||||
|
||||
@@ -20,6 +20,9 @@ const locale: typeof enUS = {
|
||||
design: {
|
||||
Accessibility: {
|
||||
closeBadge: 'Zamknij plakietkę',
|
||||
close: 'Zamknij',
|
||||
previous: 'Poprzedni',
|
||||
next: 'Następny',
|
||||
imageGallery: 'Galeria obrazów',
|
||||
image: 'Obraz {0} z {1}',
|
||||
zoomIn: 'Powiększ',
|
||||
|
||||
@@ -20,6 +20,9 @@ const locale: typeof enUS = {
|
||||
design: {
|
||||
Accessibility: {
|
||||
closeBadge: 'Fechar selo',
|
||||
close: 'Fechar',
|
||||
previous: 'Anterior',
|
||||
next: 'Próximo',
|
||||
imageGallery: 'Galeria de imagens',
|
||||
image: 'Imagem {0} de {1}',
|
||||
zoomIn: 'Ampliar',
|
||||
|
||||
@@ -20,6 +20,9 @@ const locale: typeof enUS = {
|
||||
design: {
|
||||
Accessibility: {
|
||||
closeBadge: 'Закрыть метку',
|
||||
close: 'Закрыть',
|
||||
previous: 'Предыдущее',
|
||||
next: 'Следующее',
|
||||
imageGallery: 'Галерея изображений',
|
||||
image: 'Изображение {0} из {1}',
|
||||
zoomIn: 'Увеличить',
|
||||
|
||||
@@ -20,6 +20,9 @@ const locale: typeof enUS = {
|
||||
design: {
|
||||
Accessibility: {
|
||||
closeBadge: 'Zavrieť odznak',
|
||||
close: 'Zavrieť',
|
||||
previous: 'Predchádzajúci',
|
||||
next: 'Nasledujúci',
|
||||
imageGallery: 'Galéria obrázkov',
|
||||
image: 'Obrázok {0} z {1}',
|
||||
zoomIn: 'Priblížiť',
|
||||
|
||||
@@ -20,6 +20,9 @@ const locale: typeof enUS = {
|
||||
design: {
|
||||
Accessibility: {
|
||||
closeBadge: 'Đóng huy hiệu',
|
||||
close: 'Đóng',
|
||||
previous: 'Trước',
|
||||
next: 'Tiếp theo',
|
||||
imageGallery: 'Thư viện ảnh',
|
||||
image: 'Ảnh {0} trên {1}',
|
||||
zoomIn: 'Phóng to',
|
||||
|
||||
@@ -20,6 +20,9 @@ const locale: typeof enUS = {
|
||||
design: {
|
||||
Accessibility: {
|
||||
closeBadge: '关闭徽章',
|
||||
close: '关闭',
|
||||
previous: '上一个',
|
||||
next: '下一个',
|
||||
imageGallery: '图片库',
|
||||
image: '第 {0} 张图片,共 {1} 张',
|
||||
zoomIn: '放大',
|
||||
|
||||
@@ -20,6 +20,9 @@ const locale: typeof enUS = {
|
||||
design: {
|
||||
Accessibility: {
|
||||
closeBadge: '關閉徽章',
|
||||
close: '關閉',
|
||||
previous: '上一個',
|
||||
next: '下一個',
|
||||
imageGallery: '圖片庫',
|
||||
image: '第 {0} 張圖片,共 {1} 張',
|
||||
zoomIn: '放大',
|
||||
|
||||
@@ -20,6 +20,9 @@ const locale: typeof enUS = {
|
||||
design: {
|
||||
Accessibility: {
|
||||
closeBadge: '關閉徽章',
|
||||
close: '關閉',
|
||||
previous: '上一個',
|
||||
next: '下一個',
|
||||
imageGallery: '圖片庫',
|
||||
image: '第 {0} 張圖片,共 {1} 張',
|
||||
zoomIn: '放大',
|
||||
|
||||
Reference in New Issue
Block a user