Skip to content

Commit

Permalink
ECHOES-292 Add new Inline Message and Callout components
Browse files Browse the repository at this point in the history
  • Loading branch information
jeremy-davis-sonarsource committed Jan 3, 2025
1 parent fd8040d commit 0b78242
Show file tree
Hide file tree
Showing 14 changed files with 811 additions and 4 deletions.
19 changes: 19 additions & 0 deletions i18n/keys.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
{
"inline.message.dismiss": {
"defaultMessage": "Dismiss",
"description": "ARIA-label for the dismiss button at the top of the Modal."
},
"loading": {
"defaultMessage": "Loading...",
"description": "aria-label text, to indicate that there is a spinner rotating in this place"
Expand All @@ -23,6 +27,21 @@
"defaultMessage": "SonarQube Server",
"description": "Alternative text for the SonarQube Server logo"
},
"message.prefix.danger": {
"defaultMessage": "Error:"
},
"message.prefix.discover": {
"defaultMessage": "Hint:"
},
"message.prefix.info": {
"defaultMessage": "Information:"
},
"message.prefix.success": {
"defaultMessage": "Success:"
},
"message.prefix.warning": {
"defaultMessage": "Warning:"
},
"modal.close": {
"defaultMessage": "Close",
"description": "ARIA-label for the close button at the top of the Modal."
Expand Down
1 change: 1 addition & 0 deletions src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export * from './echoes-provider';
export * from './icons';
export * from './links';
export * from './logos';
export * from './messages';
export * from './modals';
export * from './popover';
export * from './radio-button-group';
Expand Down
85 changes: 85 additions & 0 deletions src/components/messages/MessageCallout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/*
* Echoes React
* Copyright (C) 2023-2024 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { forwardRef, ReactNode, useMemo } from 'react';
import { useIntl } from 'react-intl';
import { isDefined } from '~common/helpers/types';
import { MessageDismissButton } from './MessageDismissButton';
import { MessageScreenReaderPrefix } from './MessageScreenReaderPrefix';
import {
MESSAGE_CALLOUT_TYPE_STYLE,
MESSAGE_TYPE_ICON,
MessageCalloutContainer,
MessageCalloutFooter,
MessageCalloutIconWrapper,
MessageCalloutMainContent,
MessageCalloutTextWrapper,
MessageCalloutTitleWrapper,
} from './MessageStyles';
import { MessageType } from './MessageTypes';

export interface MessageProps {
action?: ReactNode;
className?: string;
onDismiss?: VoidFunction;
screenReaderPrefix?: string;
text: ReactNode;
title?: string;
type: MessageType;
}

export const MessageCallout = forwardRef<HTMLDivElement, MessageProps>((props, ref) => {
const { action, className, onDismiss, screenReaderPrefix, text, title, type, ...radixProps } =
props;
const isDismissable = isDefined(onDismiss);

const intl = useIntl();

return (
<MessageCalloutContainer
className={className}
css={useMemo(() => MESSAGE_CALLOUT_TYPE_STYLE[type], [type])}
ref={ref}
{...radixProps}>
<MessageCalloutMainContent>
<MessageCalloutIconWrapper addMargin={isDefined(title)}>
{MESSAGE_TYPE_ICON[type]}
</MessageCalloutIconWrapper>
<MessageCalloutTextWrapper>
<MessageScreenReaderPrefix screenReaderPrefix={screenReaderPrefix} type={type} />
{isDefined(title) && <MessageCalloutTitleWrapper>{title}</MessageCalloutTitleWrapper>}
<div>{text}</div>
</MessageCalloutTextWrapper>

{isDismissable && (
<MessageDismissButton
ariaLabel={intl.formatMessage({
id: 'inline.message.dismiss',
defaultMessage: 'Dismiss',
description: 'ARIA-label for the dismiss button at the top of the Modal.',
})}
onClick={onDismiss}
/>
)}
</MessageCalloutMainContent>
{isDefined(action) && <MessageCalloutFooter>{action}</MessageCalloutFooter>}
</MessageCalloutContainer>
);
});
MessageCallout.displayName = 'MessageCallout';
64 changes: 64 additions & 0 deletions src/components/messages/MessageDismissButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* Echoes React
* Copyright (C) 2023-2024 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import styled from '@emotion/styled';
import { forwardRef } from 'react';
import { useButtonClickHandler } from '../buttons/Button';
import { ButtonStyled } from '../buttons/ButtonStyles';
import { ButtonCommonProps } from '../buttons/ButtonTypes';
import { IconX } from '../icons';
import { Tooltip } from '../tooltip';

interface MessageDismissButtonProps extends Pick<ButtonCommonProps, 'className' | 'onClick'> {
ariaLabel: string;
}

export const MessageDismissButton = forwardRef<HTMLButtonElement, MessageDismissButtonProps>(
(props, ref) => {
const { ariaLabel, onClick, ...htmlProps } = props;

const handleClick = useButtonClickHandler(props);

return (
<Tooltip content={ariaLabel}>
<MessageDismissButtonStyled
{...htmlProps}
aria-label={ariaLabel}
onClick={handleClick}
ref={ref}
type="button">
<IconX />
</MessageDismissButtonStyled>
</Tooltip>
);
},
);
MessageDismissButton.displayName = 'MessageDismissButton';

const MessageDismissButtonStyled = styled(ButtonStyled)`
flex: 0 0 auto;
height: var(--echoes-dimension-height-600);
width: var(--echoes-dimension-width-300);
justify-content: center;
background-color: var(--echoes-color-background-transparent);
border-radius: var(--echoes-border-radius-200);
`;
84 changes: 84 additions & 0 deletions src/components/messages/MessageInline.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/*
* Echoes React
* Copyright (C) 2023-2024 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import styled from '@emotion/styled';
import { forwardRef, PropsWithChildren, useMemo } from 'react';
import { MessageScreenReaderPrefix } from './MessageScreenReaderPrefix';
import { MESSAGE_TYPE_ICON } from './MessageStyles';
import { MessageInlineSize, MessageType } from './MessageTypes';

interface Props {
className?: string;
screenReaderPrefix?: string;
size?: MessageInlineSize;
type: MessageType;
}

export const MessageInline = forwardRef<HTMLDivElement, PropsWithChildren<Props>>((props, ref) => {
const { children, className, screenReaderPrefix, size, type, ...radixProps } = props;
return (
<MessageInlineContainer
className={className}
size={size}
{...radixProps}
css={useMemo(() => MESSAGE_INLINE_TYPE_STYLE[type], [type])}
ref={ref}>
{MESSAGE_TYPE_ICON[type]}
<MessageInlineTextWrapper>
<MessageScreenReaderPrefix screenReaderPrefix={screenReaderPrefix} type={type} />
{children}
</MessageInlineTextWrapper>
</MessageInlineContainer>
);
});
MessageInline.displayName = 'MessageInline';

const MESSAGE_INLINE_TYPE_STYLE = {
[MessageType.Info]: {
'--message-text-color': 'var(--echoes-color-text-info)',
},
[MessageType.Danger]: {
'--message-text-color': 'var(--echoes-color-text-danger)',
},
[MessageType.Warning]: {
'--message-text-color': 'var(--echoes-color-text-warning)',
},
[MessageType.Success]: {
'--message-text-color': 'var(--echoes-color-text-success)',
},
[MessageType.Discover]: {
'--message-text-color': 'var(--echoes-color-text-accent)',
},
};

const MESSAGE_INLINE_FONT = {
[MessageInlineSize.Small]: 'var(--echoes-typography-text-small-medium)',
[MessageInlineSize.Default]: 'var(--echoes-typography-text-default-regular)',
};

const MessageInlineContainer = styled.span<{ size?: MessageInlineSize }>`
${({ size }) => (size ? `font: ${MESSAGE_INLINE_FONT[size]};` : '')}
`;
MessageInlineContainer.displayName = 'MessageInlineContainer';

const MessageInlineTextWrapper = styled.span`
padding-left: var(--echoes-dimension-space-50);
color: var(--message-text-color);
`;
MessageInlineTextWrapper.displayName = 'MessageInlineTextWrapper';
76 changes: 76 additions & 0 deletions src/components/messages/MessageScreenReaderPrefix.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
* Echoes React
* Copyright (C) 2023-2024 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import styled from '@emotion/styled';
import { forwardRef, useMemo } from 'react';
import { useIntl } from 'react-intl';
import { screenReaderOnly } from '~common/helpers/styles';
import { MessageType } from './MessageTypes';

interface Props {
screenReaderPrefix?: string;
type: MessageType;
}

export const MessageScreenReaderPrefix = forwardRef<HTMLSpanElement, Props>((props, ref) => {
const { screenReaderPrefix, type, ...radixProps } = props;
return (
<ScreenReaderPrefix ref={ref} {...radixProps}>
{screenReaderPrefix ?? <MessagePrefix type={type} />}
</ScreenReaderPrefix>
);
});
MessageScreenReaderPrefix.displayName = 'MessageScreenReaderPrefix';

function MessagePrefix({ type }: { type: MessageType }) {
const intl = useIntl();

const messages: { [type in MessageType]: string } = useMemo(
() => ({
[MessageType.Info]: intl.formatMessage({
id: 'message.prefix.info',
defaultMessage: 'Information:',
}),
[MessageType.Danger]: intl.formatMessage({
id: 'message.prefix.danger',
defaultMessage: 'Error:',
}),
[MessageType.Warning]: intl.formatMessage({
id: 'message.prefix.warning',
defaultMessage: 'Warning:',
}),
[MessageType.Discover]: intl.formatMessage({
id: 'message.prefix.discover',
defaultMessage: 'Hint:',
}),
[MessageType.Success]: intl.formatMessage({
id: 'message.prefix.success',
defaultMessage: 'Success:',
}),
}),
[intl],
);

return messages[type];
}

const ScreenReaderPrefix = styled.span`
${screenReaderOnly}
`;
ScreenReaderPrefix.displayName = 'ScreenReaderPrefix';
Loading

0 comments on commit 0b78242

Please sign in to comment.