@indico-data/design-system 3.8.0 → 3.10.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.8.0",
3
+ "version": "3.10.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
  };
@@ -62,6 +62,13 @@ const meta: Meta = {
62
62
  category: 'callbacks',
63
63
  },
64
64
  },
65
+ faPrefix: {
66
+ control: 'text',
67
+ defaultValue: 'fas',
68
+ table: {
69
+ type: { summary: 'string' },
70
+ },
71
+ },
65
72
  },
66
73
  };
67
74
  export default meta;
@@ -3,7 +3,14 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
3
3
  import { IconName as FAIconName, findIconDefinition } from '@fortawesome/fontawesome-svg-core';
4
4
  import { IconProps } from './types';
5
5
 
6
- export const Icon = ({ name, size = 'md', className, ariaLabel, ...props }: IconProps) => {
6
+ export const Icon = ({
7
+ name,
8
+ size = 'md',
9
+ className,
10
+ ariaLabel,
11
+ faPrefix = 'fas',
12
+ ...props
13
+ }: IconProps) => {
7
14
  const label = ariaLabel || `${name} Icon`;
8
15
 
9
16
  const iconClasses = classNames(
@@ -20,7 +27,7 @@ export const Icon = ({ name, size = 'md', className, ariaLabel, ...props }: Icon
20
27
  // Otherwise, search for an indicon (registered under Font Awesome with a prefix of 'fak')
21
28
  const icon = faIconName
22
29
  ? findIconDefinition({
23
- prefix: 'fas',
30
+ prefix: faPrefix,
24
31
  iconName: faIconName as FAIconName,
25
32
  })
26
33
  : findIconDefinition({
@@ -1,6 +1,6 @@
1
1
  import { MouseEventHandler, CSSProperties } from 'react';
2
2
  import { PermafrostComponent } from '../../types';
3
- import { IconName as FAIconName } from '@fortawesome/fontawesome-svg-core';
3
+ import { IconName as FAIconName, IconPrefix } from '@fortawesome/fontawesome-svg-core';
4
4
  import { indicons } from './indicons';
5
5
 
6
6
  export type IconSizes = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl';
@@ -20,4 +20,8 @@ export type IconProps = PermafrostComponent & {
20
20
  style?: CSSProperties;
21
21
  /** Click event handler */
22
22
  onClick?: MouseEventHandler<SVGElement>;
23
+ /** The Font Awesome prefix of the icon (defaults to 'fas', the solid icon prefix)
24
+ * Only used for Font Awesome icons, not for Indicons.
25
+ */
26
+ faPrefix?: IconPrefix;
23
27
  };