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';