@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.
- package/esm/_internal/ClickAwayProvider.js +9 -5
- package/esm/_internal/ClickAwayProvider.js.map +1 -1
- package/esm/_internal/FlexBox.js.map +1 -1
- package/esm/_internal/HeadingLevelProvider.js +112 -0
- package/esm/_internal/HeadingLevelProvider.js.map +1 -0
- package/esm/_internal/ProgressTrackerStepPanel.js +2 -1
- package/esm/_internal/ProgressTrackerStepPanel.js.map +1 -1
- package/esm/_internal/Slides.js +270 -79
- package/esm/_internal/Slides.js.map +1 -1
- package/esm/_internal/TabPanel.js +2 -1
- package/esm/_internal/TabPanel.js.map +1 -1
- package/esm/_internal/Text2.js +63 -0
- package/esm/_internal/Text2.js.map +1 -0
- package/esm/_internal/_rollupPluginBabelHelpers.js +17 -1
- package/esm/_internal/_rollupPluginBabelHelpers.js.map +1 -1
- package/esm/_internal/components.js +1 -0
- package/esm/_internal/components.js.map +1 -1
- package/esm/_internal/heading.js +11 -0
- package/esm/_internal/heading.js.map +1 -0
- package/esm/_internal/progress-tracker.js +2 -1
- package/esm/_internal/progress-tracker.js.map +1 -1
- package/esm/_internal/slideshow.js +2 -0
- package/esm/_internal/slideshow.js.map +1 -1
- package/esm/_internal/state.js +145 -0
- package/esm/_internal/state.js.map +1 -0
- package/esm/_internal/tabs.js +1 -0
- package/esm/_internal/tabs.js.map +1 -1
- package/esm/_internal/text.js +10 -0
- package/esm/_internal/text.js.map +1 -0
- package/esm/_internal/useRovingTabIndex.js +9 -144
- package/esm/_internal/useRovingTabIndex.js.map +1 -1
- package/esm/index.js +5 -1
- package/esm/index.js.map +1 -1
- package/package.json +4 -5
- package/src/components/flex-box/FlexBox.stories.tsx +60 -4
- package/src/components/flex-box/FlexBox.tsx +7 -4
- package/src/components/flex-box/__snapshots__/FlexBox.test.tsx.snap +35 -0
- package/src/components/heading/Heading.stories.tsx +108 -0
- package/src/components/heading/Heading.test.tsx +77 -0
- package/src/components/heading/Heading.tsx +62 -0
- package/src/components/heading/HeadingLevelProvider.tsx +30 -0
- package/src/components/heading/constants.ts +16 -0
- package/src/components/heading/context.tsx +13 -0
- package/src/components/heading/index.ts +3 -0
- package/src/components/heading/useHeadingLevel.tsx +8 -0
- package/src/components/index.ts +1 -0
- package/src/components/slideshow/Slides.tsx +33 -3
- package/src/components/slideshow/Slideshow.stories.tsx +98 -2
- package/src/components/slideshow/Slideshow.tsx +15 -3
- package/src/components/slideshow/SlideshowControls.stories.tsx +1 -1
- package/src/components/slideshow/SlideshowControls.tsx +49 -11
- package/src/components/slideshow/SlideshowItem.tsx +0 -5
- package/src/components/slideshow/SlideshowItemGroup.tsx +63 -0
- package/src/components/slideshow/__snapshots__/Slideshow.test.tsx.snap +4 -1
- package/src/components/slideshow/useSlideFocusManagement.tsx +92 -0
- package/src/components/text/Text.stories.tsx +80 -0
- package/src/components/text/Text.test.tsx +62 -0
- package/src/components/text/Text.tsx +94 -0
- package/src/components/text/index.ts +1 -0
- package/src/hooks/useRovingTabIndex.tsx +9 -0
- package/src/index.ts +2 -0
- package/src/utils/focus/constants.ts +5 -0
- package/src/utils/focus/getFirstAndLastFocusable.ts +4 -10
- package/src/utils/focus/getFocusableElements.test.ts +151 -0
- package/src/utils/focus/getFocusableElements.ts +7 -0
- 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);
|
package/src/components/index.ts
CHANGED
|
@@ -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
|
-
{
|
|
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(
|
|
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<
|
|
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}
|