@hyphen/hyphen-components 2.25.2 → 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';
@@ -9,12 +9,14 @@ import * as Stories from './Drawer.stories';
9
9
 
10
10
  A Drawer is a panel that slides in from one edge of the viewport and overlays content on top of the page. It contains information or actions without leaving the context of the original page.
11
11
 
12
- <Canvas withSource="open" of={Stories.BasicUsage} />
12
+ <Canvas withSource="open" of={Stories.UncontrolledWithProvider} />
13
13
 
14
14
  ## Usage Guidelines
15
15
 
16
16
  - Use the drawer as way to achieve progressive disclosure, to reveal relevant information at the appropriate time.
17
- - The Drawer visibility is controlled via the `isOpen` prop, and is hidden by default. To handle closing the Drawer, provide an `onDismiss` callback that will be called when the user clicks the Overlay or Esc keyboard key.
17
+ - The Drawer can be both uncontrolled and controlled. The uncontrolled Drawer is managed by the component itself, while the controlled Drawer is managed by the consuming component.
18
+ - For an uncontrolled Drawer, use the `DrawerProvider` to manage the Drawer's visibility. You can set a default `defaultIsOpen` value to determine if the Drawer is open by default.
19
+ - For a controlled Drawer, omit the `DrawerProvider` as visibility is controlled via the `isOpen` prop. To handle closing the Drawer, provide an `onDismiss` callback that will be called when the user clicks the Overlay or Esc keyboard key.
18
20
  - When a Drawer is open, the main body is scroll-locked by default.
19
21
  - Avoid nesting Drawers to prevent usability issues.
20
22
  - The button that triggers the drawer opening should be in close proximity to the Drawer itself.
@@ -36,10 +38,11 @@ Drawers are good for short pieces of content that are related to the main screen
36
38
 
37
39
  ## Accessibility
38
40
 
39
- - Use the `ariaLabel` or `ariaLabelledBy` props to properly label a drawer to provide context for users with assistive technology such as screen readers. If a drawer is announced to the user without a label, it can be confusing and difficult to navigate.
40
- - When the Drawer is opened, focus is trapped inside the Drawer.
41
+ - Ensure the `ariaLabel` or `ariaLabelledBy` props are provided for accessibility.
42
+ - The drawer traps focus within its content when open, ensuring keyboard navigation is contained.
41
43
  - The 'Esc' key will close the Drawer.
42
44
  - After the drawer closes, focus is returned to the element that triggered it.
45
+ - The `DrawerCloseButton` component provides a close button with appropriate aria attributes.
43
46
 
44
47
  ## Props
45
48
 
@@ -51,20 +54,6 @@ The Drawer can appear from the right (default), left, top, or bottom of the scre
51
54
 
52
55
  <Canvas withSource="open" of={Stories.Placement} />
53
56
 
54
- ## Drawer Header
55
-
56
- A header will be added to the drawer content if `title` is defined, or `closeButton` is `true`. If the content of the drawer is taller than the drawer height, then the content will scroll while the header remains fixed to the top.
57
-
58
- <Canvas withSource="open" of={Stories.DrawerHeader} />
59
-
60
- ## Title and Close Button
61
-
62
- <Canvas withSource="open" of={Stories.TitleAndCloseButton} />
63
-
64
- ## Close Button Only
65
-
66
- <Canvas withSource="open" of={Stories.CloseButtonOnly} />
67
-
68
57
  ## Width
69
58
 
70
59
  Set the width of a Drawer to specific value for a `left` or `right` placement via `width`. The Drawer height will be 100% of the viewport, or if `containerRef` is used, 100% of the container.
@@ -73,11 +62,6 @@ When `placement` is set to `top` or `bottom`, the `width` prop is ignored and th
73
62
 
74
63
  <Canvas withSource="open" of={Stories.Width} />
75
64
 
76
- ## Height
77
-
78
- The height of Drawers with a `top` or `bottom` placement is determined by Drawer's contents. The width will be set to 100%.
79
-
80
- <Canvas withSource="open" of={Stories.Height} />
81
65
 
82
66
  ## Hidden Overlay
83
67
 
@@ -92,6 +76,7 @@ In cases where content in the drawer is supplemental to content on the main area
92
76
 
93
77
  <Canvas withSource="open" of={Stories.HiddenOverlay} />
94
78
 
79
+
95
80
  ## Initial Focus Ref
96
81
 
97
82
  By default the first focusable element will receive focus when the drawer opens, but you can provide a ref to focus instead.
@@ -114,4 +99,4 @@ Render the Drawer within a containing div using `containerRef`.
114
99
  variant="info"
115
100
  />
116
101
 
117
- <Canvas withSource="open" of={Stories.ContainedDrawer} />
102
+ <Canvas withSource="open" of={Stories.ContainedDrawer} />
@@ -45,10 +45,11 @@
45
45
  position: absolute;
46
46
  box-shadow: var(--drawer-box-shadow, var(--size-box-shadow-xl));
47
47
  z-index: var(--size-z-index-drawer);
48
+ overflow: auto;
48
49
 
49
50
  &.left {
50
51
  left: 0;
51
- width: var(--w, 80vw);
52
+ width: var(--drawer-width, 80vw);
52
53
  height: 100%;
53
54
 
54
55
  :global {
@@ -58,7 +59,7 @@
58
59
 
59
60
  &.right {
60
61
  right: 0;
61
- width: var(--w, 80vw);
62
+ width: var(--drawer-width, 80vw);
62
63
  height: 100%;
63
64
 
64
65
  :global {
@@ -89,7 +90,7 @@
89
90
  @media (min-width: $size-breakpoint-tablet) {
90
91
  &.right,
91
92
  &.left {
92
- width: var(--w, var(--size-dimension-8xl));
93
+ width: var(--drawer-width, var(--size-dimension-8xl));
93
94
  }
94
95
  }
95
96
  }