@indico-data/design-system 3.9.0 → 3.11.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": "3.9.0",
3
+ "version": "3.11.0",
4
4
  "description": "",
5
5
  "author": "",
6
6
  "main": "lib/index.js",
@@ -17,9 +17,16 @@ The FloatUI component is used to display content relative to another element. It
17
17
 
18
18
  FloatUI can be used in two modes:
19
19
 
20
- - **Uncontrolled Mode:** FloatUI manages its own state, toggling visibility when the trigger is clicked.
20
+ - **Uncontrolled Mode:** FloatUI manages its own state, toggling visibility when the trigger is clicked (or hovered if `hover` prop is true).
21
21
  - **Controlled Mode:** The parent component controls visibility by passing `isOpen` and `setIsOpen` props
22
22
 
23
+ ## Interaction Types
24
+
25
+ FloatUI supports two interaction types:
26
+
27
+ - **Click (default):** Opens when the trigger is clicked
28
+ - **Hover:** Opens when the trigger is hovered. Set the `hover` prop to `true` to enable hover interactions.
29
+
23
30
  ### Example: Controlled `FloatUI`
24
31
 
25
32
  ```tsx
@@ -73,6 +73,18 @@ const meta: Meta<typeof FloatUI> = {
73
73
  },
74
74
  },
75
75
  },
76
+ hover: {
77
+ control: 'boolean',
78
+ table: {
79
+ category: 'Props',
80
+ type: {
81
+ summary: 'boolean',
82
+ },
83
+ defaultValue: {
84
+ summary: 'false',
85
+ },
86
+ },
87
+ },
76
88
  },
77
89
  decorators: [
78
90
  (Story: React.ComponentType) => (
@@ -170,3 +182,35 @@ export const Controlled: Story = {
170
182
  );
171
183
  },
172
184
  };
185
+
186
+ export const Hover: Story = {
187
+ args: {
188
+ ariaLabel: 'Hover FloatUI',
189
+ hover: true,
190
+ },
191
+ render: (args) => (
192
+ <FloatUI {...args} ariaLabel="Hover FloatUI" hover>
193
+ <Button iconLeft="info" ariaLabel="Hover me" variant="action">
194
+ Hover me
195
+ </Button>
196
+ <Menu>
197
+ <Button
198
+ data-testid="hover-item-1"
199
+ ariaLabel="Item 1"
200
+ iconLeft="retrain"
201
+ onClick={() => console.log('Item 1')}
202
+ >
203
+ Item 1
204
+ </Button>
205
+ <Button
206
+ data-testid="hover-item-2"
207
+ ariaLabel="Item 2"
208
+ iconLeft="edit"
209
+ onClick={() => console.log('Item 2')}
210
+ >
211
+ Item 2
212
+ </Button>
213
+ </Menu>
214
+ </FloatUI>
215
+ ),
216
+ };
@@ -2,6 +2,7 @@ import React, { useRef, isValidElement, useState } from 'react';
2
2
  import {
3
3
  FloatingPortal,
4
4
  useClick,
5
+ useHover,
5
6
  useFloating,
6
7
  useInteractions,
7
8
  UseFloatingOptions,
@@ -27,6 +28,7 @@ export function FloatUI({
27
28
  portalOptions = {},
28
29
  floatingOptions = defaultOptions,
29
30
  className,
31
+ hover = false,
30
32
  }: FloatUIProps) {
31
33
  const [internalIsOpen, setInternalIsOpen] = useState(false);
32
34
 
@@ -60,10 +62,12 @@ export function FloatUI({
60
62
  },
61
63
  });
62
64
 
63
- const click = useClick(context);
65
+ // Can't call hooks conditionally so this enabled option is needed.
66
+ const click = useClick(context, { enabled: !hover });
67
+ const hoverHook = useHover(context, { enabled: hover });
64
68
  const dismiss = useDismiss(context);
65
69
 
66
- const { getReferenceProps, getFloatingProps } = useInteractions([click, dismiss]);
70
+ const { getReferenceProps, getFloatingProps } = useInteractions([click, hoverHook, dismiss]);
67
71
 
68
72
  const tooltipContent = (
69
73
  <div
@@ -2,6 +2,7 @@ import { render, screen, fireEvent } from '@testing-library/react';
2
2
  import { FloatUI } from '../FloatUI';
3
3
  import { Menu } from '../../menu';
4
4
  import { Button } from '../../button';
5
+ import userEvent from '@testing-library/user-event';
5
6
 
6
7
  describe('FloatUI Component', () => {
7
8
  it('does not display FloatUI content initially when rendered in uncontrolled mode', () => {
@@ -108,4 +109,35 @@ describe('FloatUI Component', () => {
108
109
  fireEvent.click(screen.getByText('Toggle'));
109
110
  expect(setIsOpen).toHaveBeenCalledWith(true, expect.any(Object), 'click');
110
111
  });
112
+
113
+ it('displays the FloatUI content when the trigger is hovered in hover mode', async () => {
114
+ const user = userEvent.setup();
115
+ render(
116
+ <FloatUI ariaLabel="Example FloatUI" hover>
117
+ <Button ariaLabel="Hover me">Hover me</Button>
118
+ <div>FloatUI Content</div>
119
+ </FloatUI>,
120
+ );
121
+
122
+ expect(screen.queryByText('FloatUI Content')).not.toBeInTheDocument();
123
+
124
+ await user.hover(screen.getByText('Hover me'));
125
+ expect(screen.getByText('FloatUI Content')).toBeInTheDocument();
126
+ });
127
+
128
+ it('hides the FloatUI content when mouse leaves in hover mode', async () => {
129
+ const user = userEvent.setup();
130
+ render(
131
+ <FloatUI ariaLabel="Example FloatUI" hover>
132
+ <Button ariaLabel="Hover me">Hover me</Button>
133
+ <div>FloatUI Content</div>
134
+ </FloatUI>,
135
+ );
136
+
137
+ await user.hover(screen.getByText('Hover me'));
138
+ expect(screen.getByText('FloatUI Content')).toBeInTheDocument();
139
+
140
+ await user.unhover(screen.getByText('Hover me'));
141
+ expect(screen.queryByText('FloatUI Content')).not.toBeInTheDocument();
142
+ });
111
143
  });
@@ -19,4 +19,6 @@ export type FloatUIProps = {
19
19
  };
20
20
  /** Function to toggle the visibility of the FloatUI (for controlled mode). */
21
21
  setIsOpen?: React.Dispatch<React.SetStateAction<boolean>>;
22
+ /** If true, opens on hover instead of click. Defaults to false. */
23
+ hover?: boolean;
22
24
  };
@@ -34,30 +34,38 @@ const OptionComponent = <OptionType extends SelectOption>({
34
34
  );
35
35
  };
36
36
 
37
- const Select = <OptionType extends SelectOption>({
38
- classNamePrefix = 'select',
39
- className,
40
- components: customComponents,
41
- label,
42
- hasHiddenLabel,
43
- name,
44
- ...props
45
- }: SelectProps<OptionType>) => {
46
- const defaultComponents = {
47
- Option: OptionComponent as React.ComponentType<OptionProps<OptionType>>,
48
- };
37
+ const Select = React.forwardRef(
38
+ <OptionType extends SelectOption>(
39
+ {
40
+ classNamePrefix = 'select',
41
+ className,
42
+ components: customComponents,
43
+ label,
44
+ hasHiddenLabel,
45
+ name,
46
+ ...props
47
+ }: SelectProps<OptionType>,
48
+ ref: React.Ref<any>,
49
+ ) => {
50
+ const defaultComponents = {
51
+ Option: OptionComponent as React.ComponentType<OptionProps<OptionType>>,
52
+ };
49
53
 
50
- const mergedComponents = { ...defaultComponents, ...customComponents };
54
+ const mergedComponents = { ...defaultComponents, ...customComponents };
51
55
 
52
- return (
53
- <ReactSelect
54
- classNamePrefix={classNamePrefix}
55
- className={classNames('select-wrapper', className)}
56
- components={mergedComponents}
57
- {...props}
58
- />
59
- );
60
- };
56
+ return (
57
+ <ReactSelect
58
+ ref={ref}
59
+ classNamePrefix={classNamePrefix}
60
+ className={classNames('select-wrapper', className)}
61
+ components={mergedComponents}
62
+ {...props}
63
+ />
64
+ );
65
+ },
66
+ ) as <OptionType extends SelectOption>(
67
+ props: SelectProps<OptionType> & { ref?: React.Ref<any> },
68
+ ) => React.ReactElement;
61
69
 
62
70
  const LabeledSelect = withLabel(Select);
63
71
 
@@ -1,6 +1,8 @@
1
+ import { ReactNode } from 'react';
2
+
1
3
  export type SelectOption = {
2
4
  label: string;
3
5
  value: string;
4
- detail?: string;
6
+ detail?: ReactNode;
5
7
  [key: string]: any; // Allow for additional properties
6
8
  };