@lumx/react 2.2.0 → 2.2.1-alpha.0

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.
@@ -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,34 @@ 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
+ linkAs,
120
+ color: ColorPalette.dark,
121
+ });
122
+ }
123
+ return <NameComponent {...nProps}>{name}</NameComponent>;
124
+ }, [isClickable, linkAs, linkProps, name, nameProps]);
100
125
 
101
126
  const fieldsBlock: ReactNode = fields && componentSize !== Size.s && (
102
127
  <div className={`${CLASSNAME}__fields`}>
103
- {fields.map((aField: string, idx: number) => (
128
+ {fields.map((field: string, idx: number) => (
104
129
  <span key={idx} className={`${CLASSNAME}__field`}>
105
- {aField}
130
+ {field}
106
131
  </span>
107
132
  ))}
108
133
  </div>
@@ -114,21 +139,21 @@ export const UserBlock: Comp<UserBlockProps, HTMLDivElement> = forwardRef((props
114
139
  {...forwardedProps}
115
140
  className={classNames(
116
141
  className,
117
- handleBasicClasses({ prefix: CLASSNAME, orientation, size: componentSize, theme }),
142
+ handleBasicClasses({ prefix: CLASSNAME, orientation, size: componentSize, theme, isClickable }),
118
143
  )}
119
144
  onMouseLeave={onMouseLeave}
120
145
  onMouseEnter={onMouseEnter}
121
146
  >
122
147
  {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>
148
+ <Avatar
149
+ linkAs={linkAs}
150
+ linkProps={linkProps}
151
+ {...avatarProps}
152
+ className={classNames(`${CLASSNAME}__avatar`, avatarProps.className)}
153
+ size={componentSize}
154
+ onClick={onClick}
155
+ theme={theme}
156
+ />
132
157
  )}
133
158
  {(fields || name) && (
134
159
  <div className={`${CLASSNAME}__wrapper`}>