@lumx/react 3.6.1-alpha.1 → 3.6.1

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
@@ -7,8 +7,8 @@
7
7
  },
8
8
  "dependencies": {
9
9
  "@juggle/resize-observer": "^3.2.0",
10
- "@lumx/core": "^3.6.1-alpha.1",
11
- "@lumx/icons": "^3.6.1-alpha.1",
10
+ "@lumx/core": "^3.6.1",
11
+ "@lumx/icons": "^3.6.1",
12
12
  "@popperjs/core": "^2.5.4",
13
13
  "body-scroll-lock": "^3.1.5",
14
14
  "classnames": "^2.3.2",
@@ -113,5 +113,5 @@
113
113
  "build:storybook": "storybook build"
114
114
  },
115
115
  "sideEffects": false,
116
- "version": "3.6.1-alpha.1"
116
+ "version": "3.6.1"
117
117
  }
@@ -172,6 +172,19 @@ export const Kind = {
172
172
  } as const;
173
173
  export type Kind = ValueOf<typeof Kind>;
174
174
 
175
+ /**
176
+ * All available white-space values
177
+ * */
178
+ export const WhiteSpace = {
179
+ normal: 'normal',
180
+ nowrap: 'nowrap',
181
+ pre: 'pre',
182
+ 'pre-wrap': 'pre-wrap',
183
+ 'pre-line': 'pre-line',
184
+ 'break-spaces': 'break-spaces',
185
+ };
186
+ export type WhiteSpace = ValueOf<typeof WhiteSpace>;
187
+
175
188
  /**
176
189
  * Re-exporting some utils types.
177
190
  */
@@ -6,9 +6,10 @@ import { textElementArgType } from '@lumx/react/stories/controls/element';
6
6
  import { withUndefined } from '@lumx/react/stories/controls/withUndefined';
7
7
  import { loremIpsum } from '@lumx/react/stories/utils/lorem';
8
8
  import { withCombinations } from '@lumx/react/stories/decorators/withCombinations';
9
- import { ColorPalette, ColorVariant, Icon } from '@lumx/react';
9
+ import { ColorPalette, ColorVariant, Icon, WhiteSpace } from '@lumx/react';
10
10
  import { mdiEarth, mdiHeart } from '@lumx/icons';
11
11
  import { withResizableBox } from '@lumx/react/stories/decorators/withResizableBox';
12
+ import { getSelectArgType } from '@lumx/react/stories/controls/selectArgType';
12
13
 
13
14
  import { Text } from './Text';
14
15
 
@@ -22,6 +23,7 @@ export default {
22
23
  typography: allTypographyArgType,
23
24
  color: colorArgType,
24
25
  colorVariant: colorVariantArgType,
26
+ whiteSpace: getSelectArgType(WhiteSpace),
25
27
  },
26
28
  };
27
29
 
@@ -71,6 +73,29 @@ export const NoWrap = {
71
73
  },
72
74
  };
73
75
 
76
+ /**
77
+ * Long text with line breaks
78
+ */
79
+ export const AllWhiteSpace = {
80
+ args: {
81
+ ...Default.args,
82
+ children: `
83
+ But ere she from the church-door stepped She smiled and told us why: 'It was a wicked woman's curse,' Quoth she,
84
+ 'and what care I?' She smiled, and smiled, and passed it off Ere from the door she stept—
85
+ `,
86
+ },
87
+ decorators: [
88
+ withCombinations({
89
+ combinations: {
90
+ rows: { key: 'whiteSpace', options: Object.values(WhiteSpace) },
91
+ },
92
+ tableStyle: { width: 500, tableLayout: 'fixed' },
93
+ firstColStyle: { width: 100 },
94
+ cellStyle: { border: '1px solid #000', width: '100%' },
95
+ }),
96
+ ],
97
+ };
98
+
74
99
  /**
75
100
  * Long text with single line truncate ellipsis
76
101
  */
@@ -61,6 +61,12 @@ describe(`<${Text.displayName}>`, () => {
61
61
  expect(element).toHaveClass('lumx-text--no-wrap');
62
62
  });
63
63
 
64
+ it('should render with custom whiteSpace', () => {
65
+ const { element } = setup({ whiteSpace: 'pre-wrap' });
66
+ expect(element.tagName).toBe('SPAN');
67
+ expect(element).toHaveStyle({ '--lumx-text-white-space': 'pre-wrap' });
68
+ });
69
+
64
70
  it('should wrap icons with spaces', () => {
65
71
  const { element } = setup({ children: ['Some text', <Icon key="icon" icon={mdiEarth} />, 'with icon'] });
66
72
  // Spaces have been inserted around the icon.
@@ -1,6 +1,6 @@
1
1
  import React, { Children, Fragment, forwardRef } from 'react';
2
2
 
3
- import { Icon, ColorPalette, ColorVariant, Typography } from '@lumx/react';
3
+ import { Icon, ColorPalette, ColorVariant, Typography, WhiteSpace } from '@lumx/react';
4
4
  import { Comp, GenericProps, TextElement, isComponent } from '@lumx/react/utils/type';
5
5
  import {
6
6
  getFontColorClassName,
@@ -41,6 +41,12 @@ export interface TextProps extends GenericProps {
41
41
  * (automatically activated when single line text truncate is activated).
42
42
  */
43
43
  noWrap?: boolean;
44
+ /**
45
+ * WhiteSpace variant
46
+ * Ignored when `noWrap` is set to true
47
+ * Ignored when `truncate` is set to true or lines: 1
48
+ * */
49
+ whiteSpace?: WhiteSpace;
44
50
  }
45
51
 
46
52
  /**
@@ -75,6 +81,7 @@ export const Text: Comp<TextProps> = forwardRef((props, ref) => {
75
81
  noWrap,
76
82
  typography,
77
83
  truncate,
84
+ whiteSpace,
78
85
  style,
79
86
  ...forwardedProps
80
87
  } = props;
@@ -88,6 +95,15 @@ export const Text: Comp<TextProps> = forwardRef((props, ref) => {
88
95
  const isTruncatedMultiline = !!truncateLinesStyle;
89
96
  const isTruncated = !!truncate;
90
97
 
98
+ /**
99
+ * Add custom white-space style if specified
100
+ * Disabled if noWrap is specified
101
+ * Disabled if truncated on one-line
102
+ * */
103
+ const whiteSpaceStyle = !noWrap &&
104
+ !(isTruncated && !isTruncatedMultiline) &&
105
+ whiteSpace && { '--lumx-text-white-space': whiteSpace };
106
+
91
107
  return (
92
108
  <Component
93
109
  ref={ref as React.Ref<any>}
@@ -102,7 +118,7 @@ export const Text: Comp<TextProps> = forwardRef((props, ref) => {
102
118
  colorClass,
103
119
  noWrap && `${CLASSNAME}--no-wrap`,
104
120
  )}
105
- style={{ ...truncateLinesStyle, ...style }}
121
+ style={{ ...truncateLinesStyle, ...whiteSpaceStyle, ...style }}
106
122
  {...forwardedProps}
107
123
  >
108
124
  {children &&
@@ -1,13 +1,8 @@
1
1
  import React from 'react';
2
2
 
3
3
  import { Button } from '@lumx/react';
4
- import { act, render, waitFor } from '@testing-library/react';
5
- import {
6
- findByClassName,
7
- getByClassName,
8
- queryAllByTagName,
9
- queryByClassName,
10
- } from '@lumx/react/testing/utils/queries';
4
+ import { screen, render, waitFor } from '@testing-library/react';
5
+ import { queryAllByTagName, queryByClassName } from '@lumx/react/testing/utils/queries';
11
6
  import { commonTestsSuiteRTL } from '@lumx/react/testing/utils';
12
7
  import userEvent from '@testing-library/user-event';
13
8
 
@@ -16,23 +11,19 @@ import { Tooltip, TooltipProps } from './Tooltip';
16
11
  const CLASSNAME = Tooltip.className as string;
17
12
 
18
13
  jest.mock('uid', () => ({ uid: () => 'uid' }));
14
+ // Skip delays
15
+ jest.mock('@lumx/react/constants', () => ({
16
+ ...jest.requireActual('@lumx/react/constants'),
17
+ TOOLTIP_HOVER_DELAY: { open: 0, close: 0 },
18
+ }));
19
19
 
20
20
  /**
21
21
  * Mounts the component and returns common DOM elements / data needed in multiple tests further down.
22
22
  */
23
23
  const setup = async (propsOverride: Partial<TooltipProps> = {}) => {
24
- const props: any = { forceOpen: true, ...propsOverride };
25
- // Hack to remove act() warning in console
26
- await (act as any)(() =>
27
- Promise.resolve(
28
- render(
29
- <Tooltip label="Tooltip" {...props}>
30
- {props.children || 'Anchor'}
31
- </Tooltip>,
32
- ),
33
- ),
34
- );
35
- const tooltip = queryByClassName(document.body, CLASSNAME);
24
+ const props: any = { forceOpen: true, label: 'Tooltip label', children: 'Anchor', ...propsOverride };
25
+ render(<Tooltip {...props} />);
26
+ const tooltip = screen.queryByRole('tooltip', { name: props.label });
36
27
  const anchorWrapper = queryByClassName(document.body, 'lumx-tooltip-anchor-wrapper');
37
28
  return { props, tooltip, anchorWrapper };
38
29
  };
@@ -67,7 +58,7 @@ describe(`<${Tooltip.displayName}>`, () => {
67
58
  });
68
59
  expect(tooltip).toBeInTheDocument();
69
60
  expect(anchorWrapper).not.toBeInTheDocument();
70
- const button = queryByClassName(document.body, Button.className as string);
61
+ const button = screen.queryByRole('button', { name: 'Anchor' });
71
62
  expect(button).toHaveAttribute('aria-describedby', tooltip?.id);
72
63
  });
73
64
 
@@ -80,7 +71,7 @@ describe(`<${Tooltip.displayName}>`, () => {
80
71
  expect(tooltip).toBeInTheDocument();
81
72
  expect(anchorWrapper).toBeInTheDocument();
82
73
  expect(anchorWrapper).toHaveAttribute('aria-describedby', tooltip?.id);
83
- const button = queryByClassName(document.body, Button.className as string);
74
+ const button = screen.queryByRole('button', { name: 'Anchor' });
84
75
  expect(button?.parentElement).toBe(anchorWrapper);
85
76
  });
86
77
 
@@ -108,11 +99,11 @@ describe(`<${Tooltip.displayName}>`, () => {
108
99
  expect(tooltip).not.toBeInTheDocument();
109
100
 
110
101
  // Hover anchor button
111
- const button = getByClassName(document.body, Button.className as string);
102
+ const button = screen.getByRole('button', { name: 'Anchor' });
112
103
  await userEvent.hover(button);
113
104
 
114
105
  // Tooltip opened
115
- tooltip = await findByClassName(document.body, CLASSNAME);
106
+ tooltip = await screen.findByRole('tooltip', { name: 'Tooltip label' });
116
107
  expect(tooltip).toBeInTheDocument();
117
108
  expect(button).toHaveAttribute('aria-describedby', tooltip?.id);
118
109
 
@@ -135,11 +126,11 @@ describe(`<${Tooltip.displayName}>`, () => {
135
126
  expect(tooltip).not.toBeInTheDocument();
136
127
 
137
128
  // Hover anchor button
138
- const button = getByClassName(document.body, Button.className as string);
129
+ const button = screen.getByRole('button', { name: 'Anchor' });
139
130
  await userEvent.hover(button);
140
131
 
141
132
  // Tooltip opened
142
- tooltip = await findByClassName(document.body, CLASSNAME);
133
+ tooltip = await screen.findByRole('tooltip', { name: 'Tooltip label' });
143
134
  expect(tooltip).toBeInTheDocument();
144
135
  expect(button).toHaveAttribute('aria-describedby', tooltip?.id);
145
136
 
@@ -157,7 +148,7 @@ describe(`<${Tooltip.displayName}>`, () => {
157
148
  });
158
149
  });
159
150
 
160
- it('should activate on anchor focus', async () => {
151
+ it('should activate on anchor focus and close on escape', async () => {
161
152
  let { tooltip } = await setup({
162
153
  label: 'Tooltip label',
163
154
  children: <Button>Anchor</Button>,
@@ -168,21 +159,27 @@ describe(`<${Tooltip.displayName}>`, () => {
168
159
 
169
160
  // Focus anchor button
170
161
  await userEvent.tab();
171
- const button = getByClassName(document.body, Button.className as string);
162
+ const button = screen.getByRole('button', { name: 'Anchor' });
172
163
  expect(button).toHaveFocus();
173
164
 
174
165
  // Tooltip opened
175
- tooltip = await findByClassName(document.body, CLASSNAME);
166
+ tooltip = await screen.findByRole('tooltip', { name: 'Tooltip label' });
176
167
  expect(tooltip).toBeInTheDocument();
177
168
  expect(button).toHaveAttribute('aria-describedby', tooltip?.id);
178
169
 
179
- // Focus next element
180
- userEvent.tab();
181
- await waitFor(() => {
182
- expect(button).not.toHaveFocus();
183
- // Tooltip closed
184
- expect(tooltip).not.toBeInTheDocument();
185
- });
170
+ // Focus next element => close tooltip
171
+ await userEvent.tab();
172
+ expect(button).not.toHaveFocus();
173
+ expect(tooltip).not.toBeInTheDocument();
174
+
175
+ // Focus again
176
+ await userEvent.tab({ shift: true });
177
+ tooltip = await screen.findByRole('tooltip', { name: 'Tooltip label' });
178
+ expect(tooltip).toBeInTheDocument();
179
+
180
+ // Escape pressed => close tooltip
181
+ await userEvent.keyboard('{Escape}');
182
+ expect(tooltip).not.toBeInTheDocument();
186
183
  });
187
184
  });
188
185
 
@@ -17,7 +17,7 @@ export function useTooltipOpen(delay: number | undefined, anchorElement: HTMLEle
17
17
 
18
18
  // Global close on escape
19
19
  const [closeCallback, setCloseCallback] = useState<undefined | (() => void)>(undefined);
20
- useCallbackOnEscape(closeCallback);
20
+ useCallbackOnEscape(isOpen ? closeCallback : undefined);
21
21
 
22
22
  useEffect(() => {
23
23
  if (!anchorElement) {