-
Notifications
You must be signed in to change notification settings - Fork 116
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(controls): Create react version of page controls (#1292)
- Loading branch information
Showing
17 changed files
with
544 additions
and
19 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
62
src/lib/viewers/controls/page/__tests__/PageControls-test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.