@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.
Files changed (58) hide show
  1. package/lib/index.css +55 -33
  2. package/lib/index.d.ts +34 -20
  3. package/lib/index.esm.css +55 -33
  4. package/lib/index.esm.js +1666 -85
  5. package/lib/index.esm.js.map +1 -1
  6. package/lib/index.js +1669 -86
  7. package/lib/index.js.map +1 -1
  8. package/lib/src/components/forms/input/Input.d.ts +5 -7
  9. package/lib/src/components/forms/passwordInput/PasswordInput.d.ts +5 -7
  10. package/lib/src/components/forms/subcomponents/Label.d.ts +6 -3
  11. package/lib/src/components/forms/textarea/Textarea.d.ts +5 -7
  12. package/lib/src/components/index.d.ts +2 -0
  13. package/lib/src/components/menu/Menu.d.ts +5 -0
  14. package/lib/src/components/menu/Menu.stories.d.ts +6 -0
  15. package/lib/src/components/menu/Menu.test.d.ts +1 -0
  16. package/lib/src/components/menu/index.d.ts +1 -0
  17. package/lib/src/components/popper/Popper.d.ts +12 -0
  18. package/lib/src/components/popper/Popper.stories.d.ts +6 -0
  19. package/lib/src/components/popper/Popper.test.d.ts +1 -0
  20. package/lib/src/components/popper/index.d.ts +1 -0
  21. package/lib/src/hooks/useClickOutside.d.ts +2 -0
  22. package/lib/src/index.d.ts +2 -0
  23. package/lib/src/storybook/labelArgTypes.d.ts +3 -0
  24. package/package.json +2 -1
  25. package/src/components/forms/input/Input.mdx +15 -2
  26. package/src/components/forms/input/Input.stories.tsx +10 -45
  27. package/src/components/forms/input/Input.tsx +22 -15
  28. package/src/components/forms/input/styles/Input.scss +0 -11
  29. package/src/components/forms/passwordInput/PasswordInput.mdx +10 -8
  30. package/src/components/forms/passwordInput/PasswordInput.stories.tsx +3 -44
  31. package/src/components/forms/passwordInput/PasswordInput.tsx +20 -15
  32. package/src/components/forms/passwordInput/styles/PasswordInput.scss +0 -11
  33. package/src/components/forms/subcomponents/Label.tsx +29 -6
  34. package/src/components/forms/subcomponents/__tests__/Label.test.tsx +63 -15
  35. package/src/components/forms/textarea/Textarea.mdx +12 -2
  36. package/src/components/forms/textarea/Textarea.stories.tsx +4 -46
  37. package/src/components/forms/textarea/Textarea.tsx +15 -13
  38. package/src/components/forms/textarea/styles/Textarea.scss +0 -11
  39. package/src/components/index.ts +2 -0
  40. package/src/components/menu/Menu.mdx +15 -0
  41. package/src/components/menu/Menu.stories.tsx +56 -0
  42. package/src/components/menu/Menu.test.tsx +88 -0
  43. package/src/components/menu/Menu.tsx +20 -0
  44. package/src/components/menu/index.ts +1 -0
  45. package/src/components/menu/styles/Menu.scss +19 -0
  46. package/src/components/menu/styles/_variables.scss +15 -0
  47. package/src/components/popper/Popper.mdx +79 -0
  48. package/src/components/popper/Popper.stories.tsx +161 -0
  49. package/src/components/popper/Popper.test.tsx +68 -0
  50. package/src/components/popper/Popper.tsx +57 -0
  51. package/src/components/popper/index.ts +1 -0
  52. package/src/components/popper/styles/Popper.scss +11 -0
  53. package/src/components/popper/styles/_variables.scss +15 -0
  54. package/src/hooks/useClickOutside.tsx +22 -0
  55. package/src/index.ts +2 -0
  56. package/src/legacy/components/buttons/commonStyles.ts +0 -4
  57. package/src/storybook/labelArgTypes.ts +50 -0
  58. package/src/styles/index.scss +2 -0
@@ -1,16 +1,18 @@
1
- interface LabelProps {
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, hasHiddenLabel }: LabelProps) => {
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` astherisc when the label is required', () => {
6
- render(<Label label={'name'} name={'name'} isRequired={true} hasHiddenLabel={false} />);
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
- it('does not render the `required` astherisc when the label is not required', () => {
11
- render(<Label label={'name'} name={'name'} isRequired={false} hasHiddenLabel={false} />);
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
- it('renders the label for screen readers while hidden visually', () => {
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={'name'} name={'name'} isRequired={false} hasHiddenLabel={false} />);
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
- it('renders the label text when the label is hidden', () => {
29
- render(<Label label={'name'} name={'name'} isRequired={false} hasHiddenLabel={true} />);
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
- expect(label).toHaveClass('is-visually-hidden');
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. (***Our components are compatible with but do not provide the plugin***)
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
- <Textarea name="first_name" isRequired helpText="This Is Help Text" placeholder="This is a placeholder" value="''" onChange={handleChange} label="Label Name" />
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 { Textarea, TextareaProps } from './Textarea';
3
- import { SetStateAction, useEffect, useState } from 'react';
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 { Label } from '../subcomponents/Label';
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
- label: string;
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
- export const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
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
- <div className="form-control">
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={`textarea ${hasErrors ? 'error' : ''}`}
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
- </div>
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
  }
@@ -12,3 +12,5 @@ export { Select } from './forms/select';
12
12
  export { Form } from './forms/form';
13
13
  export { Skeleton } from './skeleton';
14
14
  export { Card } from './card';
15
+ export { Popper } from './popper';
16
+ export { Menu } from './menu';
@@ -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} />