@awsui/components-react
Version:
On July 19th, 2022, we launched [Cloudscape Design System](https://cloudscape.design). Cloudscape is an evolution of AWS-UI. It consists of user interface guidelines, front-end components, design resources, and development tools for building intuitive, en
194 lines • 11.9 kB
JavaScript
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import React, { useEffect, useRef, useState } from 'react';
import clsx from 'clsx';
import { useMergeRefs, useUniqueId, warnOnce } from '@awsui/component-toolkit/internal';
import { useSingleTabStopNavigation } from '@awsui/component-toolkit/internal';
import { getAnalyticsLabelAttribute, getAnalyticsMetadataAttribute, } from '@awsui/component-toolkit/internal/analytics-metadata';
import { useInternalI18n } from '../i18n/context';
import Icon from '../icon/internal';
import { FunnelMetrics } from '../internal/analytics';
import { useFunnel, useFunnelStep, useFunnelSubStep } from '../internal/analytics/hooks/use-funnel';
import { DATA_ATTR_FUNNEL_VALUE, getFunnelValueSelector, getSubStepAllSelector, getTextFromSelector, } from '../internal/analytics/selectors';
import Tooltip from '../internal/components/tooltip/index.js';
import { useButtonContext } from '../internal/context/button-context';
import { fireCancelableEvent, isPlainLeftClick } from '../internal/events';
import useForwardFocus from '../internal/hooks/forward-focus';
import useHiddenDescription from '../internal/hooks/use-hidden-description';
import { useModalContextLoadingButtonComponent } from '../internal/hooks/use-modal-component-analytics';
import { usePerformanceMarks } from '../internal/hooks/use-performance-marks';
import { checkSafeUrl } from '../internal/utils/check-safe-url';
import WithNativeAttributes from '../internal/utils/with-native-attributes';
import InternalLiveRegion from '../live-region/internal';
import { LeftIcon, RightIcon } from './icon-helper';
import { getButtonStyles } from './style';
import analyticsSelectors from './analytics-metadata/styles.css.js';
import styles from './styles.css.js';
import testUtilStyles from './test-classes/styles.css.js';
export const InternalButton = React.forwardRef(({ children, iconName, __iconClass, onClick, onFollow, iconAlign = 'left', iconUrl, iconSvg, iconAlt, variant = 'normal', loading = false, loadingText, disabled = false, disabledReason, wrapText = true, href, external, target: targetOverride, rel, download, formAction = 'submit', ariaLabel, ariaDescribedby, ariaExpanded, ariaControls, fullWidth, badge, i18nStrings, style, nativeButtonAttributes, nativeAnchorAttributes, __internalRootRef, __focusable = false, __injectAnalyticsComponentMetadata = false, __title, __emitPerformanceMarks = true, __skipNativeAttributesWarnings, analyticsAction = 'click', ...props }, ref) => {
var _a;
const [showTooltip, setShowTooltip] = useState(false);
checkSafeUrl('Button', href);
const isAnchor = Boolean(href);
const target = targetOverride !== null && targetOverride !== void 0 ? targetOverride : (external ? '_blank' : undefined);
const isNotInteractive = loading || disabled;
const isDisabledWithReason = (variant === 'normal' || variant === 'primary' || variant === 'icon') && !!disabledReason && disabled;
const hasAriaDisabled = (loading && !disabled) || (disabled && __focusable) || isDisabledWithReason;
const shouldHaveContent = children &&
['icon', 'inline-icon', 'flashbar-icon', 'modal-dismiss', 'inline-icon-pointer-target'].indexOf(variant) === -1;
if ((iconName || iconUrl || iconSvg) && iconAlign === 'right' && external) {
warnOnce('Button', 'A right-aligned icon should not be combined with an external icon.');
}
const buttonRef = useRef(null);
useForwardFocus(ref, buttonRef);
const buttonContext = useButtonContext();
const i18n = useInternalI18n('button');
const uniqueId = useUniqueId('button');
const { funnelInteractionId } = useFunnel();
const { stepNumber, stepNameSelector } = useFunnelStep();
const { subStepSelector, subStepNameSelector } = useFunnelSubStep();
const performanceMarkAttributes = usePerformanceMarks('primaryButton', () => variant === 'primary' && __emitPerformanceMarks && !loading && !disabled, buttonRef, () => {
var _a;
return ({
loading,
disabled,
text: (_a = buttonRef.current) === null || _a === void 0 ? void 0 : _a.innerText,
});
}, [loading, disabled]);
useModalContextLoadingButtonComponent(variant === 'primary', loading);
const { targetProps, descriptionEl } = useHiddenDescription(disabledReason);
const handleClick = (event) => {
if (isNotInteractive) {
return event.preventDefault();
}
if (isAnchor && isPlainLeftClick(event)) {
fireCancelableEvent(onFollow, { href, target }, event);
if ((iconName === 'external' || target === '_blank') && funnelInteractionId) {
const stepName = getTextFromSelector(stepNameSelector);
const subStepName = getTextFromSelector(subStepNameSelector);
FunnelMetrics.externalLinkInteracted({
funnelInteractionId,
stepNumber,
stepName,
stepNameSelector,
subStepSelector,
subStepName,
subStepNameSelector,
elementSelector: getFunnelValueSelector(uniqueId),
subStepAllSelector: getSubStepAllSelector(),
});
}
}
const { altKey, button, ctrlKey, metaKey, shiftKey } = event;
fireCancelableEvent(onClick, { altKey, button, ctrlKey, metaKey, shiftKey }, event);
buttonContext.onClick({ variant });
};
const buttonClass = clsx(props.className, styles.button, styles[`variant-${variant}`], {
[styles.disabled]: isNotInteractive,
[styles['disabled-with-reason']]: isDisabledWithReason,
[styles['button-no-wrap']]: !wrapText,
[styles['button-no-text']]: !shouldHaveContent,
[styles['full-width']]: shouldHaveContent && fullWidth,
[styles.link]: isAnchor,
});
const explicitTabIndex = (_a = nativeButtonAttributes === null || nativeButtonAttributes === void 0 ? void 0 : nativeButtonAttributes.tabIndex) !== null && _a !== void 0 ? _a : nativeAnchorAttributes === null || nativeAnchorAttributes === void 0 ? void 0 : nativeAnchorAttributes.tabIndex;
const { tabIndex } = useSingleTabStopNavigation(buttonRef, {
tabIndex: isAnchor && isNotInteractive && !isDisabledWithReason ? -1 : explicitTabIndex,
});
const analyticsMetadata = disabled
? {}
: {
action: analyticsAction,
detail: { label: { root: 'self' } },
};
if (__injectAnalyticsComponentMetadata) {
analyticsMetadata.component = {
name: 'awsui.Button',
label: { root: 'self' },
properties: { variant, disabled: `${disabled}` },
};
}
const buttonProps = {
...props,
...performanceMarkAttributes,
tabIndex,
// https://github.com/microsoft/TypeScript/issues/36659
ref: useMergeRefs(buttonRef, __internalRootRef),
'aria-label': ariaLabel,
'aria-describedby': ariaDescribedby,
'aria-expanded': ariaExpanded,
'aria-controls': ariaControls,
// add ariaLabel as `title` as visible hint text
title: __title !== null && __title !== void 0 ? __title : ariaLabel,
className: buttonClass,
onClick: handleClick,
[DATA_ATTR_FUNNEL_VALUE]: uniqueId,
...getAnalyticsMetadataAttribute(analyticsMetadata),
...getAnalyticsLabelAttribute(shouldHaveContent ? `.${analyticsSelectors.label}` : ''),
};
const iconProps = {
loading,
iconName,
iconAlign,
iconUrl,
iconSvg,
iconAlt,
variant,
badge,
iconClass: __iconClass,
iconSize: variant === 'modal-dismiss' ? 'medium' : 'normal',
};
const buttonContent = (React.createElement(React.Fragment, null,
React.createElement(LeftIcon, { ...iconProps }),
shouldHaveContent && (React.createElement(React.Fragment, null,
React.createElement("span", { className: clsx(styles.content, analyticsSelectors.label) }, children),
external && (React.createElement(React.Fragment, null,
"\u00A0",
React.createElement(Icon, { name: "external", className: testUtilStyles['external-icon'], ariaLabel: i18n('i18nStrings.externalIconAriaLabel', i18nStrings === null || i18nStrings === void 0 ? void 0 : i18nStrings.externalIconAriaLabel) }))))),
React.createElement(RightIcon, { ...iconProps })));
const { loadingButtonCount } = useFunnel();
useEffect(() => {
if (loading) {
loadingButtonCount.current++;
return () => {
// eslint-disable-next-line react-hooks/exhaustive-deps
loadingButtonCount.current--;
};
}
}, [loading, loadingButtonCount]);
const disabledReasonProps = {
onFocus: isDisabledWithReason ? () => setShowTooltip(true) : undefined,
onBlur: isDisabledWithReason ? () => setShowTooltip(false) : undefined,
onMouseEnter: isDisabledWithReason ? () => setShowTooltip(true) : undefined,
onMouseLeave: isDisabledWithReason ? () => setShowTooltip(false) : undefined,
...(isDisabledWithReason ? targetProps : {}),
};
const disabledReasonContent = (React.createElement(React.Fragment, null,
descriptionEl,
showTooltip && (React.createElement(Tooltip, { className: testUtilStyles['disabled-reason-tooltip'], trackRef: buttonRef, value: disabledReason, onDismiss: () => setShowTooltip(false) }))));
const stylePropertiesAndVariables = getButtonStyles(style);
if (isAnchor) {
const getAnchorTabIndex = () => {
if (isNotInteractive) {
// If disabled with a reason, make it focusable so users can access the tooltip
// Otherwise, resolve to the default button props tabIndex.
return disabledReason ? 0 : buttonProps.tabIndex;
}
return buttonProps.tabIndex;
};
return (React.createElement(React.Fragment, null,
React.createElement(WithNativeAttributes, { ...buttonProps, ...disabledReasonProps, tag: "a", componentName: "Button", nativeAttributes: nativeAnchorAttributes, skipWarnings: __skipNativeAttributesWarnings, href: isNotInteractive ? undefined : href, role: isNotInteractive ? 'link' : undefined, tabIndex: getAnchorTabIndex(), target: target,
// security recommendation: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#target
rel: rel !== null && rel !== void 0 ? rel : (target === '_blank' ? 'noopener noreferrer' : undefined), "aria-disabled": isNotInteractive ? true : undefined, download: download, style: stylePropertiesAndVariables },
buttonContent,
isDisabledWithReason && disabledReasonContent),
loading && loadingText && (React.createElement(InternalLiveRegion, { tagName: "span", hidden: true }, loadingText))));
}
return (React.createElement(React.Fragment, null,
React.createElement(WithNativeAttributes, { ...buttonProps, ...disabledReasonProps, tag: "button", componentName: "Button", nativeAttributes: nativeButtonAttributes, skipWarnings: __skipNativeAttributesWarnings, type: formAction === 'none' ? 'button' : 'submit', disabled: disabled && !__focusable && !isDisabledWithReason, "aria-disabled": hasAriaDisabled ? true : undefined, style: stylePropertiesAndVariables },
buttonContent,
isDisabledWithReason && disabledReasonContent),
loading && loadingText && (React.createElement(InternalLiveRegion, { tagName: "span", hidden: true }, loadingText))));
});
export default InternalButton;
//# sourceMappingURL=internal.js.map