@indico-data/design-system 2.27.0 → 2.28.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 +10 -11
- package/lib/index.d.ts +11 -11
- package/lib/index.esm.css +10 -11
- package/lib/index.esm.js +11101 -10242
- package/lib/index.esm.js.map +1 -1
- package/lib/index.js +11150 -10276
- package/lib/index.js.map +1 -1
- package/lib/src/components/floatUI/FloatUI.d.ts +2 -0
- package/lib/src/components/floatUI/FloatUI.stories.d.ts +8 -0
- package/lib/src/components/floatUI/index.d.ts +1 -0
- package/lib/src/components/floatUI/types.d.ts +9 -0
- package/lib/src/components/index.d.ts +1 -1
- package/lib/src/index.d.ts +2 -1
- package/package.json +1 -1
- package/src/components/floatUI/FloatUI.mdx +84 -0
- package/src/components/floatUI/FloatUI.stories.tsx +157 -0
- package/src/components/floatUI/FloatUI.test.tsx +129 -0
- package/src/components/floatUI/FloatUI.tsx +82 -0
- package/src/components/floatUI/index.ts +1 -0
- package/src/components/floatUI/styles/FloatUI.scss +11 -0
- package/src/components/floatUI/styles/_variables.scss +14 -0
- package/src/components/floatUI/types.ts +10 -0
- package/src/components/index.ts +1 -1
- package/src/index.ts +4 -1
- package/src/styles/index.scss +1 -1
- package/lib/src/components/popper/Popper.d.ts +0 -12
- package/lib/src/components/popper/Popper.stories.d.ts +0 -6
- package/lib/src/components/popper/index.d.ts +0 -1
- package/src/components/popper/Popper.mdx +0 -79
- package/src/components/popper/Popper.stories.tsx +0 -160
- package/src/components/popper/Popper.test.tsx +0 -68
- package/src/components/popper/Popper.tsx +0 -57
- package/src/components/popper/index.ts +0 -1
- package/src/components/popper/styles/Popper.scss +0 -11
- package/src/components/popper/styles/_variables.scss +0 -15
- /package/lib/src/components/{popper/Popper.test.d.ts → floatUI/FloatUI.test.d.ts} +0 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { FloatUI } from './FloatUI';
|
|
3
|
+
import { FloatUIProps } from './types';
|
|
4
|
+
declare const meta: Meta<typeof FloatUI>;
|
|
5
|
+
export default meta;
|
|
6
|
+
type Story = StoryObj<FloatUIProps>;
|
|
7
|
+
export declare const Uncontrolled: Story;
|
|
8
|
+
export declare const Controlled: Story;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { FloatUI } from './FloatUI';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { ReactElement } from 'react';
|
|
2
|
+
import { UseFloatingOptions } from '@floating-ui/react-dom';
|
|
3
|
+
export type FloatUIProps = {
|
|
4
|
+
children: [ReactElement, ReactElement];
|
|
5
|
+
ariaLabel: string;
|
|
6
|
+
floatingOptions?: UseFloatingOptions;
|
|
7
|
+
isOpen?: boolean;
|
|
8
|
+
setIsOpen?: React.Dispatch<React.SetStateAction<boolean>>;
|
|
9
|
+
};
|
|
@@ -12,5 +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 {
|
|
15
|
+
export { FloatUI } from './floatUI';
|
|
16
16
|
export { Menu } from './menu';
|
package/lib/src/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import './styles/index.scss';
|
|
2
|
+
export * from '@floating-ui/react-dom';
|
|
2
3
|
export { GlobalStyles } from './legacy/styles/globals/index';
|
|
3
4
|
export { ANIMATION, BREAKPOINT, COLORS, MATH, MEDIA_QUERIES, MARGINS, PADDINGS, SPACING, TYPOGRAPHY, } from './legacy/tokens';
|
|
4
5
|
export { AbstractRadio, AbstractRadioGroup, Accordion, BarSpinner, BorderSelect, Button as LegacyButton, CirclePulse, CircleSpinner, ConfirmModal, DatePicker, EditableInput, IconButton, ListTable, LoadingAwareContainer, LoadingList, ModalBase, MultiCombobox, NoInputDatePicker, NumberInput, Pagination, PercentageRing, Radio, RadioGroup, RandomLoadingMessage, SearchInput, Section, SectionBlock, SectionBody, SectionHeader, SectionTable, Select, Shrug, SingleCombobox, TextInput, TextTruncate, Toggle, Tooltip, } from './legacy/components';
|
|
@@ -17,6 +18,6 @@ export { Select as SelectInput } from './components/forms/select';
|
|
|
17
18
|
export { Form } from './components/forms/form';
|
|
18
19
|
export { Skeleton } from './components/skeleton';
|
|
19
20
|
export { Card } from './components/card';
|
|
20
|
-
export {
|
|
21
|
+
export { FloatUI } from './components/floatUI';
|
|
21
22
|
export { Menu } from './components/menu';
|
|
22
23
|
export { Pill } from './components/pill';
|
package/package.json
CHANGED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { Canvas, Meta, Controls } from '@storybook/blocks';
|
|
2
|
+
import * as FloatUIStories from './FloatUI.stories';
|
|
3
|
+
|
|
4
|
+
<Meta title="Components/FloatUI" of={FloatUIStories} />
|
|
5
|
+
|
|
6
|
+
# FloatUI
|
|
7
|
+
|
|
8
|
+
The FloatUI component is used to display content relative to another element. It can be used for tooltips, dropdowns, and other floating elements. FloatUI is positioned using the [floating-ui](https://floating-ui.com/) library.
|
|
9
|
+
|
|
10
|
+
<Canvas of={FloatUIStories.Uncontrolled} />
|
|
11
|
+
|
|
12
|
+
### The following props are available for the FloatUI component:
|
|
13
|
+
|
|
14
|
+
<Controls of={FloatUIStories.Uncontrolled} />
|
|
15
|
+
|
|
16
|
+
## Controlling the FloatUI
|
|
17
|
+
|
|
18
|
+
FloatUI can be used in two modes:
|
|
19
|
+
|
|
20
|
+
- **Uncontrolled Mode:** FloatUI manages its own state, toggling visibility when the trigger is clicked.
|
|
21
|
+
- **Controlled Mode:** The parent component controls visibility by passing `isOpen` and `setIsOpen` props
|
|
22
|
+
|
|
23
|
+
### Example: Controlled `FloatUI`
|
|
24
|
+
|
|
25
|
+
```tsx
|
|
26
|
+
import React, { useState } from 'react';
|
|
27
|
+
import { FloatUI } from '@/components/FloatUI';
|
|
28
|
+
import { Button } from '@/components/Button';
|
|
29
|
+
import { Menu } from '@/components/Menu';
|
|
30
|
+
import { Icon } from '@/components/Icon';
|
|
31
|
+
|
|
32
|
+
const ControlledExample = () => {
|
|
33
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
34
|
+
const [selectedAction, setSelectedAction] = useState<string | null>(null);
|
|
35
|
+
|
|
36
|
+
const handleToggle = () => setIsOpen(!isOpen);
|
|
37
|
+
|
|
38
|
+
const handleActionClick = (action: string) => {
|
|
39
|
+
setSelectedAction(action);
|
|
40
|
+
setIsOpen(false);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<div>
|
|
45
|
+
<FloatUI ariaLabel="Controlled Example FloatUI" isOpen={isOpen} setIsOpen={setIsOpen}>
|
|
46
|
+
<Button iconName="kabob" ariaLabel="Toggle FloatUI" onClick={handleToggle}>
|
|
47
|
+
Actions
|
|
48
|
+
</Button>
|
|
49
|
+
<Menu>
|
|
50
|
+
<Button
|
|
51
|
+
ariaLabel="Refresh Data"
|
|
52
|
+
iconName="retrain"
|
|
53
|
+
onClick={() => handleActionClick('Refresh Data')}
|
|
54
|
+
>
|
|
55
|
+
Refresh Data
|
|
56
|
+
</Button>
|
|
57
|
+
<Button
|
|
58
|
+
ariaLabel="Configure Fields"
|
|
59
|
+
iconName="edit"
|
|
60
|
+
onClick={() => handleActionClick('Configure Fields')}
|
|
61
|
+
>
|
|
62
|
+
Configure Fields
|
|
63
|
+
</Button>
|
|
64
|
+
<Button
|
|
65
|
+
ariaLabel="Delete Library"
|
|
66
|
+
iconName="trash"
|
|
67
|
+
onClick={() => handleActionClick('Delete Library')}
|
|
68
|
+
>
|
|
69
|
+
Delete Library
|
|
70
|
+
</Button>
|
|
71
|
+
</Menu>
|
|
72
|
+
</FloatUI>
|
|
73
|
+
|
|
74
|
+
{selectedAction && (
|
|
75
|
+
<div style={{ marginTop: '16px' }}>
|
|
76
|
+
<Icon name="info" /> You selected: <strong>{selectedAction}</strong>
|
|
77
|
+
</div>
|
|
78
|
+
)}
|
|
79
|
+
</div>
|
|
80
|
+
);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export default ControlledExample;
|
|
84
|
+
```
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { FloatUI } from './FloatUI';
|
|
4
|
+
import { Button } from '../button';
|
|
5
|
+
import { Menu } from '../menu';
|
|
6
|
+
import { FloatUIProps } from './types';
|
|
7
|
+
|
|
8
|
+
const meta: Meta<typeof FloatUI> = {
|
|
9
|
+
title: 'Components/FloatUI',
|
|
10
|
+
component: FloatUI,
|
|
11
|
+
argTypes: {
|
|
12
|
+
children: {
|
|
13
|
+
control: 'object',
|
|
14
|
+
description:
|
|
15
|
+
'An array of exactly two elements: the first element is the trigger that opens the FloatUI, and the second element is the content displayed within the FloatUI.',
|
|
16
|
+
table: {
|
|
17
|
+
category: 'Props',
|
|
18
|
+
type: {
|
|
19
|
+
summary: '[trigger: ReactElement, content: ReactElement]',
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
ariaLabel: {
|
|
24
|
+
control: 'text',
|
|
25
|
+
description: 'Sets the aria-label attribute for the FloatUI.',
|
|
26
|
+
table: {
|
|
27
|
+
category: 'Props',
|
|
28
|
+
type: {
|
|
29
|
+
summary: 'string',
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
floatingOptions: {
|
|
34
|
+
control: 'object',
|
|
35
|
+
description:
|
|
36
|
+
'Options for configuring the floating UI behavior. For more, see the [floating-ui docs](https://floating-ui.com/docs/useFloating#options).',
|
|
37
|
+
table: {
|
|
38
|
+
category: 'Props',
|
|
39
|
+
type: {
|
|
40
|
+
summary: 'UseFloatingOptions',
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
isOpen: {
|
|
45
|
+
control: false,
|
|
46
|
+
description: 'Controls the visibility of the FloatUI (for controlled mode).',
|
|
47
|
+
table: {
|
|
48
|
+
category: 'Props',
|
|
49
|
+
type: {
|
|
50
|
+
summary: 'boolean',
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
setIsOpen: {
|
|
55
|
+
control: false,
|
|
56
|
+
description: 'Function to toggle the visibility of the FloatUI (for controlled mode).',
|
|
57
|
+
table: {
|
|
58
|
+
category: 'Props',
|
|
59
|
+
type: {
|
|
60
|
+
summary: '(isOpen: boolean) => void',
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
decorators: [
|
|
66
|
+
(Story: React.ComponentType) => (
|
|
67
|
+
<div
|
|
68
|
+
style={{
|
|
69
|
+
height: '160px',
|
|
70
|
+
}}
|
|
71
|
+
>
|
|
72
|
+
<Story />
|
|
73
|
+
</div>
|
|
74
|
+
),
|
|
75
|
+
],
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export default meta;
|
|
79
|
+
|
|
80
|
+
type Story = StoryObj<FloatUIProps>;
|
|
81
|
+
|
|
82
|
+
export const Uncontrolled: Story = {
|
|
83
|
+
render: (args) => (
|
|
84
|
+
<FloatUI {...args} ariaLabel="Example FloatUI">
|
|
85
|
+
<Button iconName="kabob" ariaLabel="Toggle FloatUI" />
|
|
86
|
+
<Menu>
|
|
87
|
+
<Button
|
|
88
|
+
data-testid="refresh-library"
|
|
89
|
+
ariaLabel="Refresh Data"
|
|
90
|
+
iconName="retrain"
|
|
91
|
+
onClick={() => console.log('Refresh Data')}
|
|
92
|
+
>
|
|
93
|
+
Refresh Data
|
|
94
|
+
</Button>
|
|
95
|
+
<Button
|
|
96
|
+
data-testid="configure-fields"
|
|
97
|
+
ariaLabel="Configure Fields"
|
|
98
|
+
iconName="edit"
|
|
99
|
+
onClick={() => console.log('Configure Fields')}
|
|
100
|
+
>
|
|
101
|
+
Configure Fields
|
|
102
|
+
</Button>
|
|
103
|
+
<Button
|
|
104
|
+
data-testid="delete-library"
|
|
105
|
+
ariaLabel="Delete Library"
|
|
106
|
+
iconName="trash"
|
|
107
|
+
onClick={() => console.log('Delete Library')}
|
|
108
|
+
>
|
|
109
|
+
Delete Library
|
|
110
|
+
</Button>
|
|
111
|
+
</Menu>
|
|
112
|
+
</FloatUI>
|
|
113
|
+
),
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
export const Controlled: Story = {
|
|
117
|
+
render: (args) => {
|
|
118
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
<FloatUI
|
|
122
|
+
{...args}
|
|
123
|
+
ariaLabel="Controlled Example FloatUI"
|
|
124
|
+
isOpen={isOpen}
|
|
125
|
+
setIsOpen={setIsOpen}
|
|
126
|
+
>
|
|
127
|
+
<Button iconName="kabob" ariaLabel="Toggle FloatUI" />
|
|
128
|
+
<Menu>
|
|
129
|
+
<Button
|
|
130
|
+
data-testid="refresh-library"
|
|
131
|
+
ariaLabel="Refresh Data"
|
|
132
|
+
iconName="retrain"
|
|
133
|
+
onClick={() => console.log('Refresh Data')}
|
|
134
|
+
>
|
|
135
|
+
Refresh Data
|
|
136
|
+
</Button>
|
|
137
|
+
<Button
|
|
138
|
+
data-testid="configure-fields"
|
|
139
|
+
ariaLabel="Configure Fields"
|
|
140
|
+
iconName="edit"
|
|
141
|
+
onClick={() => console.log('Configure Fields')}
|
|
142
|
+
>
|
|
143
|
+
Configure Fields
|
|
144
|
+
</Button>
|
|
145
|
+
<Button
|
|
146
|
+
data-testid="delete-library"
|
|
147
|
+
ariaLabel="Delete Library"
|
|
148
|
+
iconName="trash"
|
|
149
|
+
onClick={() => console.log('Delete Library')}
|
|
150
|
+
>
|
|
151
|
+
Delete Library
|
|
152
|
+
</Button>
|
|
153
|
+
</Menu>
|
|
154
|
+
</FloatUI>
|
|
155
|
+
);
|
|
156
|
+
},
|
|
157
|
+
};
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { render, screen, fireEvent } from '@testing-library/react';
|
|
2
|
+
import { FloatUI } from './FloatUI';
|
|
3
|
+
import { Menu } from '../menu';
|
|
4
|
+
import { Button } from '../button';
|
|
5
|
+
|
|
6
|
+
describe('FloatUI Component', () => {
|
|
7
|
+
it('does not display FloatUI content initially when rendered in uncontrolled mode', () => {
|
|
8
|
+
render(
|
|
9
|
+
<FloatUI ariaLabel="Example FloatUI">
|
|
10
|
+
<Button ariaLabel="Toggle FloatUI">Toggle</Button>
|
|
11
|
+
<div>FloatUI Content</div>
|
|
12
|
+
</FloatUI>,
|
|
13
|
+
);
|
|
14
|
+
expect(screen.queryByText('FloatUI Content')).not.toBeInTheDocument();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('displays the FloatUI content when the trigger is clicked in uncontrolled mode', () => {
|
|
18
|
+
render(
|
|
19
|
+
<FloatUI ariaLabel="Example FloatUI">
|
|
20
|
+
<Button ariaLabel="Toggle FloatUI">Toggle</Button>
|
|
21
|
+
<div>FloatUI Content</div>
|
|
22
|
+
</FloatUI>,
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
// Open the FloatUI
|
|
26
|
+
fireEvent.click(screen.getByText('Toggle'));
|
|
27
|
+
expect(screen.getByText('FloatUI Content')).toBeInTheDocument();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('closes the FloatUI when clicked outside in uncontrolled mode', () => {
|
|
31
|
+
render(
|
|
32
|
+
<div>
|
|
33
|
+
<div data-testid="outside">Outside Element</div>
|
|
34
|
+
<FloatUI ariaLabel="Example FloatUI">
|
|
35
|
+
<Button ariaLabel="Toggle FloatUI">Toggle</Button>
|
|
36
|
+
<div>FloatUI Content</div>
|
|
37
|
+
</FloatUI>
|
|
38
|
+
</div>,
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
// Open the FloatUI
|
|
42
|
+
fireEvent.click(screen.getByText('Toggle'));
|
|
43
|
+
|
|
44
|
+
fireEvent.mouseDown(screen.getByTestId('outside'));
|
|
45
|
+
expect(screen.queryByText('FloatUI Content')).not.toBeInTheDocument();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('renders the correct children inside the FloatUI when open in uncontrolled mode', () => {
|
|
49
|
+
render(
|
|
50
|
+
<FloatUI ariaLabel="Example FloatUI">
|
|
51
|
+
<Button ariaLabel="Toggle FloatUI">Toggle</Button>
|
|
52
|
+
<Menu>
|
|
53
|
+
<Button ariaLabel="Refresh Data" iconName="retrain">
|
|
54
|
+
Refresh Data
|
|
55
|
+
</Button>
|
|
56
|
+
<Button ariaLabel="Configure Fields" iconName="edit">
|
|
57
|
+
Configure Fields
|
|
58
|
+
</Button>
|
|
59
|
+
<Button ariaLabel="Delete Library" iconName="trash">
|
|
60
|
+
Delete Library
|
|
61
|
+
</Button>
|
|
62
|
+
</Menu>
|
|
63
|
+
</FloatUI>,
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
fireEvent.click(screen.getByText('Toggle'));
|
|
67
|
+
|
|
68
|
+
expect(screen.getByText('Refresh Data')).toBeInTheDocument();
|
|
69
|
+
expect(screen.getByText('Configure Fields')).toBeInTheDocument();
|
|
70
|
+
expect(screen.getByText('Delete Library')).toBeInTheDocument();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('toggles the FloatUI open and closed when the trigger is clicked in uncontrolled mode', () => {
|
|
74
|
+
render(
|
|
75
|
+
<FloatUI ariaLabel="Example FloatUI">
|
|
76
|
+
<Button ariaLabel="Toggle FloatUI">Toggle</Button>
|
|
77
|
+
<div>FloatUI Content</div>
|
|
78
|
+
</FloatUI>,
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
fireEvent.click(screen.getByText('Toggle'));
|
|
82
|
+
expect(screen.getByText('FloatUI Content')).toBeInTheDocument();
|
|
83
|
+
|
|
84
|
+
fireEvent.click(screen.getByText('Toggle'));
|
|
85
|
+
expect(screen.queryByText('FloatUI Content')).not.toBeInTheDocument();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('does not display FloatUI content initially when rendered in controlled mode', () => {
|
|
89
|
+
const controlledIsOpen = false;
|
|
90
|
+
const setIsOpen = jest.fn();
|
|
91
|
+
|
|
92
|
+
render(
|
|
93
|
+
<FloatUI ariaLabel="Example FloatUI" isOpen={controlledIsOpen} setIsOpen={setIsOpen}>
|
|
94
|
+
<Button ariaLabel="Toggle FloatUI">Toggle</Button>
|
|
95
|
+
<div>FloatUI Content</div>
|
|
96
|
+
</FloatUI>,
|
|
97
|
+
);
|
|
98
|
+
expect(screen.queryByText('FloatUI Content')).not.toBeInTheDocument();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('displays the FloatUI content when controlled isOpen is true', () => {
|
|
102
|
+
const controlledIsOpen = true;
|
|
103
|
+
const setIsOpen = jest.fn();
|
|
104
|
+
|
|
105
|
+
render(
|
|
106
|
+
<FloatUI ariaLabel="Example FloatUI" isOpen={controlledIsOpen} setIsOpen={setIsOpen}>
|
|
107
|
+
<Button ariaLabel="Toggle FloatUI">Toggle</Button>
|
|
108
|
+
<div>FloatUI Content</div>
|
|
109
|
+
</FloatUI>,
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
expect(screen.getByText('FloatUI Content')).toBeInTheDocument();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('calls setIsOpen when the trigger is clicked in controlled mode', () => {
|
|
116
|
+
const controlledIsOpen = false;
|
|
117
|
+
const setIsOpen = jest.fn();
|
|
118
|
+
|
|
119
|
+
render(
|
|
120
|
+
<FloatUI ariaLabel="Example FloatUI" isOpen={controlledIsOpen} setIsOpen={setIsOpen}>
|
|
121
|
+
<Button ariaLabel="Toggle FloatUI">Toggle</Button>
|
|
122
|
+
<div>FloatUI Content</div>
|
|
123
|
+
</FloatUI>,
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
fireEvent.click(screen.getByText('Toggle'));
|
|
127
|
+
expect(setIsOpen).toHaveBeenCalledWith(true);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import React, { useState, useRef, isValidElement, ReactNode } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
useFloating,
|
|
4
|
+
UseFloatingOptions,
|
|
5
|
+
offset,
|
|
6
|
+
flip,
|
|
7
|
+
shift,
|
|
8
|
+
Placement,
|
|
9
|
+
} from '@floating-ui/react-dom';
|
|
10
|
+
import { useClickOutside } from '@/hooks/useClickOutside';
|
|
11
|
+
import { FloatUIProps } from './types';
|
|
12
|
+
|
|
13
|
+
const defaultOptions: UseFloatingOptions = {
|
|
14
|
+
placement: 'bottom-start' as Placement,
|
|
15
|
+
middleware: [offset(5), flip(), shift()],
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export function FloatUI({
|
|
19
|
+
children,
|
|
20
|
+
ariaLabel,
|
|
21
|
+
isOpen: controlledIsOpen,
|
|
22
|
+
setIsOpen: controlledSetIsOpen,
|
|
23
|
+
floatingOptions = defaultOptions,
|
|
24
|
+
}: FloatUIProps) {
|
|
25
|
+
const [internalIsOpen, setInternalIsOpen] = useState(false);
|
|
26
|
+
|
|
27
|
+
// Determine whether the component is controlled or uncontrolled
|
|
28
|
+
const isControlled = controlledIsOpen !== undefined && controlledSetIsOpen !== undefined;
|
|
29
|
+
const isOpen = isControlled ? controlledIsOpen : internalIsOpen;
|
|
30
|
+
const setIsOpen = isControlled ? controlledSetIsOpen! : setInternalIsOpen;
|
|
31
|
+
|
|
32
|
+
const floatUIContentRef = useRef() as React.MutableRefObject<HTMLDivElement>;
|
|
33
|
+
const referenceElementRef = useRef<HTMLDivElement | null>(null);
|
|
34
|
+
|
|
35
|
+
const childrenArray = React.Children.toArray(children);
|
|
36
|
+
if (childrenArray.length !== 2) {
|
|
37
|
+
throw new Error(
|
|
38
|
+
'FloatUI requires exactly two children: a trigger element and a content element.',
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const [trigger, content] = childrenArray;
|
|
43
|
+
|
|
44
|
+
if (!isValidElement(trigger) || !isValidElement(content)) {
|
|
45
|
+
throw new Error('Both children of FloatUI must be valid React elements.');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const { x, y, strategy, refs } = useFloating({
|
|
49
|
+
elements: {
|
|
50
|
+
reference: referenceElementRef.current,
|
|
51
|
+
},
|
|
52
|
+
...floatingOptions,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
useClickOutside(floatUIContentRef, () => setIsOpen(false));
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<div>
|
|
59
|
+
<div ref={referenceElementRef} onClick={() => setIsOpen(!isOpen)}>
|
|
60
|
+
{trigger}
|
|
61
|
+
</div>
|
|
62
|
+
{isOpen && (
|
|
63
|
+
<div
|
|
64
|
+
// Used to position the floating element relative to the reference element
|
|
65
|
+
ref={refs.setFloating}
|
|
66
|
+
style={{
|
|
67
|
+
position: strategy,
|
|
68
|
+
top: y ?? 0,
|
|
69
|
+
left: x ?? 0,
|
|
70
|
+
}}
|
|
71
|
+
role="dialog"
|
|
72
|
+
aria-label={ariaLabel}
|
|
73
|
+
className="floatui-container"
|
|
74
|
+
>
|
|
75
|
+
<div ref={floatUIContentRef} className="floatui-content">
|
|
76
|
+
{content}
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
)}
|
|
80
|
+
</div>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { FloatUI } from './FloatUI';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Common Variables
|
|
2
|
+
:root,
|
|
3
|
+
:root [data-theme='light'],
|
|
4
|
+
:root [data-theme='dark'] {
|
|
5
|
+
--pf-floatui-background-color: var(--pf-white-color);
|
|
6
|
+
--pf-floatui-border-color: var(--pf-gray-color-900);
|
|
7
|
+
--pf-floatui-border-radius: var(--pf-rounded);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// Dark Theme Specific Variables
|
|
11
|
+
:root [data-theme='dark'] {
|
|
12
|
+
--pf-floatui-background-color: var(--pf-primary-color-600);
|
|
13
|
+
--pf-floatui-border-color: var(--pf-gray-color);
|
|
14
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { ReactElement } from 'react';
|
|
2
|
+
import { UseFloatingOptions } from '@floating-ui/react-dom';
|
|
3
|
+
|
|
4
|
+
export type FloatUIProps = {
|
|
5
|
+
children: [ReactElement, ReactElement];
|
|
6
|
+
ariaLabel: string;
|
|
7
|
+
floatingOptions?: UseFloatingOptions;
|
|
8
|
+
isOpen?: boolean;
|
|
9
|
+
setIsOpen?: React.Dispatch<React.SetStateAction<boolean>>;
|
|
10
|
+
};
|
package/src/components/index.ts
CHANGED
|
@@ -12,5 +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 {
|
|
15
|
+
export { FloatUI } from './floatUI';
|
|
16
16
|
export { Menu } from './menu';
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import './styles/index.scss';
|
|
2
2
|
|
|
3
|
+
// This is so consumers of the DS can import floating ui functions/hooks/types from the DS rather than the (peer dependency) floating-ui package
|
|
4
|
+
export * from '@floating-ui/react-dom';
|
|
5
|
+
|
|
3
6
|
export { GlobalStyles } from './legacy/styles/globals/index';
|
|
4
7
|
|
|
5
8
|
export {
|
|
@@ -71,6 +74,6 @@ export { Select as SelectInput } from './components/forms/select';
|
|
|
71
74
|
export { Form } from './components/forms/form';
|
|
72
75
|
export { Skeleton } from './components/skeleton';
|
|
73
76
|
export { Card } from './components/card';
|
|
74
|
-
export {
|
|
77
|
+
export { FloatUI } from './components/floatUI';
|
|
75
78
|
export { Menu } from './components/menu';
|
|
76
79
|
export { Pill } from './components/pill';
|
package/src/styles/index.scss
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
@import '../components/skeleton/styles/Skeleton.scss';
|
|
18
18
|
@import '../components/card/styles/Card.scss';
|
|
19
19
|
@import '../components/menu/styles/Menu.scss';
|
|
20
|
-
@import '../components/
|
|
20
|
+
@import '../components/floatUI/styles/FloatUI.scss';
|
|
21
21
|
@import '../legacy/components/inputs/NoInputDatePicker/NoInputDatePicker.scss';
|
|
22
22
|
@import '../components/pill/styles/Pill.scss';
|
|
23
23
|
@import 'typography';
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { Placement } from '@floating-ui/react-dom';
|
|
3
|
-
export type PopperProps = {
|
|
4
|
-
children: React.ReactNode;
|
|
5
|
-
referenceElement: HTMLElement | null;
|
|
6
|
-
isOpen: boolean;
|
|
7
|
-
onClose: () => void;
|
|
8
|
-
ariaLabel: string;
|
|
9
|
-
placement?: Placement;
|
|
10
|
-
offsetValue?: number;
|
|
11
|
-
};
|
|
12
|
-
export declare function Popper({ children, referenceElement, isOpen, onClose, ariaLabel, placement, offsetValue, }: PopperProps): import("react/jsx-runtime").JSX.Element | null;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { Popper } from './Popper';
|