@lumx/react 3.0.1 → 3.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/esm/_internal/ClickAwayProvider.js +9 -5
  2. package/esm/_internal/ClickAwayProvider.js.map +1 -1
  3. package/esm/_internal/FlexBox.js.map +1 -1
  4. package/esm/_internal/HeadingLevelProvider.js +112 -0
  5. package/esm/_internal/HeadingLevelProvider.js.map +1 -0
  6. package/esm/_internal/ProgressTrackerStepPanel.js +2 -1
  7. package/esm/_internal/ProgressTrackerStepPanel.js.map +1 -1
  8. package/esm/_internal/Slides.js +270 -79
  9. package/esm/_internal/Slides.js.map +1 -1
  10. package/esm/_internal/TabPanel.js +2 -1
  11. package/esm/_internal/TabPanel.js.map +1 -1
  12. package/esm/_internal/Text2.js +63 -0
  13. package/esm/_internal/Text2.js.map +1 -0
  14. package/esm/_internal/_rollupPluginBabelHelpers.js +17 -1
  15. package/esm/_internal/_rollupPluginBabelHelpers.js.map +1 -1
  16. package/esm/_internal/components.js +1 -0
  17. package/esm/_internal/components.js.map +1 -1
  18. package/esm/_internal/heading.js +11 -0
  19. package/esm/_internal/heading.js.map +1 -0
  20. package/esm/_internal/progress-tracker.js +2 -1
  21. package/esm/_internal/progress-tracker.js.map +1 -1
  22. package/esm/_internal/slideshow.js +2 -0
  23. package/esm/_internal/slideshow.js.map +1 -1
  24. package/esm/_internal/state.js +145 -0
  25. package/esm/_internal/state.js.map +1 -0
  26. package/esm/_internal/tabs.js +1 -0
  27. package/esm/_internal/tabs.js.map +1 -1
  28. package/esm/_internal/text.js +10 -0
  29. package/esm/_internal/text.js.map +1 -0
  30. package/esm/_internal/useRovingTabIndex.js +9 -144
  31. package/esm/_internal/useRovingTabIndex.js.map +1 -1
  32. package/esm/index.js +5 -1
  33. package/esm/index.js.map +1 -1
  34. package/package.json +4 -5
  35. package/src/components/flex-box/FlexBox.stories.tsx +60 -4
  36. package/src/components/flex-box/FlexBox.tsx +7 -4
  37. package/src/components/flex-box/__snapshots__/FlexBox.test.tsx.snap +35 -0
  38. package/src/components/heading/Heading.stories.tsx +108 -0
  39. package/src/components/heading/Heading.test.tsx +77 -0
  40. package/src/components/heading/Heading.tsx +62 -0
  41. package/src/components/heading/HeadingLevelProvider.tsx +30 -0
  42. package/src/components/heading/constants.ts +16 -0
  43. package/src/components/heading/context.tsx +13 -0
  44. package/src/components/heading/index.ts +3 -0
  45. package/src/components/heading/useHeadingLevel.tsx +8 -0
  46. package/src/components/index.ts +1 -0
  47. package/src/components/slideshow/Slides.tsx +33 -3
  48. package/src/components/slideshow/Slideshow.stories.tsx +98 -2
  49. package/src/components/slideshow/Slideshow.tsx +15 -3
  50. package/src/components/slideshow/SlideshowControls.stories.tsx +1 -1
  51. package/src/components/slideshow/SlideshowControls.tsx +49 -11
  52. package/src/components/slideshow/SlideshowItem.tsx +0 -5
  53. package/src/components/slideshow/SlideshowItemGroup.tsx +63 -0
  54. package/src/components/slideshow/__snapshots__/Slideshow.test.tsx.snap +4 -1
  55. package/src/components/slideshow/useSlideFocusManagement.tsx +92 -0
  56. package/src/components/text/Text.stories.tsx +80 -0
  57. package/src/components/text/Text.test.tsx +62 -0
  58. package/src/components/text/Text.tsx +94 -0
  59. package/src/components/text/index.ts +1 -0
  60. package/src/hooks/useRovingTabIndex.tsx +9 -0
  61. package/src/index.ts +2 -0
  62. package/src/utils/focus/constants.ts +5 -0
  63. package/src/utils/focus/getFirstAndLastFocusable.ts +4 -10
  64. package/src/utils/focus/getFocusableElements.test.ts +151 -0
  65. package/src/utils/focus/getFocusableElements.ts +7 -0
  66. package/types.d.ts +94 -7
@@ -0,0 +1,108 @@
1
+ import React from 'react';
2
+ import { imageKnob } from '@lumx/react/stories/knobs/image';
3
+ import { Orientation, Size, Typography } from '..';
4
+ import { Heading, HeadingLevelProvider } from '.';
5
+ import { FlexBox } from '../flex-box';
6
+ import { GenericBlock } from '../generic-block';
7
+ import { Thumbnail } from '../thumbnail';
8
+
9
+ export default { title: 'LumX components/heading/Heading' };
10
+
11
+ export const Default = () => {
12
+ return (
13
+ <div>
14
+ {/* This will render a h1 */}
15
+ <Heading>First level</Heading>
16
+ <HeadingLevelProvider>
17
+ {/* This will render a h2 */}
18
+ <Heading>Second Level</Heading>
19
+ <HeadingLevelProvider>
20
+ {/* This will render a h3 */}
21
+ <Heading>Third Level</Heading>
22
+ {/* This will also render a h3 */}
23
+ <Heading>Other Third Level</Heading>
24
+ <HeadingLevelProvider>
25
+ {/* This will render a h4 */}
26
+ <Heading>Fourth Level</Heading>
27
+ <HeadingLevelProvider>
28
+ {/* This will render a h5 */}
29
+ <Heading>Fifth Level</Heading>
30
+ </HeadingLevelProvider>
31
+ </HeadingLevelProvider>
32
+ </HeadingLevelProvider>
33
+ </HeadingLevelProvider>
34
+ </div>
35
+ );
36
+ };
37
+
38
+ export const LevelOverride = () => {
39
+ return (
40
+ <div>
41
+ {/* This will render a h1 */}
42
+ <Heading>First level</Heading>
43
+ <HeadingLevelProvider>
44
+ {/* This will render a h2 */}
45
+ <Heading>Second Level</Heading>
46
+ <HeadingLevelProvider level={2}>
47
+ {/* This will also render a h2 */}
48
+ <Heading>Lorem ipsum</Heading>
49
+ <Heading>Dolor sit amet</Heading>
50
+ <Heading>Reprehenderit et aute</Heading>
51
+ </HeadingLevelProvider>
52
+ </HeadingLevelProvider>
53
+ </div>
54
+ );
55
+ };
56
+
57
+ export const HeadingManualOverride = () => {
58
+ return (
59
+ <div>
60
+ {/* This will render a h1 */}
61
+ <Heading>First level</Heading>
62
+ <HeadingLevelProvider>
63
+ {/* This will render a h2 */}
64
+ <Heading as="h2">Forced second Level</Heading>
65
+ <Heading as="h3">Forced third Level</Heading>
66
+ <Heading as="h4">Forced fourth Level</Heading>
67
+ </HeadingLevelProvider>
68
+ </div>
69
+ );
70
+ };
71
+
72
+ const ListWithSubElements = () => {
73
+ return (
74
+ <HeadingLevelProvider>
75
+ <FlexBox orientation={Orientation.vertical} gap={Size.big}>
76
+ <GenericBlock figure={<Thumbnail image={imageKnob()} alt="First Item" size={Size.l} />}>
77
+ <Heading typography={Typography.subtitle2}>First item</Heading>
78
+ </GenericBlock>
79
+ <GenericBlock figure={<Thumbnail image={imageKnob()} alt="First Item" size={Size.l} />}>
80
+ <Heading typography={Typography.subtitle2}>Second item</Heading>
81
+ </GenericBlock>
82
+ <GenericBlock figure={<Thumbnail image={imageKnob()} alt="First Item" size={Size.l} />}>
83
+ <Heading typography={Typography.subtitle2}>Third item</Heading>
84
+ </GenericBlock>
85
+ </FlexBox>
86
+ </HeadingLevelProvider>
87
+ );
88
+ };
89
+
90
+ export const TypographyOverride = () => {
91
+ return (
92
+ <FlexBox orientation={Orientation.vertical} gap={Size.big}>
93
+ <Heading>My lists</Heading>
94
+
95
+ <FlexBox orientation={Orientation.horizontal} gap={Size.huge}>
96
+ <ListWithSubElements />
97
+
98
+ <FlexBox orientation={Orientation.vertical} gap={Size.big}>
99
+ <HeadingLevelProvider>
100
+ <Heading>Sub list</Heading>
101
+
102
+ <ListWithSubElements />
103
+ </HeadingLevelProvider>
104
+ </FlexBox>
105
+ </FlexBox>
106
+ </FlexBox>
107
+ );
108
+ };
@@ -0,0 +1,77 @@
1
+ import React from 'react';
2
+
3
+ import { mount, shallow } from 'enzyme';
4
+ import 'jest-enzyme';
5
+
6
+ import { commonTestsSuite } from '@lumx/react/testing/utils';
7
+ import { Heading, HeadingProps } from './Heading';
8
+ import { HeadingLevelProvider } from './HeadingLevelProvider';
9
+
10
+ const setup = (props: Partial<HeadingProps> = {}) => {
11
+ const wrapper = shallow(<Heading {...props} />);
12
+ return { props, wrapper };
13
+ };
14
+
15
+ describe(`<${Heading.displayName}>`, () => {
16
+ describe('Snapshots and structure', () => {
17
+ it('should render a Text component with h1 by default', () => {
18
+ const { wrapper } = setup({ children: 'Some text' });
19
+ expect(wrapper).toHaveDisplayName('Text');
20
+ expect(wrapper).toHaveProp('as', 'h1');
21
+ expect(wrapper.prop('className')).toBe(Heading.className);
22
+ });
23
+
24
+ it('should render with as', () => {
25
+ const { wrapper } = setup({ children: 'Some text', as: 'h2' });
26
+ expect(wrapper).toHaveDisplayName('Text');
27
+ expect(wrapper).toHaveProp('as', 'h2');
28
+ expect(wrapper.prop('className')).toBe(Heading.className);
29
+ });
30
+
31
+ it('should correctly render levels nested in HeadingLevel', () => {
32
+ const wrapper = mount(
33
+ <>
34
+ <Heading>Level 1</Heading>
35
+ <HeadingLevelProvider>
36
+ <Heading>Level 2</Heading>
37
+ <HeadingLevelProvider>
38
+ <Heading>Level 3</Heading>
39
+ <HeadingLevelProvider>
40
+ <Heading>Level 4</Heading>
41
+ <HeadingLevelProvider>
42
+ <Heading>Level 5 - 1</Heading>
43
+ <Heading>Level 5 - 2</Heading>
44
+ <HeadingLevelProvider>
45
+ <Heading>Level 6</Heading>
46
+ <HeadingLevelProvider>
47
+ <Heading>Level 7</Heading>
48
+ </HeadingLevelProvider>
49
+ </HeadingLevelProvider>
50
+ </HeadingLevelProvider>
51
+ </HeadingLevelProvider>
52
+ </HeadingLevelProvider>
53
+ </HeadingLevelProvider>
54
+ ,
55
+ </>,
56
+ );
57
+
58
+ expect(wrapper.find('h1')).toHaveText('Level 1');
59
+ expect(wrapper.find('h2')).toHaveText('Level 2');
60
+ expect(wrapper.find('h3')).toHaveText('Level 3');
61
+ expect(wrapper.find('h4')).toHaveText('Level 4');
62
+
63
+ const h5 = wrapper.find('h5');
64
+ expect(h5).toHaveLength(2);
65
+ expect(h5.at(0)).toHaveText('Level 5 - 1');
66
+ expect(h5.at(1)).toHaveText('Level 5 - 2');
67
+ // There should be 2 h6 because it is the maximum value;
68
+ const h6 = wrapper.find('h6');
69
+ expect(h6).toHaveLength(2);
70
+ expect(h6.at(0)).toHaveText('Level 6');
71
+ expect(h6.at(1)).toHaveText('Level 7');
72
+ });
73
+ });
74
+
75
+ // Common tests suite.
76
+ commonTestsSuite(setup, { className: 'wrapper', prop: 'wrapper' }, { className: Heading.className });
77
+ });
@@ -0,0 +1,62 @@
1
+ import { Comp, getRootClassName, handleBasicClasses, HeadingElement } from '@lumx/react/utils';
2
+ import classNames from 'classnames';
3
+ import React, { forwardRef } from 'react';
4
+ import { Text, TextProps } from '../text';
5
+ import { DEFAULT_TYPOGRAPHY_BY_LEVEL } from './constants';
6
+ import { useHeadingLevel } from './useHeadingLevel';
7
+
8
+ /**
9
+ * Defines the props of the component.
10
+ */
11
+ export interface HeadingProps extends Partial<TextProps> {
12
+ /**
13
+ * Display a specific heading level instead of the one provided by parent context provider.
14
+ */
15
+ as?: HeadingElement;
16
+ }
17
+
18
+ /**
19
+ * Component display name.
20
+ */
21
+ const COMPONENT_NAME = 'Heading';
22
+
23
+ /**
24
+ * Component default class name and class prefix.
25
+ */
26
+ const CLASSNAME = getRootClassName(COMPONENT_NAME);
27
+
28
+ /**
29
+ * Component default props.
30
+ */
31
+ const DEFAULT_PROPS = {} as const;
32
+
33
+ /**
34
+ * Renders a heading component.
35
+ * Extends the `Text` Component with the heading level automatically computed based on
36
+ * the current level provided by the context.
37
+ */
38
+ export const Heading: Comp<HeadingProps> = forwardRef((props, ref) => {
39
+ const { children, as, className, ...forwardedProps } = props;
40
+ const { headingElement } = useHeadingLevel();
41
+
42
+ return (
43
+ <Text
44
+ ref={ref}
45
+ className={classNames(
46
+ className,
47
+ handleBasicClasses({
48
+ prefix: CLASSNAME,
49
+ }),
50
+ )}
51
+ as={as || headingElement}
52
+ typography={DEFAULT_TYPOGRAPHY_BY_LEVEL[headingElement]}
53
+ {...forwardedProps}
54
+ >
55
+ {children}
56
+ </Text>
57
+ );
58
+ });
59
+
60
+ Heading.displayName = COMPONENT_NAME;
61
+ Heading.className = CLASSNAME;
62
+ Heading.defaultProps = DEFAULT_PROPS;
@@ -0,0 +1,30 @@
1
+ import { HeadingElement } from '@lumx/react/utils';
2
+ import React, { ReactNode } from 'react';
3
+ import { MAX_HEADING_LEVEL } from './constants';
4
+ import { HeadingLevelContext } from './context';
5
+ import { useHeadingLevel } from './useHeadingLevel';
6
+
7
+ export interface HeadingLevelProviderProps {
8
+ /** The heading level to start at. If left undefined, the parent context will be used, if any. */
9
+ level?: number;
10
+ /** The children to display */
11
+ children: ReactNode;
12
+ }
13
+
14
+ /**
15
+ * Provide a new heading level context.
16
+ */
17
+ export const HeadingLevelProvider: React.FC<HeadingLevelProviderProps> = ({ children, level }) => {
18
+ const { level: contextLevel } = useHeadingLevel();
19
+
20
+ const incrementedLevel = level || contextLevel + 1;
21
+ /** Don't allow a level beyond the maximum level. */
22
+ const nextLevel = incrementedLevel > MAX_HEADING_LEVEL ? MAX_HEADING_LEVEL : incrementedLevel;
23
+ const headingElement = `h${nextLevel}` as HeadingElement;
24
+
25
+ return (
26
+ <HeadingLevelContext.Provider value={{ level: nextLevel, headingElement }}>
27
+ {children}
28
+ </HeadingLevelContext.Provider>
29
+ );
30
+ };
@@ -0,0 +1,16 @@
1
+ import { Typography } from '..';
2
+
3
+ /** The maximum authorized heading level. */
4
+ export const MAX_HEADING_LEVEL = 6;
5
+
6
+ /**
7
+ * Typography to use by default depending on the heading level.
8
+ */
9
+ export const DEFAULT_TYPOGRAPHY_BY_LEVEL = {
10
+ h1: Typography.display1,
11
+ h2: Typography.headline,
12
+ h3: Typography.title,
13
+ h4: Typography.subtitle2,
14
+ h5: Typography.subtitle1,
15
+ h6: Typography.body2,
16
+ };
@@ -0,0 +1,13 @@
1
+ import { HeadingElement } from '@lumx/react/utils';
2
+ import { createContext } from 'react';
3
+
4
+ interface HeadingLevelContext {
5
+ /** The current level */
6
+ level: number;
7
+ /** The heading element matching the current level */
8
+ headingElement: HeadingElement;
9
+ }
10
+
11
+ const defaultContext: HeadingLevelContext = { level: 1, headingElement: 'h1' };
12
+
13
+ export const HeadingLevelContext = createContext<HeadingLevelContext>(defaultContext);
@@ -0,0 +1,3 @@
1
+ export * from './Heading';
2
+ export * from './HeadingLevelProvider';
3
+ export * from './useHeadingLevel';
@@ -0,0 +1,8 @@
1
+ import React from 'react';
2
+ import { HeadingLevelContext } from './context';
3
+
4
+ export const useHeadingLevel = () => {
5
+ const { level = 1, headingElement = 'h1' } = React.useContext(HeadingLevelContext);
6
+
7
+ return { level, headingElement };
8
+ };
@@ -11,6 +11,7 @@ export const Alignment = {
11
11
  right: 'right',
12
12
  spaceAround: 'space-around',
13
13
  spaceBetween: 'space-between',
14
+ spaceEvenly: 'space-evenly',
14
15
  start: 'start',
15
16
  top: 'top',
16
17
  } as const;
@@ -1,9 +1,11 @@
1
- import React, { CSSProperties, forwardRef } from 'react';
1
+ import React, { Children, CSSProperties, forwardRef } from 'react';
2
+ import chunk from 'lodash/chunk';
2
3
 
3
4
  import classNames from 'classnames';
4
5
 
5
6
  import { FULL_WIDTH_PERCENT } from '@lumx/react/components/slideshow/constants';
6
7
  import { Comp, GenericProps, getRootClassName, handleBasicClasses, HasTheme } from '@lumx/react/utils';
8
+ import { buildSlideShowGroupId, SlideshowItemGroup } from './SlideshowItemGroup';
7
9
 
8
10
  export interface SlidesProps extends GenericProps, HasTheme {
9
11
  /** current slide active */
@@ -24,6 +26,13 @@ export interface SlidesProps extends GenericProps, HasTheme {
24
26
  toggleAutoPlay: () => void;
25
27
  /** component to be rendered after the slides */
26
28
  afterSlides?: React.ReactNode;
29
+ /** Whether the slides have controls linked */
30
+ hasControls?: boolean;
31
+ /**
32
+ * Accessible label to set on a slide group.
33
+ * Receives the group position starting from 1 and the total number of groups.
34
+ * */
35
+ slideGroupLabel?: (groupPosition: number, groupTotal: number) => string;
27
36
  }
28
37
 
29
38
  /**
@@ -56,11 +65,22 @@ export const Slides: Comp<SlidesProps, HTMLDivElement> = forwardRef((props, ref)
56
65
  slidesId,
57
66
  children,
58
67
  afterSlides,
68
+ hasControls,
69
+ slideGroupLabel,
59
70
  ...forwardedProps
60
71
  } = props;
72
+ const wrapperRef = React.useRef<HTMLDivElement>(null);
73
+ const startIndexVisible = activeIndex;
74
+ const endIndexVisible = startIndexVisible + 1;
75
+
61
76
  // Inline style of wrapper element.
62
77
  const wrapperStyle: CSSProperties = { transform: `translateX(-${FULL_WIDTH_PERCENT * activeIndex}%)` };
63
78
 
79
+ const groups = React.useMemo(() => {
80
+ const childrenArray = Children.toArray(children);
81
+ return groupBy && groupBy > 1 ? chunk(childrenArray, groupBy) : childrenArray;
82
+ }, [children, groupBy]);
83
+
64
84
  return (
65
85
  <section
66
86
  id={id}
@@ -79,8 +99,18 @@ export const Slides: Comp<SlidesProps, HTMLDivElement> = forwardRef((props, ref)
79
99
  onMouseLeave={toggleAutoPlay}
80
100
  aria-live={isAutoPlaying ? 'off' : 'polite'}
81
101
  >
82
- <div className={`${CLASSNAME}__wrapper`} style={wrapperStyle}>
83
- {children}
102
+ <div ref={wrapperRef} className={`${CLASSNAME}__wrapper`} style={wrapperStyle}>
103
+ {groups.map((group, index) => (
104
+ <SlideshowItemGroup
105
+ key={index}
106
+ id={slidesId && buildSlideShowGroupId(slidesId, index)}
107
+ role={hasControls ? 'tabpanel' : 'group'}
108
+ label={slideGroupLabel ? slideGroupLabel(index + 1, groups.length) : undefined}
109
+ isDisplayed={index >= startIndexVisible && index < endIndexVisible}
110
+ >
111
+ {group}
112
+ </SlideshowItemGroup>
113
+ ))}
84
114
  </div>
85
115
  </div>
86
116
 
@@ -1,6 +1,6 @@
1
1
  import React from 'react';
2
2
  import range from 'lodash/range';
3
- import { AspectRatio, Button, FlexBox, ImageBlock, Slideshow, SlideshowItem } from '@lumx/react';
3
+ import { AspectRatio, Button, FlexBox, ImageBlock, Slideshow, SlideshowItem, Orientation } from '@lumx/react';
4
4
  import { boolean, number } from '@storybook/addon-knobs';
5
5
  import { thumbnailsKnob } from '@lumx/react/stories/knobs/thumbnailsKnob';
6
6
 
@@ -15,6 +15,7 @@ export const Simple = ({ theme }: any) => {
15
15
 
16
16
  return (
17
17
  <Slideshow
18
+ aria-label="Simple carousel example"
18
19
  activeIndex={activeIndex}
19
20
  autoPlay={autoPlay}
20
21
  interval={interval}
@@ -25,6 +26,7 @@ export const Simple = ({ theme }: any) => {
25
26
  theme={theme}
26
27
  groupBy={groupBy}
27
28
  style={{ width: '50%' }}
29
+ slideGroupLabel={(currentGroup, totalGroup) => `${currentGroup} of ${totalGroup}`}
28
30
  >
29
31
  {images.map(({ image, alt }, index) => (
30
32
  <SlideshowItem key={`${image}-${index}`}>
@@ -48,6 +50,7 @@ export const SimpleWithAutoPlay = ({ theme }: any) => {
48
50
 
49
51
  return (
50
52
  <Slideshow
53
+ aria-label="Simple with autoplay example"
51
54
  activeIndex={activeIndex}
52
55
  autoPlay
53
56
  interval={interval}
@@ -59,6 +62,7 @@ export const SimpleWithAutoPlay = ({ theme }: any) => {
59
62
  theme={theme}
60
63
  groupBy={groupBy}
61
64
  style={{ width: '50%' }}
65
+ slideGroupLabel={(currentGroup, totalGroup) => `${currentGroup} of ${totalGroup}`}
62
66
  >
63
67
  {images.map(({ image, alt }, index) => (
64
68
  <SlideshowItem key={`${image}-${index}`}>
@@ -75,7 +79,7 @@ export const SimpleWithAutoPlay = ({ theme }: any) => {
75
79
  };
76
80
 
77
81
  export const ResponsiveSlideShowSwipe = () => {
78
- const slides = range(3);
82
+ const slides = range(5);
79
83
  return (
80
84
  <>
81
85
  In responsive mode
@@ -86,11 +90,13 @@ export const ResponsiveSlideShowSwipe = () => {
86
90
  </ul>
87
91
  <FlexBox vAlign="center">
88
92
  <Slideshow
93
+ aria-label="Responsive SlideShow Swipe"
89
94
  activeIndex={0}
90
95
  slideshowControlsProps={{
91
96
  nextButtonProps: { label: 'Next' },
92
97
  previousButtonProps: { label: 'Previous' },
93
98
  }}
99
+ slideGroupLabel={(currentGroup, totalGroup) => `${currentGroup} of ${totalGroup}`}
94
100
  >
95
101
  {slides.map((slide) => (
96
102
  <SlideshowItem key={`${slide}`}>
@@ -114,3 +120,93 @@ export const ResponsiveSlideShowSwipe = () => {
114
120
  </>
115
121
  );
116
122
  };
123
+
124
+ const slides = [
125
+ {
126
+ id: 0,
127
+ src: 'https://www.w3.org/WAI/ARIA/apg/example-index/carousel/images/foyleswarslide__800x600.jpg',
128
+ alt: 'A man in a suit and fedora and a woman with coiffed hair look sternly into the camera.',
129
+ title: 'Foyle’s War Revisited',
130
+ subtitle: '8 pm Sunday, March 8, on TV: Sneak peek at the final season',
131
+ link: '#',
132
+ },
133
+ {
134
+ id: 1,
135
+ src: 'https://www.w3.org/WAI/ARIA/apg/example-index/carousel/images/britcomdavidslide__800x600.jpg',
136
+ alt: 'British flag with WILL-TV host David Thiel.',
137
+ title: 'Great Britain Vote: 7 pm Sat.',
138
+ link: '#',
139
+ },
140
+ {
141
+ id: 2,
142
+ src: 'https://www.w3.org/WAI/ARIA/apg/example-index/carousel/images/mag800-2__800x600.jpg',
143
+ alt: 'Mid-American Gardener panelists on the set.',
144
+ title: 'Mid-American Gardener: Thursdays at 7 pm',
145
+ subtitle: 'Watch the latest episode',
146
+ link: '#',
147
+ },
148
+ {
149
+ id: 3,
150
+ src: 'https://www.w3.org/WAI/ARIA/apg/example-index/carousel/images/foyleswarslide__800x600.jpg',
151
+ alt: 'A man in a suit and fedora and a woman with coiffed hair look sternly into the camera.',
152
+ title: 'Foyle’s War Revisited',
153
+ subtitle: '8 pm Sunday, March 8, on TV: Sneak peek at the final season',
154
+ link: '#',
155
+ },
156
+ {
157
+ id: 4,
158
+ src: 'https://www.w3.org/WAI/ARIA/apg/example-index/carousel/images/britcomdavidslide__800x600.jpg',
159
+ alt: 'British flag with WILL-TV host David Thiel.',
160
+ title: 'Great Britain Vote: 7 pm Sat.',
161
+ link: '#',
162
+ },
163
+ {
164
+ id: 5,
165
+ src: 'https://www.w3.org/WAI/ARIA/apg/example-index/carousel/images/mag800-2__800x600.jpg',
166
+ alt: 'Mid-American Gardener panelists on the set.',
167
+ title: 'Mid-American Gardener: Thursdays at 7 pm',
168
+ subtitle: 'Watch the latest episode',
169
+ link: '#',
170
+ },
171
+ ];
172
+ export const WithComplexContent = () => (
173
+ <Slideshow
174
+ aria-label="Carousel with complex content"
175
+ activeIndex={0}
176
+ groupBy={2}
177
+ slideshowControlsProps={{
178
+ nextButtonProps: { label: 'Next' },
179
+ previousButtonProps: { label: 'Previous' },
180
+ playButtonProps: { label: 'Play/Pause' },
181
+ paginationItemProps: (index) => ({ label: `Slide ${index + 1}` }),
182
+ }}
183
+ slideGroupLabel={(currentGroup, totalGroup) => `${currentGroup} of ${totalGroup}`}
184
+ >
185
+ {range(number('Slides', 6)).map((nb) => {
186
+ const slide = slides[nb % slides.length];
187
+
188
+ return (
189
+ <SlideshowItem key={slide.id}>
190
+ <a href={slide.link}>
191
+ <ImageBlock
192
+ thumbnailProps={{ aspectRatio: AspectRatio.horizontal, loading: 'eager' }}
193
+ image={slide.src}
194
+ alt={slide.alt}
195
+ />
196
+ </a>
197
+ <FlexBox orientation={Orientation.vertical}>
198
+ <h3>
199
+ <a href={slide.link}>{slide.title}</a>
200
+ {/* Add a non focusable element to test that it stays that way after a page change. */}
201
+ <button type="button" tabIndex={-1} aria-hidden="true">
202
+ Not focusable
203
+ </button>
204
+ <button type="button">Focusable</button>
205
+ </h3>
206
+ {slide.subtitle && <p>{slide.subtitle}</p>}
207
+ </FlexBox>
208
+ </SlideshowItem>
209
+ );
210
+ })}
211
+ </Slideshow>
212
+ );
@@ -5,19 +5,23 @@ import { DEFAULT_OPTIONS } from '@lumx/react/hooks/useSlideshowControls';
5
5
  import { Comp, GenericProps } from '@lumx/react/utils';
6
6
  import { useFocusWithin } from '@lumx/react/hooks/useFocusWithin';
7
7
  import { mergeRefs } from '@lumx/react/utils/mergeRefs';
8
+ import { buildSlideShowGroupId } from './SlideshowItemGroup';
8
9
 
9
10
  /**
10
11
  * Defines the props of the component.
11
12
  */
12
13
  export interface SlideshowProps
13
14
  extends GenericProps,
14
- Pick<SlidesProps, 'autoPlay' | 'slidesId' | 'id' | 'theme' | 'fillHeight' | 'groupBy'> {
15
+ Pick<SlidesProps, 'autoPlay' | 'slidesId' | 'id' | 'theme' | 'fillHeight' | 'groupBy' | 'slideGroupLabel'> {
15
16
  /** current slide active */
16
17
  activeIndex?: SlidesProps['activeIndex'];
17
18
  /** Interval between each slide when automatic rotation is enabled. */
18
19
  interval?: number;
19
20
  /** Props to pass to the slideshow controls (minus those already set by the Slideshow props). */
20
- slideshowControlsProps?: Pick<SlideshowControlsProps, 'nextButtonProps' | 'previousButtonProps'> &
21
+ slideshowControlsProps?: Pick<
22
+ SlideshowControlsProps,
23
+ 'nextButtonProps' | 'previousButtonProps' | 'paginationItemProps'
24
+ > &
21
25
  Omit<
22
26
  SlideshowControlsProps,
23
27
  | 'activeIndex'
@@ -61,6 +65,7 @@ export const Slideshow: Comp<SlideshowProps, HTMLDivElement> = forwardRef((props
61
65
  theme,
62
66
  id,
63
67
  slidesId,
68
+ slideGroupLabel,
64
69
  ...forwardedProps
65
70
  } = props;
66
71
  // Number of slideshow items.
@@ -99,6 +104,8 @@ export const Slideshow: Comp<SlideshowProps, HTMLDivElement> = forwardRef((props
99
104
  onFocusOut: startAutoPlay,
100
105
  });
101
106
 
107
+ const showControls = slideshowControlsProps && slidesCount > 1;
108
+
102
109
  return (
103
110
  <Slides
104
111
  activeIndex={currentIndex}
@@ -111,8 +118,9 @@ export const Slideshow: Comp<SlideshowProps, HTMLDivElement> = forwardRef((props
111
118
  autoPlay={autoPlay}
112
119
  slidesId={slideshowSlidesId}
113
120
  toggleAutoPlay={toggleAutoPlay}
114
- interval={interval}
115
121
  ref={mergeRefs(ref, setSlideshow)}
122
+ hasControls={showControls}
123
+ slideGroupLabel={slideGroupLabel}
116
124
  afterSlides={
117
125
  slideshowControlsProps && slidesCount > 1 ? (
118
126
  <div className={`${Slides.className}__controls`}>
@@ -143,6 +151,10 @@ export const Slideshow: Comp<SlideshowProps, HTMLDivElement> = forwardRef((props
143
151
  }
144
152
  : undefined
145
153
  }
154
+ paginationItemProps={(index) => ({
155
+ 'aria-controls': buildSlideShowGroupId(slideshowSlidesId, index),
156
+ ...slideshowControlsProps.paginationItemProps?.(index),
157
+ })}
146
158
  />
147
159
  </div>
148
160
  ) : undefined
@@ -26,6 +26,7 @@ export const Simple = () => {
26
26
  onPaginationClick={onPaginationClick}
27
27
  nextButtonProps={{ label: 'Next' }}
28
28
  previousButtonProps={{ label: 'Previous' }}
29
+ paginationItemLabel={(index) => `Slide ${index}`}
29
30
  />
30
31
  );
31
32
  };
@@ -62,7 +63,6 @@ export const ControllingSlideshow = ({ theme }: any) => {
62
63
  onFocusOut: startAutoPlay,
63
64
  });
64
65
 
65
- /* eslint-disable jsx-a11y/no-noninteractive-tabindex */
66
66
  return (
67
67
  <Slides
68
68
  activeIndex={currentIndex}