|
@@ -1,6 +1,7 @@
|
|
|
import {useState} from 'react';
|
|
|
import {css} from '@emotion/react';
|
|
|
import styled from '@emotion/styled';
|
|
|
+import {useHover} from '@react-aria/interactions';
|
|
|
import classNames from 'classnames';
|
|
|
|
|
|
import {IconCheckmark, IconChevron, IconInfo, IconNot, IconWarning} from 'sentry/icons';
|
|
@@ -19,41 +20,108 @@ export interface AlertProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
|
|
|
|
const DEFAULT_TYPE = 'info';
|
|
|
|
|
|
-const IconWrapper = styled('div')`
|
|
|
- display: flex;
|
|
|
- height: calc(${p => p.theme.fontSizeMedium} * ${p => p.theme.text.lineHeightBody});
|
|
|
- margin-right: ${space(1)};
|
|
|
- align-items: center;
|
|
|
-`;
|
|
|
+function Alert({
|
|
|
+ type = DEFAULT_TYPE,
|
|
|
+ showIcon = false,
|
|
|
+ opaque,
|
|
|
+ system,
|
|
|
+ expand,
|
|
|
+ trailingItems,
|
|
|
+ className,
|
|
|
+ children,
|
|
|
+ ...props
|
|
|
+}: AlertProps) {
|
|
|
+ const [isExpanded, setIsExpanded] = useState(false);
|
|
|
+ const showExpand = defined(expand);
|
|
|
+ const showTrailingItems = defined(trailingItems);
|
|
|
+
|
|
|
+ // Show the hover state (with darker borders) only when hovering over the
|
|
|
+ // IconWrapper or MessageContainer.
|
|
|
+ const {hoverProps: iconHoverProps, isHovered: iconIsHovered} = useHover({
|
|
|
+ isDisabled: !showExpand,
|
|
|
+ });
|
|
|
+ const {hoverProps: messageHoverProps, isHovered: messageIsHovered} = useHover({
|
|
|
+ isDisabled: !showExpand,
|
|
|
+ });
|
|
|
+
|
|
|
+ function getIcon() {
|
|
|
+ switch (type) {
|
|
|
+ case 'warning':
|
|
|
+ return <IconWarning />;
|
|
|
+ case 'success':
|
|
|
+ return <IconCheckmark />;
|
|
|
+ case 'error':
|
|
|
+ return <IconNot />;
|
|
|
+ case 'info':
|
|
|
+ default:
|
|
|
+ return <IconInfo />;
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
-const ContentWrapper = styled('div')`
|
|
|
- width: 100%;
|
|
|
-`;
|
|
|
+ function handleClick() {
|
|
|
+ showExpand && setIsExpanded(!isExpanded);
|
|
|
+ }
|
|
|
|
|
|
-const TrailingItems = styled('div')`
|
|
|
- display: grid;
|
|
|
- grid-auto-flow: column;
|
|
|
- grid-template-rows: 100%;
|
|
|
- align-items: center;
|
|
|
- gap: ${space(1)};
|
|
|
- height: calc(${p => p.theme.fontSizeMedium} * ${p => p.theme.text.lineHeightBody});
|
|
|
- margin-left: ${space(1)};
|
|
|
-`;
|
|
|
+ return (
|
|
|
+ <Wrap
|
|
|
+ type={type}
|
|
|
+ system={system}
|
|
|
+ opaque={opaque}
|
|
|
+ expand={expand}
|
|
|
+ hovered={iconIsHovered || messageIsHovered}
|
|
|
+ className={classNames(type ? `ref-${type}` : '', className)}
|
|
|
+ {...props}
|
|
|
+ >
|
|
|
+ {showIcon && (
|
|
|
+ <IconWrapper onClick={handleClick} {...iconHoverProps}>
|
|
|
+ {getIcon()}
|
|
|
+ </IconWrapper>
|
|
|
+ )}
|
|
|
+ <ContentWrapper>
|
|
|
+ <MessageContainer
|
|
|
+ onClick={handleClick}
|
|
|
+ showIcon={showIcon}
|
|
|
+ showTrailingItems={showTrailingItems}
|
|
|
+ {...messageHoverProps}
|
|
|
+ >
|
|
|
+ <Message>{children}</Message>
|
|
|
+ {(showExpand || showTrailingItems) && (
|
|
|
+ <TrailingItemsWrap>
|
|
|
+ <TrailingItems onClick={e => e.stopPropagation()}>
|
|
|
+ {trailingItems}
|
|
|
+ </TrailingItems>
|
|
|
+ {showExpand && (
|
|
|
+ <ExpandIconWrap>
|
|
|
+ <IconChevron direction={isExpanded ? 'up' : 'down'} />
|
|
|
+ </ExpandIconWrap>
|
|
|
+ )}
|
|
|
+ </TrailingItemsWrap>
|
|
|
+ )}
|
|
|
+ </MessageContainer>
|
|
|
+ {isExpanded && (
|
|
|
+ <ExpandContainer>
|
|
|
+ {Array.isArray(expand) ? expand.map(item => item) : expand}
|
|
|
+ </ExpandContainer>
|
|
|
+ )}
|
|
|
+ </ContentWrapper>
|
|
|
+ </Wrap>
|
|
|
+ );
|
|
|
+}
|
|
|
|
|
|
const alertStyles = ({
|
|
|
- theme,
|
|
|
type = DEFAULT_TYPE,
|
|
|
- trailingItems,
|
|
|
system,
|
|
|
opaque,
|
|
|
expand,
|
|
|
-}: AlertProps & {theme: Theme}) => {
|
|
|
- const alertColors = theme.alert[type] ?? theme.alert[DEFAULT_TYPE];
|
|
|
+ hovered,
|
|
|
+ theme,
|
|
|
+}: AlertProps & {theme: Theme; hovered?: boolean}) => {
|
|
|
+ const alertColors = theme.alert[type];
|
|
|
+ const showExpand = defined(expand);
|
|
|
|
|
|
return css`
|
|
|
display: flex;
|
|
|
margin: 0 0 ${space(2)};
|
|
|
- padding: ${space(1.5)} ${space(2)};
|
|
|
font-size: ${theme.fontSizeMedium};
|
|
|
border-radius: ${theme.borderRadius};
|
|
|
border: 1px solid ${alertColors.border};
|
|
@@ -61,8 +129,6 @@ const alertStyles = ({
|
|
|
? `linear-gradient(${alertColors.backgroundLight}, ${alertColors.backgroundLight}), linear-gradient(${theme.background}, ${theme.background})`
|
|
|
: `${alertColors.backgroundLight}`};
|
|
|
|
|
|
- ${defined(trailingItems) && `padding-right: ${space(1.5)};`}
|
|
|
-
|
|
|
a:not([role='button']) {
|
|
|
color: ${theme.textColor};
|
|
|
text-decoration-color: ${theme.translucentBorder};
|
|
@@ -81,21 +147,27 @@ const alertStyles = ({
|
|
|
margin: ${space(0.5)} 0 0;
|
|
|
}
|
|
|
|
|
|
- ${IconWrapper} {
|
|
|
+ ${IconWrapper}, ${ExpandIconWrap} {
|
|
|
color: ${alertColors.iconColor};
|
|
|
}
|
|
|
|
|
|
- ${expand &&
|
|
|
+ ${hovered &&
|
|
|
`
|
|
|
- cursor: pointer;
|
|
|
- &:hover {
|
|
|
- border-color: ${alertColors.borderHover}
|
|
|
- }
|
|
|
- &:hover ${IconWrapper} {
|
|
|
+ border-color: ${alertColors.borderHover};
|
|
|
+ ${IconWrapper}, ${IconChevron} {
|
|
|
color: ${alertColors.iconHoverColor};
|
|
|
}
|
|
|
`}
|
|
|
|
|
|
+ ${showExpand &&
|
|
|
+ `${IconWrapper}, ${MessageContainer} {
|
|
|
+ cursor: pointer;
|
|
|
+ }
|
|
|
+ ${TrailingItems} {
|
|
|
+ cursor: auto;
|
|
|
+ }
|
|
|
+ `}
|
|
|
+
|
|
|
${system &&
|
|
|
`
|
|
|
border-width: 0 0 1px 0;
|
|
@@ -104,92 +176,64 @@ const alertStyles = ({
|
|
|
`;
|
|
|
};
|
|
|
|
|
|
-const StyledTextBlock = styled('span')`
|
|
|
- line-height: ${p => p.theme.text.lineHeightBody};
|
|
|
- position: relative;
|
|
|
- flex: 1;
|
|
|
+const Wrap = styled('div')<AlertProps & {hovered: boolean}>`
|
|
|
+ ${alertStyles}
|
|
|
`;
|
|
|
|
|
|
-const MessageContainer = styled('div')`
|
|
|
+const IconWrapper = styled('div')`
|
|
|
display: flex;
|
|
|
+ height: calc(${p => p.theme.fontSizeMedium} * ${p => p.theme.text.lineHeightBody});
|
|
|
+ padding: ${space(1.5)} ${space(1)} ${space(1.5)} ${space(2)};
|
|
|
+ box-sizing: content-box;
|
|
|
+ align-items: center;
|
|
|
+`;
|
|
|
+
|
|
|
+const ContentWrapper = styled('div')`
|
|
|
width: 100%;
|
|
|
`;
|
|
|
|
|
|
-const ExpandContainer = styled('div')`
|
|
|
+const MessageContainer = styled('div')<{
|
|
|
+ showIcon: boolean;
|
|
|
+ showTrailingItems: boolean;
|
|
|
+}>`
|
|
|
+ display: flex;
|
|
|
+ width: 100%;
|
|
|
+ padding-top: ${space(1.5)};
|
|
|
+ padding-bottom: ${space(1.5)};
|
|
|
+ padding-left: ${p => (p.showIcon ? '0' : space(2))};
|
|
|
+ padding-right: ${p => (p.showTrailingItems ? space(1.5) : space(2))};
|
|
|
+`;
|
|
|
+
|
|
|
+const Message = styled('span')`
|
|
|
+ line-height: ${p => p.theme.text.lineHeightBody};
|
|
|
+ position: relative;
|
|
|
+ flex: 1;
|
|
|
+`;
|
|
|
+
|
|
|
+const TrailingItems = styled('div')`
|
|
|
+ height: calc(${p => p.theme.fontSizeMedium} * ${p => p.theme.text.lineHeightBody});
|
|
|
display: grid;
|
|
|
- padding-top: ${space(1)};
|
|
|
+ grid-auto-flow: column;
|
|
|
+ grid-template-rows: 100%;
|
|
|
+ align-items: center;
|
|
|
+ gap: ${space(1)};
|
|
|
`;
|
|
|
|
|
|
-const ExpandIcon = styled(props => (
|
|
|
- <IconWrapper {...props}>{<IconChevron />}</IconWrapper>
|
|
|
-))`
|
|
|
- transform: ${props => (props.isExpanded ? 'rotate(0deg)' : 'rotate(180deg)')};
|
|
|
- justify-self: flex-end;
|
|
|
+const TrailingItemsWrap = styled(TrailingItems)`
|
|
|
+ margin-left: ${space(1)};
|
|
|
`;
|
|
|
|
|
|
-const Alert = styled(
|
|
|
- ({
|
|
|
- type,
|
|
|
- children,
|
|
|
- className,
|
|
|
- showIcon = false,
|
|
|
- expand,
|
|
|
- trailingItems,
|
|
|
- opaque: _opaque, // don't forward to `div`
|
|
|
- system: _system, // don't forward to `div`
|
|
|
- ...props
|
|
|
- }: AlertProps) => {
|
|
|
- const [isExpanded, setIsExpanded] = useState(false);
|
|
|
- const showExpand = defined(expand);
|
|
|
- const showExpandItems = showExpand && isExpanded;
|
|
|
-
|
|
|
- const getIcon = () => {
|
|
|
- switch (type) {
|
|
|
- case 'warning':
|
|
|
- return <IconWarning />;
|
|
|
- case 'success':
|
|
|
- return <IconCheckmark />;
|
|
|
- case 'error':
|
|
|
- return <IconNot />;
|
|
|
- case 'info':
|
|
|
- default:
|
|
|
- return <IconInfo />;
|
|
|
- }
|
|
|
- };
|
|
|
-
|
|
|
- return (
|
|
|
- <div
|
|
|
- onClick={() => showExpand && setIsExpanded(!isExpanded)}
|
|
|
- className={classNames(type ? `ref-${type}` : '', className)}
|
|
|
- {...props}
|
|
|
- >
|
|
|
- {showIcon && <IconWrapper>{getIcon()}</IconWrapper>}
|
|
|
- <ContentWrapper>
|
|
|
- <MessageContainer>
|
|
|
- <StyledTextBlock>{children}</StyledTextBlock>
|
|
|
- {(showExpand || defined(trailingItems)) && (
|
|
|
- <TrailingItems>
|
|
|
- {trailingItems}
|
|
|
- {showExpand && <ExpandIcon isExpanded={isExpanded} />}
|
|
|
- </TrailingItems>
|
|
|
- )}
|
|
|
- </MessageContainer>
|
|
|
- {showExpandItems && (
|
|
|
- <ExpandContainer>
|
|
|
- {Array.isArray(expand) ? expand.map(item => item) : expand}
|
|
|
- </ExpandContainer>
|
|
|
- )}
|
|
|
- </ContentWrapper>
|
|
|
- </div>
|
|
|
- );
|
|
|
- }
|
|
|
-)<AlertProps>`
|
|
|
- ${alertStyles}
|
|
|
+const ExpandIconWrap = styled('div')`
|
|
|
+ height: 100%;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
`;
|
|
|
|
|
|
-Alert.defaultProps = {
|
|
|
- type: DEFAULT_TYPE,
|
|
|
-};
|
|
|
+const ExpandContainer = styled('div')`
|
|
|
+ display: grid;
|
|
|
+ padding-right: ${space(1.5)};
|
|
|
+ padding-bottom: ${space(1.5)};
|
|
|
+`;
|
|
|
|
|
|
export {alertStyles};
|
|
|
|