From 18867ec7be9dcd274234cc09c82be8b90b74426a Mon Sep 17 00:00:00 2001 From: roman Date: Thu, 13 Aug 2026 16:35:10 +0200 Subject: [PATCH] refactor(slide-carousel): migrate SlideCarousel from Flow to TypeScript --- ...rouselHeader.js => CarouselHeader.js.flow} | 0 .../slide-carousel/CarouselHeader.tsx | 14 +++ .../{Slide.js => Slide.js.flow} | 0 src/components/slide-carousel/Slide.tsx | 17 ++++ .../{SlideButton.js => SlideButton.js.flow} | 0 src/components/slide-carousel/SlideButton.tsx | 27 ++++++ ...SlideCarousel.js => SlideCarousel.js.flow} | 0 .../slide-carousel/SlideCarousel.tsx | 82 ++++++++++++++++ ...tive.js => SlideCarouselPrimitive.js.flow} | 0 .../slide-carousel/SlideCarouselPrimitive.tsx | 61 ++++++++++++ ...ideNavigator.js => SlideNavigator.js.flow} | 0 .../slide-carousel/SlideNavigator.tsx | 86 +++++++++++++++++ .../{SlidePanels.js => SlidePanels.js.flow} | 0 src/components/slide-carousel/SlidePanels.tsx | 95 +++++++++++++++++++ ...Header.test.js => CarouselHeader.test.tsx} | 4 +- .../{Slide.test.js => Slide.test.tsx} | 3 +- ...ideButton.test.js => SlideButton.test.tsx} | 3 +- ...arousel.test.js => SlideCarousel.test.tsx} | 30 +++--- ...est.js => SlideCarouselPrimitive.test.tsx} | 26 +++-- ...igator.test.js => SlideNavigator.test.tsx} | 54 ++++++----- ...idePanels.test.js => SlidePanels.test.tsx} | 41 ++++---- .../{index.js => index.js.flow} | 0 src/components/slide-carousel/index.ts | 4 + 23 files changed, 473 insertions(+), 74 deletions(-) rename src/components/slide-carousel/{CarouselHeader.js => CarouselHeader.js.flow} (100%) create mode 100644 src/components/slide-carousel/CarouselHeader.tsx rename src/components/slide-carousel/{Slide.js => Slide.js.flow} (100%) create mode 100644 src/components/slide-carousel/Slide.tsx rename src/components/slide-carousel/{SlideButton.js => SlideButton.js.flow} (100%) create mode 100644 src/components/slide-carousel/SlideButton.tsx rename src/components/slide-carousel/{SlideCarousel.js => SlideCarousel.js.flow} (100%) create mode 100644 src/components/slide-carousel/SlideCarousel.tsx rename src/components/slide-carousel/{SlideCarouselPrimitive.js => SlideCarouselPrimitive.js.flow} (100%) create mode 100644 src/components/slide-carousel/SlideCarouselPrimitive.tsx rename src/components/slide-carousel/{SlideNavigator.js => SlideNavigator.js.flow} (100%) create mode 100644 src/components/slide-carousel/SlideNavigator.tsx rename src/components/slide-carousel/{SlidePanels.js => SlidePanels.js.flow} (100%) create mode 100644 src/components/slide-carousel/SlidePanels.tsx rename src/components/slide-carousel/__tests__/{CarouselHeader.test.js => CarouselHeader.test.tsx} (78%) rename src/components/slide-carousel/__tests__/{Slide.test.js => Slide.test.tsx} (85%) rename src/components/slide-carousel/__tests__/{SlideButton.test.js => SlideButton.test.tsx} (86%) rename src/components/slide-carousel/__tests__/{SlideCarousel.test.js => SlideCarousel.test.tsx} (80%) rename src/components/slide-carousel/__tests__/{SlideCarouselPrimitive.test.js => SlideCarouselPrimitive.test.tsx} (55%) rename src/components/slide-carousel/__tests__/{SlideNavigator.test.js => SlideNavigator.test.tsx} (65%) rename src/components/slide-carousel/__tests__/{SlidePanels.test.js => SlidePanels.test.tsx} (69%) rename src/components/slide-carousel/{index.js => index.js.flow} (100%) create mode 100644 src/components/slide-carousel/index.ts diff --git a/src/components/slide-carousel/CarouselHeader.js b/src/components/slide-carousel/CarouselHeader.js.flow similarity index 100% rename from src/components/slide-carousel/CarouselHeader.js rename to src/components/slide-carousel/CarouselHeader.js.flow diff --git a/src/components/slide-carousel/CarouselHeader.tsx b/src/components/slide-carousel/CarouselHeader.tsx new file mode 100644 index 0000000000..98a86dea1a --- /dev/null +++ b/src/components/slide-carousel/CarouselHeader.tsx @@ -0,0 +1,14 @@ +import * as React from 'react'; + +export interface CarouselHeaderProps { + /** Title displayed above the carousel */ + title: string; +} + +const CarouselHeader = ({ title }: CarouselHeaderProps) => ( +
+

{title}

+
+); + +export default CarouselHeader; diff --git a/src/components/slide-carousel/Slide.js b/src/components/slide-carousel/Slide.js.flow similarity index 100% rename from src/components/slide-carousel/Slide.js rename to src/components/slide-carousel/Slide.js.flow diff --git a/src/components/slide-carousel/Slide.tsx b/src/components/slide-carousel/Slide.tsx new file mode 100644 index 0000000000..a0f6a2dfa3 --- /dev/null +++ b/src/components/slide-carousel/Slide.tsx @@ -0,0 +1,17 @@ +import classNames from 'classnames'; +import * as React from 'react'; + +export interface SlideProps extends React.HTMLAttributes { + /** Content displayed within the slide */ + children?: React.ReactNode; + /** Custom class name for the slide */ + className?: string; +} + +const Slide = ({ children, className, ...rest }: SlideProps) => ( +
+ {children} +
+); + +export default Slide; diff --git a/src/components/slide-carousel/SlideButton.js b/src/components/slide-carousel/SlideButton.js.flow similarity index 100% rename from src/components/slide-carousel/SlideButton.js rename to src/components/slide-carousel/SlideButton.js.flow diff --git a/src/components/slide-carousel/SlideButton.tsx b/src/components/slide-carousel/SlideButton.tsx new file mode 100644 index 0000000000..630cb1b47d --- /dev/null +++ b/src/components/slide-carousel/SlideButton.tsx @@ -0,0 +1,27 @@ +import * as React from 'react'; + +import { ButtonType } from '../button'; +import PlainButton, { type PlainButtonProps } from '../plain-button'; + +export interface SlideButtonProps extends Omit { + /** Ref for the underlying button element */ + buttonRef?: React.LegacyRef; + /** Whether the button represents the selected slide */ + isSelected?: boolean; + /** Handler invoked when the button is clicked */ + onClick?: (event: React.SyntheticEvent) => void; +} + +const SlideButton = ({ buttonRef, onClick, isSelected = false, ...rest }: SlideButtonProps) => ( + +); + +export default SlideButton; diff --git a/src/components/slide-carousel/SlideCarousel.js b/src/components/slide-carousel/SlideCarousel.js.flow similarity index 100% rename from src/components/slide-carousel/SlideCarousel.js rename to src/components/slide-carousel/SlideCarousel.js.flow diff --git a/src/components/slide-carousel/SlideCarousel.tsx b/src/components/slide-carousel/SlideCarousel.tsx new file mode 100644 index 0000000000..a500b6cf96 --- /dev/null +++ b/src/components/slide-carousel/SlideCarousel.tsx @@ -0,0 +1,82 @@ +import * as React from 'react'; +import uniqueId from 'lodash/uniqueId'; + +import SlideCarouselPrimitive from './SlideCarouselPrimitive'; + +import './SlideCarousel.scss'; + +export interface SlideCarouselProps { + /** Slides displayed by the carousel */ + children?: React.ReactNode; + /** Custom class name for the carousel */ + className?: string; + /** Used as the value for the content area's style height property */ + contentHeight?: string; + /** Index selected when the carousel is initialized */ + initialIndex: number; + /** Title displayed above the carousel */ + title?: string; +} + +interface SlideCarouselState { + selectedIndex: number; +} + +class SlideCarousel extends React.Component { + static defaultProps = { + className: '', + initialIndex: 0, + }; + + constructor(props: SlideCarouselProps) { + super(props); + + this.id = uniqueId('slidecarousel'); + + this.state = { + selectedIndex: props.initialIndex || 0, + }; + } + + /* + * If the selected index in the state has somehow gotten set to an + * out of bounds value (either because we were passed a bad value, + * or the number of children has reduced), compute a new selected + * index which is a floored value between 0 <= index < num children + */ + getBoundedSelectedIndex() { + const { children } = this.props; + const { selectedIndex } = this.state; + + const lastChildIndex = Math.max(React.Children.count(children) - 1, 0); + const boundedSelectedIndex = Math.max(selectedIndex || 0, 0); + + return boundedSelectedIndex > lastChildIndex ? lastChildIndex : Math.floor(boundedSelectedIndex); + } + + setSelectedIndex = (index: number) => { + this.setState({ selectedIndex: index }); + }; + + id: string; + + render() { + const { children, className, contentHeight, title } = this.props; + const selectedIndex = this.getBoundedSelectedIndex(); + + return ( + + {children} + + ); + } +} + +export default SlideCarousel; diff --git a/src/components/slide-carousel/SlideCarouselPrimitive.js b/src/components/slide-carousel/SlideCarouselPrimitive.js.flow similarity index 100% rename from src/components/slide-carousel/SlideCarouselPrimitive.js rename to src/components/slide-carousel/SlideCarouselPrimitive.js.flow diff --git a/src/components/slide-carousel/SlideCarouselPrimitive.tsx b/src/components/slide-carousel/SlideCarouselPrimitive.tsx new file mode 100644 index 0000000000..9fb96ef9b9 --- /dev/null +++ b/src/components/slide-carousel/SlideCarouselPrimitive.tsx @@ -0,0 +1,61 @@ +import classNames from 'classnames'; +import * as React from 'react'; +import noop from 'lodash/noop'; + +import CarouselHeader from './CarouselHeader'; +import SlideNavigator from './SlideNavigator'; +import SlidePanels from './SlidePanels'; + +export interface SlideCarouselPrimitiveProps { + /** Slides displayed by the carousel */ + children?: React.ReactNode; + /** Custom class name for the carousel */ + className?: string; + /** The constant value to use for the content area's style height property */ + contentHeight?: string; + /** Prefix used to create unique button and panel IDs */ + idPrefix?: string; + /** Handler invoked with the index of the selected slide */ + onSelection: (index: number) => void; + /** Index of the selected slide */ + selectedIndex: number; + /** Title displayed above the carousel */ + title?: string; +} + +const SlideCarouselPrimitive = ({ + children, + className = '', + contentHeight, + idPrefix = '', + onSelection = noop, + selectedIndex, + title, +}: SlideCarouselPrimitiveProps) => { + const buttonIdGenerator = (value: number) => `${idPrefix && `${idPrefix}-`}selector-${value}`; + const panelIdGenerator = (value: number) => `${idPrefix && `${idPrefix}-`}slide-panel-${value}`; + return ( +
+ {title && } + + {children} + + +
+ ); +}; + +SlideCarouselPrimitive.displayName = 'SlideCarouselPrimitive'; + +export default SlideCarouselPrimitive; diff --git a/src/components/slide-carousel/SlideNavigator.js b/src/components/slide-carousel/SlideNavigator.js.flow similarity index 100% rename from src/components/slide-carousel/SlideNavigator.js rename to src/components/slide-carousel/SlideNavigator.js.flow diff --git a/src/components/slide-carousel/SlideNavigator.tsx b/src/components/slide-carousel/SlideNavigator.tsx new file mode 100644 index 0000000000..ec49c89d0f --- /dev/null +++ b/src/components/slide-carousel/SlideNavigator.tsx @@ -0,0 +1,86 @@ +import range from 'lodash/range'; +import * as React from 'react'; + +import SlideButton from './SlideButton'; + +export interface SlideNavigatorProps { + /** Pure function that returns a button ID unique to the given index */ + getButtonIdFromValue: (index: number) => string; + /** Pure function that returns a panel ID unique to the given index */ + getPanelIdFromValue: (index: number) => string; + /** The number of slides. Each is associated to an index, starting from 0 */ + numOptions: number; + /** Handler invoked with the index of the selected slide */ + onSelection: (index: number) => void; + /** Index of the selected slide */ + selectedIndex: number; +} + +class SlideNavigator extends React.Component { + buttonElements: HTMLButtonElement[] = []; + + focusOnButtonElement = (index: number) => { + if (index + 1 > this.buttonElements.length || index < 0) { + return; + } + + this.buttonElements[index].focus(); + }; + + handleKeyDown = (event: React.KeyboardEvent) => { + const { numOptions, selectedIndex } = this.props; + + let nextIndex = null; + switch (event.key) { + case 'ArrowRight': + nextIndex = (selectedIndex + 1) % numOptions; + break; + + case 'ArrowLeft': + nextIndex = (selectedIndex - 1 + numOptions) % numOptions; + break; + + default: + return; + } + + this.handleSelection(nextIndex); + event.preventDefault(); + event.stopPropagation(); + }; + + handleSelection = (index: number) => { + this.focusOnButtonElement(index); + this.props.onSelection(index); + }; + + render() { + const { getButtonIdFromValue, getPanelIdFromValue, numOptions, onSelection, selectedIndex } = this.props; + + return ( + + ); + } +} + +export default SlideNavigator; diff --git a/src/components/slide-carousel/SlidePanels.js b/src/components/slide-carousel/SlidePanels.js.flow similarity index 100% rename from src/components/slide-carousel/SlidePanels.js rename to src/components/slide-carousel/SlidePanels.js.flow diff --git a/src/components/slide-carousel/SlidePanels.tsx b/src/components/slide-carousel/SlidePanels.tsx new file mode 100644 index 0000000000..481b7a598f --- /dev/null +++ b/src/components/slide-carousel/SlidePanels.tsx @@ -0,0 +1,95 @@ +import * as React from 'react'; + +export interface SlidePanelsProps { + /** Slides rendered within the panels */ + children?: React.ReactNode; + /** Pure function that returns a panel ID unique to the given index */ + getPanelIdFromValue: (index: number) => string; + /** Handler invoked with the index of the selected slide */ + onSelection?: (index: number) => void; + /** Index of the selected slide */ + selectedIndex: number; + /** Inline styles applied to the panels container */ + style?: React.CSSProperties; +} + +class SlidePanels extends React.Component { + static displayName = 'SlidePanels'; + + containerEl: HTMLDivElement | null = null; + + focusOnContainerElement = () => { + if (this.containerEl) { + this.containerEl.focus(); + } + }; + + handleKeyDown = (event: React.KeyboardEvent) => { + const { children, selectedIndex } = this.props; + + const numOptions = React.Children.count(children); + + let nextIndex = null; + switch (event.key) { + case 'ArrowRight': + nextIndex = (selectedIndex + 1) % numOptions; + break; + + case 'ArrowLeft': + nextIndex = (selectedIndex - 1 + numOptions) % numOptions; + break; + + default: + break; + } + + if (nextIndex !== null) { + this.handleSelection(nextIndex); + event.preventDefault(); + event.stopPropagation(); + } + }; + + handleSelection = (index: number) => { + const { onSelection } = this.props; + this.focusOnContainerElement(); + if (onSelection) { + onSelection(index); + } + }; + + render() { + const { getPanelIdFromValue, children, selectedIndex, style } = this.props; + + return ( + // eslint-disable-next-line jsx-a11y/no-static-element-interactions +
{ + this.containerEl = containerEl; + }} + className="slide-panels" + onKeyDown={this.handleKeyDown} + style={style} + // eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex + tabIndex={'0' as unknown as number} + > + {React.Children.map(children, (child, i) => { + const isSelected = i === selectedIndex; + return ( +
+ {child} +
+ ); + })} +
+ ); + } +} + +export default SlidePanels; diff --git a/src/components/slide-carousel/__tests__/CarouselHeader.test.js b/src/components/slide-carousel/__tests__/CarouselHeader.test.tsx similarity index 78% rename from src/components/slide-carousel/__tests__/CarouselHeader.test.js rename to src/components/slide-carousel/__tests__/CarouselHeader.test.tsx index 605b56ff7a..64bf5dd148 100644 --- a/src/components/slide-carousel/__tests__/CarouselHeader.test.js +++ b/src/components/slide-carousel/__tests__/CarouselHeader.test.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import { shallow } from 'enzyme'; import CarouselHeader from '../CarouselHeader'; @@ -7,7 +8,8 @@ describe('components/slide-carousel/CarouselHeader', () => { title: 'Blah', }; - const getWrapper = props => shallow(); + const getWrapper = (props: Record = {}) => + shallow(); test('should render a title', () => { const testTitle = 'LoveAndHappiness'; diff --git a/src/components/slide-carousel/__tests__/Slide.test.js b/src/components/slide-carousel/__tests__/Slide.test.tsx similarity index 85% rename from src/components/slide-carousel/__tests__/Slide.test.js rename to src/components/slide-carousel/__tests__/Slide.test.tsx index bab93c9808..ebdbeddf2f 100644 --- a/src/components/slide-carousel/__tests__/Slide.test.js +++ b/src/components/slide-carousel/__tests__/Slide.test.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import { shallow } from 'enzyme'; import Slide from '../Slide'; @@ -8,7 +9,7 @@ describe('components/slide-carousel/Slide', () => { children:

Holla die Waldfee

, }; - const getWrapper = props => shallow(); + const getWrapper = (props: Record = {}) => shallow(); test('should render a container div', () => { const wrapper = getWrapper(); diff --git a/src/components/slide-carousel/__tests__/SlideButton.test.js b/src/components/slide-carousel/__tests__/SlideButton.test.tsx similarity index 86% rename from src/components/slide-carousel/__tests__/SlideButton.test.js rename to src/components/slide-carousel/__tests__/SlideButton.test.tsx index 8adfa07574..2b9e27c417 100644 --- a/src/components/slide-carousel/__tests__/SlideButton.test.js +++ b/src/components/slide-carousel/__tests__/SlideButton.test.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import { shallow } from 'enzyme'; import sinon from 'sinon'; import SlideButton from '../SlideButton'; @@ -14,7 +15,7 @@ describe('components/slide-carousel/SlideButton', () => { isSelected: true, }; - const getWrapper = props => shallow(); + const getWrapper = (props: Record = {}) => shallow(); test('should have the is-selected class when selected', () => { const wrapper = getWrapper({ isSelected: true }); diff --git a/src/components/slide-carousel/__tests__/SlideCarousel.test.js b/src/components/slide-carousel/__tests__/SlideCarousel.test.tsx similarity index 80% rename from src/components/slide-carousel/__tests__/SlideCarousel.test.js rename to src/components/slide-carousel/__tests__/SlideCarousel.test.tsx index fefc450ac4..fae304560a 100644 --- a/src/components/slide-carousel/__tests__/SlideCarousel.test.js +++ b/src/components/slide-carousel/__tests__/SlideCarousel.test.tsx @@ -1,26 +1,24 @@ import * as React from 'react'; +import { shallow } from 'enzyme'; import range from 'lodash/range'; -import sinon from 'sinon'; -import SlideCarousel from '../SlideCarousel'; +import SlideCarousel, { type SlideCarouselProps } from '../SlideCarousel'; import SlideCarouselPrimitive from '../SlideCarouselPrimitive'; import Slide from '../Slide'; -const getSlides = numSlides => range(numSlides).map(i => shallow(`Slide ${i}`)); +type SlideCarouselInstance = InstanceType; -describe('components/slide-carousel/SlideCarousel', () => { - const sandbox = sinon.sandbox.create(); +const getSlides = (numSlides: number) => + range(numSlides).map(i => shallow(`Slide ${i}`)) as unknown as React.ReactNode; - const defaultProps = { +describe('components/slide-carousel/SlideCarousel', () => { + const defaultProps: SlideCarouselProps = { children: getSlides(5), initialIndex: 1, }; - afterEach(() => { - sandbox.verifyAndRestore(); - }); - - const getWrapper = props => shallow(); + const getWrapper = (props: Partial = {}) => + shallow(); describe('construction()', () => { test('should initialize selectedIndex as the initialIndex prop', () => { @@ -64,12 +62,11 @@ describe('components/slide-carousel/SlideCarousel', () => { test('should generate ID and pass to child', () => { const wrapper = getWrapper(); - expect(wrapper.prop('idPrefix')).toEqual(wrapper.instance().id); + expect(wrapper.prop('idPrefix')).toEqual((wrapper.instance() as SlideCarouselInstance).id); }); test('should pass to immediate child 0 if the number of children is zero', () => { const wrapper = getWrapper({ - id: undefined, children: getSlides(0), }); @@ -77,7 +74,7 @@ describe('components/slide-carousel/SlideCarousel', () => { }); test('should pass to immediate child 0 if state.selectedIndex is less than 0', () => { - const wrapper = getWrapper({ id: undefined }); + const wrapper = getWrapper(); wrapper.setState({ selectedIndex: -1, }); @@ -107,11 +104,12 @@ describe('components/slide-carousel/SlideCarousel', () => { describe('setSelectedIndex()', () => { test('should update selectedIndex when setSelectedIndex is called', () => { const wrapper = getWrapper({ children: getSlides(7) }); - wrapper.instance().setSelectedIndex(3); + const instance = wrapper.instance() as SlideCarouselInstance; + instance.setSelectedIndex(3); expect(wrapper.state('selectedIndex')).toBe(3); - wrapper.instance().setSelectedIndex(1); + instance.setSelectedIndex(1); expect(wrapper.state('selectedIndex')).toBe(1); }); diff --git a/src/components/slide-carousel/__tests__/SlideCarouselPrimitive.test.js b/src/components/slide-carousel/__tests__/SlideCarouselPrimitive.test.tsx similarity index 55% rename from src/components/slide-carousel/__tests__/SlideCarouselPrimitive.test.js rename to src/components/slide-carousel/__tests__/SlideCarouselPrimitive.test.tsx index 6c84f41a97..eb84da32de 100644 --- a/src/components/slide-carousel/__tests__/SlideCarouselPrimitive.test.js +++ b/src/components/slide-carousel/__tests__/SlideCarouselPrimitive.test.tsx @@ -1,27 +1,28 @@ import * as React from 'react'; +import { shallow } from 'enzyme'; import range from 'lodash/range'; import CarouselHeader from '../CarouselHeader'; import Slide from '../Slide'; -import SlideCarouselPrimitive from '../SlideCarouselPrimitive'; +import SlideCarouselPrimitive, { type SlideCarouselPrimitiveProps } from '../SlideCarouselPrimitive'; import SlideNavigator from '../SlideNavigator'; -const getSlides = numSlides => range(numSlides).map(i => shallow(`Slide ${i}`)); +const getSlides = (numSlides: number) => + range(numSlides).map(i => shallow(`Slide ${i}`)) as unknown as React.ReactNode; describe('components/slide-carousel/SlideCarouselPrimitive', () => { - const defaultProps = { + const defaultProps: Partial = { children: getSlides(5), selectedIndex: 1, }; - const getWrapper = props => shallow(); + const getWrapper = (props: Partial = {}) => { + const componentProps = { ...defaultProps, ...props } as SlideCarouselPrimitiveProps; + return shallow(); + }; test('should add the given class to the containing div', () => { - expect( - getWrapper({ className: 'someClass' }) - .first() - .hasClass('someClass'), - ).toBe(true); + expect(getWrapper({ className: 'someClass' }).first().hasClass('someClass')).toBe(true); }); test('should render a CarouselHeader with a given title', () => { @@ -32,7 +33,7 @@ describe('components/slide-carousel/SlideCarouselPrimitive', () => { test('should not render a CarouselHeader when no title is given', () => { const wrapper = getWrapper({ title: '' }); - expect(wrapper.find(CarouselHeader).length).toBe(0); + expect(wrapper.find(CarouselHeader)).toHaveLength(0); }); test('should pass 0 as numOptions to navigator when childless', () => { @@ -40,6 +41,11 @@ describe('components/slide-carousel/SlideCarouselPrimitive', () => { expect(wrapper.find(SlideNavigator).prop('numOptions')).toBe(0); }); + test('should pass 1 as numOptions to navigator for a single child', () => { + const wrapper = getWrapper({ children: Single slide }); + expect(wrapper.find(SlideNavigator).prop('numOptions')).toBe(1); + }); + test('should pass number of children to navigator', () => { const wrapper = getWrapper({ children: getSlides(4) }); expect(wrapper.find(SlideNavigator).prop('numOptions')).toBe(4); diff --git a/src/components/slide-carousel/__tests__/SlideNavigator.test.js b/src/components/slide-carousel/__tests__/SlideNavigator.test.tsx similarity index 65% rename from src/components/slide-carousel/__tests__/SlideNavigator.test.js rename to src/components/slide-carousel/__tests__/SlideNavigator.test.tsx index 966ef55e87..969e68c3fe 100644 --- a/src/components/slide-carousel/__tests__/SlideNavigator.test.js +++ b/src/components/slide-carousel/__tests__/SlideNavigator.test.tsx @@ -1,9 +1,12 @@ import * as React from 'react'; +import { shallow } from 'enzyme'; import sinon from 'sinon'; import SlideButton from '../SlideButton'; import SlideNavigator from '../SlideNavigator'; +type SlideNavigatorInstance = InstanceType; + const sandbox = sinon.sandbox.create(); describe('components/slide-carousel/SlideNavigator', () => { @@ -12,14 +15,15 @@ describe('components/slide-carousel/SlideNavigator', () => { }); const defaultProps = { - getButtonIdFromValue: val => `button-${val}`, - getPanelIdFromValue: val => `panel-${val}`, - onSelection: i => `blah${i}`, + getButtonIdFromValue: (value: number) => `button-${value}`, + getPanelIdFromValue: (value: number) => `panel-${value}`, + onSelection: (index: number) => `blah${index}`, numOptions: 5, selectedIndex: 0, }; - const getWrapper = props => shallow(); + const getWrapper = (props: Record = {}) => + shallow(); describe('handleKeyDown', () => { [ @@ -64,9 +68,10 @@ describe('components/slide-carousel/SlideNavigator', () => { selectedIndex: currIndex, numOptions, }); - const instance = wrapper.instance(); + const instance = wrapper.instance() as SlideNavigatorInstance; - instance.handleSelection = sandbox.spy(); + const handleSelectionSpy = sandbox.spy(); + instance.handleSelection = handleSelectionSpy; const shouldStopEvent = ['ArrowLeft', 'ArrowRight'].includes(key); const onKeyEvent = { key, @@ -74,12 +79,12 @@ describe('components/slide-carousel/SlideNavigator', () => { stopPropagation: shouldStopEvent ? sandbox.mock() : sandbox.mock().never(), }; - instance.handleKeyDown(onKeyEvent); + instance.handleKeyDown(onKeyEvent as unknown as React.KeyboardEvent); if (expectedSelection === null) { - sinon.assert.notCalled(instance.handleSelection); + sinon.assert.notCalled(handleSelectionSpy); } else { - sinon.assert.calledWithExactly(instance.handleSelection, expectedSelection); + sinon.assert.calledWithExactly(handleSelectionSpy, expectedSelection); } }); }); @@ -92,7 +97,7 @@ describe('components/slide-carousel/SlideNavigator', () => { const wrapperInstance = getWrapper({ onSelection: onSelectionSpy, - }).instance(); + }).instance() as SlideNavigatorInstance; wrapperInstance.focusOnButtonElement = focusOnButtonElementSpy; const index = 2; @@ -105,52 +110,51 @@ describe('components/slide-carousel/SlideNavigator', () => { test('should create as many buttons as the given number of options', () => { const wrapper = getWrapper({ numOptions: 7 }); - expect(wrapper.children().filter(SlideButton).length).toBe(7); + expect(wrapper.children().filter(SlideButton)).toHaveLength(7); }); test('should call handleKeyDown on key press', () => { const wrapper = getWrapper(); - sandbox.spy(wrapper.instance(), 'handleKeyDown'); + const instance = wrapper.instance() as SlideNavigatorInstance; + const handleKeyDownSpy = sandbox.spy(instance, 'handleKeyDown'); wrapper.setProps({}); wrapper.simulate('keyDown', { key: 'A' }); - sinon.assert.calledOnce(wrapper.instance().handleKeyDown); + sinon.assert.calledOnce(handleKeyDownSpy); }); test('should use the getButtonIdFromValue prop to generate ids for slide buttons', () => { - const getButtonIdFromValue = i => `unique${i}`; + const getButtonIdFromValue = (index: number) => `unique${index}`; const wrapper = getWrapper({ numOptions: 6, getButtonIdFromValue, }); - expect(wrapper.find(SlideButton).everyWhere((el, i) => el.prop('id') === getButtonIdFromValue(i))).toBe(true); + const buttonIds = wrapper.find(SlideButton).map(element => element.prop('id')); + expect(buttonIds.every((id, index) => id === getButtonIdFromValue(index))).toBe(true); }); test('should use the getPanelIdFromValue prop to set ids on aria-controls', () => { - const getPanelIdFromValue = i => `unique${i}`; + const getPanelIdFromValue = (index: number) => `unique${index}`; const wrapper = getWrapper({ numOptions: 6, getPanelIdFromValue, }); - expect( - wrapper.find(SlideButton).everyWhere((el, i) => el.prop('aria-controls') === getPanelIdFromValue(i)), - ).toBe(true); + const controlledPanelIds = wrapper.find(SlideButton).map(element => element.prop('aria-controls')); + expect(controlledPanelIds.every((id, index) => id === getPanelIdFromValue(index))).toBe(true); }); test('should only mark the button associated to the current selection as selected', () => { const testIndex = 4; const wrapper = getWrapper({ numOptions: 6, selectedIndex: testIndex }); - expect(wrapper.find(SlideButton).everyWhere((el, i) => el.prop('isSelected') === (i === testIndex))).toBe(true); + const selectedStates = wrapper.find(SlideButton).map(element => element.prop('isSelected')); + expect(selectedStates.every((isSelected, index) => isSelected === (index === testIndex))).toBe(true); }); test('should remove all but the button associated to the selected slide from tabbing order', () => { const testIndex = 2; const wrapper = getWrapper({ numOptions: 6, selectedIndex: testIndex }); - expect( - wrapper - .find(SlideButton) - .everyWhere((el, i) => (i === testIndex ? el.prop('tabIndex') === '0' : el.prop('tabIndex') === '-1')), - ).toBe(true); + const tabIndexes = wrapper.find(SlideButton).map(element => element.prop('tabIndex') as unknown as string); + expect(tabIndexes.every((tabIndex, index) => tabIndex === (index === testIndex ? '0' : '-1'))).toBe(true); }); }); diff --git a/src/components/slide-carousel/__tests__/SlidePanels.test.js b/src/components/slide-carousel/__tests__/SlidePanels.test.tsx similarity index 69% rename from src/components/slide-carousel/__tests__/SlidePanels.test.js rename to src/components/slide-carousel/__tests__/SlidePanels.test.tsx index 5c606e32a2..9ebc38d264 100644 --- a/src/components/slide-carousel/__tests__/SlidePanels.test.js +++ b/src/components/slide-carousel/__tests__/SlidePanels.test.tsx @@ -1,13 +1,16 @@ import * as React from 'react'; +import { shallow } from 'enzyme'; import range from 'lodash/range'; import sinon from 'sinon'; import SlidePanels from '../SlidePanels'; import Slide from '../Slide'; +type SlidePanelsInstance = InstanceType; + const sandbox = sinon.sandbox.create(); -const getSlides = numSlides => range(numSlides).map(i => shallow(`Slide ${i}`)); +const getSlides = (numSlides: number) => range(numSlides).map(i => shallow(`Slide ${i}`)); describe('components/slide-carousel/SlidePanels', () => { afterEach(() => { @@ -15,14 +18,14 @@ describe('components/slide-carousel/SlidePanels', () => { }); const defaultProps = { - getPanelIdFromValue: val => `panel-${val}`, - onSelection: i => `blah${i}`, + getPanelIdFromValue: (value: number) => `panel-${value}`, + onSelection: (index: number) => `blah${index}`, selectedIndex: 0, }; - const getNode = props => ; + const getNode = (props: Record = {}) => ; - const getWrapper = props => shallow(getNode(props)); + const getWrapper = (props: Record = {}) => shallow(getNode(props)); describe('handleKeyDown', () => { [ @@ -67,9 +70,10 @@ describe('components/slide-carousel/SlidePanels', () => { selectedIndex: currIndex, children: getSlides(numSlides), }); - const instance = wrapper.instance(); + const instance = wrapper.instance() as SlidePanelsInstance; - instance.handleSelection = sandbox.spy(); + const handleSelectionSpy = sandbox.spy(); + instance.handleSelection = handleSelectionSpy; const shouldStopEvent = ['ArrowLeft', 'ArrowRight'].includes(key); const onKeyEvent = { key, @@ -77,12 +81,12 @@ describe('components/slide-carousel/SlidePanels', () => { stopPropagation: shouldStopEvent ? sandbox.mock() : sandbox.mock().never(), }; - instance.handleKeyDown(onKeyEvent); + instance.handleKeyDown(onKeyEvent as unknown as React.KeyboardEvent); if (expectedSelection === null) { - sinon.assert.notCalled(instance.handleSelection); + sinon.assert.notCalled(handleSelectionSpy); } else { - sinon.assert.calledWithExactly(instance.handleSelection, expectedSelection); + sinon.assert.calledWithExactly(handleSelectionSpy, expectedSelection); } }); }); @@ -94,7 +98,7 @@ describe('components/slide-carousel/SlidePanels', () => { const wrapperInstance = getWrapper({ onSelection: onSelectionSpy, - }).instance(); + }).instance() as SlidePanelsInstance; wrapperInstance.focusOnContainerElement = focusOnContainerElementSpy; const index = 2; @@ -106,7 +110,7 @@ describe('components/slide-carousel/SlidePanels', () => { test('should render a div for every child', () => { const wrapper = getWrapper({ children: getSlides(5) }); - expect(wrapper.find('div.slide-panel').length).toBe(5); + expect(wrapper.find('div.slide-panel')).toHaveLength(5); }); test('should only show the selected slide', () => { @@ -114,20 +118,17 @@ describe('components/slide-carousel/SlidePanels', () => { children: getSlides(5), selectedIndex: 3, }); - expect( - wrapper.children().everyWhere((el, i) => { - const isHidden = el.prop('aria-hidden'); - return i === 3 ? !isHidden : isHidden; - }), - ).toBe(true); + const hiddenStates = wrapper.children().map(element => element.prop('aria-hidden')); + expect(hiddenStates.every((isHidden, index) => (index === 3 ? !isHidden : isHidden))).toBe(true); }); test('should use the getPanelIdFromValue prop to generate ids for slides', () => { - const getPanelIdFromValue = i => `unique${i}`; + const getPanelIdFromValue = (index: number) => `unique${index}`; const wrapper = getWrapper({ children: getSlides(5), getPanelIdFromValue, }); - expect(wrapper.children().everyWhere((el, i) => el.prop('id') === getPanelIdFromValue(i))).toBe(true); + const panelIds = wrapper.children().map(element => element.prop('id')); + expect(panelIds.every((id, index) => id === getPanelIdFromValue(index))).toBe(true); }); }); diff --git a/src/components/slide-carousel/index.js b/src/components/slide-carousel/index.js.flow similarity index 100% rename from src/components/slide-carousel/index.js rename to src/components/slide-carousel/index.js.flow diff --git a/src/components/slide-carousel/index.ts b/src/components/slide-carousel/index.ts new file mode 100644 index 0000000000..4d16451043 --- /dev/null +++ b/src/components/slide-carousel/index.ts @@ -0,0 +1,4 @@ +export { default as Slide } from './Slide'; +export type { SlideProps } from './Slide'; +export { default as SlideCarousel } from './SlideCarousel'; +export type { SlideCarouselProps } from './SlideCarousel';