/
Chip.tsx
223 lines (203 loc) · 6.53 KB
/
Chip.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
import type { ButtonHTMLAttributes, CSSProperties, ReactNode } from "react";
import { cloneElement, forwardRef, isValidElement } from "react";
import cn from "classnames";
import { TextIconSpacing, useIcon } from "@react-md/icon";
import { useInteractionStates } from "@react-md/states";
import { bem } from "@react-md/utils";
type ButtonAttributes = Omit<ButtonHTMLAttributes<HTMLButtonElement>, "type">;
export interface ChipProps extends ButtonAttributes {
/**
* The theme for the button.
*/
theme?: "outline" | "solid";
/**
* An optional icon to place to the left of the children. There will
* automatically be some margin placed between this icon and the children if
* defined.
*/
leftIcon?: ReactNode;
/**
* An optional icon to place to the right of the children. There will
* automatically be some margin placed between this icon and the children if
* defined.
*/
rightIcon?: ReactNode;
/**
* Boolean if the chip should gain elevation while the user is pressing the
* chip with mouse, touch, or keyboard click.
*/
raisable?: boolean;
/**
* An optional style to provide to the `<span>` that surrounds the `children`
* of the chip.
*
* This prop will do nothing if the `disableContentWrap` prop is enabled.
*/
contentStyle?: CSSProperties;
/**
* An optional className to provide to the `<span>` that surrounds the
* `children` of the chip.
*
* This prop will do nothing if the `disableContentWrap` prop is enabled.
*/
contentClassName?: string;
/**
* Boolean if the children should no longer be wrapped in a `<span>` that adds
* some default styles to ellipsis and truncate the children based on the
* chip's width.
*/
disableContentWrap?: boolean;
/**
* Boolean if the chip is selected or deselected which is `undefined` by
* default. Setting this prop to a boolean updates the chip to render a
* selected icon to the left of the content as well as adding a darker
* background when set to `true`. The icon will only appear once the state is
* `true` and will transition in and out when swapped between `true` and
* `false`.
*
* @remarks
*
* See the `disableIconTransition` and `selectedIcon` props for more details
* about the icon behavior
*/
selected?: boolean;
/**
* Boolean if the selection state should use a swatch of the primary color
* instead of rendering a check icon and the normal background color changes.
*/
selectedThemed?: boolean;
/**
* The icon to use as the `leftIcon` when the `selected` prop is a boolean.
* When this is omitted, it will inherit the `selected` icon from the main
* `Configuration` / `IconProvider`.
*
* If this is set to `null`, no icon will be rendered when the `selected` is set
* to `"selected"` or `"unselected"`.
*
* If the `leftIcon` prop is not `undefined`, the `leftIcon` prop will always
* be used instead of this prop.
*/
selectedIcon?: ReactNode;
/**
* Boolean if the selected icon should not animate when the `selected` is a
* boolean. This transition is just a simple "appear" transition with the
* `max-width` of the icon.
*/
disableIconTransition?: boolean;
/**
* Boolean if the chip should render as a non-interactable element (`<span>`)
* instead of a button. This can be used to just apply the chip styles.
*
* @remarks \@since 2.6.0
*/
noninteractable?: boolean;
}
const block = bem("rmd-chip");
/**
* A chip is a simplified and condensed button component that be used to create
* compact radio groups, checkboxes, and trigger actions. The chip only has a
* `"solid"` and `"outline"` theme but can be raisable once clicked or
* selectable with an inline icon. A chip also supports rendering icons, avatars,
* or circular progress bars to the left and right of the children.
*/
export const Chip = forwardRef<HTMLButtonElement, ChipProps>(function Chip(
{
"aria-pressed": ariaPressed,
className: propClassName,
children,
theme = "solid",
leftIcon: propLeftIcon,
rightIcon,
raisable = false,
disabled = false,
selected,
selectedThemed = false,
contentStyle,
contentClassName,
disableContentWrap = false,
selectedIcon: propSelectedIcon,
noninteractable = false,
disableIconTransition = false,
...props
},
ref
) {
const { ripples, className, handlers } = useInteractionStates({
handlers: props,
className: propClassName,
disabled: disabled || noninteractable,
enablePressedAndRipple: raisable && !noninteractable,
});
let content = children;
if (!disableContentWrap) {
content = (
<span
style={contentStyle}
className={cn(block("content"), contentClassName)}
>
{children}
</span>
);
}
let leftIcon = propLeftIcon;
const selectable = typeof selected === "boolean";
const selectedIcon = useIcon("selected", propSelectedIcon);
let isHiddenIcon = false;
if (
selectable &&
!selectedThemed &&
typeof leftIcon === "undefined" &&
selectedIcon
) {
leftIcon = selectedIcon;
if (!disableIconTransition && isValidElement(selectedIcon)) {
isHiddenIcon = !selected;
leftIcon = cloneElement(selectedIcon, {
className: block("selected-icon", { visible: selected }),
});
} else if (disableIconTransition && !selected) {
// don't want to render it when not selected if there's no transition
leftIcon = null;
}
}
const leading = leftIcon && !isHiddenIcon;
const trailing = rightIcon;
const Component = noninteractable ? "span" : "button";
const buttonProps = {
"aria-pressed": ariaPressed ?? (!!selected || undefined),
type: "button",
disabled,
} as const;
return (
<Component
{...(noninteractable ? {} : buttonProps)}
{...props}
{...handlers}
ref={ref}
className={cn(
block({
[theme]: true,
disabled,
selected: !disabled && selected && !selectedThemed,
themed: !disabled && selected && selectedThemed,
"solid-disabled": disabled && theme === "solid",
"leading-icon": leading && !trailing,
"trailing-icon": trailing && !leading,
surrounded: leading && trailing,
noninteractable,
}),
className
)}
>
<TextIconSpacing
icon={leftIcon}
beforeClassName={isHiddenIcon ? "" : undefined}
>
<TextIconSpacing icon={rightIcon} iconAfter>
{content}
</TextIconSpacing>
</TextIconSpacing>
{ripples}
</Component>
);
});