@lumx/react 3.9.4-alpha.3 → 3.9.4

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/package.json CHANGED
@@ -6,8 +6,8 @@
6
6
  "url": "https://github.com/lumapps/design-system/issues"
7
7
  },
8
8
  "dependencies": {
9
- "@lumx/core": "^3.9.4-alpha.3",
10
- "@lumx/icons": "^3.9.4-alpha.3",
9
+ "@lumx/core": "^3.9.4",
10
+ "@lumx/icons": "^3.9.4",
11
11
  "@popperjs/core": "^2.5.4",
12
12
  "body-scroll-lock": "^3.1.5",
13
13
  "classnames": "^2.3.2",
@@ -111,5 +111,5 @@
111
111
  "build:storybook": "storybook build"
112
112
  },
113
113
  "sideEffects": false,
114
- "version": "3.9.4-alpha.3"
114
+ "version": "3.9.4"
115
115
  }
@@ -0,0 +1,76 @@
1
+ import { mdiAccount, mdiBee, mdiHeart } from '@lumx/icons';
2
+ import { Badge, BadgeWrapper, Button, Chip, ColorPalette, FlexBox, Icon, Orientation, Size } from '@lumx/react';
3
+ import React from 'react';
4
+
5
+ export default {
6
+ title: 'LumX components/badge/BadgeWrapper',
7
+ component: BadgeWrapper,
8
+ argTypes: {
9
+ children: { control: false },
10
+ },
11
+ };
12
+
13
+ /**
14
+ * Using badge wrapper with icon
15
+ */
16
+ export const WithIcon = {
17
+ args: {
18
+ badge: (
19
+ <Badge color={ColorPalette.red}>
20
+ <Icon icon={mdiHeart} />
21
+ </Badge>
22
+ ),
23
+ children: <Icon icon={mdiAccount} size={Size.m} color={ColorPalette.dark} />,
24
+ },
25
+ };
26
+
27
+ /**
28
+ * Using badge wrapper with icon
29
+ */
30
+ export const WithButton = {
31
+ args: {
32
+ badge: (
33
+ <Badge color={ColorPalette.red}>
34
+ <Icon icon={mdiHeart} />
35
+ </Badge>
36
+ ),
37
+ children: <Button>Some button</Button>,
38
+ },
39
+ };
40
+
41
+ export const WithChip = {
42
+ args: {
43
+ badge: (
44
+ <Badge color={ColorPalette.red}>
45
+ <Icon icon={mdiHeart} />
46
+ </Badge>
47
+ ),
48
+ children: <Chip>Some chip</Chip>,
49
+ },
50
+ };
51
+
52
+ const InFlexbox = ({ orientation }: { orientation: Orientation }) => (
53
+ <FlexBox orientation={orientation} gap={Size.medium} vAlign="left">
54
+ <BadgeWrapper
55
+ badge={
56
+ <Badge color={ColorPalette.yellow}>
57
+ <Icon icon={mdiBee} />
58
+ </Badge>
59
+ }
60
+ >
61
+ <Chip color={ColorPalette.green}>Some chip</Chip>
62
+ </BadgeWrapper>
63
+ <BadgeWrapper
64
+ badge={
65
+ <Badge color={ColorPalette.red}>
66
+ <Icon icon={mdiHeart} />
67
+ </Badge>
68
+ }
69
+ >
70
+ <Chip color={ColorPalette.blue}>Some other chip</Chip>
71
+ </BadgeWrapper>
72
+ </FlexBox>
73
+ );
74
+
75
+ export const InVerticalFlexbox = () => <InFlexbox orientation={Orientation.vertical} />;
76
+ export const InHorizontalFlexbox = () => <InFlexbox orientation={Orientation.horizontal} />;
@@ -0,0 +1,49 @@
1
+ import { commonTestsSuiteRTL } from '@lumx/react/testing/utils';
2
+ import React from 'react';
3
+
4
+ import omit from 'lodash/omit';
5
+ import last from 'lodash/last';
6
+
7
+ import { mdiAccount, mdiHeart } from '@lumx/icons/override/generated';
8
+ import { Badge, BadgeWrapper, BadgeWrapperProps, ColorPalette, Icon } from '@lumx/react';
9
+ import { getByClassName } from '@lumx/react/testing/utils/queries';
10
+ import { render } from '@testing-library/react';
11
+
12
+ const CLASSNAME = BadgeWrapper.className as string;
13
+
14
+ /**
15
+ * Mounts the component and returns common DOM elements / data needed in multiple tests further down.
16
+ */
17
+ const setup = (propsOverride: Partial<BadgeWrapperProps> = {}) => {
18
+ const props = {
19
+ badge: (
20
+ <Badge color={ColorPalette.red}>
21
+ <Icon icon={mdiHeart} />
22
+ </Badge>
23
+ ),
24
+ children: <Icon icon={mdiAccount} />,
25
+ ...propsOverride,
26
+ };
27
+ render(<BadgeWrapper {...omit(props, 'children')}>{props.children}</BadgeWrapper>);
28
+ const badgeWrapper = getByClassName(document.body, CLASSNAME);
29
+ return { badgeWrapper, props };
30
+ };
31
+
32
+ describe(`<${BadgeWrapper.displayName}>`, () => {
33
+ describe('Props', () => {
34
+ it('should render badge', () => {
35
+ const { badgeWrapper } = setup();
36
+
37
+ expect(badgeWrapper).toHaveClass('lumx-badge-wrapper');
38
+ expect(badgeWrapper.children).toHaveLength(2);
39
+ expect(last(badgeWrapper.children)).toHaveClass('lumx-badge-wrapper__badge');
40
+ });
41
+ });
42
+
43
+ commonTestsSuiteRTL(setup, {
44
+ baseClassName: CLASSNAME,
45
+ forwardClassName: 'badgeWrapper',
46
+ forwardAttributes: 'badgeWrapper',
47
+ forwardRef: 'badgeWrapper',
48
+ });
49
+ });
@@ -0,0 +1,36 @@
1
+ import { getRootClassName, handleBasicClasses } from '@lumx/react/utils/className';
2
+ import { DEFAULT_PROPS } from '@lumx/react/components/select/WithSelectContext';
3
+ import { Comp, GenericProps } from '@lumx/react/utils/type';
4
+ import classNames from 'classnames';
5
+ import React, { forwardRef, ReactElement, ReactNode } from 'react';
6
+
7
+ export interface BadgeWrapperProps extends GenericProps {
8
+ /** Badge. */
9
+ badge: ReactElement;
10
+ /** Node to display the badge on */
11
+ children: ReactNode;
12
+ }
13
+
14
+ /**
15
+ * Component display name.
16
+ */
17
+ const COMPONENT_NAME = 'BadgeWrapper';
18
+
19
+ /**
20
+ * Component default class name and class prefix.
21
+ */
22
+ const CLASSNAME = getRootClassName(COMPONENT_NAME);
23
+
24
+ export const BadgeWrapper: Comp<BadgeWrapperProps, HTMLDivElement> = forwardRef((props, ref) => {
25
+ const { badge, children, className, ...forwardedProps } = props;
26
+
27
+ return (
28
+ <div ref={ref} {...forwardedProps} className={classNames(className, handleBasicClasses({ prefix: CLASSNAME }))}>
29
+ {children}
30
+ {badge && <div className={`${CLASSNAME}__badge`}>{badge}</div>}
31
+ </div>
32
+ );
33
+ });
34
+ BadgeWrapper.displayName = 'BadgeWrapper';
35
+ BadgeWrapper.className = CLASSNAME;
36
+ BadgeWrapper.defaultProps = DEFAULT_PROPS;
@@ -1 +1,2 @@
1
1
  export * from './Badge';
2
+ export * from './BadgeWrapper';
@@ -1,7 +1,7 @@
1
1
  import React from 'react';
2
2
 
3
3
  import { commonTestsSuiteRTL } from '@lumx/react/testing/utils';
4
- import { queryByRole, render } from '@testing-library/react';
4
+ import { queryByRole, render, screen } from '@testing-library/react';
5
5
  import { getByClassName, queryByClassName } from '@lumx/react/testing/utils/queries';
6
6
  import userEvent from '@testing-library/user-event';
7
7
  import { isFocusVisible } from '@lumx/react/utils/isFocusVisible';
@@ -12,12 +12,15 @@ const CLASSNAME = ExpansionPanel.className as string;
12
12
 
13
13
  jest.mock('@lumx/react/utils/isFocusVisible');
14
14
 
15
+ const mockChildrenContent = 'children content';
16
+
15
17
  /**
16
18
  * Mounts the component and returns common DOM elements / data needed in multiple tests further down.
17
19
  */
18
20
  const setup = (propsOverride: Partial<ExpansionPanelProps> = {}) => {
19
21
  const props = {
20
22
  toggleButtonProps: { label: 'Toggle' },
23
+ children: mockChildrenContent,
21
24
  ...propsOverride,
22
25
  };
23
26
  const { container } = render(<ExpansionPanel {...props} />);
@@ -28,7 +31,7 @@ const setup = (propsOverride: Partial<ExpansionPanelProps> = {}) => {
28
31
  query: {
29
32
  toggleButton: () => queryByRole(container, 'button', { name: /Toggle/i }),
30
33
  header: () => queryByClassName(container, `${CLASSNAME}__header`),
31
- content: () => queryByClassName(container, `${CLASSNAME}__wrapper`),
34
+ content: () => screen.queryByText(mockChildrenContent),
32
35
  },
33
36
  props,
34
37
  };
@@ -12,6 +12,7 @@ import { ColorPalette, DragHandle, Emphasis, IconButton, IconButtonProps, Theme
12
12
  import { Comp, GenericProps, HasTheme, isComponent } from '@lumx/react/utils/type';
13
13
  import { getRootClassName, handleBasicClasses } from '@lumx/react/utils/className';
14
14
  import { partitionMulti } from '@lumx/react/utils/partitionMulti';
15
+ import { WINDOW } from '@lumx/react/constants';
15
16
 
16
17
  /**
17
18
  * Defines the props of the component.
@@ -80,6 +81,7 @@ export const ExpansionPanel: Comp<ExpansionPanelProps, HTMLDivElement> = forward
80
81
  ...forwardedProps
81
82
  } = props;
82
83
 
84
+ const [isChildrenVisible, setIsChildrenVisible] = useState(isOpen);
83
85
  const children: ReactNode[] = Children.toArray(anyChildren);
84
86
 
85
87
  // Partition children by types.
@@ -94,15 +96,34 @@ export const ExpansionPanel: Comp<ExpansionPanelProps, HTMLDivElement> = forward
94
96
  );
95
97
 
96
98
  const toggleOpen = (event: React.MouseEvent) => {
99
+ const hasReducedMotionEnabled = WINDOW?.matchMedia?.('(prefers-reduced-motion: reduce)')?.matches;
97
100
  const shouldOpen = !isOpen;
101
+
98
102
  if (isFunction(onOpen) && shouldOpen) {
99
103
  onOpen(event);
104
+ // On open, we immediately show children
105
+ setIsChildrenVisible(true);
100
106
  }
101
107
  if (isFunction(onClose) && !shouldOpen) {
102
108
  onClose(event);
109
+ /**
110
+ * On close, we only show children immediately if reduced motion is enabled
111
+ * When disabled, the children will be hidden via the `onTransitionEnd` prop.
112
+ */
113
+ if (hasReducedMotionEnabled) {
114
+ setIsChildrenVisible(false);
115
+ }
103
116
  }
104
117
  if (isFunction(onToggleOpen)) {
105
118
  onToggleOpen(shouldOpen, event);
119
+ /**
120
+ * On toggle, we forward the show state if
121
+ * * the toggle will open the expansion panel
122
+ * * reduced motion is enabled. When disabled, the children will be hidden via the `onTransitionEnd` prop.
123
+ * */
124
+ if (shouldOpen || hasReducedMotionEnabled) {
125
+ setIsChildrenVisible(shouldOpen);
126
+ }
106
127
  }
107
128
  };
108
129
 
@@ -123,7 +144,6 @@ export const ExpansionPanel: Comp<ExpansionPanelProps, HTMLDivElement> = forward
123
144
  );
124
145
 
125
146
  const wrapperRef = useRef<HTMLDivElement>(null);
126
- const isContentVisible = (): boolean => get(wrapperRef.current, 'clientHeight', 0) > 0;
127
147
 
128
148
  // Switch max height on/off to activate the CSS transition (updates when children changes).
129
149
  const [maxHeight, setMaxHeight] = useState('0');
@@ -153,8 +173,17 @@ export const ExpansionPanel: Comp<ExpansionPanelProps, HTMLDivElement> = forward
153
173
  </div>
154
174
  </header>
155
175
 
156
- {(isOpen || isContentVisible()) && (
157
- <div className={`${CLASSNAME}__wrapper`} style={{ maxHeight }}>
176
+ {(isOpen || isChildrenVisible) && (
177
+ <div
178
+ className={`${CLASSNAME}__wrapper`}
179
+ style={{ maxHeight }}
180
+ // At the end of the closing transition, remove the children from the DOM
181
+ onTransitionEnd={() => {
182
+ if (!isOpen) {
183
+ setIsChildrenVisible(false);
184
+ }
185
+ }}
186
+ >
158
187
  <div className={`${CLASSNAME}__container`} ref={wrapperRef}>
159
188
  <div className={`${CLASSNAME}__content`}>{content}</div>
160
189
 
@@ -31,7 +31,6 @@ export const ImageSlideshow: React.FC<ImageSlideshowProps> = ({
31
31
  activeIndex,
32
32
  slideshowId,
33
33
  setSlideshow,
34
- slideshow,
35
34
  slideshowSlidesId,
36
35
  slidesCount,
37
36
  onNextClick,
@@ -62,7 +61,6 @@ export const ImageSlideshow: React.FC<ImageSlideshowProps> = ({
62
61
  onNextClick={onNextClick}
63
62
  onPreviousClick={onPreviousClick}
64
63
  onPaginationClick={onPaginationClick}
65
- parentRef={slideshow}
66
64
  {...slideshowControlsProps}
67
65
  paginationItemProps={(index: number) => {
68
66
  const props = slideshowControlsProps?.paginationItemProps?.(index) || {};
@@ -74,7 +74,7 @@ export function usePointerZoom(
74
74
  const pointers = Object.values(activePointers);
75
75
 
76
76
  // Make sure we run computation on one of the pointer in the group
77
- if (pointers[0].pointerId !== event.pointerId) {
77
+ if (pointers[0]?.pointerId !== event.pointerId) {
78
78
  return;
79
79
  }
80
80
 
@@ -3,11 +3,9 @@ import chunk from 'lodash/chunk';
3
3
 
4
4
  import classNames from 'classnames';
5
5
 
6
- import { FULL_WIDTH_PERCENT, NEXT_SLIDE_EVENT, PREV_SLIDE_EVENT } from '@lumx/react/components/slideshow/constants';
6
+ import { FULL_WIDTH_PERCENT } from '@lumx/react/components/slideshow/constants';
7
7
  import { Comp, GenericProps, HasTheme } from '@lumx/react/utils/type';
8
8
  import { getRootClassName, handleBasicClasses } from '@lumx/react/utils/className';
9
- import { useMergeRefs } from '@lumx/react/utils/mergeRefs';
10
- import { useKeyNavigate } from '@lumx/react/components/slideshow/useKeyNavigate';
11
9
  import { buildSlideShowGroupId, SlideshowItemGroup } from './SlideshowItemGroup';
12
10
 
13
11
  export interface SlidesProps extends GenericProps, HasTheme {
@@ -34,7 +32,7 @@ export interface SlidesProps extends GenericProps, HasTheme {
34
32
  /**
35
33
  * Accessible label to set on a slide group.
36
34
  * Receives the group position starting from 1 and the total number of groups.
37
- */
35
+ * */
38
36
  slideGroupLabel?: (groupPosition: number, groupTotal: number) => string;
39
37
  }
40
38
 
@@ -72,6 +70,7 @@ export const Slides: Comp<SlidesProps, HTMLDivElement> = forwardRef((props, ref)
72
70
  slideGroupLabel,
73
71
  ...forwardedProps
74
72
  } = props;
73
+ const wrapperRef = React.useRef<HTMLDivElement>(null);
75
74
  const startIndexVisible = activeIndex;
76
75
  const endIndexVisible = startIndexVisible + 1;
77
76
 
@@ -83,17 +82,10 @@ export const Slides: Comp<SlidesProps, HTMLDivElement> = forwardRef((props, ref)
83
82
  return groupBy && groupBy > 1 ? chunk(childrenArray, groupBy) : childrenArray;
84
83
  }, [children, groupBy]);
85
84
 
86
- const slidesRef = React.useRef<HTMLDivElement>(null);
87
-
88
- const slide = slidesRef.current;
89
- const onNextSlide = React.useCallback(() => slide?.dispatchEvent(new CustomEvent(NEXT_SLIDE_EVENT)), [slide]);
90
- const onPrevSlide = React.useCallback(() => slide?.dispatchEvent(new CustomEvent(PREV_SLIDE_EVENT)), [slide]);
91
- useKeyNavigate(slide, onNextSlide, onPrevSlide);
92
-
93
85
  return (
94
86
  <section
95
87
  id={id}
96
- ref={useMergeRefs(slidesRef, ref)}
88
+ ref={ref}
97
89
  {...forwardedProps}
98
90
  className={classNames(className, handleBasicClasses({ prefix: CLASSNAME, theme }), {
99
91
  [`${CLASSNAME}--fill-height`]: fillHeight,
@@ -108,14 +100,14 @@ export const Slides: Comp<SlidesProps, HTMLDivElement> = forwardRef((props, ref)
108
100
  onMouseLeave={toggleAutoPlay}
109
101
  aria-live={isAutoPlaying ? 'off' : 'polite'}
110
102
  >
111
- <div className={`${CLASSNAME}__wrapper`} style={wrapperStyle}>
103
+ <div ref={wrapperRef} className={`${CLASSNAME}__wrapper`} style={wrapperStyle}>
112
104
  {groups.map((group, index) => (
113
105
  <SlideshowItemGroup
114
106
  key={index}
115
107
  id={slidesId && buildSlideShowGroupId(slidesId, index)}
116
- label={slideGroupLabel?.(index + 1, groups.length)}
108
+ role={hasControls ? 'tabpanel' : 'group'}
109
+ label={slideGroupLabel ? slideGroupLabel(index + 1, groups.length) : undefined}
117
110
  isDisplayed={index >= startIndexVisible && index < endIndexVisible}
118
- slidesRef={slidesRef}
119
111
  >
120
112
  {group}
121
113
  </SlideshowItemGroup>
@@ -1,7 +1,6 @@
1
- /* eslint-disable jsx-a11y/anchor-is-valid */
2
1
  import React from 'react';
3
2
  import range from 'lodash/range';
4
- import { AspectRatio, Button, FlexBox, ImageBlock, Slideshow, SlideshowItem, Orientation, Link } from '@lumx/react';
3
+ import { AspectRatio, Button, FlexBox, ImageBlock, Slideshow, SlideshowItem, Orientation } from '@lumx/react';
5
4
  import { IMAGES, LANDSCAPE_IMAGES } from '@lumx/react/stories/controls/image';
6
5
 
7
6
  export default {
@@ -66,16 +65,6 @@ export const ResponsiveSlideShowSwipe = () => {
66
65
  }}
67
66
  slideGroupLabel={(currentGroup, totalGroup) => `${currentGroup} of ${totalGroup}`}
68
67
  >
69
- <SlideshowItem>
70
- <FlexBox
71
- style={{ border: '1px solid grey', maxWidth: 300, height: 300 }}
72
- hAlign="center"
73
- vAlign="center"
74
- >
75
- <Link href="#">A link</Link>
76
- <Button>A button</Button>
77
- </FlexBox>
78
- </SlideshowItem>
79
68
  {slides.map((slide) => (
80
69
  <SlideshowItem key={`${slide}`}>
81
70
  <FlexBox
@@ -1,10 +1,11 @@
1
1
  import React, { forwardRef } from 'react';
2
2
 
3
3
  import { SlideshowControls, SlideshowControlsProps, Theme, Slides, SlidesProps } from '@lumx/react';
4
+ import { DEFAULT_OPTIONS } from '@lumx/react/hooks/useSlideshowControls';
4
5
  import { Comp, GenericProps } from '@lumx/react/utils/type';
5
6
  import { useFocusWithin } from '@lumx/react/hooks/useFocusWithin';
6
7
  import { mergeRefs } from '@lumx/react/utils/mergeRefs';
7
- import { DEFAULT_OPTIONS } from './useSlideshowControls';
8
+ import { buildSlideShowGroupId } from './SlideshowItemGroup';
8
9
 
9
10
  /**
10
11
  * Defines the props of the component.
@@ -13,7 +14,7 @@ export interface SlideshowProps
13
14
  extends GenericProps,
14
15
  Pick<SlidesProps, 'autoPlay' | 'slidesId' | 'id' | 'theme' | 'fillHeight' | 'groupBy' | 'slideGroupLabel'> {
15
16
  /** current slide active */
16
- activeIndex?: number;
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). */
@@ -133,14 +134,27 @@ export const Slideshow: Comp<SlideshowProps, HTMLDivElement> = forwardRef((props
133
134
  parentRef={slideshow}
134
135
  theme={theme}
135
136
  isAutoPlaying={isAutoPlaying}
137
+ nextButtonProps={{
138
+ 'aria-controls': slideshowSlidesId,
139
+ ...slideshowControlsProps.nextButtonProps,
140
+ }}
141
+ previousButtonProps={{
142
+ 'aria-controls': slideshowSlidesId,
143
+ ...slideshowControlsProps.previousButtonProps,
144
+ }}
136
145
  playButtonProps={
137
146
  autoPlay
138
147
  ? {
148
+ 'aria-controls': slideshowSlidesId,
139
149
  onClick: toggleForcePause,
140
150
  ...slideshowControlsProps.playButtonProps,
141
151
  }
142
152
  : undefined
143
153
  }
154
+ paginationItemProps={(index) => ({
155
+ 'aria-controls': buildSlideShowGroupId(slideshowSlidesId, index),
156
+ ...slideshowControlsProps.paginationItemProps?.(index),
157
+ })}
144
158
  />
145
159
  </div>
146
160
  ) : undefined
@@ -78,10 +78,11 @@ export const ControllingSlideshow = ({ images = Object.values(LANDSCAPE_IMAGES),
78
78
  parentRef={slideshow}
79
79
  theme={theme}
80
80
  isAutoPlaying={isAutoPlaying}
81
- nextButtonProps={{ label: 'Next' }}
82
- previousButtonProps={{ label: 'Previous' }}
81
+ nextButtonProps={{ label: 'Next', 'aria-controls': slideshowSlidesId }}
82
+ previousButtonProps={{ label: 'Previous', 'aria-controls': slideshowSlidesId }}
83
83
  playButtonProps={{
84
84
  label: 'Play/Pause',
85
+ 'aria-controls': slideshowSlidesId,
85
86
  onClick: toggleForcePause,
86
87
  }}
87
88
  paginationItemLabel={(index) => `Slide ${index}`}