Skip to content

Commit e1299ed

Browse files
feat(Carousel): Added Keyboard Navigation (#103)
1 parent 83553c0 commit e1299ed

File tree

7 files changed

+238
-162
lines changed

7 files changed

+238
-162
lines changed

packages/main/__karma_snapshots__/Carousel.md

Lines changed: 108 additions & 108 deletions
Large diffs are not rendered by default.

packages/main/src/components/Carousel/Carousel.jss.ts

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,29 @@ const styles = ({ parameters }: JSSTheme) => ({
1010
minWidth: '15.5rem',
1111
fontFamily: parameters.sapUiFontFamily,
1212
backgroundColor: parameters.sapUiBaseBG,
13-
'&:hover': {
14-
'& [data-value="paginationArrow"]': {
15-
opacity: 1
13+
'&:focus': {
14+
outline: 'none',
15+
'&:before': {
16+
border: '1px solid #000000',
17+
position: 'absolute',
18+
content: '" "',
19+
top: '0',
20+
right: '0',
21+
bottom: '0',
22+
left: '0',
23+
zIndex: '2',
24+
pointerEvents: 'none'
25+
},
26+
'&:after': {
27+
border: '1px dotted #ffffff',
28+
position: 'absolute',
29+
content: '" "',
30+
top: '0',
31+
right: '0',
32+
bottom: '0',
33+
left: '0',
34+
zIndex: '2',
35+
pointerEvents: 'none'
1636
}
1737
}
1838
},
@@ -34,9 +54,19 @@ const styles = ({ parameters }: JSSTheme) => ({
3454
fontSize: '1rem',
3555
visibility: 'hidden'
3656
},
37-
carouselItemContentIndicator: {
38-
padding: '0 4rem',
39-
width: 'calc(100% - 8rem)'
57+
carouselArrowPlacementContent: {
58+
'&:hover': {
59+
'& [data-value="paginationArrow"]': {
60+
opacity: 1,
61+
'& ui5-icon': {
62+
transform: 'rotate(0deg)'
63+
}
64+
}
65+
},
66+
'& $carouselItem': {
67+
padding: '0 4rem',
68+
width: 'calc(100% - 8rem)'
69+
}
4070
}
4171
});
4272

packages/main/src/components/Carousel/Carousel.karma.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,4 +150,24 @@ describe('Carousel', () => {
150150
);
151151
expect(wrapper.render().find('[data-value="paginationArrow"]')).to.have.length(0);
152152
});
153+
154+
it('Navigation to next page with Keyboard', () => {
155+
const callback = sinon.spy();
156+
const wrapper = mountThemedComponent(renderCarousel({ activePage: 0, onPageChanged: callback }));
157+
wrapper
158+
.find('div[role="list"]')
159+
.last()
160+
.simulate('keydown', { key: 'ArrowRight' });
161+
expect(getEventFromCallback(callback).getParameter('selectedIndex')).to.equal(1);
162+
});
163+
164+
it('Navigation to previous page with Keyboard', () => {
165+
const callback = sinon.spy();
166+
const wrapper = mountThemedComponent(renderCarousel({ activePage: 1, onPageChanged: callback }));
167+
wrapper
168+
.find('div[role="list"]')
169+
.first()
170+
.simulate('keydown', { key: 'ArrowLeft' });
171+
expect(getEventFromCallback(callback).getParameter('selectedIndex')).to.equal(0);
172+
});
153173
});

packages/main/src/components/Carousel/CarouselPagination.jss.ts

Lines changed: 13 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -39,15 +39,14 @@ const styles = ({ parameters }: JSSTheme) => ({
3939
borderRadius: '50%',
4040
alignSelf: 'center',
4141
boxSizing: 'border-box',
42-
backgroundColor: parameters.sapUiContentNonInteractiveIconColor,
43-
'&$paginationIconActive': {
44-
margin: '0 0.25rem',
45-
width: '0.5rem',
46-
height: '0.5rem',
47-
backgroundColor: parameters.sapUiSelected
48-
}
42+
backgroundColor: parameters.sapUiContentNonInteractiveIconColor
43+
},
44+
paginationIconActive: {
45+
margin: '0 0.25rem',
46+
width: '0.5rem',
47+
height: '0.5rem',
48+
backgroundColor: parameters.sapUiSelected
4949
},
50-
paginationIconActive: {},
5150
paginationArrow: {
5251
boxShadow: 'none',
5352
border: `1px solid ${parameters.sapUiButtonBorderColor}`,
@@ -66,6 +65,9 @@ const styles = ({ parameters }: JSSTheme) => ({
6665
color: parameters.sapUiButtonEmphasizedTextColor
6766
}
6867
},
68+
'@global html[dir="rtl"] div[data-value="paginationArrow"] ui5-icon': {
69+
transform: 'rotate(180deg)'
70+
},
6971
paginationArrowContent: {
7072
'& $paginationArrow': {
7173
boxShadow: parameters.sapUiShadowLevel1,
@@ -86,21 +88,13 @@ const styles = ({ parameters }: JSSTheme) => ({
8688
}
8789
},
8890
paginationArrowContentNoBar: {
91+
composes: ['$paginationArrowContent'],
8992
'& $paginationArrow': {
90-
boxShadow: parameters.sapUiShadowLevel1,
9193
'&:first-child': {
92-
position: 'absolute',
93-
top: 'calc(50% - 1rem)',
94-
left: '0.5rem',
95-
opacity: 0,
96-
zIndex: ZIndex.InputModal
94+
top: 'calc(50% - 1rem)'
9795
},
9896
'&:last-child': {
99-
position: 'absolute',
100-
top: 'calc(50% - 1rem)',
101-
right: '0.5rem',
102-
opacity: 0,
103-
zIndex: ZIndex.InputModal
97+
top: 'calc(50% - 1rem)'
10498
}
10599
}
106100
}

packages/main/src/components/Carousel/CarouselPagination.tsx

Lines changed: 15 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { Label } from '../../lib/Label';
88
import { PlacementType } from '../../lib/PlacementType';
99
import styles from './CarouselPagination.jss';
1010

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

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

59-
const paginationClasses = StyleClassHelper.of(classes.pagination);
60-
if (arrowsPlacement === CarouselArrowsPlacement.Content) {
61-
paginationClasses.put(classes.paginationArrowContent);
62-
}
63-
if (pageIndicatorPlacement === PlacementType.Top) {
64-
paginationClasses.put(classes.paginationTop);
65-
}
66-
if (pageIndicatorPlacement === PlacementType.Bottom) {
67-
paginationClasses.put(classes.paginationBottom);
68-
}
69-
7059
const shouldRenderPaginationBar = useMemo(() => {
7160
return showPageIndicator || arrowsPlacement === CarouselArrowsPlacement.PageIndicator;
7261
}, [showPageIndicator, arrowsPlacement]);
@@ -84,13 +73,20 @@ const CarouselPagination: FC<CarouselPaginationPropTypes> = (props) => {
8473
);
8574
}
8675

76+
const paginationClasses = StyleClassHelper.of(classes.pagination);
77+
if (arrowsPlacement === CarouselArrowsPlacement.Content) {
78+
paginationClasses.put(classes.paginationArrowContent);
79+
}
80+
if (pageIndicatorPlacement === PlacementType.Top) {
81+
paginationClasses.put(classes.paginationTop);
82+
}
83+
if (pageIndicatorPlacement === PlacementType.Bottom) {
84+
paginationClasses.put(classes.paginationBottom);
85+
}
86+
8787
return (
8888
<div className={paginationClasses.valueOf()}>
89-
<div
90-
data-value={arrowsPlacement === CarouselArrowsPlacement.Content ? 'paginationArrow' : null}
91-
className={classes.paginationArrow}
92-
onClick={goToPreviousPage}
93-
>
89+
<div data-value="paginationArrow" className={classes.paginationArrow} onClick={goToPreviousPage}>
9490
<Icon src="sap-icon://slim-arrow-left" />
9591
</div>
9692

@@ -102,19 +98,15 @@ const CarouselPagination: FC<CarouselPaginationPropTypes> = (props) => {
10298
Children.map(children, (item, index) => (
10399
<span
104100
key={index}
105-
className={`${activePage === index ? classes.paginationIconActive : null} ${classes.paginationIcon}`}
101+
className={`${classes.paginationIcon}${activePage === index ? ` ${classes.paginationIconActive}` : ''}`}
106102
aria-label={`Item ${index + 1} of ${numberOfChildren} displayed`}
107103
>
108104
{index + 1}
109105
</span>
110106
))}
111107
</div>
112108

113-
<div
114-
data-value={arrowsPlacement === CarouselArrowsPlacement.Content ? 'paginationArrow' : null}
115-
className={classes.paginationArrow}
116-
onClick={goToNextPage}
117-
>
109+
<div data-value="paginationArrow" className={classes.paginationArrow} onClick={goToNextPage}>
118110
<Icon src="sap-icon://slim-arrow-right" />
119111
</div>
120112
</div>

packages/main/src/components/Carousel/demo.stories.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { PlacementType } from '../../lib/PlacementType';
1010
function renderCarousel() {
1111
return (
1212
<Carousel
13+
width="90%"
1314
activePage={number('active', 0)}
1415
onPageChanged={action('onPageChanged')}
1516
arrowsPlacement={select(

packages/main/src/components/Carousel/index.tsx

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ const Carousel: FC<CarouselPropTypes> = forwardRef((props: CarouselPropTypes, re
8383

8484
const carouselItemClasses = StyleClassHelper.of(classes.carouselItem);
8585
if (arrowsPlacement === CarouselArrowsPlacement.Content) {
86-
carouselItemClasses.put(classes.carouselItemContentIndicator);
86+
classNameString.put(classes.carouselArrowPlacementContent);
8787
}
8888

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

9797
const childElementCount = Children.count(children);
9898
const goToNextPage = useCallback(
99-
(e) => {
99+
(e, skipManualInversion = false) => {
100+
if (
101+
document.dir === 'rtl' &&
102+
arrowsPlacement === CarouselArrowsPlacement.Content &&
103+
e.type === 'click' &&
104+
!skipManualInversion
105+
) {
106+
return goToPreviousPage(e, true);
107+
}
100108
if (loop === false && currentlyActivePage === childElementCount - 1) {
101109
return;
102110
}
103111
const nextPage = currentlyActivePage === childElementCount - 1 ? 0 : currentlyActivePage + 1;
104112
selectPageAtIndex(nextPage, e);
105113
},
106-
[loop, currentlyActivePage, selectPageAtIndex, childElementCount]
114+
[loop, currentlyActivePage, selectPageAtIndex, childElementCount, arrowsPlacement]
107115
);
108116

109117
const goToPreviousPage = useCallback(
110-
(e) => {
118+
(e, skipManualInversion = false) => {
119+
if (
120+
document.dir === 'rtl' &&
121+
arrowsPlacement === CarouselArrowsPlacement.Content &&
122+
e.type === 'click' &&
123+
!skipManualInversion
124+
) {
125+
return goToNextPage(e, true);
126+
}
111127
if (loop === false && currentlyActivePage === 0) {
112128
return;
113129
}
114130
const previousPage = currentlyActivePage === 0 ? childElementCount - 1 : currentlyActivePage - 1;
115131
selectPageAtIndex(previousPage, e);
116132
},
117-
[loop, childElementCount, currentlyActivePage, selectPageAtIndex]
133+
[loop, childElementCount, currentlyActivePage, selectPageAtIndex, arrowsPlacement, goToNextPage]
134+
);
135+
136+
const onKeyDown = useCallback(
137+
(e) => {
138+
if (e.key === 'ArrowRight') {
139+
if (document.dir === 'rtl') {
140+
goToPreviousPage(e);
141+
} else {
142+
goToNextPage(e);
143+
}
144+
}
145+
if (e.key === 'ArrowLeft') {
146+
if (document.dir === 'rtl') {
147+
goToNextPage(e);
148+
} else {
149+
goToPreviousPage(e);
150+
}
151+
}
152+
},
153+
[goToPreviousPage, goToNextPage]
118154
);
119155

156+
const translateXPrefix = document.dir === 'rtl' ? '' : '-';
120157
return (
121158
<div
122159
className={classNameString.toString()}
@@ -125,6 +162,8 @@ const Carousel: FC<CarouselPropTypes> = forwardRef((props: CarouselPropTypes, re
125162
slot={props['slot']}
126163
ref={ref}
127164
role="list"
165+
tabIndex={0}
166+
onKeyDown={onKeyDown}
128167
>
129168
{childElementCount > 1 && pageIndicatorPlacement === PlacementType.Top && (
130169
<CarouselPagination
@@ -140,7 +179,7 @@ const Carousel: FC<CarouselPropTypes> = forwardRef((props: CarouselPropTypes, re
140179
<div
141180
className={classes.carouselInner}
142181
style={{
143-
transform: `translateX(-${currentlyActivePage * 100}%)`
182+
transform: `translateX(${translateXPrefix}${currentlyActivePage * 100}%)`
144183
}}
145184
>
146185
{Children.map(children, (item, index) => (

0 commit comments

Comments
 (0)