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
3 changes: 3 additions & 0 deletions UNRELEASED.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Use [the changelog guidelines](https://git.io/polaris-changelog-guidelines) to f
### Enhancements

- Updated `MediaCard` to accept ReactNode as title and make `primaryAction` optional ([#3552](https://github.com/Shopify/polaris-react/pull/3552))
- **`UnstyledButton`:** Added `loading` prop to apply `role` and `aria-busy` attributes ([#3494](https://github.com/Shopify/polaris-react/pull/3494))

### Bug fixes

Expand All @@ -20,4 +21,6 @@ Use [the changelog guidelines](https://git.io/polaris-changelog-guidelines) to f

### Code quality

- **`Button`:** Reduced redundant code repeated within `UnstyledButton` ([#3494](https://github.com/Shopify/polaris-react/pull/3494))

### Deprecations
201 changes: 77 additions & 124 deletions src/components/Button/Button.tsx
Original file line number Diff line number Diff line change
@@ -1,104 +1,94 @@
import React, {useRef, useState, useCallback} from 'react';
import React, {useCallback, useState} from 'react';
import {CaretDownMinor} from '@shopify/polaris-icons';

import type {BaseButton, ConnectedDisclosure, IconSource} from '../../types';
import {classNames, variationName} from '../../utilities/css';
import {handleMouseUpByBlurring} from '../../utilities/focus';
import {
handleMouseUpByBlurring,
MouseUpBlurHandler,
} from '../../utilities/focus';
import {useFeatures} from '../../utilities/features';
import {useI18n} from '../../utilities/i18n';
import {Icon} from '../Icon';
import type {IconProps, ConnectedDisclosure} from '../../types';
import {Spinner} from '../Spinner';
import {Popover} from '../Popover';
import {ActionList} from '../ActionList';
import {UnstyledButton} from '../UnstyledButton';
import {UnstyledButton, UnstyledButtonProps} from '../UnstyledButton';

import styles from './Button.scss';

type Size = 'slim' | 'medium' | 'large';
type TextAlign = 'left' | 'right' | 'center';
type IconSource = IconProps['source'];

export interface ButtonProps {
export interface ButtonProps extends BaseButton {
/** The content to display inside the button */
children?: string | string[];
/** A destination to link to, rendered in the href attribute of a link */
url?: string;
/** A unique identifier for the button */
id?: string;
/** Provides extra visual weight and identifies the primary action in a set of buttons */
primary?: boolean;
/** Indicates a dangerous or potentially negative action */
destructive?: boolean;
/** Disables the button, disallowing merchant interaction */
disabled?: boolean;
/** Replaces button text with a spinner while a background action is being performed */
loading?: boolean;
/**
* Changes the size of the button, giving it more or less padding
* @default 'medium'
*/
size?: Size;
size?: 'slim' | 'medium' | 'large';
/** Changes the inner text alignment of the button */
textAlign?: TextAlign;
textAlign?: 'left' | 'right' | 'center';
/** Gives the button a subtle alternative to the default button styling, appropriate for certain backdrops */
outline?: boolean;
/** Gives the button the appearance of being pressed */
pressed?: boolean;
/** Allows the button to grow to the width of its container */
fullWidth?: boolean;
/** Displays the button with a disclosure icon. Defaults to `down` when set to true */
disclosure?: 'down' | 'up' | boolean;
/** Allows the button to submit a form */
submit?: boolean;
/** Renders a button that looks like a link */
plain?: boolean;
/** Makes `plain` and `outline` Button colors (text, borders, icons) the same as the current text color. Also adds an underline to `plain` Buttons */
monochrome?: boolean;
/** Forces url to open in a new tab */
external?: boolean;
/** Tells the browser to download the url instead of opening it. Provides a hint for the downloaded filename if it is a string value */
download?: string | boolean;
/** Icon to display to the left of the button content */
icon?: React.ReactElement | IconSource;
/** Visually hidden text for screen readers */
accessibilityLabel?: string;
/** Id of the element the button controls */
ariaControls?: string;
/** Tells screen reader the controlled element is expanded */
ariaExpanded?: boolean;
/**
* @deprecated As of release 4.7.0, replaced by {@link https://polaris.shopify.com/components/structure/page#props-pressed}
* Tells screen reader the element is pressed
*/
ariaPressed?: boolean;
/** Disclosure button connected right of the button. Toggles a popover action list. */
connectedDisclosure?: ConnectedDisclosure;
/** Callback when clicked */
onClick?(): void;
/** Callback when button becomes focussed */
onFocus?(): void;
/** Callback when focus leaves button */
onBlur?(): void;
/** Callback when a keypress event is registered on the button */
onKeyPress?(event: React.KeyboardEvent<HTMLButtonElement>): void;
/** Callback when a keyup event is registered on the button */
onKeyUp?(event: React.KeyboardEvent<HTMLButtonElement>): void;
/** Callback when a keydown event is registered on the button */
onKeyDown?(event: React.KeyboardEvent<HTMLButtonElement>): void;
/** Callback when mouse enter */
onMouseEnter?(): void;
/** Callback when element is touched */
onTouchStart?(): void;
}

interface CommonButtonProps
extends Pick<
ButtonProps,
| 'id'
| 'accessibilityLabel'
| 'onClick'
| 'onFocus'
| 'onBlur'
| 'onMouseEnter'
| 'onTouchStart'
> {
className: UnstyledButtonProps['className'];
onMouseUp: MouseUpBlurHandler;
}

type LinkButtonProps = Pick<ButtonProps, 'url' | 'external' | 'download'>;

type ActionButtonProps = Pick<
ButtonProps,
| 'submit'
| 'disabled'
| 'loading'
| 'ariaControls'
| 'ariaExpanded'
| 'ariaPressed'
| 'onKeyDown'
| 'onKeyUp'
| 'onKeyPress'
>;

const DEFAULT_SIZE = 'medium';

export function Button({
id,
children,
url,
disabled,
external,
download,
submit,
loading,
children,
pressed,
accessibilityLabel,
ariaControls,
ariaExpanded,
Expand All @@ -111,33 +101,19 @@ export function Button({
onKeyUp,
onMouseEnter,
onTouchStart,
external,
download,
icon,
primary,
outline,
destructive,
disclosure,
plain,
monochrome,
submit,
size = DEFAULT_SIZE,
textAlign,
fullWidth,
pressed,
connectedDisclosure,
}: ButtonProps) {
const {newDesignLanguage} = useFeatures();
const hasGivenDeprecationWarning = useRef(false);

if (ariaPressed && !hasGivenDeprecationWarning.current) {
// eslint-disable-next-line no-console
console.warn(
'Deprecation: The ariaPressed prop has been replaced with pressed',
);
hasGivenDeprecationWarning.current = true;
}

const i18n = useI18n();

const isDisabled = disabled || loading;
Expand Down Expand Up @@ -226,7 +202,6 @@ export function Button({
</span>
);

const type = submit ? 'submit' : 'button';
const ariaPressedStatus = pressed !== undefined ? pressed : ariaPressed;

const [disclosureActive, setDisclosureActive] = useState(false);
Expand Down Expand Up @@ -289,61 +264,39 @@ export function Button({
);
}

let buttonMarkup;
const commonProps: CommonButtonProps = {
id,
className,
accessibilityLabel,
onClick,
onFocus,
onBlur,
onMouseUp: handleMouseUpByBlurring,
onMouseEnter,
onTouchStart,
};
const linkProps: LinkButtonProps = {
url,
external,
download,
};
const actionProps: ActionButtonProps = {
submit,
disabled: isDisabled,
loading,
ariaControls,
ariaExpanded,
ariaPressed: ariaPressedStatus,
onKeyDown,
onKeyUp,
onKeyPress,
};

if (url) {
buttonMarkup = isDisabled ? (
// Render an `<a>` so toggling disabled/enabled state changes only the
// `href` attribute instead of replacing the whole element.
// eslint-disable-next-line jsx-a11y/anchor-is-valid
<a id={id} className={className} aria-label={accessibilityLabel}>
{content}
</a>
) : (
<UnstyledButton
id={id}
url={url}
external={external}
download={download}
onClick={onClick}
onFocus={onFocus}
onBlur={onBlur}
onMouseUp={handleMouseUpByBlurring}
onMouseEnter={onMouseEnter}
onTouchStart={onTouchStart}
className={className}
aria-label={accessibilityLabel}
>
{content}
</UnstyledButton>
);
} else {
buttonMarkup = (
<UnstyledButton
id={id}
type={type}
onClick={onClick}
onFocus={onFocus}
onBlur={onBlur}
onKeyDown={onKeyDown}
onKeyUp={onKeyUp}
onKeyPress={onKeyPress}
onMouseUp={handleMouseUpByBlurring}
onMouseEnter={onMouseEnter}
onTouchStart={onTouchStart}
className={className}
disabled={isDisabled}
aria-label={accessibilityLabel}
aria-controls={ariaControls}
aria-expanded={ariaExpanded}
aria-pressed={ariaPressedStatus}
role={loading ? 'alert' : undefined}
aria-busy={loading ? true : undefined}
>
{content}
</UnstyledButton>
);
}
const buttonMarkup = (
<UnstyledButton {...commonProps} {...linkProps} {...actionProps}>
{content}
</UnstyledButton>
);

return connectedDisclosureMarkup ? (
<div className={styles.ConnectedDisclosureWrapper}>
Expand Down
Loading