@indico-data/design-system 2.56.0 → 2.58.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indico-data/design-system",
3
- "version": "2.56.0",
3
+ "version": "2.58.0",
4
4
  "description": "",
5
5
  "author": "",
6
6
  "main": "lib/index.js",
@@ -45,6 +45,7 @@
45
45
  "date-fns": "^3.6.0",
46
46
  "focus-trap-react": "^10.2.3",
47
47
  "html-webpack-plugin": "^5.6.0",
48
+ "nanoid": "^5.1.5",
48
49
  "prop-types": "^15.8.1",
49
50
  "react-aria-components": "^1.2.1",
50
51
  "react-data-table-component": "^7.6.2",
@@ -54,6 +55,7 @@
54
55
  "react-modal": "^3.16.1",
55
56
  "react-popper": "^2.3.0",
56
57
  "react-stately": "^3.31.0",
58
+ "react-toastify": "^11.0.5",
57
59
  "react-tooltip": "5.28.0",
58
60
  "svgo": "^3.2.2",
59
61
  "ts-jest": "^29.3.1",
@@ -25,3 +25,5 @@ export { TanstackTable } from './tanstackTable';
25
25
  export { Tooltip } from './tooltip';
26
26
  export { BarSpinner } from './loading-indicators/BarSpinner/BarSpinner';
27
27
  export { CirclePulse } from './loading-indicators/CirclePulse/CirclePulse';
28
+ export { Truncate } from './truncate';
29
+ export { toast, ToastContainer } from 'react-toastify';
@@ -0,0 +1,14 @@
1
+ import { Canvas, Meta, Controls, Story } from '@storybook/blocks';
2
+ import * as Toast from './Toast.stories';
3
+
4
+ <Meta title="Components/Toast" of={Toast} />
5
+
6
+ # Toast
7
+
8
+ The Toast component is used to display a toast message. A Toast message is a notification that appears at the top of the screen to provide feedback to the user. This is a wrapper around the `react-toastify` library. To view their documentation, please visit [react-toastify](https://fkhadra.github.io/react-toastify/introduction).
9
+
10
+ <Canvas of={Toast.Default} />
11
+
12
+ ### The following props are available for the Toast component, but not limited to:
13
+
14
+ <Controls of={Toast.Default} />
@@ -0,0 +1,55 @@
1
+ import { Meta, StoryObj } from '@storybook/react';
2
+ import { toast, ToastContainer } from './index';
3
+ import { Button } from '../button';
4
+
5
+ const meta: Meta<typeof ToastContainer> = {
6
+ title: 'Components/Toast',
7
+ component: ToastContainer,
8
+ argTypes: {
9
+ position: {
10
+ control: 'select',
11
+ options: [
12
+ 'top-right',
13
+ 'top-center',
14
+ 'top-left',
15
+ 'bottom-right',
16
+ 'bottom-center',
17
+ 'bottom-left',
18
+ ],
19
+ },
20
+ theme: {
21
+ control: 'select',
22
+ options: ['light', 'dark'],
23
+ },
24
+ autoClose: {
25
+ control: 'number',
26
+ },
27
+ },
28
+ decorators: [
29
+ (Story) => (
30
+ <div style={{ height: '400px' }}>
31
+ <Story />
32
+ </div>
33
+ ),
34
+ ],
35
+ };
36
+
37
+ export default meta;
38
+
39
+ type Story = StoryObj<typeof ToastContainer>;
40
+
41
+ export const Default: Story = {
42
+ args: {
43
+ position: 'top-right',
44
+ theme: 'dark',
45
+ autoClose: 3000,
46
+ },
47
+ render: (args) => (
48
+ <div>
49
+ <Button ariaLabel="Click me" onClick={() => toast('Hello World')}>
50
+ Fire a toast
51
+ </Button>
52
+ <ToastContainer {...args} />
53
+ </div>
54
+ ),
55
+ };
@@ -0,0 +1 @@
1
+ export { toast, ToastContainer } from 'react-toastify';
@@ -0,0 +1,17 @@
1
+ // Common Variables
2
+ :root,
3
+ :root [data-theme='light'],
4
+ :root [data-theme='dark'] {
5
+ --toastify-color-dark: var(--pf-background-color-light);
6
+ --toastify-color-progress-dark: var(--pf-primary-color-400);
7
+ --toastify-icon-color-success: var(--pf-success-color);
8
+ --toastify-color-progress-bgo: 0.2;
9
+ }
10
+
11
+ // Dark Theme Specific Variables
12
+ :root [data-theme='dark'] {
13
+ --toastify-color-dark: var(--pf-background-color-light);
14
+ --toastify-color-progress-dark: var(--pf-primary-color-400);
15
+ --toastify-icon-color-success: var(--pf-success-color);
16
+ --toastify-color-progress-bgo: 0.2;
17
+ }
@@ -0,0 +1,34 @@
1
+ import { Canvas, Meta, Controls, Story } from '@storybook/blocks';
2
+ import * as Truncate from './Truncate.stories';
3
+
4
+ <Meta title="Utilities/Truncate" name="Truncate" of={Truncate} />
5
+
6
+ # Truncate
7
+
8
+ The truncate component accepts a string prop and will truncate the string to fit the available space. It will add an `...` ellipsis to the end of the string. If the truncation is detected by the observer, it will show a tooltip by default, unless the `hasTooltip` prop is set to `false`.
9
+
10
+ <Canvas of={Truncate.Default} />
11
+
12
+ ### The following props are available for the Truncate component:
13
+
14
+ <Controls of={Truncate.Default} />
15
+
16
+
17
+ ## Usage
18
+ Simply pass in a string to the `truncateString` prop.
19
+
20
+
21
+ ### With Line Clamp
22
+
23
+ If you want to truncate the text to a specific number of lines, set the `lineClamp` prop to the number of lines you want to truncate to.
24
+
25
+ <Canvas of={Truncate.WithLineClamp} />
26
+
27
+ ### No Tooltip
28
+
29
+ If you want to disable the tooltip, set the `hasTooltip` prop to `false`.
30
+
31
+ <Canvas of={Truncate.NoTooltip} />
32
+
33
+ ### Notes on Tooltips
34
+ If a duplicate ID is provided, the tooltip will only show on the first instance of the ID.
@@ -0,0 +1,86 @@
1
+ import { Meta, StoryObj } from '@storybook/react';
2
+ import { Truncate } from './Truncate';
3
+ import { TruncateProps } from './types';
4
+
5
+ const meta: Meta<typeof Truncate> = {
6
+ title: 'Utilities/Truncate',
7
+ component: Truncate,
8
+ decorators: [
9
+ (Story) => (
10
+ <div style={{ width: '300px' }}>
11
+ <Story />
12
+ </div>
13
+ ),
14
+ ],
15
+ argTypes: {
16
+ truncateString: {
17
+ required: true,
18
+ control: 'text',
19
+ description:
20
+ 'The string to truncate. This value will also be displayed in the tooltip when the text is truncated.',
21
+ table: {
22
+ category: 'Props',
23
+ },
24
+ },
25
+ lineClamp: {
26
+ control: 'number',
27
+ defaultValue: 0,
28
+ description:
29
+ 'The number of lines to truncate the text to. If left blank, it will default to 1 line.',
30
+ table: {
31
+ category: 'Props',
32
+ },
33
+ },
34
+ tooltipId: {
35
+ control: 'text',
36
+ description:
37
+ 'The id of the tooltip. If an ID is not provided, it will generate one from nanoid',
38
+ table: {
39
+ category: 'Props',
40
+ },
41
+ },
42
+ hasTooltip: {
43
+ control: 'boolean',
44
+ defaultValue: true,
45
+ description:
46
+ 'Whether to show the tooltip. If left blank, it will default to true. If set to false, the tooltip will not be shown.',
47
+ table: {
48
+ category: 'Props',
49
+ },
50
+ },
51
+ },
52
+ };
53
+
54
+ export default meta;
55
+
56
+ type Story = StoryObj<TruncateProps>;
57
+
58
+ export const Default: Story = {
59
+ args: {
60
+ lineClamp: 0,
61
+ hasTooltip: true,
62
+ tooltipId: 'truncate-tooltip',
63
+ truncateString: 'Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos.',
64
+ },
65
+ render: (args) => <Truncate {...args} />,
66
+ };
67
+
68
+ export const WithLineClamp: Story = {
69
+ args: {
70
+ lineClamp: 2,
71
+ hasTooltip: true,
72
+ tooltipId: 'truncate-tooltip-line-clamp',
73
+ truncateString: 'Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos.',
74
+ },
75
+ render: (args) => <Truncate {...args} />,
76
+ };
77
+
78
+ export const NoTooltip: Story = {
79
+ args: {
80
+ lineClamp: 2,
81
+ hasTooltip: false,
82
+ tooltipId: 'truncate-tooltip-no-tooltip',
83
+ truncateString: 'Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos.',
84
+ },
85
+ render: (args) => <Truncate {...args} />,
86
+ };
@@ -0,0 +1,55 @@
1
+ import { nanoid } from 'nanoid';
2
+ import { useState, useEffect, CSSProperties } from 'react';
3
+ import { Tooltip } from '../tooltip';
4
+ import { TruncateProps } from './types';
5
+
6
+ export const Truncate = ({
7
+ lineClamp = 0,
8
+ truncateString,
9
+ hasTooltip = true,
10
+ tooltipId,
11
+ ...rest
12
+ }: TruncateProps) => {
13
+ const [isTruncated, setIsTruncated] = useState(false);
14
+ const id = (tooltipId ?? nanoid()).replace(/[^a-zA-Z0-9-_]/g, '_');
15
+
16
+ useEffect(() => {
17
+ const checkTruncation = () => {
18
+ const element = document.querySelector(`[data-tooltip-id="${id}"]`);
19
+ if (element) {
20
+ if (lineClamp === 0) {
21
+ setIsTruncated(element.scrollWidth > element.clientWidth);
22
+ } else {
23
+ setIsTruncated(element.scrollHeight > element.clientHeight);
24
+ }
25
+ }
26
+ };
27
+
28
+ checkTruncation();
29
+ window.addEventListener('resize', checkTruncation);
30
+ return () => window.removeEventListener('resize', checkTruncation);
31
+ }, [id, lineClamp]);
32
+
33
+ const truncateStyle = {
34
+ '--line-clamp': lineClamp,
35
+ } as CSSProperties;
36
+
37
+ return (
38
+ <div className="truncate-wrapper" style={truncateStyle}>
39
+ <span
40
+ data-testid={`truncate-${id}-${isTruncated ? 'truncated' : 'not-truncated'}`}
41
+ data-tooltip-id={id}
42
+ data-tooltip-content={isTruncated ? truncateString : undefined}
43
+ className={lineClamp > 0 ? 'truncate-clip' : 'truncate'}
44
+ {...rest}
45
+ >
46
+ {truncateString}
47
+ </span>
48
+ {isTruncated && truncateString && hasTooltip && (
49
+ <Tooltip data-tooltip-delay-hide={100} id={id} border="solid 1px var(--pf-border-color)">
50
+ {truncateString}
51
+ </Tooltip>
52
+ )}
53
+ </div>
54
+ );
55
+ };
@@ -0,0 +1,61 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import { Truncate } from '../Truncate';
3
+
4
+ // Mock nanoid to return a predictable ID
5
+ jest.mock('nanoid', () => ({
6
+ nanoid: () => 'test-id',
7
+ }));
8
+
9
+ describe('Truncate', () => {
10
+ beforeAll(() => {
11
+ // Mock offsetWidth and scrollWidth
12
+ Object.defineProperty(HTMLElement.prototype, 'offsetWidth', {
13
+ configurable: true,
14
+ value: 100,
15
+ });
16
+ Object.defineProperty(HTMLElement.prototype, 'scrollWidth', {
17
+ configurable: true,
18
+ value: 200, // larger than offsetWidth to simulate overflow
19
+ });
20
+ });
21
+
22
+ afterAll(() => {
23
+ jest.restoreAllMocks();
24
+ });
25
+
26
+ // Only way to test is by checking testid, validated this works in app
27
+ it('shows truncated state when content overflows', () => {
28
+ render(<Truncate truncateString="This is a very long text that should be truncated" />);
29
+
30
+ // Now we can use exact IDs instead of regex
31
+ expect(screen.getByTestId('truncate-test-id-truncated')).toBeInTheDocument();
32
+
33
+ // Verify tooltip appears when truncated
34
+ expect(screen.getByTestId('truncate-test-id-truncated')).toHaveAttribute(
35
+ 'data-tooltip-content',
36
+ 'This is a very long text that should be truncated',
37
+ );
38
+ });
39
+
40
+ it('shows non-truncated state when content fits', () => {
41
+ // Override both scrollWidth and clientWidth to simulate content fitting
42
+ Object.defineProperty(HTMLElement.prototype, 'scrollWidth', {
43
+ configurable: true,
44
+ value: 100,
45
+ });
46
+ Object.defineProperty(HTMLElement.prototype, 'clientWidth', {
47
+ configurable: true,
48
+ value: 200,
49
+ });
50
+
51
+ render(<Truncate truncateString="Short text" />);
52
+
53
+ // Now we can use exact IDs
54
+ expect(screen.getByTestId('truncate-test-id-not-truncated')).toBeInTheDocument();
55
+
56
+ // Verify no tooltip when not truncated
57
+ expect(screen.getByTestId('truncate-test-id-not-truncated')).not.toHaveAttribute(
58
+ 'data-tooltip-content',
59
+ );
60
+ });
61
+ });
@@ -0,0 +1 @@
1
+ export { Truncate } from './Truncate';
@@ -0,0 +1,22 @@
1
+ .truncate-wrapper {
2
+ width: 100%;
3
+ display: block;
4
+
5
+ .truncate {
6
+ white-space: nowrap;
7
+ overflow: hidden;
8
+ text-overflow: ellipsis;
9
+ width: 100%;
10
+ display: inline-block;
11
+ }
12
+
13
+ .truncate-clip {
14
+ display: -webkit-box;
15
+ -webkit-box-orient: vertical;
16
+ -webkit-line-clamp: var(--line-clamp);
17
+ line-clamp: var(--line-clamp);
18
+ overflow: hidden;
19
+ text-overflow: ellipsis;
20
+ width: 100%;
21
+ }
22
+ }
@@ -0,0 +1,7 @@
1
+ export interface TruncateProps {
2
+ lineClamp?: number;
3
+ truncateString: string;
4
+ hasTooltip?: boolean;
5
+ tooltipId?: string;
6
+ [key: string]: any;
7
+ }
package/src/index.ts CHANGED
@@ -32,9 +32,10 @@ export { Tooltip } from './components/tooltip';
32
32
  export { Pagination } from './components/pagination';
33
33
  export { CirclePulse } from './components/loading-indicators/CirclePulse';
34
34
  export { BarSpinner } from './components/loading-indicators/BarSpinner/BarSpinner';
35
-
35
+ export { Truncate } from './components/truncate';
36
36
  // Utilities
37
37
  export { registerFontAwesomeIcons } from './setup/setupIcons';
38
+ export { toast, ToastContainer } from './components/toast';
38
39
 
39
40
  // Types
40
41
  export type {
@@ -29,6 +29,8 @@
29
29
  @import '../components/tooltip/styles/Tooltip.scss';
30
30
  @import '../components/loading-indicators/BarSpinner/styles/BarSpinner.scss';
31
31
  @import '../components/loading-indicators/CirclePulse/CirclePulse.scss';
32
+ @import '../components/truncate/styles/Truncate.scss';
33
+ @import '../components/toast/styles/Toast.scss';
32
34
  @import 'sheets'; // Port to an sheets component when we build it
33
35
  @import 'typography';
34
36
  @import 'colors';