Skip to content

Commit

Permalink
feat(controls): Create react version of page controls (#1292)
Browse files Browse the repository at this point in the history
  • Loading branch information
cweeii authored Nov 18, 2020
1 parent 39eec82 commit 20739ec
Show file tree
Hide file tree
Showing 17 changed files with 544 additions and 19 deletions.
2 changes: 1 addition & 1 deletion src/lib/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -385,7 +385,7 @@ export function loadScripts(urls, disableAMD = false) {
* Function to decode key down events into keys
*
* @public
* @param {Event} event - Keydown event
* @param {KeyboardEvent<Element>} event - Keydown event
* @return {string} Decoded keydown key
*/
export function decodeKeydown(event) {
Expand Down
2 changes: 2 additions & 0 deletions src/lib/viewers/controls/_styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
justify-content: center;
width: $width;
height: $height;
margin: 0;
padding: 0;
color: $white;
background: transparent;
border: 1px solid transparent;
Expand Down
9 changes: 9 additions & 0 deletions src/lib/viewers/controls/page/PageControls.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
@import '../styles';

.bp-PageControls {
@include bp-ControlGroup;
}

.bp-PageControls-button {
@include bp-ControlButton($width: 32px);
}
40 changes: 40 additions & 0 deletions src/lib/viewers/controls/page/PageControls.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import React from 'react';
import IconArrowDown24 from '../icons/IconArrowDown24';
import IconArrowUp24 from '../icons/IconArrowUp24';
import PageControlsForm from './PageControlsForm';
import './PageControls.scss';

export type Props = {
onPageChange: (newPageNumber: number) => void;
onPageSubmit: (newPageNumber: number) => void;
pageCount: number;
pageNumber: number;
};

export default function PageControls({ onPageChange, onPageSubmit, pageCount, pageNumber }: Props): JSX.Element {
return (
<div className="bp-PageControls">
<button
className="bp-PageControls-button"
data-testid="bp-PageControls-previous"
disabled={pageNumber === 1}
onClick={(): void => onPageChange(pageNumber - 1)}
title={__('previous_page')}
type="button"
>
<IconArrowUp24 />
</button>
<PageControlsForm onPageSubmit={onPageSubmit} pageCount={pageCount} pageNumber={pageNumber} />
<button
className="bp-PageControls-button"
data-testid="bp-PageControls-next"
disabled={pageNumber === pageCount}
onClick={(): void => onPageChange(pageNumber + 1)}
title={__('next_page')}
type="button"
>
<IconArrowDown24 />
</button>
</div>
);
}
52 changes: 52 additions & 0 deletions src/lib/viewers/controls/page/PageControlsForm.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
@import '../styles';

.bp-PageControlsForm {
// input[type='number'] is needed to raise specificity level to override Annotations styles
input[type='number'].bp-PageControlsForm-input {
width: 44px;
margin: 5px;
padding: 7px;
color: $twos;
font-size: 14px;
text-align: center;
border: 1px solid $seesee;
border-radius: 3px;
transition: border-color linear .15s;
-webkit-font-smoothing: antialiased;

/* stylelint-disable property-no-vendor-prefix */
// Removes the spinner for number type inputs in Firefox
-moz-appearance: textfield;

// Removes the spinner for number type inputs in Webkit browsers
&::-webkit-outer-spin-button,
&::-webkit-inner-spin-button {
-webkit-appearance: none;
}

/* stylelint-enable property-no-vendor-prefix */

&:focus {
border: 1px solid $primary-color;
outline: 0;
box-shadow: none;
}
}
}

.bp-PageControlsForm-button {
@include bp-ControlButton($width: auto);
}

.bp-PageControlsForm-button-label {
display: flex;
align-items: center;
justify-content: center;
margin: 5px;
padding: 7px 5px;
color: $white;
font-size: 14px;
white-space: nowrap;
background-color: $fours;
border-radius: 3px;
}
120 changes: 120 additions & 0 deletions src/lib/viewers/controls/page/PageControlsForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import React from 'react';
import { decodeKeydown } from '../../../util';
import './PageControlsForm.scss';

export type Props = {
onPageSubmit: (newPageNumber: number) => void;
pageCount: number;
pageNumber: number;
};

export const ENTER = 'Enter';
export const ESCAPE = 'Escape';

export default function PageControlsForm({ onPageSubmit, pageNumber, pageCount }: Props): JSX.Element {
const [isInputShown, setIsInputShown] = React.useState(false);
const [inputValue, setInputValue] = React.useState(pageNumber);

const buttonElRef = React.useRef<HTMLButtonElement>(null);
const inputElRef = React.useRef<HTMLInputElement>(null);
const isRetryRef = React.useRef(false);

const setPage = (allowRetry = false): void => {
if (!Number.isNaN(inputValue) && inputValue >= 1 && inputValue <= pageCount && inputValue !== pageNumber) {
onPageSubmit(inputValue);
} else {
setInputValue(pageNumber); // Reset the invalid input value to the current page

if (allowRetry) {
isRetryRef.current = true;
}
}

setIsInputShown(false);
};

const handleNumInputChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
setInputValue(parseInt(event.target.value, 10));
};

const handleNumInputBlur = (): void => {
setPage();
};

const handleNumInputKeyDown = (event: React.KeyboardEvent<HTMLInputElement>): void => {
const key = decodeKeydown(event);

switch (key) {
case ENTER:
event.stopPropagation();
event.preventDefault();

setPage(true);
break;
case ESCAPE:
event.stopPropagation();
event.preventDefault();

isRetryRef.current = true;
setIsInputShown(false);
setInputValue(pageNumber);
break;
default:
break;
}
};

React.useEffect(() => {
setInputValue(pageNumber); // Keep internal state in sync with prop as it changes
}, [pageNumber]);

React.useLayoutEffect(() => {
if (inputElRef.current && isInputShown) {
inputElRef.current.select();
}

if (buttonElRef.current && !isInputShown) {
if (isRetryRef.current) {
buttonElRef.current.focus();
}

isRetryRef.current = false;
}
}, [isInputShown]);

return (
<div className="bp-PageControlsForm">
{isInputShown ? (
<input
ref={inputElRef}
className="bp-PageControlsForm-input"
data-testid="bp-PageControlsForm-input"
min="1"
onBlur={handleNumInputBlur}
onChange={handleNumInputChange}
onKeyDown={handleNumInputKeyDown}
pattern="[0-9]*"
size={3}
title={__('enter_page_num')}
type="number"
value={inputValue.toString()}
/>
) : (
<button
ref={buttonElRef}
className="bp-PageControlsForm-button"
data-testid="bp-PageControlsForm-button"
disabled={pageCount <= 1}
onClick={(): void => setIsInputShown(true)}
title={__('enter_page_num')}
type="button"
>
<span
className="bp-PageControlsForm-button-label"
data-testid="bp-PageControlsForm-button-label"
>{`${pageNumber} / ${pageCount}`}</span>
</button>
)}
</div>
);
}
62 changes: 62 additions & 0 deletions src/lib/viewers/controls/page/__tests__/PageControls-test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import React from 'react';
import noop from 'lodash/noop';
import { shallow, ShallowWrapper } from 'enzyme';
import PageControls from '../PageControls';
import PageControlsForm from '../PageControlsForm';

describe('PageControls', () => {
const getWrapper = (props = {}): ShallowWrapper =>
shallow(<PageControls onPageChange={noop} onPageSubmit={noop} pageCount={3} pageNumber={1} {...props} />);
const getPreviousPageButton = (wrapper: ShallowWrapper): ShallowWrapper =>
wrapper.find('[data-testid="bp-PageControls-previous"]');
const getNextPageButton = (wrapper: ShallowWrapper): ShallowWrapper =>
wrapper.find('[data-testid="bp-PageControls-next"]');

describe('event handlers', () => {
test('should handle previous page click', () => {
const pageNumber = 2;
const onPageChange = jest.fn();
const wrapper = getWrapper({ onPageChange, pageCount: 3, pageNumber });

getPreviousPageButton(wrapper).simulate('click');

expect(onPageChange).toBeCalledWith(pageNumber - 1);
});

test('should handle next page click', () => {
const pageNumber = 2;
const onPageChange = jest.fn();
const wrapper = getWrapper({ onPageChange, pageCount: 3, pageNumber });

getNextPageButton(wrapper).simulate('click');

expect(onPageChange).toBeCalledWith(pageNumber + 1);
});
});

describe('render', () => {
test.each`
pageNumber | isPrevButtonDisabled | isNextButtonDisabled
${1} | ${true} | ${false}
${3} | ${false} | ${true}
${2} | ${false} | ${false}
`(
'should handle the disable prop correctly when pageNumber is $pageNumber and pageCount is $pageCount',
({ pageNumber, isNextButtonDisabled, isPrevButtonDisabled }) => {
const wrapper = getWrapper({ pageCount: 3, pageNumber });

expect(getPreviousPageButton(wrapper).prop('disabled')).toBe(isPrevButtonDisabled);
expect(getNextPageButton(wrapper).prop('disabled')).toBe(isNextButtonDisabled);
},
);

test('should return a valid wrapper', () => {
const wrapper = getWrapper();

expect(getPreviousPageButton(wrapper).exists()).toBe(true);
expect(getNextPageButton(wrapper).exists()).toBe(true);
expect(wrapper.find(PageControlsForm).exists()).toBe(true);
expect(wrapper.hasClass('bp-PageControls')).toBe(true);
});
});
});
Loading

0 comments on commit 20739ec

Please sign in to comment.