@spark-web/button 1.0.1 → 1.1.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/src/Button.tsx DELETED
@@ -1,110 +0,0 @@
1
- import { Box } from '@spark-web/box';
2
- import { buildDataAttributes } from '@spark-web/utils/internal';
3
- import * as React from 'react';
4
-
5
- import { resolveButtonChildren } from './resolveButtonChildren';
6
- import type { CommonButtonProps, NativeButtonProps } from './types';
7
- import { useButtonStyles } from './useButtonStyles';
8
-
9
- // TODO:
10
- // discuss with design:
11
- // - sizes
12
- // - variants
13
- // implementation details:
14
- // - handle fragments?
15
- // - validate children?
16
-
17
- export type ButtonProps = CommonButtonProps & {
18
- 'aria-controls'?: NativeButtonProps['aria-controls'];
19
- 'aria-describedby'?: NativeButtonProps['aria-describedby'];
20
- 'aria-expanded'?: NativeButtonProps['aria-expanded'];
21
- onClick?: NativeButtonProps['onClick'];
22
- size?: CommonButtonProps['size'];
23
- type?: 'button' | 'submit' | 'reset';
24
- disabled?: boolean;
25
- };
26
-
27
- /**
28
- * Buttons are used to initialize an action, their label should express what
29
- * action will occur when the user interacts with it.
30
- */
31
- export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
32
- (
33
- {
34
- 'aria-controls': ariaControls,
35
- 'aria-describedby': ariaDescribedBy,
36
- 'aria-expanded': ariaExpanded,
37
- data,
38
- disabled = false,
39
- id,
40
- onClick,
41
- prominence = 'high',
42
- size = 'medium',
43
- tone = 'primary',
44
- type = 'button',
45
- ...props
46
- },
47
- ref
48
- ) => {
49
- const iconOnly = Boolean(props.label);
50
- const buttonStyleProps = useButtonStyles({
51
- iconOnly,
52
- size,
53
- tone,
54
- prominence,
55
- });
56
-
57
- // TODO: add loading state for button
58
- const isDisabled = disabled; /* || loading */
59
- /**
60
- * handle "disabled" behaviour w/o disabling buttons
61
- * @see https://axesslab.com/disabled-buttons-suck/
62
- */
63
- const handleClick = getPreventableClickHandler(onClick, isDisabled);
64
-
65
- return (
66
- <Box
67
- aria-controls={ariaControls}
68
- aria-describedby={ariaDescribedBy}
69
- aria-disabled={isDisabled}
70
- aria-expanded={ariaExpanded}
71
- aria-label={props.label}
72
- as="button"
73
- id={id}
74
- onClick={handleClick}
75
- ref={ref}
76
- type={type}
77
- {...buttonStyleProps}
78
- {...(data ? buildDataAttributes(data) : undefined)}
79
- >
80
- {resolveButtonChildren({
81
- ...props,
82
- prominence,
83
- size,
84
- tone,
85
- })}
86
- </Box>
87
- );
88
- }
89
- );
90
- Button.displayName = 'Buttton';
91
-
92
- /**
93
- * Prevent click events when the component is "disabled".
94
- * Note: we don't want to actually disable a button element for several reasons.
95
- * One being because that would prohibit the use of tooltips.
96
- */
97
- export function getPreventableClickHandler(
98
- onClick: NativeButtonProps['onClick'],
99
- disabled: boolean
100
- ) {
101
- return function handleClick(
102
- event: React.MouseEvent<HTMLButtonElement, MouseEvent>
103
- ) {
104
- if (disabled) {
105
- event.preventDefault();
106
- } else {
107
- onClick?.(event);
108
- }
109
- };
110
- }
@@ -1,56 +0,0 @@
1
- import { Box } from '@spark-web/box';
2
- import type { LinkComponentProps } from '@spark-web/link';
3
- import { useLinkComponent } from '@spark-web/link';
4
- import { buildDataAttributes } from '@spark-web/utils/internal';
5
- import { forwardRefWithAs } from '@spark-web/utils/ts';
6
-
7
- import { resolveButtonChildren } from './resolveButtonChildren';
8
- import type { CommonButtonProps } from './types';
9
- import { useButtonStyles } from './useButtonStyles';
10
-
11
- export type ButtonLinkProps = LinkComponentProps & CommonButtonProps;
12
-
13
- /** The appearance of a `Button`, with the semantics of a link. */
14
- export const ButtonLink = forwardRefWithAs<'a', ButtonLinkProps>(
15
- (
16
- {
17
- data,
18
- href,
19
- id,
20
- prominence = 'high',
21
- size = 'medium',
22
- tone = 'primary',
23
- ...props
24
- },
25
- ref
26
- ) => {
27
- const LinkComponent = useLinkComponent(ref);
28
- const iconOnly = Boolean(props.label);
29
- const buttonStyleProps = useButtonStyles({
30
- iconOnly,
31
- size,
32
- tone,
33
- prominence,
34
- });
35
-
36
- return (
37
- <Box
38
- aria-label={props.label}
39
- as={LinkComponent}
40
- asElement="a"
41
- id={id}
42
- href={href}
43
- ref={ref}
44
- {...buttonStyleProps}
45
- {...(data ? buildDataAttributes(data) : undefined)}
46
- >
47
- {resolveButtonChildren({
48
- ...props,
49
- prominence,
50
- size,
51
- tone,
52
- })}
53
- </Box>
54
- );
55
- }
56
- );
package/src/index.ts DELETED
@@ -1,7 +0,0 @@
1
- export { Button } from './Button';
2
- export { ButtonLink } from './ButtonLink';
3
-
4
- // types
5
-
6
- export type { ButtonProps } from './Button';
7
- export type { ButtonLinkProps } from './ButtonLink';
@@ -1,57 +0,0 @@
1
- import { Text } from '@spark-web/text';
2
- import { Children, cloneElement, isValidElement } from 'react';
3
-
4
- import type {
5
- ButtonChildrenProps,
6
- ButtonProminence,
7
- ButtonSize,
8
- ButtonTone,
9
- } from './types';
10
- import { mapTokens, variants } from './utils';
11
-
12
- type ResolveButtonChildren = ButtonChildrenProps & {
13
- prominence: ButtonProminence;
14
- size: ButtonSize;
15
- tone: ButtonTone;
16
- };
17
-
18
- export const resolveButtonChildren = ({
19
- children,
20
- prominence,
21
- size,
22
- tone,
23
- }: ResolveButtonChildren): JSX.Element[] => {
24
- const variant = variants[prominence][tone];
25
-
26
- return Children.map(children, child => {
27
- if (typeof child === 'string') {
28
- return (
29
- <Text
30
- as="span"
31
- baseline={false}
32
- overflowStrategy="nowrap"
33
- weight="strong"
34
- size={mapTokens.fontSize[size]}
35
- tone={variant?.textTone}
36
- >
37
- {child}
38
- </Text>
39
- );
40
- }
41
-
42
- if (isValidElement(child)) {
43
- return cloneElement(child, {
44
- // Dismiss buttons need to be `xxsmall`
45
- // For everything else, we force them to be `xsmall`
46
- size: child.props.size === 'xxsmall' ? child.props.size : 'xsmall',
47
-
48
- // If the button is low prominence with a decorative tone we want to force
49
- // the tone to be the same as the button
50
- // We also don't want users to override the tone of the icon inside of the button
51
- tone: variant?.textTone,
52
- });
53
- }
54
-
55
- return null;
56
- });
57
- };
package/src/types.ts DELETED
@@ -1,42 +0,0 @@
1
- import type { BackgroundTone } from '@spark-web/a11y';
2
- import type { IconProps } from '@spark-web/icon';
3
- import type { DataAttributeMap } from '@spark-web/utils/internal';
4
- import type { ButtonHTMLAttributes, ReactElement } from 'react';
5
-
6
- import type { mapTokens } from './utils';
7
-
8
- export type ButtonSize = keyof typeof mapTokens[keyof typeof mapTokens];
9
- export type ButtonProminence = 'high' | 'low' | 'none';
10
- export type ButtonTone = BackgroundTone;
11
-
12
- type ChildrenWithText = {
13
- label?: never;
14
- children:
15
- | string
16
- // Strict tuple type to allow only 1 icon and 1 string
17
- | [ReactElement<IconProps>, string]
18
- | [string, ReactElement<IconProps>];
19
- };
20
- type IconOnly = {
21
- label: string;
22
- children: ReactElement<IconProps>;
23
- };
24
-
25
- export type ButtonChildrenProps = ChildrenWithText | IconOnly;
26
-
27
- export type NativeButtonProps = ButtonHTMLAttributes<HTMLButtonElement>;
28
-
29
- export type CommonButtonProps = {
30
- data?: DataAttributeMap;
31
- id?: string;
32
- } & ButtonChildrenProps &
33
- ButtonStyleProps;
34
-
35
- export type ButtonStyleProps = {
36
- /** Sets the visual prominence of the button. */
37
- prominence?: ButtonProminence;
38
- /** Sets the size of the button. */
39
- size?: ButtonSize;
40
- /** Sets the tone of the button. */
41
- tone?: ButtonTone;
42
- };
@@ -1,96 +0,0 @@
1
- import { css } from '@emotion/css';
2
- import { useFocusRing } from '@spark-web/a11y';
3
- import type { BoxProps } from '@spark-web/box';
4
- import { useTheme } from '@spark-web/theme';
5
-
6
- import type { ButtonProminence, ButtonSize, ButtonTone } from './types';
7
- import { mapTokens, variants } from './utils';
8
-
9
- type UseButtonStylesProps = {
10
- iconOnly: boolean;
11
- prominence: ButtonProminence;
12
- size: ButtonSize;
13
- tone: ButtonTone;
14
- };
15
-
16
- export function useButtonStyles({
17
- iconOnly,
18
- prominence,
19
- size,
20
- tone,
21
- }: UseButtonStylesProps) {
22
- const theme = useTheme();
23
- const focusRingStyles = useFocusRing({ tone });
24
- const variant = variants[prominence][tone];
25
- const isLarge = size === 'large';
26
-
27
- const transitionColours = {
28
- transitionProperty:
29
- 'color, background-color, border-color, text-decoration-color',
30
- transitionTimingFunction: 'cubic-bezier(0.02, 1.505, 0.745, 1.235)',
31
- transitionDuration: `${theme.animation.standard.duration}ms`,
32
- };
33
-
34
- const buttonStyleProps: Partial<BoxProps> = {
35
- alignItems: 'center',
36
- background: variant?.background,
37
- border: variant?.border,
38
- borderWidth: variant?.borderWidth,
39
- borderRadius: isLarge ? 'medium' : 'small',
40
- cursor: 'pointer',
41
- display: 'inline-flex',
42
- gap: 'small',
43
- height: mapTokens.size[size],
44
- justifyContent: 'center',
45
- paddingX: iconOnly ? undefined : mapTokens.spacing[size],
46
- position: 'relative',
47
- width: iconOnly ? mapTokens.size[size] : undefined,
48
- // interactions styles
49
- className: css({
50
- ...transitionColours,
51
-
52
- '&:hover': {
53
- borderColor: variant?.borderHover
54
- ? theme.border.color[variant.borderHover]
55
- : undefined,
56
- backgroundColor: variant?.backgroundHover
57
- ? theme.backgroundInteractions[variant.backgroundHover]
58
- : undefined,
59
-
60
- // Style button text when hovering
61
- '> *': {
62
- ...transitionColours,
63
- color: variant?.textToneHover
64
- ? theme.color.foreground[variant.textToneHover]
65
- : undefined,
66
- stroke: variant?.textToneHover
67
- ? theme.color.foreground[variant.textToneHover]
68
- : undefined,
69
- },
70
- },
71
- '&:active': {
72
- borderColor: variant?.borderActive
73
- ? theme.border.color[variant.borderActive]
74
- : undefined,
75
- backgroundColor: variant?.backgroundActive
76
- ? theme.backgroundInteractions[variant?.backgroundActive]
77
- : undefined,
78
- transform: 'scale(0.98)',
79
-
80
- // Style button text when it's active
81
- '> *': {
82
- ...transitionColours,
83
- color: variant?.textToneActive
84
- ? theme.color.foreground[variant.textToneActive]
85
- : undefined,
86
- stroke: variant?.textToneActive
87
- ? theme.color.foreground[variant.textToneActive]
88
- : undefined,
89
- },
90
- },
91
- ':focus': focusRingStyles,
92
- }),
93
- };
94
-
95
- return buttonStyleProps;
96
- }
package/src/utils.ts DELETED
@@ -1,170 +0,0 @@
1
- import type { BoxProps } from '@spark-web/box';
2
- import type { ForegroundTone } from '@spark-web/text';
3
- import type { BrighteTheme } from '@spark-web/theme';
4
-
5
- import type { ButtonProminence, ButtonTone } from './types';
6
-
7
- type ButtonStyles = {
8
- background: BoxProps['background'];
9
- border?: BoxProps['border'];
10
- borderWidth?: BoxProps['borderWidth'];
11
- textTone?: ForegroundTone;
12
- // Hover
13
- backgroundHover: keyof BrighteTheme['backgroundInteractions'];
14
- borderHover?: keyof BrighteTheme['border']['color'];
15
- textToneHover?: keyof BrighteTheme['color']['foreground'];
16
- // Active
17
- backgroundActive: keyof BrighteTheme['backgroundInteractions'];
18
- borderActive?: keyof BrighteTheme['border']['color'];
19
- textToneActive?: keyof BrighteTheme['color']['foreground'];
20
- };
21
-
22
- export const variants: Record<
23
- ButtonProminence,
24
- Record<ButtonTone, ButtonStyles | undefined>
25
- > = {
26
- high: {
27
- primary: {
28
- background: 'primary',
29
- backgroundHover: 'primaryHover',
30
- backgroundActive: 'primaryActive',
31
- },
32
- secondary: {
33
- background: 'secondary',
34
- backgroundHover: 'secondaryHover',
35
- backgroundActive: 'secondaryActive',
36
- },
37
- neutral: {
38
- background: 'neutral',
39
- border: 'field',
40
- backgroundHover: 'neutralHover',
41
- backgroundActive: 'neutralActive',
42
- },
43
- positive: {
44
- background: 'positive',
45
- backgroundHover: 'positiveHover',
46
- backgroundActive: 'positiveActive',
47
- },
48
- critical: {
49
- background: 'critical',
50
- backgroundHover: 'criticalHover',
51
- backgroundActive: 'criticalActive',
52
- },
53
- caution: undefined,
54
- info: undefined,
55
- },
56
- low: {
57
- primary: {
58
- background: 'surface',
59
- border: 'primary',
60
- borderWidth: 'large',
61
- textTone: 'primary',
62
-
63
- backgroundHover: 'none',
64
- borderHover: 'primaryHover',
65
- textToneHover: 'primaryHover',
66
-
67
- backgroundActive: 'none',
68
- borderActive: 'primaryActive',
69
- textToneActive: 'primaryActive',
70
- },
71
- secondary: {
72
- background: 'surface',
73
- border: 'secondary',
74
- borderWidth: 'large',
75
- textTone: 'secondary',
76
-
77
- backgroundHover: 'none',
78
- borderHover: 'secondaryHover',
79
- textToneHover: 'secondaryHover',
80
-
81
- backgroundActive: 'none',
82
- borderActive: 'secondaryActive',
83
- textToneActive: 'secondaryActive',
84
- },
85
- neutral: {
86
- background: 'neutralLow',
87
- backgroundHover: 'neutralLowHover',
88
- backgroundActive: 'neutralLowActive',
89
- },
90
- positive: {
91
- background: 'positiveLow',
92
- backgroundHover: 'positiveLowHover',
93
- backgroundActive: 'positiveLowActive',
94
- },
95
- caution: {
96
- background: 'cautionLow',
97
- backgroundHover: 'cautionLowHover',
98
- backgroundActive: 'cautionLowActive',
99
- },
100
- critical: {
101
- background: 'criticalLow',
102
- backgroundHover: 'criticalLowHover',
103
- backgroundActive: 'criticalLowActive',
104
- },
105
- info: {
106
- background: 'infoLow',
107
- backgroundHover: 'infoLowHover',
108
- backgroundActive: 'infoLowActive',
109
- },
110
- },
111
- none: {
112
- primary: {
113
- background: 'surface',
114
- textTone: 'primaryActive',
115
- backgroundHover: 'primaryLowHover',
116
- backgroundActive: 'primaryLowActive',
117
- },
118
- secondary: {
119
- background: 'surface',
120
- textTone: 'secondaryActive',
121
- backgroundHover: 'secondaryLowHover',
122
- backgroundActive: 'secondaryLowActive',
123
- },
124
- neutral: {
125
- background: 'surface',
126
- textTone: 'neutral',
127
- backgroundHover: 'neutralLowHover',
128
- backgroundActive: 'neutralLowActive',
129
- },
130
- positive: {
131
- background: 'surface',
132
- textTone: 'positive',
133
- backgroundHover: 'positiveLowHover',
134
- backgroundActive: 'positiveLowActive',
135
- },
136
- caution: {
137
- background: 'surface',
138
- textTone: 'caution',
139
- backgroundHover: 'cautionLowHover',
140
- backgroundActive: 'cautionLowActive',
141
- },
142
- critical: {
143
- background: 'surface',
144
- textTone: 'critical',
145
- backgroundHover: 'criticalLowHover',
146
- backgroundActive: 'criticalLowActive',
147
- },
148
- info: {
149
- background: 'surface',
150
- textTone: 'info',
151
- backgroundHover: 'infoLowHover',
152
- backgroundActive: 'infoLowActive',
153
- },
154
- },
155
- } as const;
156
-
157
- export const mapTokens = {
158
- fontSize: {
159
- medium: 'small',
160
- large: 'standard',
161
- },
162
- size: {
163
- medium: 'medium',
164
- large: 'large',
165
- },
166
- spacing: {
167
- medium: 'medium',
168
- large: 'xlarge',
169
- },
170
- } as const;