@atlaskit/lozenge
Version:
A lozenge is a visual indicator used to highlight an item's status for quick recognition.
281 lines (278 loc) • 13.8 kB
JavaScript
/* lozenge-base.tsx generated by @compiled/babel-plugin v3.0.2 */
import _extends from "@babel/runtime/helpers/extends";
import "./lozenge-base.compiled.css";
import * as React from 'react';
import { ax, ix } from "@compiled/react/runtime";
import { forwardRef, memo, useLayoutEffect, useRef, useState } from 'react';
import Badge from '@atlaskit/badge/badge-new';
import { cx } from '@atlaskit/css';
import ChevronDownIcon from '@atlaskit/icon/core/chevron-down';
import { useResizing } from '@atlaskit/motion/use-resizing';
import { fg } from '@atlaskit/platform-feature-flags/fg';
// eslint-disable-next-line @atlaskit/design-system/no-emotion-primitives -- TODO: migrate to @atlaskit/primitives/compiled
import Pressable from '@atlaskit/primitives/pressable';
import Spinner from '@atlaskit/spinner/spinner';
import { getThemeStyles } from './get-theme-styles';
import IconRenderer from './icon-renderer';
import { resolveLozengeColor } from './utils';
// Get the pressed background color for the selected lozenge dropdown trigger
const pressedBackgroundMapping = {
success: "var(--ds-background-success-subtler-pressed, #B3DF72)",
warning: "var(--ds-background-warning-subtler-pressed, #FBC828)",
danger: "var(--ds-background-danger-subtler-pressed, #FD9891)",
information: "var(--ds-background-information-subtler-pressed, #8FB8F6)",
neutral: "var(--ds-background-neutral-pressed, #080F214A)",
discovery: "var(--ds-background-discovery-subtler-pressed, #D8A0F7)",
'accent-red': "var(--ds-background-accent-red-subtler-pressed, #FD9891)",
'accent-orange': "var(--ds-background-accent-orange-subtler-pressed, #FBC828)",
'accent-yellow': "var(--ds-background-accent-yellow-subtler-pressed, #EED12B)",
'accent-lime': "var(--ds-background-accent-lime-subtler-pressed, #B3DF72)",
'accent-green': "var(--ds-background-accent-green-subtler-pressed, #7EE2B8)",
'accent-teal': "var(--ds-background-accent-teal-subtler-pressed, #9DD9EE)",
'accent-blue': "var(--ds-background-accent-blue-subtler-pressed, #8FB8F6)",
'accent-purple': "var(--ds-background-accent-purple-subtler-pressed, #D8A0F7)",
'accent-magenta': "var(--ds-background-accent-magenta-subtler-pressed, #F797D2)",
'accent-gray': "var(--ds-background-accent-gray-subtlest-pressed, #B7B9BE)"
};
const styles = {
container: "_2rko12b0 _1reo15vq _18m915vq _189ee4h9 _1dqonqa1 _1h6d1j28 _kqswh2mm _1e0c116y _4cvr1h6o _vchhusvi _4t3i1k92 _1q51v77o _85i5v77o _bozg1b66 _y4ti1b66",
motionContainer: "_k8m01rje _1oeci9qs _6fl41d08",
containerSpacious: "_2rko1qi0 _1tkeviql _1q511b66 _85i51b66 _bozgutpp _y4tiutpp",
containerBadgePadding: "_y4ti303w",
text: "_11c8wadc _1reo15vq _18m915vq _o5721q9c",
textEllipsis: "_1bto1l2s",
textClip: "_1btozryt",
textSpacious: "_11c8fhey _k48p1wq8",
textSelected: "_syaz6x5g",
loadingOverlay: "_1reo15vq _18m915vq _1e0c1txw _kqswstnw _4cvr1h6o _1bah1h6o _u7coze3t _152tze3t _rjxpze3t _1e02ze3t _lcxvglyw",
loadingOverlayMotion: "_j7hq1bym _5sag1ign _1pglindk",
metricBadgeWrapper: "_1e0c1txw",
'semantic.success': "_3wu02gvx _bfhk14j0 _syaz1p1e",
'semantic.warning': "_3wu01wfx _bfhk10f4 _syazmgs1",
'semantic.danger': "_3wu01mzp _bfhk18wg _syaz1blx",
'semantic.information': "_3wu0wn4u _bfhk8lbg _syaz1qd9",
'semantic.neutral': "_3wu01l7x _bfhki8nm _syazi7uo",
'semantic.discovery': "_3wu019v3 _bfhke670 _syaz1g72",
'accent.red': "_3wu0n9dc _bfhk1rtt _syazhwvp",
'accent.orange': "_3wu01ip5 _bfhk165s _syaz7i1p",
'accent.yellow': "_3wu01s7j _bfhk1773 _syazekll",
'accent.lime': "_3wu01m7d _bfhkz2ec _syaz1ik3",
'accent.green': "_3wu05d2u _bfhk1wnw _syaz1y78",
'accent.teal': "_3wu0131p _bfhk1a17 _syaz12v7",
'accent.blue': "_3wu018hp _bfhk1dy8 _syaz1kyx",
'accent.purple': "_3wu0luwt _bfhk1nm4 _syaz1mn1",
'accent.magenta': "_3wu0132u _bfhkv9ra _syaz1qur",
'accent.gray': "_3wu01l7x _bfhkysee _syaz1tmo",
'interactive.semantic.success': "_irr31r82 _1di6d6jc",
'interactive.semantic.warning': "_irr31yx4 _1di6bkku",
'interactive.semantic.danger': "_irr370oi _1di6jkmi",
'interactive.semantic.information': "_irr315aw _1di6ar1y",
'interactive.semantic.neutral': "_irr3plhp _1di61gdz",
'interactive.semantic.discovery': "_irr3rumk _1di65usi",
'interactive.accent.red': "_irr3affd _1di61j5a",
'interactive.accent.orange': "_irr31ei0 _1di6dpo3",
'interactive.accent.yellow': "_irr31uw7 _1di61r9w",
'interactive.accent.lime': "_irr31gf0 _1di62kxc",
'interactive.accent.green': "_irr31e2h _1di6ybhx",
'interactive.accent.teal': "_irr3kwwr _1di61jb0",
'interactive.accent.blue': "_irr3815t _1di6rwxf",
'interactive.accent.purple': "_irr3attl _1di63nfk",
'interactive.accent.magenta': "_irr31axx _1di642ri",
'interactive.accent.gray': "_irr3wejn _1di618ut",
iconBorderFilter: "_1h6d74of",
content: "_zulp1b66 _1e0c116y _4cvr1h6o",
maxWidth: "_p12f1osq",
contentSpacious: "_zulp12x7",
loadingContent: "_tzy4idpf",
loadingMotion: "_k8m0brmi _1oec1ign _6fl4indk",
containerBadgePressed: "_1axmtoke _17i7toke",
containerBadgeNeutralAppearance: "_1ftrnkaw _1axmnkaw _17i7nkaw"
};
const interactiveMotionStyles = {
motion: "_v564k6bl _1ggl5xtf"
};
/**
* __New Lozenge__
*
* A lozenge is a visual indicator used to highlight an item's status for quick recognition.
* This is the updated version with the new North Star visual language.
*/
const LozengeBase = /*#__PURE__*/memo( /*#__PURE__*/forwardRef(({
children,
testId,
appearance,
iconBefore,
trailingMetric,
trailingMetricAppearance,
maxWidth = 200,
spacing = 'default',
isSelected,
// for dropdown trigger
isLoading = false,
// for dropdown trigger
onClick,
// for dropdown trigger
style,
analyticsContext,
interactionName,
'aria-controls': ariaControls,
'aria-expanded': ariaExpanded,
'aria-haspopup': ariaHaspopup,
'aria-label': ariaLabel
}, ref) => {
const [resizing, setResizing] = useState(false);
const onFinishMotion = () => setResizing(false);
const resizingWidth = useResizing({
dimension: 'width',
duration: "var(--ds-duration-medium, 200ms)",
easing: "var(--ds-easing-inout-bold, cubic-bezier(0.4, 0, 0, 1))",
onFinishMotion
});
const isInitialRender = useRef(true);
const isInteractive = typeof onClick === 'function';
// Determine the effective color, with fallback logic for legacy appearances
const resolvedColor = resolveLozengeColor(appearance) || 'neutral';
const {
category,
key
} = getThemeStyles(resolvedColor);
const colorStyleKey = `${category}.${key}`;
const interactiveStyleKey = `interactive.${category}.${key}`;
const maxWidthValue = typeof maxWidth === 'string' ? maxWidth : `${maxWidth}px`;
const maxWidthIsPc = typeof maxWidth === 'string' && maxWidth.endsWith('%');
const resolvedTrailingMetricAppearance = trailingMetricAppearance ? trailingMetricAppearance === 'inverse' ? 'inverse' : resolveLozengeColor(trailingMetricAppearance) : resolvedColor;
// Map the resolved trailing metric appearance to a Badge appearance.
// Semantic colors map to their new bold variants (e.g. successBold),
// which provide sufficient color emphasis on top of the subtler
// lozenge background. Accent colors and unknown values fall back to
// the neutral appearance.
const metricBadgeAppearance = (() => {
if (resolvedTrailingMetricAppearance === 'inverse') {
return 'inverse';
}
if (resolvedTrailingMetricAppearance == null) {
return 'neutral';
}
if (resolvedTrailingMetricAppearance.startsWith('accent-')) {
return 'neutral';
}
switch (resolvedTrailingMetricAppearance) {
case 'success':
return 'successBold';
case 'warning':
return 'warningBold';
case 'danger':
return 'dangerBold';
case 'information':
return 'informationBold';
case 'discovery':
return 'discoveryBold';
case 'neutral':
default:
return 'neutral';
}
})();
const commonStyleOverrides = {
backgroundColor: style === null || style === void 0 ? void 0 : style.backgroundColor,
// Constrain the container to the smaller of the explicit maxWidth and 100% of
// the parent. Using min(...) means the lozenge never overflows its parent,
// while still honouring the explicit maxWidth prop when the parent is wider.
maxWidth: maxWidthIsPc ? maxWidth : `min(${maxWidthValue}, 100%)`
};
const hasTrailingMetric = trailingMetric != null && trailingMetric !== '';
const childrenKey = typeof children === 'string' ? children : undefined;
useLayoutEffect(() => {
// Ignore initial render
if (isInitialRender.current) {
isInitialRender.current = false;
return;
}
setResizing(true);
}, [childrenKey]);
const enableMotionFG = fg('platform-dst-motion-uplift');
const enableButtonMotionFG = fg('platform-dst-motion-uplift-button');
const innerContent = /*#__PURE__*/React.createElement("span", _extends({
"data-testid": testId ? `${testId}--content` : undefined
}, enableMotionFG ? resizingWidth : undefined, {
className: ax([styles.content, spacing === 'spacious' && styles.contentSpacious, isInteractive && enableButtonMotionFG && styles.loadingMotion, isLoading && styles.loadingContent,
// Constrain the content wrapper to its container so text truncation
// works correctly within the flex layout, regardless of whether maxWidth
// is a percentage, fixed value, or the parent container is narrower than
// the explicit maxWidth.
styles.maxWidth])
}), iconBefore && /*#__PURE__*/React.createElement(IconRenderer, {
size: spacing === 'spacious' ? 'medium' : 'small',
icon: iconBefore,
color: resolvedColor,
testId: testId && `${testId}--icon`
}), /*#__PURE__*/React.createElement("span", {
style: {
// The text fills 100% of the inner content wrapper. The container's
// own max-width (set on the container via commonStyleOverrides) handles
// the actual size constraint, so the text just needs to fill the available
// space and truncate when it overflows.
maxWidth: maxWidthIsPc ? '100%' : `calc(${maxWidthValue} - ${"var(--ds-space-100, 8px)"})`,
color: style === null || style === void 0 ? void 0 : style.color
},
"data-testid": testId && `${testId}--text`,
className: ax([styles.text, enableMotionFG && resizing && !maxWidth ? styles.textClip : styles.textEllipsis, spacing === 'spacious' && styles.textSpacious])
}, children), hasTrailingMetric && !resolvedColor.startsWith('accent-') && /*#__PURE__*/React.createElement("span", {
"data-lozenge-metric-wrapper": true,
"data-testid": testId && `${testId}--metric`,
className: ax([styles.metricBadgeWrapper])
}, /*#__PURE__*/React.createElement(Badge, {
appearance: metricBadgeAppearance
}, trailingMetric)), isInteractive && /*#__PURE__*/React.createElement(ChevronDownIcon, {
label: "",
size: "small",
color: 'currentColor',
testId: testId && `${testId}--chevron`
}));
if (isInteractive) {
return /*#__PURE__*/React.createElement(Pressable, _extends({
ref: ref,
xcss: cx(styles.container, !isLoading && enableButtonMotionFG ? interactiveMotionStyles.motion : enableMotionFG && styles.motionContainer, spacing === 'spacious' && styles.containerSpacious, !isSelected && styles.iconBorderFilter, styles[colorStyleKey], !isLoading && styles[interactiveStyleKey], hasTrailingMetric && styles.containerBadgePressed, hasTrailingMetric && resolvedTrailingMetricAppearance === 'neutral' && styles.containerBadgeNeutralAppearance)
}, isLoading && {
'aria-busy': true,
'aria-disabled': true,
isDisabled: true
}, {
"aria-label": isLoading ? 'Loading' : ariaLabel,
"aria-controls": ariaControls,
"aria-expanded": ariaExpanded,
"aria-haspopup": ariaHaspopup,
onClick: isLoading ? undefined : onClick,
style: {
// eslint-disable-next-line @atlaskit/ui-styling-standard/enforce-style-prop
...commonStyleOverrides,
// Specified because Pressable has a default border:none which overrides the border specified on the container
// eslint-disable-next-line @atlaskit/ui-styling-standard/enforce-style-prop
border: `solid ${"var(--ds-border-width, 1px)"} ${isSelected ? 'var(--border-color) !important' : 'transparent'}`,
backgroundColor: isSelected ? pressedBackgroundMapping[resolvedColor] : undefined,
cursor: isLoading ? 'progress' : 'pointer'
},
testId: testId,
analyticsContext: analyticsContext,
interactionName: interactionName,
componentName: "LozengeDropdownTrigger"
// `data-selected` mirrors the `isSelected` prop so CSS can target the
// pressed-state badge override via attribute selector.
}, isSelected && {
'data-selected': 'true'
}), innerContent, isLoading && /*#__PURE__*/React.createElement("span", {
"data-testid": testId ? `${testId}--loading-overlay` : undefined,
className: ax([styles.loadingOverlay, enableButtonMotionFG && styles.loadingOverlayMotion])
}, /*#__PURE__*/React.createElement(Spinner, {
size: spacing === 'spacious' ? 'small' : 'xsmall',
label: ", Loading",
testId: testId ? `${testId}--loading-spinner` : undefined
})));
}
return /*#__PURE__*/React.createElement("span", {
ref: ref,
style: commonStyleOverrides,
"data-testid": testId,
className: ax([styles.container, spacing === 'spacious' && styles.containerSpacious, spacing !== 'spacious' && hasTrailingMetric && styles.containerBadgePadding, styles[colorStyleKey], styles.iconBorderFilter, hasTrailingMetric && resolvedTrailingMetricAppearance === 'neutral' && styles.containerBadgeNeutralAppearance])
}, innerContent);
}));
export default LozengeBase;