Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
17 changes: 15 additions & 2 deletions packages/shared/src/components/buttons/Button.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
ButtonVariant,
ButtonIconPosition,
} from './Button';
import { UpvoteIcon } from '../icons';
import { ArrowIcon, UpvoteIcon } from '../icons';

const renderComponent = <Tag extends AllowedTags>(
props: Partial<ButtonProps<Tag>> = {},
Expand Down Expand Up @@ -78,7 +78,7 @@ describe('Button', () => {
icon: <UpvoteIcon data-testid="icon" />,
children: 'Upvote',
});
expect(await screen.findByTestId('icon')).toBeInTheDocument();
expect(await screen.findByTestId('icon')).toHaveClass('btn-icon-left');
expect(await screen.findByRole('button')).not.toHaveClass('iconOnly');
});

Expand All @@ -94,6 +94,7 @@ describe('Button', () => {

expect(button).toBeInTheDocument();
expect(rightIcon).toBeInTheDocument();
expect(rightIcon).toHaveClass('btn-icon-right');

// check if icon appears AFTER the label
expect(button.innerHTML.indexOf('Upvote')).toBeLessThan(
Expand Down Expand Up @@ -141,6 +142,18 @@ describe('Button', () => {
expect(loadingLabel).toHaveClass('invisible');
});

it('does not wrap mixed button content in the label span', () => {
render(
<Button variant={ButtonVariant.Primary}>
Button
<ArrowIcon data-testid="child-icon" />
</Button>,
);

expect(screen.getByTestId('child-icon')).toBeInTheDocument();
expect(screen.getByRole('button').querySelector('.btn-label')).toBeNull();
});

it('should set aria-pressed when pressed is true', async () => {
renderComponent({ children: 'Button', pressed: true });
expect(await screen.findByRole('button')).toHaveAttribute(
Expand Down
30 changes: 22 additions & 8 deletions packages/shared/src/components/buttons/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import {
VariantColorToClassName,
VariantToClassName,
} from './common';
import { isNullOrUndefined } from '../../lib/func';
import classed from '../../lib/classed';

export type IconType = React.ReactElement<IconProps>;
Expand Down Expand Up @@ -83,34 +82,47 @@ function ButtonComponent<TagName extends AllowedTags>(
}: ButtonProps<TagName>,
ref?: Ref<ButtonElementType<TagName>>,
): ReactElement {
const hasChildren = !isNullOrUndefined(children);
const childNodes = React.Children.toArray(children);
const hasChildren = childNodes.length > 0;
const shouldWrapLabel =
hasChildren &&
childNodes.every(
(child) => typeof child === 'string' || typeof child === 'number',
);
const iconOnly = icon && !hasChildren;
const getIconWithSize = useGetIconWithSize(
size,
iconOnly ?? false,
iconPosition,
);
const isAnchor = Tag === 'a';
const anchorClickProps =
isAnchor && onClick
? combinedClicks<HTMLAnchorElement>(
onClick as React.MouseEventHandler<HTMLAnchorElement>,
)
: {};
const isOptionOrQuiz =
variant === ButtonVariant.Option || variant === ButtonVariant.Quiz;
const [isHovering, setIsHovering] = useState(false);

return (
<Tag
{...props}
{...(isAnchor ? combinedClicks(onClick) : { onClick })}
{...(isAnchor ? anchorClickProps : { onClick })}
aria-busy={loading}
aria-pressed={pressed}
ref={ref}
className={classNames(
`btn focus-outline inline-flex cursor-pointer select-none flex-row
items-center border no-underline shadow-none transition
duration-200 ease-in-out typo-callout`,
![ButtonVariant.Option, ButtonVariant.Quiz].includes(variant) &&
'justify-center font-bold',
!isOptionOrQuiz && 'justify-center font-bold',
{ iconOnly },
iconOnly ? IconOnlySizeToClassName[size] : SizeToClassName[size],
iconPosition === ButtonIconPosition.Top && `flex-col !px-2`,
!color && VariantToClassName[variant],
VariantColorToClassName[variant]?.[color],
variant && !color && VariantToClassName[variant],
variant && color && VariantColorToClassName[variant]?.[color],
className,
)}
onMouseEnter={(e: React.MouseEvent<AllowedElements>) => {
Expand All @@ -127,10 +139,12 @@ function ButtonComponent<TagName extends AllowedTags>(
iconPosition,
) &&
getIconWithSize(icon, iconSecondaryOnHover ? isHovering : false)}
{hasChildren && (
{shouldWrapLabel ? (
<span className={classNames('btn-label', loading && 'invisible')}>
{children}
</span>
) : (
children
)}
{icon &&
iconPosition === ButtonIconPosition.Right &&
Expand Down
10 changes: 8 additions & 2 deletions packages/shared/src/components/buttons/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,13 +232,19 @@ export const useGetIconWithSize = (
size: icon.props?.size ?? buttonSizeToIconSize[size],
className: classNames(
icon.props.className,
'btn-icon',
!iconOnly && 'text-base',
!iconOnly && iconPosition === ButtonIconPosition.Left && 'mr-1',
!iconOnly &&
iconPosition === ButtonIconPosition.Left &&
'btn-icon-left mr-1',
!iconOnly &&
!icon.props?.size &&
iconPosition === ButtonIconPosition.Left &&
'-ml-2',
!iconOnly && iconPosition === ButtonIconPosition.Right && 'ml-1',
!iconOnly &&
iconPosition === ButtonIconPosition.Right &&
'btn-icon-right ml-1',
!iconOnly && iconPosition === ButtonIconPosition.Top && 'btn-icon-top',
!iconOnly &&
!icon.props?.size &&
iconPosition === ButtonIconPosition.Right &&
Expand Down
14 changes: 5 additions & 9 deletions packages/shared/src/styles/components/buttons.css
Original file line number Diff line number Diff line change
Expand Up @@ -49,18 +49,14 @@
}

&:not(.iconOnly) {
& .icon {
& .btn-icon-left {
margin-left: -0.5rem;
margin-right: 0.25rem;
}

&:not(:first-child) {
margin-left: 0.25rem;
margin-right: -0.5rem;
}

&:only-child {
margin-right: -0.5rem;
}
& .btn-icon-right {
margin-left: 0.25rem;
margin-right: -0.5rem;
}

& .socialIcon {
Expand Down
Loading