@lumx/react 2.2.0 → 2.2.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 (29) hide show
  1. package/esm/_internal/Avatar2.js +5 -1
  2. package/esm/_internal/Avatar2.js.map +1 -1
  3. package/esm/_internal/Thumbnail2.js +2 -0
  4. package/esm/_internal/Thumbnail2.js.map +1 -1
  5. package/esm/_internal/Tooltip2.js +0 -5
  6. package/esm/_internal/Tooltip2.js.map +1 -1
  7. package/esm/_internal/UserBlock.js +42 -17
  8. package/esm/_internal/UserBlock.js.map +1 -1
  9. package/esm/_internal/user-block.js +2 -0
  10. package/esm/_internal/user-block.js.map +1 -1
  11. package/package.json +4 -4
  12. package/src/components/avatar/Avatar.stories.tsx +30 -53
  13. package/src/components/avatar/Avatar.tsx +8 -0
  14. package/src/components/avatar/__snapshots__/Avatar.test.tsx.snap +193 -330
  15. package/src/components/thumbnail/Thumbnail.stories.tsx +8 -7
  16. package/src/components/thumbnail/Thumbnail.test.tsx +1 -7
  17. package/src/components/thumbnail/Thumbnail.tsx +2 -0
  18. package/src/components/thumbnail/__snapshots__/Thumbnail.test.tsx.snap +4 -2
  19. package/src/components/tooltip/Tooltip.stories.tsx +7 -4
  20. package/src/components/tooltip/useInjectTooltipRef.tsx +1 -3
  21. package/src/components/user-block/UserBlock.stories.tsx +65 -105
  22. package/src/components/user-block/UserBlock.test.tsx +6 -0
  23. package/src/components/user-block/UserBlock.tsx +51 -25
  24. package/src/components/user-block/__snapshots__/UserBlock.test.tsx.snap +113 -143
  25. package/src/stories/generated/Badge/Demos.stories.tsx +1 -0
  26. package/src/stories/generated/Flag/Demos.stories.tsx +6 -0
  27. package/src/stories/generated/Thumbnail/Demos.stories.tsx +1 -0
  28. package/src/stories/utils/CustomLink.tsx +7 -0
  29. package/types.d.ts +14 -4
@@ -19,6 +19,7 @@ import { focusKnob } from '@lumx/react/stories/knobs/focusKnob';
19
19
  import { sizeKnob } from '@lumx/react/stories/knobs/sizeKnob';
20
20
  import { action } from '@storybook/addon-actions';
21
21
  import classNames from 'classnames';
22
+ import { CustomLink } from '@lumx/react/stories/utils/CustomLink';
22
23
 
23
24
  export default { title: 'LumX components/thumbnail/Thumbnail' };
24
25
 
@@ -130,18 +131,12 @@ export const ClickableLink = () => (
130
131
  />
131
132
  );
132
133
 
133
- const CustomLinkComponent = (props: any) => (
134
- <a {...props} className={classNames('custom-link-component', props.className)}>
135
- {props.children}
136
- </a>
137
- );
138
-
139
134
  export const ClickableCustomLink = () => (
140
135
  <Thumbnail
141
136
  alt="Click me"
142
137
  image={imageKnob()}
143
138
  size={sizeKnob('Size', Size.xxl)}
144
- linkAs={CustomLinkComponent}
139
+ linkAs={CustomLink}
145
140
  linkProps={{ href: 'https://google.fr', className: 'custom-class-name' }}
146
141
  />
147
142
  );
@@ -280,11 +275,13 @@ export const Vertical = () => (
280
275
  <Thumbnail alt="" aspectRatio="vertical" image={IMAGES.portrait1s200} size="xxl" />
281
276
  </FlexBox>
282
277
  <h2>With size & smaller image & fill height</h2>
278
+ <small>Unsupported use case (use ratio free with fill height)</small>
283
279
  <FlexBox orientation="horizontal" vAlign="center" gap="huge">
284
280
  <Thumbnail alt="" aspectRatio="vertical" image={IMAGES.landscape1s200} size="xxl" fillHeight />
285
281
  <Thumbnail alt="" aspectRatio="vertical" image={IMAGES.portrait1s200} size="xxl" fillHeight />
286
282
  </FlexBox>
287
283
  <h2>Constrained parent size & smaller image & fill height</h2>
284
+ <small>Unsupported use case (use ratio free with fill height)</small>
288
285
  <FlexBox orientation="horizontal" vAlign="center" gap="huge">
289
286
  <div className="parent" style={{ width: 220 }}>
290
287
  <Thumbnail alt="" aspectRatio="vertical" image={IMAGES.landscape1s200} fillHeight />
@@ -334,11 +331,13 @@ export const Wide = () => (
334
331
  <Thumbnail alt="" aspectRatio="wide" image={IMAGES.portrait1s200} size="xxl" />
335
332
  </FlexBox>
336
333
  <h2>With size & smaller image & fill height</h2>
334
+ <small>Unsupported use case (use ratio free with fill height)</small>
337
335
  <FlexBox orientation="horizontal" vAlign="center" gap="huge">
338
336
  <Thumbnail alt="" aspectRatio="wide" image={IMAGES.landscape1s200} size="xxl" fillHeight />
339
337
  <Thumbnail alt="" aspectRatio="wide" image={IMAGES.portrait1s200} size="xxl" fillHeight />
340
338
  </FlexBox>
341
339
  <h2>Constrained parent size & smaller image & fill height</h2>
340
+ <small>Unsupported use case (use ratio free with fill height)</small>
342
341
  <FlexBox orientation="horizontal" vAlign="center" gap="huge">
343
342
  <div className="parent" style={{ width: 220 }}>
344
343
  <Thumbnail alt="" aspectRatio="wide" image={IMAGES.landscape1s200} fillHeight />
@@ -388,11 +387,13 @@ export const Square = () => (
388
387
  <Thumbnail alt="" aspectRatio="square" image={IMAGES.portrait1s200} size="xxl" />
389
388
  </FlexBox>
390
389
  <h2>With size & smaller image & fill height</h2>
390
+ <small>Unsupported use case (use ratio free with fill height)</small>
391
391
  <FlexBox orientation="horizontal" vAlign="center" gap="huge">
392
392
  <Thumbnail alt="" aspectRatio="square" image={IMAGES.landscape1s200} size="xxl" fillHeight />
393
393
  <Thumbnail alt="" aspectRatio="square" image={IMAGES.portrait1s200} size="xxl" fillHeight />
394
394
  </FlexBox>
395
395
  <h2>Constrained parent size & smaller image & fill height</h2>
396
+ <small>Unsupported use case (use ratio free with fill height)</small>
396
397
  <FlexBox orientation="horizontal" vAlign="center" gap="huge">
397
398
  <div className="parent" style={{ width: 220 }}>
398
399
  <Thumbnail alt="" aspectRatio="square" image={IMAGES.landscape1s200} fillHeight />
@@ -5,13 +5,7 @@ import 'jest-enzyme';
5
5
  import { commonTestsSuite, itShouldRenderStories } from '@lumx/react/testing/utils';
6
6
 
7
7
  import { Thumbnail, ThumbnailProps } from './Thumbnail';
8
- import {
9
- Clickable,
10
- ClickableCustomLink,
11
- ClickableLink,
12
- Default,
13
- WithBadge,
14
- } from './Thumbnail.stories';
8
+ import { Clickable, ClickableCustomLink, ClickableLink, Default, WithBadge } from './Thumbnail.stories';
15
9
 
16
10
  const CLASSNAME = Thumbnail.className as string;
17
11
 
@@ -151,6 +151,8 @@ export const Thumbnail: Comp<ThumbnailProps> = forwardRef((props, ref) => {
151
151
  Object.assign(wrapperProps, linkProps);
152
152
  } else if (isButton) {
153
153
  Wrapper = 'button';
154
+ wrapperProps.type = forwardedProps.type || 'button';
155
+ wrapperProps['aria-label'] = forwardedProps['aria-label'] || alt;
154
156
  }
155
157
 
156
158
  return (
@@ -2,8 +2,10 @@
2
2
 
3
3
  exports[`<Thumbnail> Snapshots and structure should render story 'Clickable' 1`] = `
4
4
  <button
5
+ aria-label="Click me"
5
6
  className="lumx-thumbnail lumx-thumbnail--aspect-ratio-original lumx-thumbnail--size-xxl lumx-thumbnail--theme-light lumx-thumbnail--is-clickable lumx-thumbnail--is-loading"
6
7
  onClick={[Function]}
8
+ type="button"
7
9
  >
8
10
  <div
9
11
  className="lumx-thumbnail__background"
@@ -20,7 +22,7 @@ exports[`<Thumbnail> Snapshots and structure should render story 'Clickable' 1`]
20
22
  `;
21
23
 
22
24
  exports[`<Thumbnail> Snapshots and structure should render story 'ClickableCustomLink' 1`] = `
23
- <CustomLinkComponent
25
+ <CustomLink
24
26
  className="custom-class-name lumx-thumbnail lumx-thumbnail--aspect-ratio-original lumx-thumbnail--size-xxl lumx-thumbnail--theme-light lumx-thumbnail--is-clickable lumx-thumbnail--is-loading"
25
27
  href="https://google.fr"
26
28
  >
@@ -35,7 +37,7 @@ exports[`<Thumbnail> Snapshots and structure should render story 'ClickableCusto
35
37
  style={Object {}}
36
38
  />
37
39
  </div>
38
- </CustomLinkComponent>
40
+ </CustomLink>
39
41
  `;
40
42
 
41
43
  exports[`<Thumbnail> Snapshots and structure should render story 'ClickableLink' 1`] = `
@@ -76,13 +76,16 @@ export const EmptyTooltip = () => (
76
76
  );
77
77
 
78
78
  export const TooltipWithDropdown = () => {
79
- const buttonRef = useRef(null);
79
+ const [button, setButton] = useState<HTMLElement | null>(null);
80
+ const [isOpen, setOpen] = useState(false);
80
81
  return (
81
82
  <>
82
- <Tooltip label="Tooltip">
83
- <Button ref={buttonRef}>Anchor</Button>
83
+ <Tooltip label={!isOpen && 'Tooltip'} placement="top">
84
+ <Button ref={setButton} onClick={() => setOpen((o) => !o)}>
85
+ Anchor
86
+ </Button>
84
87
  </Tooltip>
85
- <Dropdown anchorRef={buttonRef} isOpen>
88
+ <Dropdown anchorRef={{ current: button }} isOpen={isOpen}>
86
89
  Dropdown
87
90
  </Dropdown>
88
91
  </>
@@ -28,9 +28,7 @@ export const useInjectTooltipRef = (
28
28
  get(children, 'props.isDisabled') !== true
29
29
  ) {
30
30
  const element = children as any;
31
- if (element.ref) {
32
- setAnchorElement(element.ref.current);
33
- }
31
+
34
32
  return cloneElement(element, {
35
33
  ...element.props,
36
34
  ...ariaProps,
@@ -1,116 +1,76 @@
1
- import { mdiStar } from '@lumx/icons';
2
- import { Badge, ColorPalette, Icon, List, ListItem, Size } from '@lumx/react';
3
- import { AVATAR_IMAGES, avatarImageKnob } from '@lumx/react/stories/knobs/image';
4
1
  import React from 'react';
2
+
3
+ import { mdiStar } from '@lumx/icons';
4
+ import { Badge, ColorPalette, Icon, Size } from '@lumx/react';
5
+ import { avatarImageKnob } from '@lumx/react/stories/knobs/image';
6
+ import { CustomLink } from '@lumx/react/stories/utils/CustomLink';
7
+
5
8
  import { UserBlock } from './UserBlock';
6
9
 
7
10
  export default { title: 'LumX components/user-block/UserBlock' };
8
11
 
9
- export const Sizes = () => {
10
- const logAction = (action: string) => () => console.log(action);
11
- return [Size.s, Size.m, Size.l].map((size: any) => (
12
- <div className="demo-grid" key={size}>
13
- <UserBlock
14
- name="Emmitt O. Lum"
15
- fields={['Creative developer', 'Denpasar']}
16
- avatarProps={{ image: avatarImageKnob(), alt: 'Avatar' }}
17
- size={size}
18
- onMouseEnter={logAction('Mouse entered')}
19
- onMouseLeave={logAction('Mouse left')}
20
- onClick={logAction('UserBlock clicked')}
21
- />
22
- </div>
12
+ const logAction = (action: string) => () => console.log(action);
13
+ const sizes = [Size.s, Size.m, Size.l];
14
+
15
+ export const Default = ({ theme }: any) => (
16
+ <UserBlock
17
+ theme={theme}
18
+ name="Emmitt O. Lum"
19
+ fields={['Creative developer', 'Denpasar']}
20
+ avatarProps={{ image: avatarImageKnob(), alt: 'Avatar' }}
21
+ onMouseEnter={logAction('Mouse entered')}
22
+ onMouseLeave={logAction('Mouse left')}
23
+ />
24
+ );
25
+
26
+ export const Sizes = ({ theme }: any) =>
27
+ sizes.map((size) => (
28
+ <UserBlock
29
+ key={size}
30
+ theme={theme}
31
+ name="Emmitt O. Lum"
32
+ fields={['Creative developer', 'Denpasar']}
33
+ avatarProps={{ image: avatarImageKnob(), alt: 'Avatar' }}
34
+ size={size}
35
+ onMouseEnter={logAction('Mouse entered')}
36
+ onMouseLeave={logAction('Mouse left')}
37
+ />
23
38
  ));
24
- };
25
39
 
26
- export const WithBadge = () => {
27
- const logAction = (action: string) => () => console.log(action);
40
+ export const Clickable = ({ theme }: any) => {
41
+ const baseProps = {
42
+ theme,
43
+ name: 'Emmitt O. Lum',
44
+ fields: ['Creative developer', 'Denpasar'],
45
+ avatarProps: { image: avatarImageKnob(), alt: 'Avatar' },
46
+ } as any;
28
47
  return (
29
- <div className="demo-grid">
30
- <UserBlock
31
- name="Emmitt O. Lum"
32
- fields={['Creative developer', 'Denpasar']}
33
- avatarProps={{
34
- image: avatarImageKnob(),
35
- alt: 'Avatar',
36
- badge: (
37
- <Badge color={ColorPalette.blue}>
38
- <Icon icon={mdiStar} />
39
- </Badge>
40
- ),
41
- }}
42
- size={Size.m}
43
- onMouseEnter={logAction('Mouse entered')}
44
- onMouseLeave={logAction('Mouse left')}
45
- onClick={logAction('UserBlock clicked')}
46
- />
47
- </div>
48
- );
49
- };
48
+ <>
49
+ <p>As a button</p>
50
+ <UserBlock {...baseProps} onClick={logAction('UserBlock clicked')} />
50
51
 
51
- export const InList = () => {
52
- const logAction = (action: string) => () => console.log(action);
53
- return (
54
- <div className="demo-grid">
55
- <List itemPadding={Size.big}>
56
- <ListItem className="lumx-color-background-dark-L6" size={Size.big}>
57
- <UserBlock
58
- name="Emmitt O. Lum"
59
- fields={['Creative developer', 'Denpasar']}
60
- avatarProps={{
61
- image: avatarImageKnob('Avatar 1', AVATAR_IMAGES.avatar1),
62
- alt: 'Avatar',
63
- badge: (
64
- <Badge color={ColorPalette.blue}>
65
- <Icon icon={mdiStar} />
66
- </Badge>
67
- ),
68
- }}
69
- size={Size.m}
70
- onMouseEnter={logAction('Mouse entered')}
71
- onMouseLeave={logAction('Mouse left')}
72
- onClick={logAction('UserBlock clicked')}
73
- />
74
- </ListItem>
75
- <ListItem className="lumx-color-background-dark-L6" size={Size.big}>
76
- <UserBlock
77
- name="Emmitt O. Lum"
78
- fields={['Creative developer', 'Denpasar']}
79
- avatarProps={{
80
- image: avatarImageKnob('Avatar 2', AVATAR_IMAGES.avatar2),
81
- alt: 'Avatar',
82
- badge: (
83
- <Badge color={ColorPalette.blue}>
84
- <Icon icon={mdiStar} />
85
- </Badge>
86
- ),
87
- }}
88
- size={Size.m}
89
- onMouseEnter={logAction('Mouse entered')}
90
- onMouseLeave={logAction('Mouse left')}
91
- onClick={logAction('UserBlock clicked')}
92
- />
93
- </ListItem>
94
- <ListItem className="lumx-color-background-dark-L6" size={Size.big}>
95
- <UserBlock
96
- name="Emmitt O. Lum"
97
- fields={['Creative developer', 'Denpasar']}
98
- avatarProps={{
99
- image: avatarImageKnob('Avatar 3', AVATAR_IMAGES.avatar3),
100
- alt: 'Avatar',
101
- badge: (
102
- <Badge color={ColorPalette.blue}>
103
- <Icon icon={mdiStar} />
104
- </Badge>
105
- ),
106
- }}
107
- size={Size.m}
108
- onMouseEnter={logAction('Mouse entered')}
109
- onMouseLeave={logAction('Mouse left')}
110
- onClick={logAction('UserBlock clicked')}
111
- />
112
- </ListItem>
113
- </List>
114
- </div>
52
+ <p>As a link</p>
53
+ <UserBlock {...baseProps} linkProps={{ href: 'https://example.com' }} />
54
+
55
+ <p>As a custom link component</p>
56
+ <UserBlock {...baseProps} linkAs={CustomLink} />
57
+ </>
115
58
  );
116
59
  };
60
+
61
+ export const WithBadge = ({ theme }: any) => (
62
+ <UserBlock
63
+ theme={theme}
64
+ name="Emmitt O. Lum"
65
+ fields={['Creative developer', 'Denpasar']}
66
+ avatarProps={{
67
+ image: avatarImageKnob(),
68
+ alt: 'Avatar',
69
+ badge: (
70
+ <Badge color={ColorPalette.blue}>
71
+ <Icon icon={mdiStar} />
72
+ </Badge>
73
+ ),
74
+ }}
75
+ />
76
+ );
@@ -25,6 +25,12 @@ describe(`<${UserBlock.displayName}>`, () => {
25
25
  // 1. Test render via snapshot.
26
26
  describe('Snapshots and structure', () => {
27
27
  itShouldRenderStories(stories, UserBlock);
28
+
29
+ it('should forward name props', () => {
30
+ const { wrapper } = setup({ name: 'John Doe', nameProps: { 'data-custom-attribute': true } });
31
+
32
+ expect(wrapper.find('.lumx-user-block__name[data-custom-attribute]')).toHaveLength(1);
33
+ });
28
34
  });
29
35
 
30
36
  // Common tests suite.
@@ -1,10 +1,10 @@
1
1
  import React, { forwardRef, ReactNode } from 'react';
2
-
2
+ import isEmpty from 'lodash/isEmpty';
3
3
  import classNames from 'classnames';
4
4
 
5
- import { Avatar, Orientation, Size, Theme } from '@lumx/react';
6
-
5
+ import { Avatar, ColorPalette, Link, Orientation, Size, Theme } from '@lumx/react';
7
6
  import { Comp, GenericProps, getRootClassName, handleBasicClasses } from '@lumx/react/utils';
7
+
8
8
  import { AvatarProps } from '../avatar/Avatar';
9
9
 
10
10
  /**
@@ -18,16 +18,22 @@ export type UserBlockSize = Extract<Size, 's' | 'm' | 'l'>;
18
18
  export interface UserBlockProps extends GenericProps {
19
19
  /** Props to pass to the avatar. */
20
20
  avatarProps?: AvatarProps;
21
- /** Simple action toolbar content. */
22
- simpleAction?: ReactNode;
23
- /** Multiple action toolbar content. */
24
- multipleActions?: ReactNode;
25
21
  /** Additional fields used to describe the user. */
26
22
  fields?: string[];
23
+ /** Props to pass to the link wrapping the avatar thumbnail. */
24
+ linkProps?: React.DetailedHTMLProps<React.AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>;
25
+ /** Custom react component for the link (can be used to inject react router Link). */
26
+ linkAs?: 'a' | any;
27
+ /** Multiple action toolbar content. */
28
+ multipleActions?: ReactNode;
27
29
  /** User name. */
28
30
  name?: string;
31
+ /** Props to pass to the name block. */
32
+ nameProps?: GenericProps;
29
33
  /** Orientation. */
30
34
  orientation?: Orientation;
35
+ /** Simple action toolbar content. */
36
+ simpleAction?: ReactNode;
31
37
  /** Size variant. */
32
38
  size?: UserBlockSize;
33
39
  /** Theme adapting the component to light or dark background. */
@@ -71,8 +77,11 @@ export const UserBlock: Comp<UserBlockProps, HTMLDivElement> = forwardRef((props
71
77
  avatarProps,
72
78
  className,
73
79
  fields,
80
+ linkProps,
81
+ linkAs,
74
82
  multipleActions,
75
83
  name,
84
+ nameProps,
76
85
  onClick,
77
86
  onMouseEnter,
78
87
  onMouseLeave,
@@ -91,18 +100,35 @@ export const UserBlock: Comp<UserBlockProps, HTMLDivElement> = forwardRef((props
91
100
 
92
101
  const shouldDisplayActions: boolean = orientation === Orientation.vertical;
93
102
 
94
- const nameBlock: ReactNode = name && (
95
- // eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-noninteractive-tabindex,jsx-a11y/no-static-element-interactions
96
- <span className={`${CLASSNAME}__name`} onClick={onClick} tabIndex={onClick ? 0 : -1}>
97
- {name}
98
- </span>
99
- );
103
+ const isLink = Boolean(linkProps?.href || linkAs);
104
+ const isClickable = !!onClick || isLink;
105
+
106
+ const nameBlock: ReactNode = React.useMemo(() => {
107
+ if (isEmpty(name)) {
108
+ return null;
109
+ }
110
+ let NameComponent: any = 'span';
111
+ const nProps: any = {
112
+ ...nameProps,
113
+ className: classNames(`${CLASSNAME}__name`, linkProps?.className, nameProps?.className),
114
+ };
115
+ if (isClickable) {
116
+ NameComponent = Link;
117
+ Object.assign(nProps, {
118
+ ...linkProps,
119
+ onClick,
120
+ linkAs,
121
+ color: ColorPalette.dark,
122
+ });
123
+ }
124
+ return <NameComponent {...nProps}>{name}</NameComponent>;
125
+ }, [isClickable, linkAs, linkProps, name, nameProps, onClick]);
100
126
 
101
127
  const fieldsBlock: ReactNode = fields && componentSize !== Size.s && (
102
128
  <div className={`${CLASSNAME}__fields`}>
103
- {fields.map((aField: string, idx: number) => (
129
+ {fields.map((field: string, idx: number) => (
104
130
  <span key={idx} className={`${CLASSNAME}__field`}>
105
- {aField}
131
+ {field}
106
132
  </span>
107
133
  ))}
108
134
  </div>
@@ -114,21 +140,21 @@ export const UserBlock: Comp<UserBlockProps, HTMLDivElement> = forwardRef((props
114
140
  {...forwardedProps}
115
141
  className={classNames(
116
142
  className,
117
- handleBasicClasses({ prefix: CLASSNAME, orientation, size: componentSize, theme }),
143
+ handleBasicClasses({ prefix: CLASSNAME, orientation, size: componentSize, theme, isClickable }),
118
144
  )}
119
145
  onMouseLeave={onMouseLeave}
120
146
  onMouseEnter={onMouseEnter}
121
147
  >
122
148
  {avatarProps && (
123
- <div className={`${CLASSNAME}__avatar`}>
124
- <Avatar
125
- {...avatarProps}
126
- size={componentSize}
127
- onClick={onClick}
128
- tabIndex={onClick ? 0 : -1}
129
- theme={theme}
130
- />
131
- </div>
149
+ <Avatar
150
+ linkAs={linkAs}
151
+ linkProps={linkProps}
152
+ {...avatarProps}
153
+ className={classNames(`${CLASSNAME}__avatar`, avatarProps.className)}
154
+ size={componentSize}
155
+ onClick={onClick}
156
+ theme={theme}
157
+ />
132
158
  )}
133
159
  {(fields || name) && (
134
160
  <div className={`${CLASSNAME}__wrapper`}>