Skip to content
This repository was archived by the owner on Mar 4, 2020. It is now read-only.

Commit cbe7db9

Browse files
authored
feat(carousel): adding actionable items into carousel (#2271)
* adding carousel with actionable items * improvements to labelling * typo fix * carousel improvement * prettier * change the name * adressing pr review, adding unit tests * adding specification line * fix the test adding carousel as circular * amending changelog * prettier
1 parent d954616 commit cbe7db9

File tree

6 files changed

+188
-2
lines changed

6 files changed

+188
-2
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
3434
- Check input and button refs exist before focus in `Dropdown` @silviuavram ([#2248](https://github.com/microsoft/fluent-ui-react/pull/2248))
3535
- Fix `forceUpdate` to get synced updates in React's Concurrent mode @layershifter ([#2268](https://github.com/microsoft/fluent-ui-react/pull/2268))
3636
- Fix element reference memory leaks @jurokapsiar ([#2270](https://github.com/microsoft/fluent-ui-react/pull/2270))
37+
- Adding actionable items into `Carousel` @kolaps33 ([#2271](https://github.com/microsoft/fluent-ui-react/pull/2271))
3738

3839
### Features
3940
- Allow `useRef` hook used for storing debugging data to be defined in any order with other hooks in functional components @layershifter, @mnajdova ([#2236](https://github.com/microsoft/fluent-ui-react/pull/2236))
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import * as React from 'react'
2+
import { Carousel, Image, Flex, Text, Button, Toolbar, Header } from '@fluentui/react'
3+
4+
const imageAltTags = {
5+
ade: 'Portrait of Ade',
6+
elliot: 'Portrait of Elliot',
7+
kristy: 'Portrait of Kristy',
8+
nan: 'Portrait of Nan',
9+
}
10+
11+
const tabAriaLabel = {
12+
ade: 'Ade',
13+
elliot: 'Elliot',
14+
kristy: 'Kristy',
15+
nan: 'Nan',
16+
}
17+
18+
const carouselTextContent = (
19+
<Text>
20+
<Header as="h3"> Card </Header>
21+
text or any other text 1 , text or any other text 2, text or any other text 3 text or any other
22+
text 4, text or any other text 5, text or any other text 6
23+
</Text>
24+
)
25+
26+
const buttonStyles = { margin: '40px 0px 10px 10px' }
27+
const imageStyles = { maxWidth: '70px', maxHeight: '70px', margin: '15px 0px 0px 5px' }
28+
29+
const carouselToolbarContent = (
30+
<Toolbar
31+
aria-label="Actions"
32+
styles={{ marginTop: '40px' }}
33+
items={[
34+
{
35+
key: 'custom-button-1',
36+
kind: 'custom',
37+
content: <Button content="First" />,
38+
fitted: 'horizontally',
39+
},
40+
{
41+
key: 'custom-button-2',
42+
kind: 'custom',
43+
content: <Button content="Second" />,
44+
fitted: 'horizontally',
45+
},
46+
{
47+
key: 'custom-button-3',
48+
kind: 'custom',
49+
content: <Button content="Third" />,
50+
fitted: 'horizontally',
51+
},
52+
]}
53+
/>
54+
)
55+
56+
const carouselItems = [
57+
{
58+
key: 'ade',
59+
id: 'ade',
60+
content: (
61+
<div>
62+
<Flex gap="gap.medium">
63+
<Image
64+
styles={imageStyles}
65+
src="public/images/avatar/large/ade.jpg"
66+
fluid
67+
alt={imageAltTags.ade}
68+
/>
69+
{carouselTextContent}
70+
</Flex>
71+
<Button content="Open" styles={buttonStyles} />
72+
</div>
73+
),
74+
'aria-label': 'Ade card',
75+
},
76+
{
77+
key: 'elliot',
78+
id: 'elliot',
79+
content: (
80+
<div>
81+
<Flex gap="gap.medium">
82+
<Image
83+
styles={imageStyles}
84+
src="public/images/avatar/large/elliot.jpg"
85+
fluid
86+
alt={imageAltTags.elliot}
87+
/>
88+
{carouselTextContent}
89+
</Flex>
90+
{carouselToolbarContent}
91+
</div>
92+
),
93+
'aria-label': 'Elliot card',
94+
},
95+
{
96+
key: 'kristy',
97+
id: 'kristy',
98+
content: (
99+
<div>
100+
<Flex gap="gap.medium">
101+
<Image
102+
styles={imageStyles}
103+
src="public/images/avatar/large/kristy.png"
104+
fluid
105+
alt={imageAltTags.kristy}
106+
/>
107+
{carouselTextContent}
108+
</Flex>
109+
<Flex gap="gap.medium" styles={buttonStyles}>
110+
<Button content="Call" />
111+
<Button content="Video call" />
112+
</Flex>
113+
</div>
114+
),
115+
'aria-label': 'Kristy card',
116+
},
117+
]
118+
119+
const CarouselExample = () => (
120+
<Carousel
121+
ariaRoleDescription="carousel"
122+
navigation={{
123+
'aria-label': 'people cards',
124+
items: carouselItems.map((item, index) => ({
125+
key: item.id,
126+
'aria-label': tabAriaLabel[item.id],
127+
'aria-controls': item.id,
128+
})),
129+
}}
130+
items={carouselItems}
131+
getItemPositionText={(index: number, size: number) => `${index + 1} of ${size}`}
132+
/>
133+
)
134+
135+
export default CarouselExample

docs/src/examples/components/Carousel/Variations/index.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@ const Variations = () => (
99
description="A Carousel's items navigation can be circular."
1010
examplePath="components/Carousel/Variations/CarouselCircularExample"
1111
/>
12+
13+
<ComponentExample
14+
title="Carousel with actionable elements"
15+
description="A Carousel can have actionable elements inside."
16+
examplePath="components/Carousel/Variations/CarouselExampleWithFocusableElements"
17+
/>
1218
</ExampleSection>
1319
)
1420

packages/accessibility/src/behaviors/Carousel/carouselItemBehavior.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import { Accessibility } from '../../types'
2+
import * as keyboardKey from 'keyboard-key'
23

34
/**
45
* @specification
56
* Adds attribute 'role=tabpanel' to 'root' slot if 'navigation' property is true. Sets the attribute to 'group' otherwise.
67
* Adds attribute 'aria-hidden=false' to 'root' slot if 'active' property is true. Sets the attribute to 'true' otherwise.
78
* Adds attribute 'tabIndex=0' to 'root' slot if 'active' property is true. Sets the attribute to '-1' otherwise.
9+
* Triggers 'arrowKeysNavigationStopPropagation' action with 'ArrowRight' or 'ArrowLeft' on 'root'.
810
*/
911
const carouselItemBehavior: Accessibility<CarouselItemProps> = props => ({
1012
attributes: {
@@ -16,7 +18,11 @@ const carouselItemBehavior: Accessibility<CarouselItemProps> = props => ({
1618
},
1719

1820
keyActions: {
19-
root: {},
21+
root: {
22+
arrowKeysNavigationStopPropagation: {
23+
keyCombinations: [{ keyCode: keyboardKey.ArrowRight }, { keyCode: keyboardKey.ArrowLeft }],
24+
},
25+
},
2026
},
2127
})
2228

packages/react/src/components/Carousel/CarouselItem.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,15 @@ class CarouselItem extends UIComponent<WithAsProp<CarouselItemProps>> {
5959
itemPositionText: `${CarouselItem.className}__itemPositionText`,
6060
}
6161

62+
actionHandlers = {
63+
arrowKeysNavigationStopPropagation: e => {
64+
// let event propagate, when it was invoke on the element where arrow keys should rotate carousel
65+
if (e.currentTarget !== e.target) {
66+
e.stopPropagation()
67+
}
68+
},
69+
}
70+
6271
renderComponent({ ElementType, classes, styles, accessibility, unhandledProps }) {
6372
const { children, content, itemPositionText } = this.props
6473
return (

packages/react/test/specs/components/Carousel/Carousel-test.tsx

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,24 @@ import * as React from 'react'
22

33
import { isConformant } from 'test/specs/commonTests'
44
import Carousel, { CarouselProps } from 'src/components/Carousel/Carousel'
5+
import Button from 'src/components/Button/Button'
56
import CarouselItem from 'src/components/Carousel/CarouselItem'
67
import CarouselNavigation from 'src/components/Carousel/CarouselNavigation'
78
import CarouselNavigationItem from 'src/components/Carousel/CarouselNavigationItem'
89
import Text from 'src/components/Text/Text'
910
import { ReactWrapper, CommonWrapper } from 'enzyme'
1011
import { findIntrinsicElement, mountWithProvider } from 'test/utils'
1112

13+
const buttonName = 'button-to-test'
14+
1215
const items = [
1316
{
1417
key: 'item1',
15-
content: <Text content={'item1'} />,
18+
content: (
19+
<div>
20+
<Text content={'item1'} /> <Button id={buttonName} content={buttonName} />
21+
</div>
22+
),
1623
},
1724
{
1825
key: 'item2',
@@ -54,6 +61,8 @@ const getNavigationNavigationItemAtIndexWrapper = (
5461
): CommonWrapper => findIntrinsicElement(wrapper, `.${CarouselNavigationItem.className}`).at(index)
5562
const getItemAtIndexWrapper = (wrapper: ReactWrapper, index: number): CommonWrapper =>
5663
findIntrinsicElement(wrapper, `.${CarouselItem.className}`).at(index)
64+
const getButtonWrapper = (wrapper: ReactWrapper): CommonWrapper =>
65+
findIntrinsicElement(wrapper, `#${buttonName}`)
5766

5867
jest.useFakeTimers()
5968

@@ -150,6 +159,26 @@ describe('Carousel', () => {
150159

151160
expect(pagination.getDOMNode().textContent).toBe(`1 of ${items.length}`)
152161
})
162+
163+
it('should not change at arrow left if event is invoked on child element', () => {
164+
const wrapper = renderCarousel({ circular: true })
165+
const button = getButtonWrapper(wrapper)
166+
const pagination = getPaginationWrapper(wrapper)
167+
168+
button.simulate('keydown', { key: 'ArrowLeft' })
169+
170+
expect(pagination.getDOMNode().textContent).toBe(`1 of ${items.length}`)
171+
})
172+
173+
it('should not change at arrow right if event is invoked on child element', () => {
174+
const wrapper = renderCarousel()
175+
const button = getButtonWrapper(wrapper)
176+
const pagination = getPaginationWrapper(wrapper)
177+
178+
button.simulate('keydown', { key: 'ArrowRight' })
179+
180+
expect(pagination.getDOMNode().textContent).toBe(`1 of ${items.length}`)
181+
})
153182
})
154183

155184
describe('paddle', () => {

0 commit comments

Comments
 (0)