Skip to content

Commit bf49d8d

Browse files
authored
Merge pull request #486 from mashmatrix/support-slds-2-icon-button
Update `Icon` and `Button` for SLDS2
2 parents ca09ade + 893d631 commit bf49d8d

File tree

7 files changed

+127
-146
lines changed

7 files changed

+127
-146
lines changed

src/scripts/Button.tsx

Lines changed: 25 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
import React, { FC, ReactNode, ButtonHTMLAttributes, Ref, useRef } from 'react';
22
import classnames from 'classnames';
3-
import { Icon } from './Icon';
3+
import { SvgIcon, IconCategory } from './Icon';
44
import { Spinner } from './Spinner';
55
import { useEventCallback, useMergeRefs } from './hooks';
66

77
export type ButtonType =
88
| 'neutral'
99
| 'brand'
10+
| 'outline-brand'
1011
| 'destructive'
12+
| 'text-destructive'
13+
| 'success'
1114
| 'inverse'
1215
| 'icon'
1316
| 'icon-bare'
@@ -31,6 +34,7 @@ export type ButtonIconMoreSize = 'x-small' | 'small' | 'medium' | 'large';
3134
*/
3235
export type ButtonIconProps = {
3336
className?: string;
37+
category?: IconCategory;
3438
icon: string;
3539
align?: ButtonIconAlign;
3640
size?: ButtonIconSize;
@@ -43,9 +47,9 @@ export type ButtonIconProps = {
4347
*/
4448
export const ButtonIcon: FC<ButtonIconProps> = ({
4549
icon,
50+
category = 'utility',
4651
align,
4752
size,
48-
inverse,
4953
className,
5054
style,
5155
...props
@@ -56,19 +60,22 @@ export const ButtonIcon: FC<ButtonIconProps> = ({
5660
: null;
5761
const sizeClassName =
5862
size && ICON_SIZES.indexOf(size) >= 0 ? `slds-button__icon_${size}` : null;
59-
const inverseClassName = inverse ? 'slds-button__icon_inverse' : null;
6063
const iconClassNames = classnames(
6164
'slds-button__icon',
6265
alignClassName,
6366
sizeClassName,
64-
inverseClassName,
6567
className
6668
);
69+
70+
if (icon.indexOf(':') > 0) {
71+
[category, icon] = icon.split(':') as [IconCategory, string];
72+
}
73+
6774
return (
68-
<Icon
75+
<SvgIcon
6976
className={iconClassNames}
7077
icon={icon}
71-
textColor={null}
78+
category={category}
7279
pointerEvents='none'
7380
style={style}
7481
{...props}
@@ -118,6 +125,7 @@ export const Button: FC<ButtonProps> = (props) => {
118125
buttonRef: buttonRef_,
119126
iconMoreSize: iconMoreSize_,
120127
onClick: onClick_,
128+
tabIndex,
121129
...rprops
122130
} = props;
123131

@@ -135,19 +143,24 @@ export const Button: FC<ButtonProps> = (props) => {
135143
onClick_?.(e);
136144
});
137145

146+
const content = children || label;
147+
const isIconOnly = type && /^icon-/.test(type) && icon && !content;
148+
138149
const typeClassName = type ? `slds-button_${type}` : null;
139150
const btnClassNames = classnames(className, 'slds-button', typeClassName, {
140151
'slds-is-selected': selected,
152+
['slds-button_icon']: /^icon-/.test(type ?? ''),
141153
[`slds-button_icon-${size ?? ''}`]:
142154
/^(x-small|small)$/.test(size ?? '') && /^icon-/.test(type ?? ''),
143155
});
144156

145-
const buttonContent = (
146-
// eslint-disable-next-line react/button-has-type
157+
return (
147158
<button
148159
ref={buttonRef}
149160
className={btnClassNames}
150161
type={htmlType}
162+
title={isIconOnly || alt ? alt ?? icon : undefined}
163+
tabIndex={tabIndex ?? -1}
151164
{...rprops}
152165
onClick={onClick}
153166
>
@@ -159,7 +172,7 @@ export const Button: FC<ButtonProps> = (props) => {
159172
inverse={inverse}
160173
/>
161174
) : undefined}
162-
{children || label}
175+
{content}
163176
{icon && iconAlign === 'right' ? (
164177
<ButtonIcon
165178
icon={icon}
@@ -171,22 +184,10 @@ export const Button: FC<ButtonProps> = (props) => {
171184
{iconMore ? (
172185
<ButtonIcon icon={iconMore} align='right' size={iconMoreSize} />
173186
) : undefined}
174-
{alt ? <span className='slds-assistive-text'>{alt}</span> : undefined}
187+
{isIconOnly || alt ? (
188+
<span className='slds-assistive-text'>{alt ?? icon}</span>
189+
) : undefined}
175190
{loading ? <Spinner /> : undefined}
176191
</button>
177192
);
178-
179-
if (props.tabIndex != null) {
180-
return (
181-
<span
182-
className='react-slds-button-focus-wrapper'
183-
style={{ outline: 0 }}
184-
tabIndex={-1}
185-
>
186-
{buttonContent}
187-
</span>
188-
);
189-
}
190-
191-
return buttonContent;
192193
};

src/scripts/Icon.tsx

Lines changed: 74 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,13 @@ import React, {
44
SVGAttributes,
55
useContext,
66
useRef,
7-
useState,
87
useEffect,
98
useCallback,
109
} from 'react';
1110
import classnames from 'classnames';
1211
import svg4everybody from 'svg4everybody';
1312
import { registerStyle, getAssetRoot } from './util';
1413
import { ComponentSettingsContext } from './ComponentSettings';
15-
import { useEventCallback } from './hooks';
1614
import { createFC } from './common';
1715

1816
svg4everybody();
@@ -124,23 +122,19 @@ function useInitComponentStyle() {
124122

125123
function getIconColor(
126124
fillColor: string | undefined,
127-
category: string | undefined,
125+
category: IconCategory,
128126
icon: string
129127
) {
130128
/* eslint-disable no-unneeded-ternary */
131-
return category === 'doctype'
132-
? null
133-
: fillColor === 'none'
129+
return fillColor === 'none'
134130
? null
135131
: fillColor
136132
? fillColor
137133
: category === 'utility'
138134
? null
139-
: category === 'custom'
140-
? icon.replace(/^custom/, 'custom-')
141-
: category === 'action' && /^new_custom/.test(icon)
135+
: category === 'action' && /^new_custom/.test(icon) // not needed for the current SLDS2 icons
142136
? icon.replace(/^new_custom/, 'custom-')
143-
: `${category ?? ''}-${(icon ?? '').replace(/_/g, '-')}`;
137+
: `${category}-${icon.replace(/_/g, '-')}`;
144138
/* eslint-enable no-unneeded-ternary */
145139
}
146140

@@ -154,8 +148,15 @@ export type IconCategory =
154148
| 'standard'
155149
| 'utility';
156150
export type IconSize = 'xx-small' | 'x-small' | 'small' | 'medium' | 'large';
157-
export type IconContainer = boolean | 'default' | 'circle';
158-
export type IconTextColor = 'default' | 'warning' | 'error' | null;
151+
export type IconContainer = 'circle';
152+
export type IconTextColor =
153+
| 'default'
154+
| 'currentColor'
155+
| 'success'
156+
| 'warning'
157+
| 'error'
158+
| 'light'
159+
| null;
159160

160161
/**
161162
*
@@ -167,55 +168,56 @@ export type IconProps = {
167168
size?: IconSize;
168169
align?: 'left' | 'right';
169170
container?: IconContainer;
170-
color?: string;
171171
textColor?: IconTextColor;
172-
tabIndex?: number;
173172
fillColor?: string;
173+
title?: string;
174+
flip?: boolean;
174175
} & SVGAttributes<SVGElement>;
175176

176177
/**
177178
*
178179
*/
179180
type SvgIconProps = IconProps & {
180-
iconColor: string | null;
181+
iconColor?: string | null;
181182
};
182183

183184
/**
184185
*
185186
*/
186-
const SvgIcon = forwardRef(
187+
export const SvgIcon = forwardRef(
187188
(props: SvgIconProps, ref: ForwardedRef<SVGSVGElement | null>) => {
188189
const {
189190
className = '',
190191
category: category_ = 'utility',
191192
icon: icon_,
192-
iconColor,
193+
iconColor = null,
193194
size = '',
194195
align,
195-
container,
196196
textColor = 'default',
197197
style,
198198
...rprops
199199
} = props;
200200
const { assetRoot = getAssetRoot() } = useContext(ComponentSettingsContext);
201-
const iconClassNames = classnames(
202-
'react-slds-icon',
203-
{
204-
'slds-icon': !/slds-button__icon/.test(className),
205-
[`slds-icon_${size}`]: /^(xx-small|x-small|small|medium|large)$/.test(
206-
size
207-
),
208-
[`slds-icon-text-${textColor ?? 'default'}`]:
209-
/^(default|warning|error)$/.test(textColor ?? '') && !iconColor,
210-
[`slds-icon-${iconColor ?? ''}`]: !container && iconColor,
211-
'slds-m-left_x-small': align === 'right',
212-
'slds-m-right_x-small': align === 'left',
213-
},
214-
className
215-
);
201+
202+
const inIcon = !/slds-button__icon/.test(className);
203+
const iconOnlyClassNames = classnames('react-slds-icon', 'slds-icon', {
204+
[`slds-icon_${size}`]: /^(xx-small|x-small|small|medium|large)$/.test(
205+
size
206+
),
207+
[`slds-icon-text-${textColor ?? 'default'}`]:
208+
/^(default|success|warning|error|light)$/.test(textColor ?? '') &&
209+
!iconColor,
210+
'slds-m-left_x-small': align === 'right',
211+
'slds-m-right_x-small': align === 'left',
212+
});
213+
214+
const iconClassNames = classnames(className, {
215+
[iconOnlyClassNames]: inIcon,
216+
});
217+
216218
// icon and category prop should not include chars other than alphanumerics, underscore, and hyphen
217-
const icon = (icon_ ?? '').replace(/[^\w-]/g, ''); // eslint-disable-line no-param-reassign
218-
const category = (category_ ?? '').replace(/[^\w-]/g, ''); // eslint-disable-line no-param-reassign
219+
const icon = (icon_ ?? '').replace(/[^\w-]/g, '');
220+
const category = (category_ ?? '').replace(/[^\w-]/g, '');
219221
const iconUrl = `${assetRoot}/icons/${category}-sprite/svg/symbols.svg#${icon}`;
220222
return (
221223
<svg
@@ -236,13 +238,19 @@ const SvgIcon = forwardRef(
236238
*/
237239
export const Icon = createFC<IconProps, { ICONS: typeof ICONS }>(
238240
(props) => {
239-
const { container, containerClassName, fillColor, ...rprops } = props;
241+
const {
242+
container,
243+
containerClassName: containerClassName_,
244+
fillColor,
245+
textColor = 'default',
246+
title,
247+
flip,
248+
...rprops
249+
} = props;
240250
let { category = 'utility', icon } = props;
241251

242252
useInitComponentStyle();
243253

244-
const iconContainerRef = useRef<HTMLSpanElement | null>(null);
245-
246254
const svgIconRef = useRef<SVGSVGElement | null>(null);
247255

248256
const svgIconRefCallback = useCallback(
@@ -255,72 +263,44 @@ export const Icon = createFC<IconProps, { ICONS: typeof ICONS }>(
255263
[props.tabIndex]
256264
);
257265

258-
const [iconColor, setIconColor] = useState<string | null>(null);
259-
260-
const checkIconColor = useEventCallback(() => {
261-
if (
262-
fillColor ||
263-
category === 'doctype' ||
264-
(!fillColor && category === 'utility') ||
265-
iconColor === 'standard-default'
266-
) {
267-
return;
268-
}
269-
const el = container ? iconContainerRef.current : svgIconRef.current;
270-
if (!el) {
271-
return;
272-
}
273-
const bgColorStyle = getComputedStyle(el).backgroundColor;
274-
// if no background color set to the icon
275-
if (
276-
bgColorStyle &&
277-
/^(transparent|rgba\(0,\s*0,\s*0,\s*0\))$/.test(bgColorStyle)
278-
) {
279-
setIconColor('standard-default');
280-
}
281-
});
282-
283266
useEffect(() => {
284267
svgIconRefCallback(svgIconRef.current);
285268
}, [svgIconRefCallback]);
286269

287-
useEffect(() => {
288-
checkIconColor();
289-
}, [checkIconColor]);
290-
291270
if (icon.indexOf(':') > 0) {
292271
[category, icon] = icon.split(':') as [IconCategory, string];
293272
}
294273

295-
const fillIconColor =
296-
iconColor || container ? getIconColor(fillColor, category, icon) : null;
274+
const fillIconColor = getIconColor(fillColor, category, icon);
297275

298-
const svgIcon = (
299-
<SvgIcon
300-
ref={svgIconRefCallback}
301-
{...rprops}
302-
{...{
303-
container,
304-
category,
305-
icon,
306-
iconColor: fillIconColor,
307-
}}
308-
/>
276+
const containerClassName = classnames(
277+
containerClassName_,
278+
'slds-icon_container',
279+
container === 'circle' ? 'slds-icon_container_circle' : null,
280+
category === 'utility'
281+
? `slds-icon-utility-${icon.replace(/_/g, '-')}`
282+
: null,
283+
fillIconColor ? `slds-icon-${fillIconColor}` : null,
284+
{
285+
'slds-current-color': textColor === 'currentColor',
286+
'slds-icon_flip': flip,
287+
}
288+
);
289+
290+
return (
291+
<span className={containerClassName} title={title}>
292+
<SvgIcon
293+
ref={svgIconRefCallback}
294+
{...rprops}
295+
{...{
296+
category,
297+
icon,
298+
iconColor: fillIconColor,
299+
}}
300+
/>
301+
{title ? <span className='slds-assistive-text'>{title}</span> : null}
302+
</span>
309303
);
310-
if (container) {
311-
const ccontainerClassName = classnames(
312-
containerClassName,
313-
'slds-icon_container',
314-
container === 'circle' ? 'slds-icon_container_circle' : null,
315-
fillIconColor ? `slds-icon-${fillIconColor}` : null
316-
);
317-
return (
318-
<span className={ccontainerClassName} ref={iconContainerRef}>
319-
{svgIcon}
320-
</span>
321-
);
322-
}
323-
return svgIcon;
324304
},
325305
{ ICONS }
326306
);

0 commit comments

Comments
 (0)