Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
fdc4e0d
(Icon): update icon types
msmx-mnakagawa Jun 6, 2025
189b004
(Icon): update markups and classnames
msmx-mnakagawa Jun 6, 2025
b05c36d
(Button): update markups and classnames
msmx-mnakagawa Jun 6, 2025
df1c712
(Button): add new button types
msmx-mnakagawa Jun 13, 2025
4e27579
(Button): reflect `iconSize` to `ButtonIcon`
msmx-mnakagawa Jun 13, 2025
096208e
(Button): remove unused classnames
msmx-mnakagawa Jun 13, 2025
90902c8
(Icon): update an interface about `container`
msmx-mnakagawa Jun 13, 2025
b78c6a6
(Icon): improve a category icon classname
msmx-mnakagawa Jun 13, 2025
73109a9
(Icon): add `title`, `currentColor`, and `flip` props
msmx-mnakagawa Jun 13, 2025
1d538cc
(Icon, Button): remove unnecessary `eslint-disable-` comments
msmx-mnakagawa Jul 2, 2025
9ea3e18
(Icon): update a naming convention
msmx-mnakagawa Jul 2, 2025
87ab6e1
Revert "(Icon): update icon types"
msmx-mnakagawa Jul 3, 2025
288d955
(Icon, Button): unify `SvgButtonIcon` with `SvgIcon`
msmx-mnakagawa Jul 3, 2025
ef99975
(Icon): reflect interface update to stories
msmx-mnakagawa Jul 3, 2025
5a141a3
Merge branch 'support-slds-2' into support-slds-2-icon-button
msmx-mnakagawa Jul 3, 2025
49a0628
(Button): change the order of properties
msmx-mnakagawa Jul 7, 2025
da113a2
(Icon): replace `circleContainer` with `container`
msmx-mnakagawa Jul 7, 2025
8235e6f
(Icon): make `textColor` and `currentColor` mutually exclusive
msmx-mnakagawa Jul 7, 2025
9bef3e3
(Icon): add the `Current Color` story
msmx-mnakagawa Jul 7, 2025
f57f21b
(Icon): unify the `label` into the `title`
msmx-mnakagawa Jul 7, 2025
893d631
remove duplicated type definitions
msmx-mnakagawa Jul 9, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 25 additions & 24 deletions src/scripts/Button.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import React, { FC, ReactNode, ButtonHTMLAttributes, Ref, useRef } from 'react';
import classnames from 'classnames';
import { Icon } from './Icon';
import { SvgIcon, IconCategory } from './Icon';
import { Spinner } from './Spinner';
import { useEventCallback, useMergeRefs } from './hooks';

export type ButtonType =
| 'neutral'
| 'brand'
| 'outline-brand'
| 'destructive'
| 'text-destructive'
| 'success'
| 'inverse'
| 'icon'
| 'icon-bare'
Expand All @@ -31,6 +34,7 @@ export type ButtonIconMoreSize = 'x-small' | 'small' | 'medium' | 'large';
*/
export type ButtonIconProps = {
className?: string;
category?: IconCategory;
icon: string;
align?: ButtonIconAlign;
size?: ButtonIconSize;
Expand All @@ -43,9 +47,9 @@ export type ButtonIconProps = {
*/
export const ButtonIcon: FC<ButtonIconProps> = ({
icon,
category = 'utility',
align,
size,
inverse,
className,
style,
...props
Expand All @@ -56,19 +60,22 @@ export const ButtonIcon: FC<ButtonIconProps> = ({
: null;
const sizeClassName =
size && ICON_SIZES.indexOf(size) >= 0 ? `slds-button__icon_${size}` : null;
const inverseClassName = inverse ? 'slds-button__icon_inverse' : null;
const iconClassNames = classnames(
'slds-button__icon',
alignClassName,
sizeClassName,
inverseClassName,
className
);

if (icon.indexOf(':') > 0) {
[category, icon] = icon.split(':') as [IconCategory, string];
}

return (
<Icon
<SvgIcon
className={iconClassNames}
icon={icon}
textColor={null}
category={category}
pointerEvents='none'
style={style}
{...props}
Expand Down Expand Up @@ -118,6 +125,7 @@ export const Button: FC<ButtonProps> = (props) => {
buttonRef: buttonRef_,
iconMoreSize: iconMoreSize_,
onClick: onClick_,
tabIndex,
...rprops
} = props;

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

const content = children || label;
const isIconOnly = type && /^icon-/.test(type) && icon && !content;

const typeClassName = type ? `slds-button_${type}` : null;
const btnClassNames = classnames(className, 'slds-button', typeClassName, {
'slds-is-selected': selected,
['slds-button_icon']: /^icon-/.test(type ?? ''),
[`slds-button_icon-${size ?? ''}`]:
/^(x-small|small)$/.test(size ?? '') && /^icon-/.test(type ?? ''),
});

const buttonContent = (
// eslint-disable-next-line react/button-has-type
return (
<button
ref={buttonRef}
className={btnClassNames}
type={htmlType}
title={isIconOnly || alt ? alt ?? icon : undefined}
tabIndex={tabIndex ?? -1}
{...rprops}
onClick={onClick}
>
Expand All @@ -159,7 +172,7 @@ export const Button: FC<ButtonProps> = (props) => {
inverse={inverse}
/>
) : undefined}
{children || label}
{content}
{icon && iconAlign === 'right' ? (
<ButtonIcon
icon={icon}
Expand All @@ -171,22 +184,10 @@ export const Button: FC<ButtonProps> = (props) => {
{iconMore ? (
<ButtonIcon icon={iconMore} align='right' size={iconMoreSize} />
) : undefined}
{alt ? <span className='slds-assistive-text'>{alt}</span> : undefined}
{isIconOnly || alt ? (
<span className='slds-assistive-text'>{alt ?? icon}</span>
) : undefined}
{loading ? <Spinner /> : undefined}
</button>
);

if (props.tabIndex != null) {
return (
<span
className='react-slds-button-focus-wrapper'
style={{ outline: 0 }}
tabIndex={-1}
>
{buttonContent}
</span>
);
}

return buttonContent;
};
168 changes: 74 additions & 94 deletions src/scripts/Icon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,13 @@ import React, {
SVGAttributes,
useContext,
useRef,
useState,
useEffect,
useCallback,
} from 'react';
import classnames from 'classnames';
import svg4everybody from 'svg4everybody';
import { registerStyle, getAssetRoot } from './util';
import { ComponentSettingsContext } from './ComponentSettings';
import { useEventCallback } from './hooks';
import { createFC } from './common';

svg4everybody();
Expand Down Expand Up @@ -124,23 +122,19 @@ function useInitComponentStyle() {

function getIconColor(
fillColor: string | undefined,
category: string | undefined,
category: IconCategory,
icon: string
) {
/* eslint-disable no-unneeded-ternary */
return category === 'doctype'
? null
: fillColor === 'none'
return fillColor === 'none'
? null
: fillColor
? fillColor
: category === 'utility'
? null
: category === 'custom'
? icon.replace(/^custom/, 'custom-')
: category === 'action' && /^new_custom/.test(icon)
: category === 'action' && /^new_custom/.test(icon) // not needed for the current SLDS2 icons
? icon.replace(/^new_custom/, 'custom-')
: `${category ?? ''}-${(icon ?? '').replace(/_/g, '-')}`;
: `${category}-${icon.replace(/_/g, '-')}`;
/* eslint-enable no-unneeded-ternary */
}

Expand All @@ -154,8 +148,15 @@ export type IconCategory =
| 'standard'
| 'utility';
export type IconSize = 'xx-small' | 'x-small' | 'small' | 'medium' | 'large';
export type IconContainer = boolean | 'default' | 'circle';
export type IconTextColor = 'default' | 'warning' | 'error' | null;
export type IconContainer = 'circle';
export type IconTextColor =
| 'default'
| 'currentColor'
| 'success'
| 'warning'
| 'error'
| 'light'
| null;

/**
*
Expand All @@ -167,55 +168,56 @@ export type IconProps = {
size?: IconSize;
align?: 'left' | 'right';
container?: IconContainer;
color?: string;
textColor?: IconTextColor;
tabIndex?: number;
fillColor?: string;
title?: string;
flip?: boolean;
} & SVGAttributes<SVGElement>;

/**
*
*/
type SvgIconProps = IconProps & {
iconColor: string | null;
iconColor?: string | null;
};

/**
*
*/
const SvgIcon = forwardRef(
export const SvgIcon = forwardRef(
(props: SvgIconProps, ref: ForwardedRef<SVGSVGElement | null>) => {
const {
className = '',
category: category_ = 'utility',
icon: icon_,
iconColor,
iconColor = null,
size = '',
align,
container,
textColor = 'default',
style,
...rprops
} = props;
const { assetRoot = getAssetRoot() } = useContext(ComponentSettingsContext);
const iconClassNames = classnames(
'react-slds-icon',
{
'slds-icon': !/slds-button__icon/.test(className),
[`slds-icon_${size}`]: /^(xx-small|x-small|small|medium|large)$/.test(
size
),
[`slds-icon-text-${textColor ?? 'default'}`]:
/^(default|warning|error)$/.test(textColor ?? '') && !iconColor,
[`slds-icon-${iconColor ?? ''}`]: !container && iconColor,
'slds-m-left_x-small': align === 'right',
'slds-m-right_x-small': align === 'left',
},
className
);

const inIcon = !/slds-button__icon/.test(className);
const iconOnlyClassNames = classnames('react-slds-icon', 'slds-icon', {
[`slds-icon_${size}`]: /^(xx-small|x-small|small|medium|large)$/.test(
size
),
[`slds-icon-text-${textColor ?? 'default'}`]:
/^(default|success|warning|error|light)$/.test(textColor ?? '') &&
!iconColor,
'slds-m-left_x-small': align === 'right',
'slds-m-right_x-small': align === 'left',
});

const iconClassNames = classnames(className, {
[iconOnlyClassNames]: inIcon,
});

// icon and category prop should not include chars other than alphanumerics, underscore, and hyphen
const icon = (icon_ ?? '').replace(/[^\w-]/g, ''); // eslint-disable-line no-param-reassign
const category = (category_ ?? '').replace(/[^\w-]/g, ''); // eslint-disable-line no-param-reassign
const icon = (icon_ ?? '').replace(/[^\w-]/g, '');
const category = (category_ ?? '').replace(/[^\w-]/g, '');
const iconUrl = `${assetRoot}/icons/${category}-sprite/svg/symbols.svg#${icon}`;
return (
<svg
Expand All @@ -236,13 +238,19 @@ const SvgIcon = forwardRef(
*/
export const Icon = createFC<IconProps, { ICONS: typeof ICONS }>(
(props) => {
const { container, containerClassName, fillColor, ...rprops } = props;
const {
container,
containerClassName: containerClassName_,
fillColor,
textColor = 'default',
title,
flip,
...rprops
} = props;
let { category = 'utility', icon } = props;

useInitComponentStyle();

const iconContainerRef = useRef<HTMLSpanElement | null>(null);

const svgIconRef = useRef<SVGSVGElement | null>(null);

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

const [iconColor, setIconColor] = useState<string | null>(null);

const checkIconColor = useEventCallback(() => {
if (
fillColor ||
category === 'doctype' ||
(!fillColor && category === 'utility') ||
iconColor === 'standard-default'
) {
return;
}
const el = container ? iconContainerRef.current : svgIconRef.current;
if (!el) {
return;
}
const bgColorStyle = getComputedStyle(el).backgroundColor;
// if no background color set to the icon
if (
bgColorStyle &&
/^(transparent|rgba\(0,\s*0,\s*0,\s*0\))$/.test(bgColorStyle)
) {
setIconColor('standard-default');
}
});

useEffect(() => {
svgIconRefCallback(svgIconRef.current);
}, [svgIconRefCallback]);

useEffect(() => {
checkIconColor();
}, [checkIconColor]);

if (icon.indexOf(':') > 0) {
[category, icon] = icon.split(':') as [IconCategory, string];
}

const fillIconColor =
iconColor || container ? getIconColor(fillColor, category, icon) : null;
const fillIconColor = getIconColor(fillColor, category, icon);

const svgIcon = (
<SvgIcon
ref={svgIconRefCallback}
{...rprops}
{...{
container,
category,
icon,
iconColor: fillIconColor,
}}
/>
const containerClassName = classnames(
containerClassName_,
'slds-icon_container',
container === 'circle' ? 'slds-icon_container_circle' : null,
category === 'utility'
? `slds-icon-utility-${icon.replace(/_/g, '-')}`
: null,
fillIconColor ? `slds-icon-${fillIconColor}` : null,
{
'slds-current-color': textColor === 'currentColor',
'slds-icon_flip': flip,
}
);

return (
<span className={containerClassName} title={title}>
<SvgIcon
ref={svgIconRefCallback}
{...rprops}
{...{
category,
icon,
iconColor: fillIconColor,
}}
/>
{title ? <span className='slds-assistive-text'>{title}</span> : null}
</span>
);
if (container) {
const ccontainerClassName = classnames(
containerClassName,
'slds-icon_container',
container === 'circle' ? 'slds-icon_container_circle' : null,
fillIconColor ? `slds-icon-${fillIconColor}` : null
);
return (
<span className={ccontainerClassName} ref={iconContainerRef}>
{svgIcon}
</span>
);
}
return svgIcon;
},
{ ICONS }
);
Loading