@hyphen/hyphen-components 3.0.0 → 4.0.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.
@@ -1,21 +1,11 @@
1
1
  import { BoxShadowSize, IconName, ResponsiveProp } from '../../types';
2
- import React, {
3
- AnchorHTMLAttributes,
4
- ButtonHTMLAttributes,
5
- FocusEvent,
6
- MouseEvent,
7
- ReactNode,
8
- createElement,
9
- forwardRef,
10
- } from 'react';
2
+ import { Slot, Slottable } from '@radix-ui/react-slot';
3
+ import React, { ButtonHTMLAttributes, forwardRef } from 'react';
11
4
 
12
- import { Box } from '../Box/Box';
13
5
  import { Icon } from '../Icon/Icon';
14
6
  import { Spinner } from '../Spinner/Spinner';
15
7
  import classNames from 'classnames';
16
8
  import { generateResponsiveClasses } from '../../lib/generateResponsiveClasses';
17
- import { getElementType } from '../../lib/getElementType';
18
- import { handleReactRouterClick } from '../../lib/reactRouterClickHandler';
19
9
  import styles from './Button.module.scss';
20
10
 
21
11
  export type ButtonVariant = 'primary' | 'secondary' | 'tertiary' | 'danger';
@@ -24,9 +14,9 @@ export type ButtonSize = 'sm' | 'md' | 'lg';
24
14
 
25
15
  export interface BaseButtonProps {
26
16
  /**
27
- * Contents of the button.
17
+ * The button element to render as. Useful for when you want to render a Link that looks like a button.
28
18
  */
29
- children?: ReactNode;
19
+ asChild?: boolean;
30
20
  /**
31
21
  * Additional ClassNames to add to button.
32
22
  */
@@ -36,116 +26,54 @@ export interface BaseButtonProps {
36
26
  */
37
27
  fullWidth?: boolean;
38
28
  /**
39
- * Name of the icon to include before the button text
29
+ * Icon displayed before the button label
40
30
  */
41
31
  iconPrefix?: IconName;
42
32
  /**
43
- * Name of the icon to include after the button text
33
+ * Icon displayed after the button label
44
34
  */
45
35
  iconSuffix?: IconName;
46
36
  /**
47
- * A unique identifier for the button.
48
- */
49
- id?: string;
50
- /**
51
- * URL to navigate to when clicked. Passing this attribute automatically
52
- * renders an anchor <a> tag, NOT a <button> element.
53
- */
54
- href?: string;
55
- /**
56
- * Disables the button, making it inoperable.
37
+ * Disables the button
57
38
  */
58
39
  isDisabled?: boolean;
59
40
  /**
60
- * Replaces the button text with a loading indicator and disables the button.
41
+ * Displays a loading spinner and disables the button
61
42
  */
62
43
  isLoading?: boolean;
63
44
  /**
64
- * Prop reserved for when component is wrapped by `<Link>` from react-router.
65
- */
66
- navigate?: () => void;
67
- /**
68
- * Callback when Button is pressed.
69
- */
70
- onClick?: (event: MouseEvent<HTMLButtonElement | HTMLAnchorElement>) => void;
71
- /**
72
- * Callback when focus leaves Button.
73
- */
74
- onBlur?: (event: FocusEvent<HTMLButtonElement | HTMLAnchorElement>) => void;
75
- /**
76
- * Callback when Button receives focus.
77
- */
78
- onFocus?: (event: FocusEvent<HTMLButtonElement | HTMLAnchorElement>) => void;
79
- /**
80
- * The size of the drop shadow applied to the Box
45
+ * Size of the drop shadow applied to the Box
81
46
  */
82
47
  shadow?: BoxShadowSize | ResponsiveProp<BoxShadowSize>;
83
48
  /**
84
- * Specify the tabIndex of the button.
85
- */
86
- tabIndex?: number;
87
- /**
88
- * Useful when using button as an anchor tag.
89
- */
90
- target?: AnchorHTMLAttributes<HTMLAnchorElement>['target'];
91
- /**
92
- * The size of the button.
49
+ * Size of the button.
93
50
  */
94
51
  size?: ButtonSize | ResponsiveProp<ButtonSize>;
95
52
  /**
96
- * The color variant of the button
53
+ * Visual variant of the button
97
54
  */
98
55
  variant?: ButtonVariant;
99
- /**
100
- * ref - currently cannot be typed due to limitations of using the `as` prop
101
- */
102
56
  }
103
57
 
104
- export type AnchorButtonProps = { as: 'a' } & BaseButtonProps &
105
- Omit<
106
- React.DetailedHTMLProps<
107
- AnchorHTMLAttributes<HTMLAnchorElement>,
108
- HTMLAnchorElement
109
- >,
110
- 'ref'
111
- >;
112
-
113
- export type NormalButtonProps = { as?: 'button' } & BaseButtonProps &
114
- Omit<
115
- React.DetailedHTMLProps<
116
- ButtonHTMLAttributes<HTMLButtonElement>,
117
- HTMLButtonElement
118
- >,
119
- 'ref'
120
- >;
58
+ export type ButtonMergedProps = BaseButtonProps &
59
+ ButtonHTMLAttributes<HTMLButtonElement>;
121
60
 
122
- export type ButtonProps = NormalButtonProps | AnchorButtonProps;
123
-
124
- export const Button = forwardRef<
125
- HTMLAnchorElement | HTMLButtonElement,
126
- ButtonProps
127
- >(
61
+ export const Button = forwardRef<HTMLButtonElement, ButtonMergedProps>(
128
62
  (
129
63
  {
130
- children = undefined,
131
- as = 'button',
132
- className = '',
133
- fullWidth = false,
134
- id = undefined,
135
- href = undefined,
136
- iconPrefix = undefined,
137
- iconSuffix = undefined,
138
- isDisabled = false,
139
- isLoading = false,
140
- navigate = undefined,
141
- onClick = undefined,
142
- onFocus = undefined,
143
- onBlur = undefined,
144
- shadow = undefined,
64
+ asChild,
65
+ children,
66
+ className,
67
+ fullWidth,
68
+ iconPrefix,
69
+ iconSuffix,
70
+ isDisabled,
71
+ isLoading,
72
+ onClick,
73
+ onBlur,
74
+ onFocus,
75
+ shadow,
145
76
  size = 'md',
146
- tabIndex = undefined,
147
- target = undefined,
148
- type = undefined,
149
77
  variant = 'primary',
150
78
  ...restProps
151
79
  },
@@ -153,9 +81,9 @@ export const Button = forwardRef<
153
81
  ) => {
154
82
  const disabled = isLoading || isDisabled;
155
83
 
156
- const responsiveClasses = generateResponsiveClasses('size', size).map(
157
- (c) => styles[c]
158
- );
84
+ const responsiveClasses = generateResponsiveClasses('size', size)
85
+ .map((c) => styles[c])
86
+ .filter(Boolean);
159
87
 
160
88
  const buttonClasses = classNames(
161
89
  'hyphen-components__variables__form-control',
@@ -170,83 +98,57 @@ export const Button = forwardRef<
170
98
  }
171
99
  );
172
100
 
173
- const handleClick = handleReactRouterClick;
174
-
175
- const handleFocus = (
176
- event: FocusEvent<HTMLButtonElement | HTMLAnchorElement>
177
- ) => {
178
- if (onFocus) onFocus(event);
179
- };
180
-
181
- const handleBlur = (
182
- event: FocusEvent<HTMLButtonElement | HTMLAnchorElement>
183
- ) => {
184
- if (onBlur) onBlur(event);
185
- };
186
-
187
- const buttonContent =
188
- iconPrefix || iconSuffix ? (
189
- <Box display="inline-flex" direction="row" alignItems="center" gap="md">
190
- {isLoading && <Spinner className={styles['spinner-wrapper']} />}
191
- {iconPrefix && (
192
- <Icon
193
- className={styles.label}
194
- name={iconPrefix}
195
- aria-hidden="true"
196
- focusable="false"
197
- data-testid="prefixIcon"
198
- size={size === 'md' ? 'sm' : size}
199
- />
200
- )}
201
- {children && <span className={styles.label}>{children}</span>}
202
- {iconSuffix && (
203
- <Icon
204
- className={styles.label}
205
- name={iconSuffix}
206
- aria-hidden="true"
207
- focusable="false"
208
- data-testid="suffixIcon"
209
- size={size === 'md' ? 'sm' : size}
210
- />
211
- )}
212
- </Box>
213
- ) : (
214
- <>
215
- {isLoading && <Spinner className={styles['spinner-wrapper']} />}
216
- {(() => {
217
- if (children) {
218
- return <span className={styles.label}>{children}</span>;
219
- }
220
- return null;
221
- })()}
222
- </>
223
- );
224
-
225
- const buttonElement = getElementType(Button, { as });
226
-
227
- return createElement(
228
- buttonElement,
229
- {
230
- ['aria-disabled']: disabled,
231
- id,
232
- href,
233
- className: buttonClasses,
234
- disabled,
235
- target: as === 'a' && href ? target : null,
236
- rel:
237
- as === 'a' && href && target === '_blank'
238
- ? 'noopener noreferrer'
239
- : null,
240
- onBlur: handleBlur,
241
- onClick: (event: MouseEvent<HTMLAnchorElement | HTMLButtonElement>) =>
242
- handleClick(event, onClick, target, navigate),
243
- onFocus: handleFocus,
244
- ref,
245
- type: type || (as !== 'a' && !href ? 'button' : undefined),
246
- tabIndex,
247
- ...restProps,
248
- },
249
- buttonContent
101
+ const handleClick = !disabled ? onClick : undefined;
102
+ const handleBlur = !disabled ? onBlur : undefined;
103
+ const handleFocus = !disabled ? onFocus : undefined;
104
+
105
+ const Comp = asChild ? Slot : 'button';
106
+
107
+ return (
108
+ <Comp
109
+ {...(disabled && { 'aria-disabled': true })}
110
+ disabled={disabled}
111
+ className={buttonClasses}
112
+ onClick={handleClick}
113
+ onBlur={handleBlur}
114
+ onFocus={handleFocus}
115
+ ref={ref}
116
+ {...restProps}
117
+ >
118
+ {isLoading && <Spinner className={styles['spinner-wrapper']} />}
119
+ {iconPrefix && (
120
+ <Icon
121
+ className={styles.label}
122
+ name={iconPrefix}
123
+ aria-hidden="true"
124
+ focusable="false"
125
+ data-testid="prefixIcon"
126
+ size={size === 'md' ? 'sm' : size}
127
+ />
128
+ )}
129
+ {children && (
130
+ <Slottable>
131
+ {asChild ? (
132
+ children
133
+ ) : (
134
+ <span className={styles.label}>{children}</span>
135
+ )}
136
+ </Slottable>
137
+ )}
138
+
139
+ {iconSuffix && (
140
+ <Icon
141
+ className={styles.label}
142
+ name={iconSuffix}
143
+ aria-hidden="true"
144
+ focusable="false"
145
+ data-testid="suffixIcon"
146
+ size={size === 'md' ? 'sm' : size}
147
+ />
148
+ )}
149
+ </Comp>
250
150
  );
251
151
  }
252
152
  );
153
+
154
+ Button.displayName = 'Button';
@@ -16,7 +16,7 @@ import classNames from 'classnames';
16
16
  import { DimensionSize, CssDimensionValue } from '../../types';
17
17
  import { Box, BoxProps } from '../Box/Box';
18
18
  import styles from './Drawer.module.scss';
19
- import { Button } from '../Button/Button';
19
+ import { Button, ButtonMergedProps } from '../Button/Button';
20
20
 
21
21
  interface DrawerContextProps {
22
22
  open: boolean;
@@ -354,18 +354,14 @@ const DrawerTitle = React.forwardRef<HTMLDivElement, BoxProps>(
354
354
  }
355
355
  );
356
356
 
357
- const DrawerCloseButton = React.forwardRef<
358
- React.ElementRef<typeof Button>,
359
- React.ComponentProps<typeof Button> & {
360
- onClose?: () => void; // Fallback to onClose if provided
361
- }
362
- >(({ className, onClick, onClose, ...props }, ref) => {
357
+ const DrawerCloseButton = forwardRef<
358
+ HTMLButtonElement,
359
+ ButtonMergedProps & { onClose?: () => void }
360
+ >(({ className, onClick, onClose, ...rest }, ref) => {
363
361
  const context = useContext(DrawerContext);
364
362
  const isStandalone = !context;
365
363
 
366
- const handleClick = (
367
- event: React.MouseEvent<HTMLButtonElement | HTMLAnchorElement>
368
- ) => {
364
+ const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
369
365
  onClick?.(event);
370
366
 
371
367
  if (isStandalone) {
@@ -379,7 +375,6 @@ const DrawerCloseButton = React.forwardRef<
379
375
 
380
376
  return (
381
377
  <Button
382
- ref={ref}
383
378
  variant="tertiary"
384
379
  aria-label="close"
385
380
  type="button"
@@ -388,7 +383,8 @@ const DrawerCloseButton = React.forwardRef<
388
383
  className={classNames('m-left-auto', className)}
389
384
  size="sm"
390
385
  onClick={handleClick}
391
- {...props}
386
+ ref={ref}
387
+ {...rest}
392
388
  />
393
389
  );
394
390
  });
@@ -220,7 +220,7 @@ export const SidebarExample = () => {
220
220
  <DropdownMenuSeparator />
221
221
  <DropdownMenuItem>
222
222
  <a
223
- href="#"
223
+ href="https://ux.hyphen.ai"
224
224
  className="display-flex flex-direction-row g-sm align-items-center"
225
225
  >
226
226
  <Icon name="add" color="tertiary" />
@@ -313,14 +313,14 @@ export const SidebarExample = () => {
313
313
  </DropdownMenuTrigger>
314
314
  <DropdownMenuContent side="bottom" align="end">
315
315
  <DropdownMenuItem>
316
- <a href="#">View</a>
316
+ <a href="https://ux.hyphen.ai">View</a>
317
317
  </DropdownMenuItem>
318
318
  <DropdownMenuItem>
319
- <a href="#">Share</a>
319
+ <a href="https://ux.hyphen.ai">Share</a>
320
320
  </DropdownMenuItem>
321
321
  <DropdownMenuSeparator />
322
322
  <DropdownMenuItem>
323
- <a href="#">Remove</a>
323
+ <a href="https://ux.hyphen.ai">Remove</a>
324
324
  </DropdownMenuItem>
325
325
  </DropdownMenuContent>
326
326
  </DropdownMenu>
package/src/lib/tokens.ts CHANGED
@@ -44,14 +44,17 @@ export const BREAKPOINT_OPTIONS = Object.keys(
44
44
  export const BREAKPOINTS = [
45
45
  ...Object.entries(designTokens.size.breakpoint),
46
46
  ['base', { value: '0' }],
47
- ].map(([name, data]) => {
48
- if (typeof data === 'object' && data !== null && 'value' in data) {
49
- return {
50
- name,
51
- minWidth: parseInt(data['value'] as string, 10),
52
- };
53
- }
54
- }) as Breakpoint[];
47
+ ]
48
+ .map(([name, data]) => {
49
+ if (typeof data === 'object' && data !== null && 'value' in data) {
50
+ return {
51
+ name,
52
+ minWidth: parseInt(data['value'] as string, 10),
53
+ };
54
+ }
55
+ return undefined;
56
+ })
57
+ .filter((breakpoint): breakpoint is Breakpoint => breakpoint !== undefined);
55
58
 
56
59
  export const BASE_COLOR_OPTIONS = (
57
60
  Object.keys(designTokens.color.base) as ColorName[]