@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
|
@@ -8,9 +8,9 @@ import { Emphasis, IconButton, IconButtonProps, Theme } from '@lumx/react';
|
|
|
8
8
|
import { Comp, GenericProps, getRootClassName, handleBasicClasses, HasTheme } from '@lumx/react/utils';
|
|
9
9
|
import { WINDOW } from '@lumx/react/constants';
|
|
10
10
|
import { useSlideshowControls, DEFAULT_OPTIONS } from '@lumx/react/hooks/useSlideshowControls';
|
|
11
|
+
import { useRovingTabIndex } from '@lumx/react/hooks/useRovingTabIndex';
|
|
11
12
|
|
|
12
13
|
import { useSwipeNavigate } from './useSwipeNavigate';
|
|
13
|
-
import { useKeyNavigate } from './useKeyNavigate';
|
|
14
14
|
import { PAGINATION_ITEM_SIZE, PAGINATION_ITEMS_MAX } from './constants';
|
|
15
15
|
import { usePaginationVisibleRange } from './usePaginationVisibleRange';
|
|
16
16
|
|
|
@@ -28,6 +28,8 @@ export interface SlideshowControlsProps extends GenericProps, HasTheme {
|
|
|
28
28
|
/** Props to pass to the previous button (minus those already set by the SlideshowControls props). */
|
|
29
29
|
previousButtonProps: Pick<IconButtonProps, 'label'> &
|
|
30
30
|
Omit<IconButtonProps, 'label' | 'onClick' | 'icon' | 'emphasis' | 'color'>;
|
|
31
|
+
/** Props to pass to the pagination wrapper */
|
|
32
|
+
paginationProps?: Omit<React.HTMLAttributes<HTMLDivElement>, 'className' | 'style' | 'role'>;
|
|
31
33
|
/** Number of slides. */
|
|
32
34
|
slidesCount: number;
|
|
33
35
|
/** On next button click callback. */
|
|
@@ -38,8 +40,15 @@ export interface SlideshowControlsProps extends GenericProps, HasTheme {
|
|
|
38
40
|
onPreviousClick?(loopback?: boolean): void;
|
|
39
41
|
/** whether the slideshow is currently playing */
|
|
40
42
|
isAutoPlaying?: boolean;
|
|
41
|
-
/**
|
|
43
|
+
/**
|
|
44
|
+
* function to be executed in order to retrieve the label for the pagination item
|
|
45
|
+
* @deprecated Use paginationItemProps instead.
|
|
46
|
+
* */
|
|
42
47
|
paginationItemLabel?: (index: number) => string;
|
|
48
|
+
/**
|
|
49
|
+
* function to be executed in order to retrieve the props for a pagination item.
|
|
50
|
+
*/
|
|
51
|
+
paginationItemProps?: (itemIndex: number) => React.HTMLAttributes<HTMLButtonElement> & { label?: string };
|
|
43
52
|
/** Props to pass to the lay button (minus those already set by the SlideshowControls props). */
|
|
44
53
|
playButtonProps?: Pick<IconButtonProps, 'label'> &
|
|
45
54
|
Omit<IconButtonProps, 'label' | 'onClick' | 'icon' | 'emphasis' | 'color'>;
|
|
@@ -80,11 +89,13 @@ const InternalSlideshowControls: Comp<SlideshowControlsProps, HTMLDivElement> =
|
|
|
80
89
|
onPreviousClick,
|
|
81
90
|
parentRef,
|
|
82
91
|
previousButtonProps,
|
|
92
|
+
paginationProps,
|
|
83
93
|
slidesCount,
|
|
84
94
|
theme,
|
|
85
95
|
isAutoPlaying = false,
|
|
86
96
|
playButtonProps,
|
|
87
97
|
paginationItemLabel,
|
|
98
|
+
paginationItemProps,
|
|
88
99
|
...forwardedProps
|
|
89
100
|
} = props;
|
|
90
101
|
|
|
@@ -93,8 +104,7 @@ const InternalSlideshowControls: Comp<SlideshowControlsProps, HTMLDivElement> =
|
|
|
93
104
|
// Checking window object to avoid errors in SSR.
|
|
94
105
|
parent = parentRef instanceof HTMLElement ? parentRef : parentRef?.current;
|
|
95
106
|
}
|
|
96
|
-
|
|
97
|
-
useKeyNavigate(parent, onNextClick, onPreviousClick);
|
|
107
|
+
const paginationRef = React.useRef(null);
|
|
98
108
|
// Listen to touch swipe navigate left & right.
|
|
99
109
|
useSwipeNavigate(
|
|
100
110
|
parent,
|
|
@@ -104,6 +114,18 @@ const InternalSlideshowControls: Comp<SlideshowControlsProps, HTMLDivElement> =
|
|
|
104
114
|
useCallback(() => onPreviousClick?.(false), [onPreviousClick]),
|
|
105
115
|
);
|
|
106
116
|
|
|
117
|
+
/**
|
|
118
|
+
* Add roving tab index pattern to pagination items and activate slide on focus.
|
|
119
|
+
*/
|
|
120
|
+
useRovingTabIndex({
|
|
121
|
+
parentRef: paginationRef,
|
|
122
|
+
elementSelector: 'button',
|
|
123
|
+
keepTabIndex: true,
|
|
124
|
+
onElementFocus: (element) => {
|
|
125
|
+
element.click();
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
|
|
107
129
|
// Pagination "bullet" range.
|
|
108
130
|
const visibleRange = usePaginationVisibleRange(activeIndex as number, slidesCount);
|
|
109
131
|
|
|
@@ -126,8 +148,13 @@ const InternalSlideshowControls: Comp<SlideshowControlsProps, HTMLDivElement> =
|
|
|
126
148
|
emphasis={Emphasis.low}
|
|
127
149
|
onClick={onPreviousClick}
|
|
128
150
|
/>
|
|
129
|
-
<div className={`${CLASSNAME}__pagination`}>
|
|
130
|
-
<div
|
|
151
|
+
<div ref={paginationRef} className={`${CLASSNAME}__pagination`}>
|
|
152
|
+
<div
|
|
153
|
+
className={`${CLASSNAME}__pagination-items`}
|
|
154
|
+
style={wrapperStyle}
|
|
155
|
+
role="tablist"
|
|
156
|
+
{...paginationProps}
|
|
157
|
+
>
|
|
131
158
|
{useMemo(
|
|
132
159
|
() =>
|
|
133
160
|
range(slidesCount).map((index) => {
|
|
@@ -137,6 +164,15 @@ const InternalSlideshowControls: Comp<SlideshowControlsProps, HTMLDivElement> =
|
|
|
137
164
|
(index === visibleRange.min || index === visibleRange.max);
|
|
138
165
|
const isActive = activeIndex === index;
|
|
139
166
|
const isOutRange = index < visibleRange.min || index > visibleRange.max;
|
|
167
|
+
const {
|
|
168
|
+
className: itemClassName = undefined,
|
|
169
|
+
label = undefined,
|
|
170
|
+
...itemProps
|
|
171
|
+
} = paginationItemProps ? paginationItemProps(index) : {};
|
|
172
|
+
|
|
173
|
+
const ariaLabel =
|
|
174
|
+
label || paginationItemLabel?.(index) || `${index + 1} / ${slidesCount}`;
|
|
175
|
+
|
|
140
176
|
return (
|
|
141
177
|
<button
|
|
142
178
|
className={classNames(
|
|
@@ -146,15 +182,16 @@ const InternalSlideshowControls: Comp<SlideshowControlsProps, HTMLDivElement> =
|
|
|
146
182
|
isOnEdge,
|
|
147
183
|
isOutRange,
|
|
148
184
|
}),
|
|
185
|
+
itemClassName,
|
|
149
186
|
)}
|
|
150
187
|
key={index}
|
|
151
188
|
type="button"
|
|
189
|
+
tabIndex={isActive ? undefined : -1}
|
|
190
|
+
role="tab"
|
|
191
|
+
aria-selected={isActive}
|
|
152
192
|
onClick={() => onPaginationClick?.(index)}
|
|
153
|
-
{
|
|
154
|
-
|
|
155
|
-
? paginationItemLabel(index)
|
|
156
|
-
: `${index + 1} / ${slidesCount}`,
|
|
157
|
-
}}
|
|
193
|
+
aria-label={ariaLabel}
|
|
194
|
+
{...itemProps}
|
|
158
195
|
/>
|
|
159
196
|
);
|
|
160
197
|
}),
|
|
@@ -163,6 +200,7 @@ const InternalSlideshowControls: Comp<SlideshowControlsProps, HTMLDivElement> =
|
|
|
163
200
|
visibleRange.min,
|
|
164
201
|
visibleRange.max,
|
|
165
202
|
activeIndex,
|
|
203
|
+
paginationItemProps,
|
|
166
204
|
paginationItemLabel,
|
|
167
205
|
onPaginationClick,
|
|
168
206
|
],
|
|
@@ -8,8 +8,6 @@ import { Comp, GenericProps, getRootClassName, handleBasicClasses } from '@lumx/
|
|
|
8
8
|
* Defines the props of the component.
|
|
9
9
|
*/
|
|
10
10
|
export interface SlideshowItemProps extends GenericProps {
|
|
11
|
-
/** whether the slideshow item is currently visible */
|
|
12
|
-
isCurrentlyVisible?: boolean;
|
|
13
11
|
/** interval in which slides are automatically shown */
|
|
14
12
|
interval?: number;
|
|
15
13
|
}
|
|
@@ -33,7 +31,6 @@ const CLASSNAME = getRootClassName(COMPONENT_NAME);
|
|
|
33
31
|
*/
|
|
34
32
|
export const SlideshowItem: Comp<SlideshowItemProps, HTMLDivElement> = forwardRef((props, ref) => {
|
|
35
33
|
const { className, children, ...forwardedProps } = props;
|
|
36
|
-
|
|
37
34
|
return (
|
|
38
35
|
<div
|
|
39
36
|
ref={ref}
|
|
@@ -43,8 +40,6 @@ export const SlideshowItem: Comp<SlideshowItemProps, HTMLDivElement> = forwardRe
|
|
|
43
40
|
prefix: CLASSNAME,
|
|
44
41
|
}),
|
|
45
42
|
)}
|
|
46
|
-
aria-roledescription="slide"
|
|
47
|
-
role="group"
|
|
48
43
|
{...forwardedProps}
|
|
49
44
|
>
|
|
50
45
|
{children}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import React, { forwardRef } from 'react';
|
|
2
|
+
|
|
3
|
+
import classNames from 'classnames';
|
|
4
|
+
import { mergeRefs } from '@lumx/react/utils/mergeRefs';
|
|
5
|
+
|
|
6
|
+
import { Comp, GenericProps, getRootClassName, handleBasicClasses } from '@lumx/react/utils';
|
|
7
|
+
import { useSlideFocusManagement } from './useSlideFocusManagement';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Defines the props of the component.
|
|
11
|
+
*/
|
|
12
|
+
export interface SlideshowItemGroupProps extends GenericProps {
|
|
13
|
+
role?: 'tabpanel' | 'group';
|
|
14
|
+
label?: string;
|
|
15
|
+
isDisplayed?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Component display name.
|
|
20
|
+
*/
|
|
21
|
+
const COMPONENT_NAME = 'SlideshowItemGroup';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Component default class name and class prefix.
|
|
25
|
+
*/
|
|
26
|
+
export const CLASSNAME = getRootClassName(COMPONENT_NAME);
|
|
27
|
+
|
|
28
|
+
export const buildSlideShowGroupId = (slidesId: string, index: number) => `${slidesId}-slide-${index}`;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* SlideshowItemGroup component.
|
|
32
|
+
*
|
|
33
|
+
* @param props Component props.
|
|
34
|
+
* @param ref Component ref.
|
|
35
|
+
* @return React element.
|
|
36
|
+
*/
|
|
37
|
+
export const SlideshowItemGroup: Comp<SlideshowItemGroupProps, HTMLDivElement> = forwardRef((props, ref) => {
|
|
38
|
+
const { className, children, role = 'group', label, isDisplayed, ...forwardedProps } = props;
|
|
39
|
+
const groupRef = React.useRef<HTMLDivElement>(null);
|
|
40
|
+
|
|
41
|
+
useSlideFocusManagement({ isSlideDisplayed: isDisplayed, slideRef: groupRef });
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<div
|
|
45
|
+
ref={mergeRefs(groupRef, ref)}
|
|
46
|
+
role={role}
|
|
47
|
+
className={classNames(
|
|
48
|
+
className,
|
|
49
|
+
handleBasicClasses({
|
|
50
|
+
prefix: CLASSNAME,
|
|
51
|
+
}),
|
|
52
|
+
)}
|
|
53
|
+
aria-roledescription="slide"
|
|
54
|
+
aria-label={label}
|
|
55
|
+
{...forwardedProps}
|
|
56
|
+
>
|
|
57
|
+
{children}
|
|
58
|
+
</div>
|
|
59
|
+
);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
SlideshowItemGroup.displayName = COMPONENT_NAME;
|
|
63
|
+
SlideshowItemGroup.className = CLASSNAME;
|
|
@@ -20,6 +20,7 @@ Array [
|
|
|
20
20
|
onNextClick={[Function]}
|
|
21
21
|
onPaginationClick={[Function]}
|
|
22
22
|
onPreviousClick={[Function]}
|
|
23
|
+
paginationItemProps={[Function]}
|
|
23
24
|
parentRef={null}
|
|
24
25
|
previousButtonProps={
|
|
25
26
|
Object {
|
|
@@ -32,11 +33,13 @@ Array [
|
|
|
32
33
|
/>
|
|
33
34
|
</div>
|
|
34
35
|
}
|
|
36
|
+
aria-label="Simple carousel example"
|
|
35
37
|
autoPlay={false}
|
|
36
38
|
groupBy={1}
|
|
39
|
+
hasControls={true}
|
|
37
40
|
id="slideshow1"
|
|
38
|
-
interval={1000}
|
|
39
41
|
isAutoPlaying={false}
|
|
42
|
+
slideGroupLabel={[Function]}
|
|
40
43
|
slidesId="slideshow-slides2"
|
|
41
44
|
style={
|
|
42
45
|
Object {
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import React, { useEffect } from 'react';
|
|
2
|
+
import { getFocusableElements } from '@lumx/react/utils/focus/getFocusableElements';
|
|
3
|
+
|
|
4
|
+
export interface UseSlideFocusManagementProps {
|
|
5
|
+
isSlideDisplayed?: boolean;
|
|
6
|
+
slideRef: React.RefObject<HTMLDivElement>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Classname set on elements whose focus was blocked.
|
|
11
|
+
* This is to easily find elements that have been tempered with,
|
|
12
|
+
* and not elements whose focus was already initially blocked.
|
|
13
|
+
* */
|
|
14
|
+
const BLOCKED_FOCUS_CLASSNAME = `focus-blocked`;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Manage how slides must behave when visible or not.
|
|
18
|
+
* When not visible, they should be hidden from screen readers and not focusable.
|
|
19
|
+
*/
|
|
20
|
+
export const useSlideFocusManagement = ({ isSlideDisplayed, slideRef }: UseSlideFocusManagementProps) => {
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
const element = slideRef?.current;
|
|
23
|
+
|
|
24
|
+
if (!element) {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Display given slide to screen readers and, if focus was blocked, restore focus on elements.
|
|
30
|
+
*/
|
|
31
|
+
const enableSlide = () => {
|
|
32
|
+
// Hide from screen readers
|
|
33
|
+
element.setAttribute('aria-hidden', 'false');
|
|
34
|
+
// Find elements we have blocked focus on
|
|
35
|
+
element.querySelectorAll(`.${BLOCKED_FOCUS_CLASSNAME}`).forEach((focusableElement) => {
|
|
36
|
+
focusableElement.removeAttribute('tabindex');
|
|
37
|
+
focusableElement.classList.remove(BLOCKED_FOCUS_CLASSNAME);
|
|
38
|
+
});
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Hide given slide from screen readers and block focus on all focusable elements within.
|
|
43
|
+
*/
|
|
44
|
+
const blockSlide = () => {
|
|
45
|
+
element.setAttribute('aria-hidden', 'true');
|
|
46
|
+
getFocusableElements(element).forEach((focusableElement) => {
|
|
47
|
+
focusableElement.setAttribute('tabindex', '-1');
|
|
48
|
+
focusableElement.classList.add(BLOCKED_FOCUS_CLASSNAME);
|
|
49
|
+
});
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const handleDisplay = () => {
|
|
53
|
+
if (!element) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (isSlideDisplayed) {
|
|
57
|
+
enableSlide();
|
|
58
|
+
} else {
|
|
59
|
+
blockSlide();
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// Callback function to execute when mutations are observed
|
|
64
|
+
const callback: MutationCallback = (mutationsList) => {
|
|
65
|
+
if (element) {
|
|
66
|
+
for (const mutation of mutationsList) {
|
|
67
|
+
if (mutation.type === 'childList') {
|
|
68
|
+
handleDisplay();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// Create an observer instance linked to the callback function
|
|
75
|
+
const observer = new MutationObserver(callback);
|
|
76
|
+
|
|
77
|
+
if (element) {
|
|
78
|
+
handleDisplay();
|
|
79
|
+
|
|
80
|
+
/** If slide is hidden, start observing for elements to block focus */
|
|
81
|
+
if (!isSlideDisplayed) {
|
|
82
|
+
observer.observe(element, { attributes: true, childList: true, subtree: true });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return () => {
|
|
87
|
+
if (!isSlideDisplayed) {
|
|
88
|
+
observer.disconnect();
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
}, [isSlideDisplayed, slideRef]);
|
|
92
|
+
};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { ColorPalette, ColorVariant, TypographyCustom, TypographyInterface } from '@lumx/react';
|
|
3
|
+
import { Text } from './Text';
|
|
4
|
+
|
|
5
|
+
export default { title: 'LumX components/text/Text' };
|
|
6
|
+
|
|
7
|
+
export const Default = () => <Text as="p">Some text</Text>;
|
|
8
|
+
|
|
9
|
+
const withResizableBox = (Story: any) => (
|
|
10
|
+
<div
|
|
11
|
+
style={{
|
|
12
|
+
width: 150,
|
|
13
|
+
height: 60,
|
|
14
|
+
border: '1px solid red',
|
|
15
|
+
resize: 'both',
|
|
16
|
+
overflow: 'hidden',
|
|
17
|
+
}}
|
|
18
|
+
>
|
|
19
|
+
<Story />
|
|
20
|
+
</div>
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
export const Truncate = () => (
|
|
24
|
+
<Text as="p" truncate>
|
|
25
|
+
Some very very very long text
|
|
26
|
+
</Text>
|
|
27
|
+
);
|
|
28
|
+
Truncate.decorators = [withResizableBox];
|
|
29
|
+
|
|
30
|
+
export const TruncateMultiline = () => (
|
|
31
|
+
<Text as="p" truncate={{ lines: 2 }}>
|
|
32
|
+
Some very very very very very very very very very long text
|
|
33
|
+
</Text>
|
|
34
|
+
);
|
|
35
|
+
TruncateMultiline.decorators = [withResizableBox];
|
|
36
|
+
|
|
37
|
+
export const AllTypography = () => {
|
|
38
|
+
const typographies = [undefined, ...Object.values(TypographyInterface), ...Object.values(TypographyCustom)];
|
|
39
|
+
return (
|
|
40
|
+
<table>
|
|
41
|
+
{typographies.map((typography) => (
|
|
42
|
+
<tr key={typography}>
|
|
43
|
+
<td>{typography}</td>
|
|
44
|
+
<td>
|
|
45
|
+
<Text as="p" typography={typography}>
|
|
46
|
+
Some text
|
|
47
|
+
</Text>
|
|
48
|
+
</td>
|
|
49
|
+
</tr>
|
|
50
|
+
))}
|
|
51
|
+
</table>
|
|
52
|
+
);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export const AllColor = () => {
|
|
56
|
+
const colorVariants = [undefined, ...Object.values(ColorVariant)];
|
|
57
|
+
const colors = [undefined, ...Object.values(ColorPalette)];
|
|
58
|
+
return (
|
|
59
|
+
<table style={{ borderCollapse: 'separate', borderSpacing: 5 }}>
|
|
60
|
+
<tr>
|
|
61
|
+
<td />
|
|
62
|
+
{colorVariants.map((colorVariant) => (
|
|
63
|
+
<td key={colorVariant}>{colorVariant}</td>
|
|
64
|
+
))}
|
|
65
|
+
</tr>
|
|
66
|
+
{colors.map((color) => (
|
|
67
|
+
<tr key={color}>
|
|
68
|
+
<td>{color}</td>
|
|
69
|
+
{colorVariants.map((colorVariant) => (
|
|
70
|
+
<td key={colorVariant}>
|
|
71
|
+
<Text as="p" color={color} colorVariant={colorVariant}>
|
|
72
|
+
Some text
|
|
73
|
+
</Text>
|
|
74
|
+
</td>
|
|
75
|
+
))}
|
|
76
|
+
</tr>
|
|
77
|
+
))}
|
|
78
|
+
</table>
|
|
79
|
+
);
|
|
80
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
import { shallow } from 'enzyme';
|
|
4
|
+
import 'jest-enzyme';
|
|
5
|
+
|
|
6
|
+
import { commonTestsSuite } from '@lumx/react/testing/utils';
|
|
7
|
+
import { Text, TextProps } from './Text';
|
|
8
|
+
|
|
9
|
+
const setup = (props: Partial<TextProps> = {}) => {
|
|
10
|
+
const wrapper = shallow(<Text as="span" {...props} />);
|
|
11
|
+
return { props, wrapper };
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
describe(`<${Text.displayName}>`, () => {
|
|
15
|
+
describe('Snapshots and structure', () => {
|
|
16
|
+
it('should render default', () => {
|
|
17
|
+
const { wrapper } = setup({ children: 'Some text' });
|
|
18
|
+
expect(wrapper).toHaveDisplayName('span');
|
|
19
|
+
expect(wrapper.prop('className')).toBe(Text.className);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('should render with as', () => {
|
|
23
|
+
const { wrapper } = setup({ children: 'Some text', as: 'p' });
|
|
24
|
+
expect(wrapper).toHaveDisplayName('p');
|
|
25
|
+
expect(wrapper.prop('className')).toBe(Text.className);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should render with typography', () => {
|
|
29
|
+
const { wrapper } = setup({ typography: 'body2', children: 'Some text' });
|
|
30
|
+
expect(wrapper).toHaveDisplayName('span');
|
|
31
|
+
expect(wrapper).toHaveClassName('lumx-typography-body2');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should render with color', () => {
|
|
35
|
+
const { wrapper } = setup({ color: 'blue', children: 'Some text' });
|
|
36
|
+
expect(wrapper).toHaveDisplayName('span');
|
|
37
|
+
expect(wrapper).toHaveClassName('lumx-color-font-blue-N');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should render with color and variant', () => {
|
|
41
|
+
const { wrapper } = setup({ color: 'blue', colorVariant: 'D2', children: 'Some text' });
|
|
42
|
+
expect(wrapper).toHaveDisplayName('span');
|
|
43
|
+
expect(wrapper).toHaveClassName('lumx-color-font-blue-D2');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('should render truncated', () => {
|
|
47
|
+
const { wrapper } = setup({ truncate: true });
|
|
48
|
+
expect(wrapper).toHaveDisplayName('span');
|
|
49
|
+
expect(wrapper).toHaveClassName('lumx-text--is-truncated');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should render truncated multiline', () => {
|
|
53
|
+
const { wrapper } = setup({ truncate: { lines: 2 } });
|
|
54
|
+
expect(wrapper).toHaveDisplayName('span');
|
|
55
|
+
expect(wrapper).toHaveClassName('lumx-text--is-truncated-multiline');
|
|
56
|
+
expect(wrapper).toHaveStyle({ '--lumx-text-truncate-lines': 2 });
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Common tests suite.
|
|
61
|
+
commonTestsSuite(setup, { className: 'wrapper', prop: 'wrapper' }, { className: Text.className });
|
|
62
|
+
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import React, { forwardRef } from 'react';
|
|
2
|
+
|
|
3
|
+
import { Color, ColorVariant, Typography } from '@lumx/react';
|
|
4
|
+
import { Comp, GenericProps, getRootClassName, handleBasicClasses, HeadingElement } from '@lumx/react/utils';
|
|
5
|
+
import classNames from 'classnames';
|
|
6
|
+
|
|
7
|
+
type TextComponents = 'span' | 'p' | HeadingElement;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Defines the props of the component.
|
|
11
|
+
*/
|
|
12
|
+
export interface TextProps extends GenericProps {
|
|
13
|
+
/**
|
|
14
|
+
* Color variant.
|
|
15
|
+
*/
|
|
16
|
+
color?: Color;
|
|
17
|
+
/**
|
|
18
|
+
* Lightened or darkened variant of the selected color.
|
|
19
|
+
*/
|
|
20
|
+
colorVariant?: ColorVariant;
|
|
21
|
+
/**
|
|
22
|
+
* Typography variant.
|
|
23
|
+
*/
|
|
24
|
+
typography?: Typography;
|
|
25
|
+
/**
|
|
26
|
+
* Custom component to render the text.
|
|
27
|
+
*/
|
|
28
|
+
as: TextComponents;
|
|
29
|
+
/**
|
|
30
|
+
* Control whether the text should truncate or not.
|
|
31
|
+
* Setting as `true` will make the text truncate on a single line.
|
|
32
|
+
* Setting as `{ lines: number }` will make the text truncate on a multiple lines.
|
|
33
|
+
*/
|
|
34
|
+
truncate?: boolean | { lines: number };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Component display name.
|
|
39
|
+
*/
|
|
40
|
+
const COMPONENT_NAME = 'Text';
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Component default class name and class prefix.
|
|
44
|
+
*/
|
|
45
|
+
const CLASSNAME = getRootClassName(COMPONENT_NAME);
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Component default props.
|
|
49
|
+
*/
|
|
50
|
+
const DEFAULT_PROPS = {} as const;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Text component.
|
|
54
|
+
*
|
|
55
|
+
* @param props Component props.
|
|
56
|
+
* @param ref Component ref.
|
|
57
|
+
* @return React element.
|
|
58
|
+
*/
|
|
59
|
+
export const Text: Comp<TextProps> = forwardRef((props, ref) => {
|
|
60
|
+
const { as, children, className, color, colorVariant, typography, truncate, style, ...forwardedProps } = props;
|
|
61
|
+
|
|
62
|
+
const Component = as as TextComponents;
|
|
63
|
+
const colorClass = color && `lumx-color-font-${color}-${colorVariant || ColorVariant.N}`;
|
|
64
|
+
const typographyClass = typography && `lumx-typography-${typography}`;
|
|
65
|
+
|
|
66
|
+
// Truncate mode
|
|
67
|
+
const truncateLinesStyle = typeof truncate === 'object' &&
|
|
68
|
+
truncate.lines > 1 && { '--lumx-text-truncate-lines': truncate.lines };
|
|
69
|
+
const isTruncatedMultiline = !!truncateLinesStyle;
|
|
70
|
+
const isTruncated = !!truncate;
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<Component
|
|
74
|
+
ref={ref as React.Ref<any>}
|
|
75
|
+
className={classNames(
|
|
76
|
+
className,
|
|
77
|
+
handleBasicClasses({
|
|
78
|
+
prefix: CLASSNAME,
|
|
79
|
+
isTruncated: isTruncated && !isTruncatedMultiline,
|
|
80
|
+
isTruncatedMultiline,
|
|
81
|
+
}),
|
|
82
|
+
typographyClass,
|
|
83
|
+
colorClass,
|
|
84
|
+
)}
|
|
85
|
+
style={{ ...truncateLinesStyle, ...style }}
|
|
86
|
+
{...forwardedProps}
|
|
87
|
+
>
|
|
88
|
+
{children}
|
|
89
|
+
</Component>
|
|
90
|
+
);
|
|
91
|
+
});
|
|
92
|
+
Text.displayName = COMPONENT_NAME;
|
|
93
|
+
Text.className = CLASSNAME;
|
|
94
|
+
Text.defaultProps = DEFAULT_PROPS;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './Text';
|
|
@@ -4,6 +4,8 @@ interface UseRovingTabIndexOptions {
|
|
|
4
4
|
parentRef: RefObject<HTMLElement>;
|
|
5
5
|
elementSelector: string;
|
|
6
6
|
keepTabIndex?: boolean;
|
|
7
|
+
/** Action to trigger when an element is focused using roving tab index */
|
|
8
|
+
onElementFocus?: (element: HTMLElement) => void;
|
|
7
9
|
/** List of values to be used as extra dependencies of the useEffect */
|
|
8
10
|
extraDependencies?: any[];
|
|
9
11
|
}
|
|
@@ -12,6 +14,7 @@ export const useRovingTabIndex = ({
|
|
|
12
14
|
parentRef,
|
|
13
15
|
elementSelector,
|
|
14
16
|
keepTabIndex,
|
|
17
|
+
onElementFocus,
|
|
15
18
|
extraDependencies = [],
|
|
16
19
|
}: UseRovingTabIndexOptions): void => {
|
|
17
20
|
useEffect(
|
|
@@ -47,6 +50,12 @@ export const useRovingTabIndex = ({
|
|
|
47
50
|
}
|
|
48
51
|
const newElement = elements[newTabFocus];
|
|
49
52
|
newElement?.focus();
|
|
53
|
+
|
|
54
|
+
// When an element is focused using roving tab index, trigger the onElementFocus callback
|
|
55
|
+
if (newElement && onElementFocus) {
|
|
56
|
+
onElementFocus(newElement);
|
|
57
|
+
}
|
|
58
|
+
|
|
50
59
|
if (keepTabIndex) {
|
|
51
60
|
(evt.currentTarget as HTMLElement).setAttribute('tabindex', '-1');
|
|
52
61
|
newElement?.setAttribute('tabindex', '0');
|
package/src/index.ts
CHANGED
|
@@ -16,6 +16,7 @@ export * from './components/expansion-panel';
|
|
|
16
16
|
export * from './components/flag';
|
|
17
17
|
export * from './components/flex-box';
|
|
18
18
|
export * from './components/generic-block';
|
|
19
|
+
export * from './components/heading';
|
|
19
20
|
export * from './components/grid';
|
|
20
21
|
export * from './components/icon';
|
|
21
22
|
export * from './components/image-block';
|
|
@@ -41,6 +42,7 @@ export * from './components/slideshow';
|
|
|
41
42
|
export * from './components/switch';
|
|
42
43
|
export * from './components/table';
|
|
43
44
|
export * from './components/tabs';
|
|
45
|
+
export * from './components/text';
|
|
44
46
|
export * from './components/text-field';
|
|
45
47
|
export * from './components/thumbnail';
|
|
46
48
|
export * from './components/toolbar';
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** CSS selector listing all tabbable elements. */
|
|
2
|
+
export const TABBABLE_ELEMENTS_SELECTOR = `a[href], button, textarea, input:not([type="hidden"]):not([hidden]), [tabindex]`;
|
|
3
|
+
|
|
4
|
+
/** CSS selector matching element that are disabled (should not receive focus). */
|
|
5
|
+
export const DISABLED_SELECTOR = `[hidden], [tabindex="-1"], [disabled]:not([disabled="false"]), [aria-disabled]:not([aria-disabled="false"])`;
|
|
@@ -1,10 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
const TABBABLE_ELEMENTS_SELECTOR = `a[href], button, textarea, input:not([type="hidden"]):not([hidden]), [tabindex]`;
|
|
3
|
-
|
|
4
|
-
/** CSS selector matching element that are disabled (should not receive focus). */
|
|
5
|
-
const DISABLED_SELECTOR = `[hidden], [tabindex="-1"], [disabled]:not([disabled="false"]), [aria-disabled]:not([aria-disabled="false"])`;
|
|
6
|
-
|
|
7
|
-
const isNotDisabled = (element: HTMLElement) => !element.matches(DISABLED_SELECTOR);
|
|
1
|
+
import { getFocusableElements } from './getFocusableElements';
|
|
8
2
|
|
|
9
3
|
/**
|
|
10
4
|
* Get first and last elements focusable in an element.
|
|
@@ -13,12 +7,12 @@ const isNotDisabled = (element: HTMLElement) => !element.matches(DISABLED_SELECT
|
|
|
13
7
|
* @return first and last focusable elements
|
|
14
8
|
*/
|
|
15
9
|
export function getFirstAndLastFocusable(parentElement: HTMLElement) {
|
|
16
|
-
const focusableElements =
|
|
10
|
+
const focusableElements = getFocusableElements(parentElement);
|
|
17
11
|
|
|
18
12
|
// First non disabled element.
|
|
19
|
-
const first = focusableElements
|
|
13
|
+
const first = focusableElements[0];
|
|
20
14
|
// Last non disabled element.
|
|
21
|
-
const last = focusableElements.
|
|
15
|
+
const last = focusableElements[focusableElements.length - 1];
|
|
22
16
|
|
|
23
17
|
if (last && first) {
|
|
24
18
|
return { first, last };
|