@indico-data/design-system 2.18.0 → 2.20.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/lib/index.css +55 -33
- package/lib/index.d.ts +34 -20
- package/lib/index.esm.css +55 -33
- package/lib/index.esm.js +1666 -85
- package/lib/index.esm.js.map +1 -1
- package/lib/index.js +1669 -86
- package/lib/index.js.map +1 -1
- package/lib/src/components/forms/input/Input.d.ts +5 -7
- package/lib/src/components/forms/passwordInput/PasswordInput.d.ts +5 -7
- package/lib/src/components/forms/subcomponents/Label.d.ts +6 -3
- package/lib/src/components/forms/textarea/Textarea.d.ts +5 -7
- package/lib/src/components/index.d.ts +2 -0
- package/lib/src/components/menu/Menu.d.ts +5 -0
- package/lib/src/components/menu/Menu.stories.d.ts +6 -0
- package/lib/src/components/menu/Menu.test.d.ts +1 -0
- package/lib/src/components/menu/index.d.ts +1 -0
- package/lib/src/components/popper/Popper.d.ts +12 -0
- package/lib/src/components/popper/Popper.stories.d.ts +6 -0
- package/lib/src/components/popper/Popper.test.d.ts +1 -0
- package/lib/src/components/popper/index.d.ts +1 -0
- package/lib/src/hooks/useClickOutside.d.ts +2 -0
- package/lib/src/index.d.ts +2 -0
- package/lib/src/storybook/labelArgTypes.d.ts +3 -0
- package/package.json +2 -1
- package/src/components/forms/input/Input.mdx +15 -2
- package/src/components/forms/input/Input.stories.tsx +10 -45
- package/src/components/forms/input/Input.tsx +22 -15
- package/src/components/forms/input/styles/Input.scss +0 -11
- package/src/components/forms/passwordInput/PasswordInput.mdx +10 -8
- package/src/components/forms/passwordInput/PasswordInput.stories.tsx +3 -44
- package/src/components/forms/passwordInput/PasswordInput.tsx +20 -15
- package/src/components/forms/passwordInput/styles/PasswordInput.scss +0 -11
- package/src/components/forms/subcomponents/Label.tsx +29 -6
- package/src/components/forms/subcomponents/__tests__/Label.test.tsx +63 -15
- package/src/components/forms/textarea/Textarea.mdx +12 -2
- package/src/components/forms/textarea/Textarea.stories.tsx +4 -46
- package/src/components/forms/textarea/Textarea.tsx +15 -13
- package/src/components/forms/textarea/styles/Textarea.scss +0 -11
- package/src/components/index.ts +2 -0
- package/src/components/menu/Menu.mdx +15 -0
- package/src/components/menu/Menu.stories.tsx +56 -0
- package/src/components/menu/Menu.test.tsx +88 -0
- package/src/components/menu/Menu.tsx +20 -0
- package/src/components/menu/index.ts +1 -0
- package/src/components/menu/styles/Menu.scss +19 -0
- package/src/components/menu/styles/_variables.scss +15 -0
- package/src/components/popper/Popper.mdx +79 -0
- package/src/components/popper/Popper.stories.tsx +161 -0
- package/src/components/popper/Popper.test.tsx +68 -0
- package/src/components/popper/Popper.tsx +57 -0
- package/src/components/popper/index.ts +1 -0
- package/src/components/popper/styles/Popper.scss +11 -0
- package/src/components/popper/styles/_variables.scss +15 -0
- package/src/hooks/useClickOutside.tsx +22 -0
- package/src/index.ts +2 -0
- package/src/legacy/components/buttons/commonStyles.ts +0 -4
- package/src/storybook/labelArgTypes.ts +50 -0
- package/src/styles/index.scss +2 -0
|
@@ -1,16 +1,18 @@
|
|
|
1
|
-
|
|
1
|
+
import React, { forwardRef } from 'react';
|
|
2
|
+
|
|
3
|
+
export interface LabelProps {
|
|
2
4
|
label: string;
|
|
3
5
|
name: string;
|
|
4
6
|
isRequired?: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface WithLabelProps extends LabelProps {
|
|
5
10
|
hasHiddenLabel?: boolean;
|
|
6
11
|
}
|
|
7
12
|
|
|
8
|
-
export const Label = ({ label, name, isRequired
|
|
13
|
+
export const Label = ({ label, name, isRequired }: LabelProps) => {
|
|
9
14
|
return (
|
|
10
|
-
<div
|
|
11
|
-
data-testid={`${name}-testId`}
|
|
12
|
-
className={`form-label ${hasHiddenLabel ? 'is-visually-hidden' : ''}`}
|
|
13
|
-
>
|
|
15
|
+
<div data-testid={`${name}-testId`} className={`form-label`}>
|
|
14
16
|
<label htmlFor={`${name}`}>
|
|
15
17
|
{label}
|
|
16
18
|
{isRequired ? <span className="text-error"> *</span> : ''}
|
|
@@ -18,3 +20,24 @@ export const Label = ({ label, name, isRequired, hasHiddenLabel }: LabelProps) =
|
|
|
18
20
|
</div>
|
|
19
21
|
);
|
|
20
22
|
};
|
|
23
|
+
|
|
24
|
+
// HOC to add common label functionality to components
|
|
25
|
+
export function withLabel<P extends object>(WrappedComponent: React.ComponentType<P>) {
|
|
26
|
+
const WithLabelComponent = (
|
|
27
|
+
{ label, hasHiddenLabel = false, name, isRequired, ...rest }: P & WithLabelProps,
|
|
28
|
+
ref: React.Ref<any>,
|
|
29
|
+
) => {
|
|
30
|
+
const ariaLabel = hasHiddenLabel
|
|
31
|
+
? { 'aria-label': isRequired ? `${label} (required)` : label }
|
|
32
|
+
: {};
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<div className="form-control">
|
|
36
|
+
{!hasHiddenLabel && <Label label={label} name={name} isRequired={isRequired} />}
|
|
37
|
+
<WrappedComponent {...(rest as P)} id={name} name={name} {...ariaLabel} ref={ref} />
|
|
38
|
+
</div>
|
|
39
|
+
);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
return forwardRef(WithLabelComponent);
|
|
43
|
+
}
|
|
@@ -1,33 +1,81 @@
|
|
|
1
1
|
import { render, screen } from '@testing-library/react';
|
|
2
|
-
import { Label } from '@/components/forms/subcomponents/Label';
|
|
2
|
+
import { Label, withLabel } from '@/components/forms/subcomponents/Label';
|
|
3
|
+
|
|
4
|
+
// Mock component to wrap with HOC
|
|
5
|
+
const MockComponent: React.FC = (props) => {
|
|
6
|
+
return <input {...props} />;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const LabeledMockComponent = withLabel(MockComponent);
|
|
3
10
|
|
|
4
11
|
describe('Label', () => {
|
|
5
|
-
it('renders the `required`
|
|
6
|
-
render(<Label label=
|
|
12
|
+
it('renders the `required` asterisk when the label is required', () => {
|
|
13
|
+
render(<Label label="name" name="name" isRequired={true} />);
|
|
7
14
|
expect(screen.getByText('*')).toBeInTheDocument();
|
|
8
15
|
expect(screen.getByText('*')).toBeVisible();
|
|
9
16
|
});
|
|
10
|
-
|
|
11
|
-
|
|
17
|
+
|
|
18
|
+
it('does not render the `required` asterisk when the label is not required', () => {
|
|
19
|
+
render(<Label label="name" name="name" isRequired={false} />);
|
|
12
20
|
expect(screen.queryByText('*')).not.toBeInTheDocument();
|
|
13
21
|
expect(screen.queryByText('*')).toBeNull();
|
|
14
22
|
});
|
|
15
|
-
|
|
16
|
-
render(<Label label={'name'} name={'name'} isRequired={false} hasHiddenLabel={true} />);
|
|
17
|
-
expect(screen.getByText('name')).toBeInTheDocument();
|
|
18
|
-
expect(screen.getByText('name')).toBeVisible();
|
|
19
|
-
});
|
|
23
|
+
|
|
20
24
|
it('renders the label text', () => {
|
|
21
|
-
render(<Label label=
|
|
25
|
+
render(<Label label="name" name="name" isRequired={false} />);
|
|
22
26
|
const label = screen.getByTestId('name-testId');
|
|
23
27
|
expect(label).toBeInTheDocument();
|
|
24
28
|
expect(label).toBeVisible();
|
|
25
|
-
expect(label).not.toHaveClass('is-visually-hidden');
|
|
26
29
|
});
|
|
30
|
+
});
|
|
27
31
|
|
|
28
|
-
|
|
29
|
-
|
|
32
|
+
describe('withLabel HOC', () => {
|
|
33
|
+
it('renders the wrapped component with a visible label', () => {
|
|
34
|
+
render(
|
|
35
|
+
<LabeledMockComponent label="name" name="name" isRequired={false} hasHiddenLabel={false} />,
|
|
36
|
+
);
|
|
30
37
|
const label = screen.getByTestId('name-testId');
|
|
31
|
-
|
|
38
|
+
const input = screen.getByRole('textbox');
|
|
39
|
+
|
|
40
|
+
expect(label).toBeInTheDocument();
|
|
41
|
+
expect(label).toBeVisible();
|
|
42
|
+
expect(input).toBeInTheDocument();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('renders the wrapped component with an aria-label when hasHiddenLabel is true', () => {
|
|
46
|
+
render(<LabeledMockComponent label="name" name="name" isRequired={false} hasHiddenLabel />);
|
|
47
|
+
const input = screen.getByRole('textbox');
|
|
48
|
+
|
|
49
|
+
expect(input).toBeInTheDocument();
|
|
50
|
+
expect(input).toHaveAttribute('aria-label', 'name');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('renders the wrapped component with an aria-label including "required" when isRequired is true and hasHiddenLabel is true', () => {
|
|
54
|
+
render(<LabeledMockComponent label="name" name="name" isRequired={true} hasHiddenLabel />);
|
|
55
|
+
const input = screen.getByRole('textbox');
|
|
56
|
+
|
|
57
|
+
expect(input).toBeInTheDocument();
|
|
58
|
+
expect(input).toHaveAttribute('aria-label', 'name (required)');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('does not render the visible label when hasHiddenLabel is true', () => {
|
|
62
|
+
render(<LabeledMockComponent label="name" name="name" isRequired={false} hasHiddenLabel />);
|
|
63
|
+
expect(screen.queryByTestId('name-testId')).not.toBeInTheDocument();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('renders the `required` asterisk when isRequired is true and hasHiddenLabel is false', () => {
|
|
67
|
+
render(
|
|
68
|
+
<LabeledMockComponent label="name" name="name" isRequired={true} hasHiddenLabel={false} />,
|
|
69
|
+
);
|
|
70
|
+
expect(screen.getByText('*')).toBeInTheDocument();
|
|
71
|
+
expect(screen.getByText('*')).toBeVisible();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('does not render the `required` asterisk when isRequired is false and hasHiddenLabel is false', () => {
|
|
75
|
+
render(
|
|
76
|
+
<LabeledMockComponent label="name" name="name" isRequired={false} hasHiddenLabel={false} />,
|
|
77
|
+
);
|
|
78
|
+
expect(screen.queryByText('*')).not.toBeInTheDocument();
|
|
79
|
+
expect(screen.queryByText('*')).toBeNull();
|
|
32
80
|
});
|
|
33
81
|
});
|
|
@@ -5,13 +5,23 @@ import * as Textarea from './Textarea.stories';
|
|
|
5
5
|
|
|
6
6
|
# Textarea
|
|
7
7
|
|
|
8
|
-
The Textarea component is the building block of any form. Below you will find the accepted properties for this component. It is encouraged to build forms utilizing [React Hook Form](https://react-hook-form.com/) library in your application. This will facilitate form state management and enforce best practices. (
|
|
8
|
+
The Textarea component is the building block of any form. Below you will find the accepted properties for this component. It is encouraged to build forms utilizing [React Hook Form](https://react-hook-form.com/) library in your application. This will facilitate form state management and enforce best practices. (**_Our components are compatible with but do not provide the plugin_**)
|
|
9
9
|
|
|
10
10
|
<Canvas
|
|
11
11
|
of={Textarea.Default}
|
|
12
12
|
source={{
|
|
13
13
|
code: `
|
|
14
|
-
|
|
14
|
+
<Textarea
|
|
15
|
+
label="Label Name"
|
|
16
|
+
name="textarea"
|
|
17
|
+
placeholder="Please enter a value"
|
|
18
|
+
helpText="This Is Help Text"
|
|
19
|
+
isRequired
|
|
20
|
+
isDisabled={false}
|
|
21
|
+
errorMessage=""
|
|
22
|
+
value=""
|
|
23
|
+
onChange={() => {}}
|
|
24
|
+
/>
|
|
15
25
|
`,
|
|
16
26
|
}}
|
|
17
27
|
/>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
1
2
|
import { Meta, StoryObj } from '@storybook/react';
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
3
|
+
import labelArgTypes from '@/storybook/labelArgTypes';
|
|
4
|
+
import { Textarea } from './Textarea';
|
|
4
5
|
|
|
5
6
|
const meta: Meta = {
|
|
6
7
|
title: 'Forms/Textarea',
|
|
@@ -17,28 +18,6 @@ const meta: Meta = {
|
|
|
17
18
|
},
|
|
18
19
|
action: 'onChange',
|
|
19
20
|
},
|
|
20
|
-
label: {
|
|
21
|
-
control: 'text',
|
|
22
|
-
description: 'The label for the textarea field',
|
|
23
|
-
table: {
|
|
24
|
-
category: 'Props',
|
|
25
|
-
type: {
|
|
26
|
-
summary: 'string',
|
|
27
|
-
},
|
|
28
|
-
},
|
|
29
|
-
defaultValue: { summary: '' },
|
|
30
|
-
},
|
|
31
|
-
name: {
|
|
32
|
-
control: 'text',
|
|
33
|
-
description: 'The name for the textarea field',
|
|
34
|
-
table: {
|
|
35
|
-
category: 'Props',
|
|
36
|
-
type: {
|
|
37
|
-
summary: 'string',
|
|
38
|
-
},
|
|
39
|
-
},
|
|
40
|
-
defaultValue: { summary: '' },
|
|
41
|
-
},
|
|
42
21
|
placeholder: {
|
|
43
22
|
control: 'text',
|
|
44
23
|
description: 'The placeholder for the textarea field',
|
|
@@ -61,17 +40,6 @@ const meta: Meta = {
|
|
|
61
40
|
},
|
|
62
41
|
defaultValue: { summary: '' },
|
|
63
42
|
},
|
|
64
|
-
isRequired: {
|
|
65
|
-
control: 'boolean',
|
|
66
|
-
description: 'Toggles the required astherisc on the label',
|
|
67
|
-
table: {
|
|
68
|
-
category: 'Props',
|
|
69
|
-
type: {
|
|
70
|
-
summary: 'boolean',
|
|
71
|
-
},
|
|
72
|
-
},
|
|
73
|
-
defaultValue: { summary: 'false' },
|
|
74
|
-
},
|
|
75
43
|
isDisabled: {
|
|
76
44
|
control: 'boolean',
|
|
77
45
|
description: 'Toggles the disabled state of the textarea field',
|
|
@@ -105,17 +73,6 @@ const meta: Meta = {
|
|
|
105
73
|
},
|
|
106
74
|
defaultValue: { summary: '' },
|
|
107
75
|
},
|
|
108
|
-
hasHiddenLabel: {
|
|
109
|
-
control: 'boolean',
|
|
110
|
-
description: 'Hides the label visually (retains it for screen readers)',
|
|
111
|
-
table: {
|
|
112
|
-
category: 'Props',
|
|
113
|
-
type: {
|
|
114
|
-
summary: 'boolean',
|
|
115
|
-
},
|
|
116
|
-
},
|
|
117
|
-
defaultValue: { summary: 'false' },
|
|
118
|
-
},
|
|
119
76
|
autofocus: {
|
|
120
77
|
control: 'boolean',
|
|
121
78
|
description: ' Specifies that a text area should automatically get focus when the page loads',
|
|
@@ -198,6 +155,7 @@ const meta: Meta = {
|
|
|
198
155
|
disable: true,
|
|
199
156
|
},
|
|
200
157
|
},
|
|
158
|
+
...labelArgTypes,
|
|
201
159
|
},
|
|
202
160
|
};
|
|
203
161
|
|
|
@@ -1,19 +1,17 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import
|
|
2
|
+
import classNames from 'classnames';
|
|
3
|
+
|
|
4
|
+
import { withLabel, LabelProps, WithLabelProps } from '../subcomponents/Label';
|
|
3
5
|
import { DisplayFormError } from '../subcomponents/DisplayFormError';
|
|
4
6
|
|
|
5
|
-
export interface TextareaProps {
|
|
7
|
+
export interface TextareaProps extends WithLabelProps {
|
|
6
8
|
ref?: React.LegacyRef<HTMLTextAreaElement>;
|
|
7
|
-
|
|
8
|
-
name: string;
|
|
9
|
-
placeholder: string;
|
|
9
|
+
placeholder?: string;
|
|
10
10
|
value?: string | undefined;
|
|
11
11
|
onChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void;
|
|
12
|
-
isRequired?: boolean;
|
|
13
12
|
isDisabled?: boolean;
|
|
14
13
|
errorMessage?: string | undefined;
|
|
15
14
|
helpText?: string;
|
|
16
|
-
hasHiddenLabel?: boolean;
|
|
17
15
|
rows?: number;
|
|
18
16
|
cols?: number;
|
|
19
17
|
readonly?: boolean;
|
|
@@ -23,7 +21,7 @@ export interface TextareaProps {
|
|
|
23
21
|
autofocus?: boolean;
|
|
24
22
|
defaultValue?: string;
|
|
25
23
|
}
|
|
26
|
-
|
|
24
|
+
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
|
27
25
|
(
|
|
28
26
|
{
|
|
29
27
|
label,
|
|
@@ -49,9 +47,10 @@ export const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
|
|
49
47
|
) => {
|
|
50
48
|
const hasErrors = errorMessage && errorMessage.length > 0;
|
|
51
49
|
|
|
50
|
+
const textareaClasses = classNames('textarea', { error: hasErrors });
|
|
51
|
+
|
|
52
52
|
return (
|
|
53
|
-
|
|
54
|
-
<Label label={label} name={name} isRequired={isRequired} hasHiddenLabel={hasHiddenLabel} />
|
|
53
|
+
<>
|
|
55
54
|
<div className="textarea-wrapper">
|
|
56
55
|
<textarea
|
|
57
56
|
ref={ref}
|
|
@@ -67,11 +66,10 @@ export const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
|
|
67
66
|
disabled={isDisabled}
|
|
68
67
|
placeholder={placeholder}
|
|
69
68
|
onChange={onChange}
|
|
70
|
-
className={
|
|
69
|
+
className={textareaClasses}
|
|
71
70
|
aria-invalid={hasErrors ? true : undefined}
|
|
72
71
|
aria-describedby={hasErrors || helpText ? `${name}-helper` : undefined}
|
|
73
72
|
aria-required={isRequired}
|
|
74
|
-
aria-label={label}
|
|
75
73
|
{...rest}
|
|
76
74
|
/>
|
|
77
75
|
</div>
|
|
@@ -81,7 +79,11 @@ export const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
|
|
81
79
|
{helpText}
|
|
82
80
|
</div>
|
|
83
81
|
)}
|
|
84
|
-
|
|
82
|
+
</>
|
|
85
83
|
);
|
|
86
84
|
},
|
|
87
85
|
);
|
|
86
|
+
|
|
87
|
+
const LabeledTextarea = withLabel(Textarea);
|
|
88
|
+
|
|
89
|
+
export { LabeledTextarea as Textarea };
|
|
@@ -84,17 +84,6 @@
|
|
|
84
84
|
color: var(--pf-textarea-help-text-color);
|
|
85
85
|
font-size: var(--pf-font-size-subtitle2);
|
|
86
86
|
}
|
|
87
|
-
.is-visually-hidden {
|
|
88
|
-
position: absolute;
|
|
89
|
-
width: 1px;
|
|
90
|
-
height: 1px;
|
|
91
|
-
padding: 0;
|
|
92
|
-
margin: -1px;
|
|
93
|
-
overflow: hidden;
|
|
94
|
-
clip: rect(0, 0, 0, 0);
|
|
95
|
-
white-space: nowrap;
|
|
96
|
-
border: 0;
|
|
97
|
-
}
|
|
98
87
|
.form-label {
|
|
99
88
|
margin-bottom: var(--pf-margin-2);
|
|
100
89
|
}
|
package/src/components/index.ts
CHANGED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Canvas, Meta, Controls, Story } from '@storybook/blocks';
|
|
2
|
+
import * as Menu from './Menu.stories';
|
|
3
|
+
import { Col, Row } from '@/components';
|
|
4
|
+
|
|
5
|
+
<Meta title="Components/Menu" of={Menu} />
|
|
6
|
+
|
|
7
|
+
# Menu
|
|
8
|
+
|
|
9
|
+
The Menu component is used to display a list of actionable items. Typically used in dropdowns, poppers, or sidebars.
|
|
10
|
+
|
|
11
|
+
<Canvas of={Menu.Default} />
|
|
12
|
+
|
|
13
|
+
### The following props are available for the Menu component:
|
|
14
|
+
|
|
15
|
+
<Controls of={Menu.Default} />
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { Menu, MenuProps } from './Menu';
|
|
3
|
+
import { Button } from '../button';
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof Menu> = {
|
|
6
|
+
title: 'Components/Menu',
|
|
7
|
+
component: Menu,
|
|
8
|
+
argTypes: {
|
|
9
|
+
children: {
|
|
10
|
+
control: 'text',
|
|
11
|
+
description:
|
|
12
|
+
'The children of the Menu component, which will be automatically styled as menu-items.',
|
|
13
|
+
table: {
|
|
14
|
+
category: 'Props',
|
|
15
|
+
type: {
|
|
16
|
+
summary: 'React.ReactNode',
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export default meta;
|
|
24
|
+
|
|
25
|
+
type Story = StoryObj<MenuProps>;
|
|
26
|
+
|
|
27
|
+
export const Default: Story = {
|
|
28
|
+
render: (args) => (
|
|
29
|
+
<Menu {...args}>
|
|
30
|
+
<Button
|
|
31
|
+
data-testid="refresh-library"
|
|
32
|
+
ariaLabel="Refresh Data"
|
|
33
|
+
iconName="retrain"
|
|
34
|
+
onClick={() => console.log('Refresh Data')}
|
|
35
|
+
>
|
|
36
|
+
Refresh Data
|
|
37
|
+
</Button>
|
|
38
|
+
<Button
|
|
39
|
+
data-testid="configure-fields"
|
|
40
|
+
ariaLabel="Configure Fields"
|
|
41
|
+
iconName="edit"
|
|
42
|
+
onClick={() => console.log('Configure Fields')}
|
|
43
|
+
>
|
|
44
|
+
Configure Fields
|
|
45
|
+
</Button>
|
|
46
|
+
<Button
|
|
47
|
+
data-testid="delete-library"
|
|
48
|
+
ariaLabel="Delete Library"
|
|
49
|
+
iconName="trash"
|
|
50
|
+
onClick={() => console.log('Delete Library')}
|
|
51
|
+
>
|
|
52
|
+
Delete Library
|
|
53
|
+
</Button>
|
|
54
|
+
</Menu>
|
|
55
|
+
),
|
|
56
|
+
};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, screen, fireEvent } from '@testing-library/react';
|
|
3
|
+
import { Menu } from './Menu';
|
|
4
|
+
import { Button } from '../button';
|
|
5
|
+
|
|
6
|
+
describe('Menu Component', () => {
|
|
7
|
+
it('renders children inside of the menu', () => {
|
|
8
|
+
render(
|
|
9
|
+
<Menu>
|
|
10
|
+
<Button data-testid="refresh-library" ariaLabel="Refresh Data" iconName="retrain">
|
|
11
|
+
Refresh Data
|
|
12
|
+
</Button>
|
|
13
|
+
<Button data-testid="configure-fields" ariaLabel="Configure Fields" iconName="edit">
|
|
14
|
+
Configure Fields
|
|
15
|
+
</Button>
|
|
16
|
+
<Button data-testid="delete-library" ariaLabel="Delete Library" iconName="trash">
|
|
17
|
+
Delete Library
|
|
18
|
+
</Button>
|
|
19
|
+
</Menu>,
|
|
20
|
+
);
|
|
21
|
+
expect(screen.getByText('Refresh Data')).toBeInTheDocument();
|
|
22
|
+
expect(screen.getByText('Configure Fields')).toBeInTheDocument();
|
|
23
|
+
expect(screen.getByText('Delete Library')).toBeInTheDocument();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('applies the menu-item class to its children', () => {
|
|
27
|
+
render(
|
|
28
|
+
<Menu>
|
|
29
|
+
<Button data-testid="refresh-library" ariaLabel="Refresh Data" iconName="retrain">
|
|
30
|
+
Refresh Data
|
|
31
|
+
</Button>
|
|
32
|
+
<Button data-testid="configure-fields" ariaLabel="Configure Fields" iconName="edit">
|
|
33
|
+
Configure Fields
|
|
34
|
+
</Button>
|
|
35
|
+
<Button data-testid="delete-library" ariaLabel="Delete Library" iconName="trash">
|
|
36
|
+
Delete Library
|
|
37
|
+
</Button>
|
|
38
|
+
</Menu>,
|
|
39
|
+
);
|
|
40
|
+
const menuItems = screen.getAllByRole('button');
|
|
41
|
+
menuItems.forEach((item) => {
|
|
42
|
+
expect(item).toHaveClass('menu-item');
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('handles click events for its children', () => {
|
|
47
|
+
const mockRefresh = jest.fn();
|
|
48
|
+
const mockConfigure = jest.fn();
|
|
49
|
+
const mockDelete = jest.fn();
|
|
50
|
+
|
|
51
|
+
render(
|
|
52
|
+
<Menu>
|
|
53
|
+
<Button
|
|
54
|
+
data-testid="refresh-library"
|
|
55
|
+
ariaLabel="Refresh Data"
|
|
56
|
+
iconName="retrain"
|
|
57
|
+
onClick={mockRefresh}
|
|
58
|
+
>
|
|
59
|
+
Refresh Data
|
|
60
|
+
</Button>
|
|
61
|
+
<Button
|
|
62
|
+
data-testid="configure-fields"
|
|
63
|
+
ariaLabel="Configure Fields"
|
|
64
|
+
iconName="edit"
|
|
65
|
+
onClick={mockConfigure}
|
|
66
|
+
>
|
|
67
|
+
Configure Fields
|
|
68
|
+
</Button>
|
|
69
|
+
<Button
|
|
70
|
+
data-testid="delete-library"
|
|
71
|
+
ariaLabel="Delete Library"
|
|
72
|
+
iconName="trash"
|
|
73
|
+
onClick={mockDelete}
|
|
74
|
+
>
|
|
75
|
+
Delete Library
|
|
76
|
+
</Button>
|
|
77
|
+
</Menu>,
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
fireEvent.click(screen.getByText('Refresh Data'));
|
|
81
|
+
fireEvent.click(screen.getByText('Configure Fields'));
|
|
82
|
+
fireEvent.click(screen.getByText('Delete Library'));
|
|
83
|
+
|
|
84
|
+
expect(mockRefresh).toHaveBeenCalledTimes(1);
|
|
85
|
+
expect(mockConfigure).toHaveBeenCalledTimes(1);
|
|
86
|
+
expect(mockDelete).toHaveBeenCalledTimes(1);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import React, { ReactElement } from 'react';
|
|
2
|
+
import classNames from 'classnames';
|
|
3
|
+
|
|
4
|
+
export type MenuProps = {
|
|
5
|
+
children: React.ReactNode;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export function Menu({ children }: MenuProps) {
|
|
9
|
+
return (
|
|
10
|
+
<div className="menu">
|
|
11
|
+
{React.Children.map(children, (child) =>
|
|
12
|
+
React.isValidElement(child)
|
|
13
|
+
? React.cloneElement(child as ReactElement, {
|
|
14
|
+
className: classNames((child as ReactElement).props.className, 'menu-item'),
|
|
15
|
+
})
|
|
16
|
+
: child,
|
|
17
|
+
)}
|
|
18
|
+
</div>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Menu } from './Menu';
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
@import './variables.scss';
|
|
2
|
+
|
|
3
|
+
.menu {
|
|
4
|
+
border-radius: var(--pf-menu-rounded);
|
|
5
|
+
|
|
6
|
+
.menu-item {
|
|
7
|
+
width: 100%;
|
|
8
|
+
background: var(--pf-menu-item-background-color);
|
|
9
|
+
color: var(--pf-menu-item-color);
|
|
10
|
+
display: block;
|
|
11
|
+
width: 100%;
|
|
12
|
+
text-align: left;
|
|
13
|
+
border: none;
|
|
14
|
+
|
|
15
|
+
&:hover {
|
|
16
|
+
background: var(--pf-menu-item-hover-color);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
:root,
|
|
2
|
+
:root [data-theme='light'],
|
|
3
|
+
:root [data-theme='dark'] {
|
|
4
|
+
--pf-menu-rounded: var(--pf-rounded);
|
|
5
|
+
--pf-menu-item-hover-color: var(--pf-primary-color-100);
|
|
6
|
+
--pf-menu-item-background-color: var(--pf-white-color);
|
|
7
|
+
--pf-menu-item-color: var(--pf-gray-color);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// Dark Theme Specific Variables
|
|
11
|
+
:root [data-theme='dark'] {
|
|
12
|
+
--pf-menu-item-hover-color: var(--pf-primary-color-300);
|
|
13
|
+
--pf-menu-item-background-color: var(--pf-primary-color-600);
|
|
14
|
+
--pf-menu-item-color: var(--pf-gray-color-100);
|
|
15
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { Canvas, Meta, Controls } from '@storybook/blocks';
|
|
2
|
+
import * as PopperStories from './Popper.stories';
|
|
3
|
+
import { Col, Row } from '@/components';
|
|
4
|
+
|
|
5
|
+
<Meta title="Components/Popper" of={PopperStories} />
|
|
6
|
+
|
|
7
|
+
# Popper
|
|
8
|
+
|
|
9
|
+
The Popper component is used to display content relative to another element. It can be used for tooltips, dropdowns, and other floating elements. The Popper is positioned using the `@floating-ui/react-dom` library.
|
|
10
|
+
|
|
11
|
+
<Canvas
|
|
12
|
+
of={PopperStories.Default}
|
|
13
|
+
source={{
|
|
14
|
+
code: `
|
|
15
|
+
import React, { useState, useRef } from 'react';
|
|
16
|
+
import { Popper } from '@/components/Popper';
|
|
17
|
+
import { Button } from '@/components/Button';
|
|
18
|
+
import { Menu } from '@/components/Menu';
|
|
19
|
+
|
|
20
|
+
const Example = () => {
|
|
21
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
22
|
+
const buttonRef = useRef<HTMLDivElement | null>(null);
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<div>
|
|
26
|
+
<div ref={buttonRef}>
|
|
27
|
+
<Button
|
|
28
|
+
onClick={() => setIsOpen((prev) => !prev)}
|
|
29
|
+
iconName="kabob"
|
|
30
|
+
ariaLabel="Toggle Popper"
|
|
31
|
+
/>
|
|
32
|
+
</div>
|
|
33
|
+
<Popper
|
|
34
|
+
referenceElement={buttonRef.current}
|
|
35
|
+
isOpen={isOpen}
|
|
36
|
+
onClose={() => setIsOpen(false)}
|
|
37
|
+
ariaLabel="Example Popper"
|
|
38
|
+
placement="bottom-start"
|
|
39
|
+
offsetValue={5}
|
|
40
|
+
>
|
|
41
|
+
<Menu>
|
|
42
|
+
<Button
|
|
43
|
+
data-testid="refresh-library"
|
|
44
|
+
ariaLabel="Refresh Data"
|
|
45
|
+
iconName="retrain"
|
|
46
|
+
onClick={() => console.log('Refresh Data')}
|
|
47
|
+
>
|
|
48
|
+
Refresh Data
|
|
49
|
+
</Button>
|
|
50
|
+
<Button
|
|
51
|
+
data-testid="configure-fields"
|
|
52
|
+
ariaLabel="Configure Fields"
|
|
53
|
+
iconName="edit"
|
|
54
|
+
onClick={() => console.log('Configure Fields')}
|
|
55
|
+
>
|
|
56
|
+
Configure Fields
|
|
57
|
+
</Button>
|
|
58
|
+
<Button
|
|
59
|
+
data-testid="delete-library"
|
|
60
|
+
ariaLabel="Delete Library"
|
|
61
|
+
iconName="trash"
|
|
62
|
+
onClick={() => console.log('Delete Library')}
|
|
63
|
+
>
|
|
64
|
+
Delete Library
|
|
65
|
+
</Button>
|
|
66
|
+
</Menu>
|
|
67
|
+
</Popper>
|
|
68
|
+
</div>
|
|
69
|
+
);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export default Example;
|
|
73
|
+
`,
|
|
74
|
+
}}
|
|
75
|
+
/>
|
|
76
|
+
|
|
77
|
+
### The following props are available for the Popper component:
|
|
78
|
+
|
|
79
|
+
<Controls of={PopperStories.Default} />
|