Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(Carousel): Added Keyboard Navigation #103

Merged
merged 2 commits into from
Aug 22, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
216 changes: 108 additions & 108 deletions packages/main/__karma_snapshots__/Carousel.md

Large diffs are not rendered by default.

42 changes: 36 additions & 6 deletions packages/main/src/components/Carousel/Carousel.jss.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,29 @@ const styles = ({ parameters }: JSSTheme) => ({
minWidth: '15.5rem',
fontFamily: parameters.sapUiFontFamily,
backgroundColor: parameters.sapUiBaseBG,
'&:hover': {
'& [data-value="paginationArrow"]': {
opacity: 1
'&:focus': {
outline: 'none',
'&:before': {
border: '1px solid #000000',
position: 'absolute',
content: '" "',
top: '0',
right: '0',
bottom: '0',
left: '0',
zIndex: '2',
pointerEvents: 'none'
},
'&:after': {
border: '1px dotted #ffffff',
position: 'absolute',
content: '" "',
top: '0',
right: '0',
bottom: '0',
left: '0',
zIndex: '2',
pointerEvents: 'none'
}
}
},
Expand All @@ -34,9 +54,19 @@ const styles = ({ parameters }: JSSTheme) => ({
fontSize: '1rem',
visibility: 'hidden'
},
carouselItemContentIndicator: {
padding: '0 4rem',
width: 'calc(100% - 8rem)'
carouselArrowPlacementContent: {
'&:hover': {
'& [data-value="paginationArrow"]': {
opacity: 1,
'& ui5-icon': {
transform: 'rotate(0deg)'
}
}
},
'& $carouselItem': {
padding: '0 4rem',
width: 'calc(100% - 8rem)'
}
}
});

Expand Down
20 changes: 20 additions & 0 deletions packages/main/src/components/Carousel/Carousel.karma.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -150,4 +150,24 @@ describe('Carousel', () => {
);
expect(wrapper.render().find('[data-value="paginationArrow"]')).to.have.length(0);
});

it('Navigation to next page with Keyboard', () => {
const callback = sinon.spy();
const wrapper = mountThemedComponent(renderCarousel({ activePage: 0, onPageChanged: callback }));
wrapper
.find('div[role="list"]')
.last()
.simulate('keydown', { key: 'ArrowRight' });
expect(getEventFromCallback(callback).getParameter('selectedIndex')).to.equal(1);
});

it('Navigation to previous page with Keyboard', () => {
const callback = sinon.spy();
const wrapper = mountThemedComponent(renderCarousel({ activePage: 1, onPageChanged: callback }));
wrapper
.find('div[role="list"]')
.first()
.simulate('keydown', { key: 'ArrowLeft' });
expect(getEventFromCallback(callback).getParameter('selectedIndex')).to.equal(0);
});
});
32 changes: 13 additions & 19 deletions packages/main/src/components/Carousel/CarouselPagination.jss.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,14 @@ const styles = ({ parameters }: JSSTheme) => ({
borderRadius: '50%',
alignSelf: 'center',
boxSizing: 'border-box',
backgroundColor: parameters.sapUiContentNonInteractiveIconColor,
'&$paginationIconActive': {
margin: '0 0.25rem',
width: '0.5rem',
height: '0.5rem',
backgroundColor: parameters.sapUiSelected
}
backgroundColor: parameters.sapUiContentNonInteractiveIconColor
},
paginationIconActive: {
margin: '0 0.25rem',
width: '0.5rem',
height: '0.5rem',
backgroundColor: parameters.sapUiSelected
},
paginationIconActive: {},
paginationArrow: {
boxShadow: 'none',
border: `1px solid ${parameters.sapUiButtonBorderColor}`,
Expand All @@ -66,6 +65,9 @@ const styles = ({ parameters }: JSSTheme) => ({
color: parameters.sapUiButtonEmphasizedTextColor
}
},
'@global html[dir="rtl"] div[data-value="paginationArrow"] ui5-icon': {
transform: 'rotate(180deg)'
},
paginationArrowContent: {
'& $paginationArrow': {
boxShadow: parameters.sapUiShadowLevel1,
Expand All @@ -86,21 +88,13 @@ const styles = ({ parameters }: JSSTheme) => ({
}
},
paginationArrowContentNoBar: {
composes: ['$paginationArrowContent'],
'& $paginationArrow': {
boxShadow: parameters.sapUiShadowLevel1,
'&:first-child': {
position: 'absolute',
top: 'calc(50% - 1rem)',
left: '0.5rem',
opacity: 0,
zIndex: ZIndex.InputModal
top: 'calc(50% - 1rem)'
},
'&:last-child': {
position: 'absolute',
top: 'calc(50% - 1rem)',
right: '0.5rem',
opacity: 0,
zIndex: ZIndex.InputModal
top: 'calc(50% - 1rem)'
}
}
}
Expand Down
38 changes: 15 additions & 23 deletions packages/main/src/components/Carousel/CarouselPagination.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { Label } from '../../lib/Label';
import { PlacementType } from '../../lib/PlacementType';
import styles from './CarouselPagination.jss';

const useStyles = createUseStyles<JSSTheme, keyof ReturnType<typeof styles>>(styles);
const useStyles = createUseStyles<JSSTheme, keyof ReturnType<typeof styles>>(styles, { name: 'CarouselPagination' });

export interface CarouselPaginationPropTypes {
/**
Expand Down Expand Up @@ -56,17 +56,6 @@ const CarouselPagination: FC<CarouselPaginationPropTypes> = (props) => {
const numberOfChildren = React.Children.count(children);
const showTextIndicator = numberOfChildren >= TEXT_INDICATOR_THRESHOLD;

const paginationClasses = StyleClassHelper.of(classes.pagination);
if (arrowsPlacement === CarouselArrowsPlacement.Content) {
paginationClasses.put(classes.paginationArrowContent);
}
if (pageIndicatorPlacement === PlacementType.Top) {
paginationClasses.put(classes.paginationTop);
}
if (pageIndicatorPlacement === PlacementType.Bottom) {
paginationClasses.put(classes.paginationBottom);
}

const shouldRenderPaginationBar = useMemo(() => {
return showPageIndicator || arrowsPlacement === CarouselArrowsPlacement.PageIndicator;
}, [showPageIndicator, arrowsPlacement]);
Expand All @@ -84,13 +73,20 @@ const CarouselPagination: FC<CarouselPaginationPropTypes> = (props) => {
);
}

const paginationClasses = StyleClassHelper.of(classes.pagination);
if (arrowsPlacement === CarouselArrowsPlacement.Content) {
paginationClasses.put(classes.paginationArrowContent);
}
if (pageIndicatorPlacement === PlacementType.Top) {
paginationClasses.put(classes.paginationTop);
}
if (pageIndicatorPlacement === PlacementType.Bottom) {
paginationClasses.put(classes.paginationBottom);
}

return (
<div className={paginationClasses.valueOf()}>
<div
data-value={arrowsPlacement === CarouselArrowsPlacement.Content ? 'paginationArrow' : null}
className={classes.paginationArrow}
onClick={goToPreviousPage}
>
<div data-value="paginationArrow" className={classes.paginationArrow} onClick={goToPreviousPage}>
<Icon src="sap-icon://slim-arrow-left" />
</div>

Expand All @@ -102,19 +98,15 @@ const CarouselPagination: FC<CarouselPaginationPropTypes> = (props) => {
Children.map(children, (item, index) => (
<span
key={index}
className={`${activePage === index ? classes.paginationIconActive : null} ${classes.paginationIcon}`}
className={`${classes.paginationIcon}${activePage === index ? ` ${classes.paginationIconActive}` : ''}`}
aria-label={`Item ${index + 1} of ${numberOfChildren} displayed`}
>
{index + 1}
</span>
))}
</div>

<div
data-value={arrowsPlacement === CarouselArrowsPlacement.Content ? 'paginationArrow' : null}
className={classes.paginationArrow}
onClick={goToNextPage}
>
<div data-value="paginationArrow" className={classes.paginationArrow} onClick={goToNextPage}>
<Icon src="sap-icon://slim-arrow-right" />
</div>
</div>
Expand Down
1 change: 1 addition & 0 deletions packages/main/src/components/Carousel/demo.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { PlacementType } from '../../lib/PlacementType';
function renderCarousel() {
return (
<Carousel
width="90%"
activePage={number('active', 0)}
onPageChanged={action('onPageChanged')}
arrowsPlacement={select(
Expand Down
51 changes: 45 additions & 6 deletions packages/main/src/components/Carousel/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ const Carousel: FC<CarouselPropTypes> = forwardRef((props: CarouselPropTypes, re

const carouselItemClasses = StyleClassHelper.of(classes.carouselItem);
if (arrowsPlacement === CarouselArrowsPlacement.Content) {
carouselItemClasses.put(classes.carouselItemContentIndicator);
classNameString.put(classes.carouselArrowPlacementContent);
}

const selectPageAtIndex = useCallback(
Expand All @@ -96,27 +96,64 @@ const Carousel: FC<CarouselPropTypes> = forwardRef((props: CarouselPropTypes, re

const childElementCount = Children.count(children);
const goToNextPage = useCallback(
(e) => {
(e, skipManualInversion = false) => {
if (
document.dir === 'rtl' &&
arrowsPlacement === CarouselArrowsPlacement.Content &&
e.type === 'click' &&
!skipManualInversion
) {
return goToPreviousPage(e, true);
}
if (loop === false && currentlyActivePage === childElementCount - 1) {
return;
}
const nextPage = currentlyActivePage === childElementCount - 1 ? 0 : currentlyActivePage + 1;
selectPageAtIndex(nextPage, e);
},
[loop, currentlyActivePage, selectPageAtIndex, childElementCount]
[loop, currentlyActivePage, selectPageAtIndex, childElementCount, arrowsPlacement]
);

const goToPreviousPage = useCallback(
(e) => {
(e, skipManualInversion = false) => {
if (
document.dir === 'rtl' &&
arrowsPlacement === CarouselArrowsPlacement.Content &&
e.type === 'click' &&
!skipManualInversion
) {
return goToNextPage(e, true);
}
if (loop === false && currentlyActivePage === 0) {
return;
}
const previousPage = currentlyActivePage === 0 ? childElementCount - 1 : currentlyActivePage - 1;
selectPageAtIndex(previousPage, e);
},
[loop, childElementCount, currentlyActivePage, selectPageAtIndex]
[loop, childElementCount, currentlyActivePage, selectPageAtIndex, arrowsPlacement, goToNextPage]
);

const onKeyDown = useCallback(
(e) => {
if (e.key === 'ArrowRight') {
if (document.dir === 'rtl') {
goToPreviousPage(e);
} else {
goToNextPage(e);
}
}
if (e.key === 'ArrowLeft') {
if (document.dir === 'rtl') {
goToNextPage(e);
} else {
goToPreviousPage(e);
}
}
},
[goToPreviousPage, goToNextPage]
);

const translateXPrefix = document.dir === 'rtl' ? '' : '-';
return (
<div
className={classNameString.toString()}
Expand All @@ -125,6 +162,8 @@ const Carousel: FC<CarouselPropTypes> = forwardRef((props: CarouselPropTypes, re
slot={props['slot']}
ref={ref}
role="list"
tabIndex={0}
onKeyDown={onKeyDown}
>
{childElementCount > 1 && pageIndicatorPlacement === PlacementType.Top && (
<CarouselPagination
Expand All @@ -140,7 +179,7 @@ const Carousel: FC<CarouselPropTypes> = forwardRef((props: CarouselPropTypes, re
<div
className={classes.carouselInner}
style={{
transform: `translateX(-${currentlyActivePage * 100}%)`
transform: `translateX(${translateXPrefix}${currentlyActivePage * 100}%)`
}}
>
{Children.map(children, (item, index) => (
Expand Down