diff --git a/CHANGELOG.md b/CHANGELOG.md index 2101ee1f2f..1423d4efa7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,9 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ### Fixes - Add aria posinset and setsize, hide menu indicator from narration @jurokapsiar ([#1066](https://github.com/stardust-ui/react/pull/1066)) +### Features +- Add `Alert` component @Bugaa92 ([#1063](https://github.com/stardust-ui/react/pull/1063)) + ## [v0.23.1](https://github.com/stardust-ui/react/tree/v0.23.1) (2019-03-13) [Compare changes](https://github.com/stardust-ui/react/compare/v0.23.0...v0.23.1) diff --git a/docs/src/examples/components/Alert/Rtl/AlertExample.rtl.tsx b/docs/src/examples/components/Alert/Rtl/AlertExample.rtl.tsx new file mode 100644 index 0000000000..d5df3c8a65 --- /dev/null +++ b/docs/src/examples/components/Alert/Rtl/AlertExample.rtl.tsx @@ -0,0 +1,6 @@ +import * as React from 'react' +import { Alert } from '@stardust-ui/react' + +const AlertExampleRtl = () => + +export default AlertExampleRtl diff --git a/docs/src/examples/components/Alert/Rtl/index.tsx b/docs/src/examples/components/Alert/Rtl/index.tsx new file mode 100644 index 0000000000..56b1579a6f --- /dev/null +++ b/docs/src/examples/components/Alert/Rtl/index.tsx @@ -0,0 +1,12 @@ +import * as React from 'react' + +import ComponentExample from 'docs/src/components/ComponentDoc/ComponentExample' +import NonPublicSection from 'docs/src/components/ComponentDoc/NonPublicSection' + +const Rtl = () => ( + + + +) + +export default Rtl diff --git a/docs/src/examples/components/Alert/Types/AlertExample.shorthand.tsx b/docs/src/examples/components/Alert/Types/AlertExample.shorthand.tsx new file mode 100644 index 0000000000..68011a6e89 --- /dev/null +++ b/docs/src/examples/components/Alert/Types/AlertExample.shorthand.tsx @@ -0,0 +1,6 @@ +import * as React from 'react' +import { Alert } from '@stardust-ui/react' + +const AlertExample = () => + +export default AlertExample diff --git a/docs/src/examples/components/Alert/Types/AlertExample.tsx b/docs/src/examples/components/Alert/Types/AlertExample.tsx new file mode 100644 index 0000000000..2fa5c3c5cf --- /dev/null +++ b/docs/src/examples/components/Alert/Types/AlertExample.tsx @@ -0,0 +1,6 @@ +import * as React from 'react' +import { Alert } from '@stardust-ui/react' + +const AlertExample = () => This is a default alert + +export default AlertExample diff --git a/docs/src/examples/components/Alert/Types/index.tsx b/docs/src/examples/components/Alert/Types/index.tsx new file mode 100644 index 0000000000..5a88ffd819 --- /dev/null +++ b/docs/src/examples/components/Alert/Types/index.tsx @@ -0,0 +1,16 @@ +import * as React from 'react' + +import ComponentExample from 'docs/src/components/ComponentDoc/ComponentExample' +import ExampleSection from 'docs/src/components/ComponentDoc/ExampleSection' + +const Types = () => ( + + + +) + +export default Types diff --git a/docs/src/examples/components/Alert/Variations/AlertExampleAction.shorthand.tsx b/docs/src/examples/components/Alert/Variations/AlertExampleAction.shorthand.tsx new file mode 100644 index 0000000000..36c358d092 --- /dev/null +++ b/docs/src/examples/components/Alert/Variations/AlertExampleAction.shorthand.tsx @@ -0,0 +1,19 @@ +import * as React from 'react' +import { Alert } from '@stardust-ui/react' + +class AlertExampleClosable extends React.Component<{}, { open: boolean }> { + state = { open: true } + + render() { + return ( + this.state.open && ( + this.setState({ open: false }) }} + /> + ) + ) + } +} + +export default AlertExampleClosable diff --git a/docs/src/examples/components/Alert/Variations/AlertExampleAttached.shorthand.tsx b/docs/src/examples/components/Alert/Variations/AlertExampleAttached.shorthand.tsx new file mode 100644 index 0000000000..b0f644fb2f --- /dev/null +++ b/docs/src/examples/components/Alert/Variations/AlertExampleAttached.shorthand.tsx @@ -0,0 +1,14 @@ +import * as React from 'react' +import { Alert, Input } from '@stardust-ui/react' + +const AlertExampleShorthand = () => ( + <> + + +

+ + + +) + +export default AlertExampleShorthand diff --git a/docs/src/examples/components/Alert/Variations/AlertExampleAttached.tsx b/docs/src/examples/components/Alert/Variations/AlertExampleAttached.tsx new file mode 100644 index 0000000000..5e6e521d35 --- /dev/null +++ b/docs/src/examples/components/Alert/Variations/AlertExampleAttached.tsx @@ -0,0 +1,14 @@ +import * as React from 'react' +import { Alert, Input } from '@stardust-ui/react' + +const AlertExampleShorthand = () => ( + <> + This is a top attached alert + +

+ + This is a bottom attached alert + +) + +export default AlertExampleShorthand diff --git a/docs/src/examples/components/Alert/Variations/AlertExampleDanger.shorthand.tsx b/docs/src/examples/components/Alert/Variations/AlertExampleDanger.shorthand.tsx new file mode 100644 index 0000000000..3e221e90b5 --- /dev/null +++ b/docs/src/examples/components/Alert/Variations/AlertExampleDanger.shorthand.tsx @@ -0,0 +1,6 @@ +import * as React from 'react' +import { Alert } from '@stardust-ui/react' + +const AlertExampleDanger = () => + +export default AlertExampleDanger diff --git a/docs/src/examples/components/Alert/Variations/AlertExampleDanger.tsx b/docs/src/examples/components/Alert/Variations/AlertExampleDanger.tsx new file mode 100644 index 0000000000..ad75bdef22 --- /dev/null +++ b/docs/src/examples/components/Alert/Variations/AlertExampleDanger.tsx @@ -0,0 +1,6 @@ +import * as React from 'react' +import { Alert } from '@stardust-ui/react' + +const AlertExampleDanger = () => This is a danger alert + +export default AlertExampleDanger diff --git a/docs/src/examples/components/Alert/Variations/AlertExampleInfo.shorthand.tsx b/docs/src/examples/components/Alert/Variations/AlertExampleInfo.shorthand.tsx new file mode 100644 index 0000000000..1a84904c9d --- /dev/null +++ b/docs/src/examples/components/Alert/Variations/AlertExampleInfo.shorthand.tsx @@ -0,0 +1,6 @@ +import * as React from 'react' +import { Alert } from '@stardust-ui/react' + +const AlertExampleInfo = () => + +export default AlertExampleInfo diff --git a/docs/src/examples/components/Alert/Variations/AlertExampleInfo.tsx b/docs/src/examples/components/Alert/Variations/AlertExampleInfo.tsx new file mode 100644 index 0000000000..a0a9f1900b --- /dev/null +++ b/docs/src/examples/components/Alert/Variations/AlertExampleInfo.tsx @@ -0,0 +1,6 @@ +import * as React from 'react' +import { Alert } from '@stardust-ui/react' + +const AlertExampleInfo = () => This is an informational alert + +export default AlertExampleInfo diff --git a/docs/src/examples/components/Alert/Variations/AlertExampleSuccess.shorthand.tsx b/docs/src/examples/components/Alert/Variations/AlertExampleSuccess.shorthand.tsx new file mode 100644 index 0000000000..f434b3d111 --- /dev/null +++ b/docs/src/examples/components/Alert/Variations/AlertExampleSuccess.shorthand.tsx @@ -0,0 +1,6 @@ +import * as React from 'react' +import { Alert } from '@stardust-ui/react' + +const AlertExampleSuccess = () => + +export default AlertExampleSuccess diff --git a/docs/src/examples/components/Alert/Variations/AlertExampleSuccess.tsx b/docs/src/examples/components/Alert/Variations/AlertExampleSuccess.tsx new file mode 100644 index 0000000000..a6006b0aa9 --- /dev/null +++ b/docs/src/examples/components/Alert/Variations/AlertExampleSuccess.tsx @@ -0,0 +1,6 @@ +import * as React from 'react' +import { Alert } from '@stardust-ui/react' + +const AlertExampleSuccess = () => This is a success alert + +export default AlertExampleSuccess diff --git a/docs/src/examples/components/Alert/Variations/AlertExampleWarning.shorthand.tsx b/docs/src/examples/components/Alert/Variations/AlertExampleWarning.shorthand.tsx new file mode 100644 index 0000000000..9cce695bc7 --- /dev/null +++ b/docs/src/examples/components/Alert/Variations/AlertExampleWarning.shorthand.tsx @@ -0,0 +1,6 @@ +import * as React from 'react' +import { Alert } from '@stardust-ui/react' + +const AlertExampleWarning = () => + +export default AlertExampleWarning diff --git a/docs/src/examples/components/Alert/Variations/AlertExampleWarning.tsx b/docs/src/examples/components/Alert/Variations/AlertExampleWarning.tsx new file mode 100644 index 0000000000..859403edde --- /dev/null +++ b/docs/src/examples/components/Alert/Variations/AlertExampleWarning.tsx @@ -0,0 +1,6 @@ +import * as React from 'react' +import { Alert } from '@stardust-ui/react' + +const AlertExampleWarning = () => This is a warning alert + +export default AlertExampleWarning diff --git a/docs/src/examples/components/Alert/Variations/index.tsx b/docs/src/examples/components/Alert/Variations/index.tsx new file mode 100644 index 0000000000..14cd1fe4a1 --- /dev/null +++ b/docs/src/examples/components/Alert/Variations/index.tsx @@ -0,0 +1,40 @@ +import * as React from 'react' +import ComponentExample from 'docs/src/components/ComponentDoc/ComponentExample' +import ExampleSection from 'docs/src/components/ComponentDoc/ExampleSection' + +const Variations = () => ( + + + + + + + + +) + +export default Variations diff --git a/docs/src/examples/components/Alert/index.tsx b/docs/src/examples/components/Alert/index.tsx new file mode 100644 index 0000000000..c184793bbf --- /dev/null +++ b/docs/src/examples/components/Alert/index.tsx @@ -0,0 +1,15 @@ +import * as React from 'react' + +import Rtl from './Rtl' +import Types from './Types' +import Variations from './Variations' + +const AlertExamples = () => ( + <> + + + + +) + +export default AlertExamples diff --git a/packages/react/src/components/Alert/Alert.tsx b/packages/react/src/components/Alert/Alert.tsx new file mode 100644 index 0000000000..745d3c4133 --- /dev/null +++ b/packages/react/src/components/Alert/Alert.tsx @@ -0,0 +1,119 @@ +import * as _ from 'lodash' +import * as PropTypes from 'prop-types' +import * as React from 'react' + +import { + UIComponent, + UIComponentProps, + ContentComponentProps, + commonPropTypes, + customPropTypes, + childrenExist, + rtlTextContainer, +} from '../../lib' +import { RenderResultConfig } from 'src/lib/renderComponent' +import { defaultBehavior } from '../../lib/accessibility' +import { Accessibility } from '../../lib/accessibility/types' +import { ReactProps, ShorthandValue } from '../../types' +import Box from '../Box/Box' +import Button, { ButtonProps } from '../Button/Button' + +export interface AlertSlotClassNames { + content: string + action: string +} + +export interface AlertProps extends UIComponentProps, ContentComponentProps { + /** + * Accessibility behavior if overridden by the user. + * @default defaultBehavior + */ + accessibility?: Accessibility + + /** Button shorthand for the action slot. */ + action?: ShorthandValue + + /** Controls Alert's relation to neighboring items. */ + attached?: boolean | 'top' | 'bottom' + + /** An alert may be formatted to display a danger message. */ + danger?: boolean + + /** An alert may be formatted to display information. */ + info?: boolean + + /** An alert may be formatted to display a successful message. */ + success?: boolean + + /** An alert may be formatted to display a warning message. */ + warning?: boolean +} + +/** + * A Alert displays information that explains nearby content. + * @accessibility + * Other considerations: + * - if Alert contains action slot, textual representation needs to be provided by using 'title', 'aria-label' or 'aria-labelledby' attributes + */ +class Alert extends UIComponent> { + static displayName = 'Alert' + static className = 'ui-alert' + + static slotClassNames: AlertSlotClassNames = { + content: `${Alert.className}__content`, + action: `${Alert.className}__action`, + } + + static propTypes = { + ...commonPropTypes.createCommon({ content: 'shorthand' }), + action: customPropTypes.itemShorthand, + attached: PropTypes.oneOfType([PropTypes.bool, PropTypes.oneOf(['top', 'bottom'])]), + danger: PropTypes.bool, + info: PropTypes.bool, + success: PropTypes.bool, + warning: PropTypes.bool, + } + + static defaultProps = { accessibility: defaultBehavior } + + renderContent = ({ styles }: RenderResultConfig) => { + const { action, content } = this.props + + return ( + <> + {Box.create(content, { + defaultProps: { + className: Alert.slotClassNames.content, + styles: styles.content, + }, + })} + {Button.create(action, { + defaultProps: { + iconOnly: true, + text: true, + className: Alert.slotClassNames.action, + styles: styles.action, + }, + })} + + ) + } + + renderComponent(config: RenderResultConfig) { + const { accessibility, classes, ElementType, unhandledProps } = config + const { children, content } = this.props + + return ( + + {childrenExist(children) ? children : this.renderContent(config)} + + ) + } +} + +export default Alert diff --git a/packages/react/src/components/Button/Button.tsx b/packages/react/src/components/Button/Button.tsx index 767ca05c4f..b76cb848eb 100644 --- a/packages/react/src/components/Button/Button.tsx +++ b/packages/react/src/components/Button/Button.tsx @@ -82,7 +82,7 @@ export interface ButtonState { * @accessibility * Other considerations: * - for disabled buttons, add 'disabled' attribute so that the state is properly recognized by the screen reader - * - if button includes icon only, textual representation needs to be provided by using 'title', 'aria-label', or 'aria-labelledby' attributes + * - if button includes icon only, textual representation needs to be provided by using 'title', 'aria-label' or 'aria-labelledby' attributes */ class Button extends UIComponent, ButtonState> { static create: Function diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index ec4a1463a7..eaf525dab5 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -12,6 +12,8 @@ export * from './themes/types' export { default as Accordion, AccordionProps } from './components/Accordion/Accordion' +export { default as Alert, AlertProps } from './components/Alert/Alert' + export { default as Attachment, AttachmentProps } from './components/Attachment/Attachment' export { default as Avatar, AvatarProps } from './components/Avatar/Avatar' diff --git a/packages/react/src/themes/teams/componentStyles.ts b/packages/react/src/themes/teams/componentStyles.ts index 639529a9f9..c69164a190 100644 --- a/packages/react/src/themes/teams/componentStyles.ts +++ b/packages/react/src/themes/teams/componentStyles.ts @@ -2,6 +2,8 @@ export { default as Accordion } from './components/Accordion/accordionStyles' export { default as AccordionContent } from './components/Accordion/accordionContentStyles' export { default as AccordionTitle } from './components/Accordion/accordionTitleStyles' +export { default as Alert } from './components/Alert/alertStyles' + export { default as Attachment } from './components/Attachment/attachmentStyles' export { default as Avatar } from './components/Avatar/avatarStyles' diff --git a/packages/react/src/themes/teams/componentVariables.ts b/packages/react/src/themes/teams/componentVariables.ts index 02e2dc6621..61a93c4583 100644 --- a/packages/react/src/themes/teams/componentVariables.ts +++ b/packages/react/src/themes/teams/componentVariables.ts @@ -2,6 +2,8 @@ export { default as AccordionContent } from './components/Accordion/accordionCon export { default as Attachment } from './components/Attachment/attachmentVariables' +export { default as Alert } from './components/Alert/alertVariables' + export { default as Avatar } from './components/Avatar/avatarVariables' export { default as Button } from './components/Button/buttonVariables' diff --git a/packages/react/src/themes/teams/components/Alert/alertStyles.ts b/packages/react/src/themes/teams/components/Alert/alertStyles.ts new file mode 100644 index 0000000000..49935fb345 --- /dev/null +++ b/packages/react/src/themes/teams/components/Alert/alertStyles.ts @@ -0,0 +1,92 @@ +import * as React from 'react' +import { + ComponentSlotStylesInput, + ICSSInJSStyle, + SiteVariablesPrepared, + NaturalColors, +} from '../../../types' +import { AlertProps } from '../../../../components/Alert/Alert' +import { AlertVariables } from './alertVariables' + +const getIntentColorsFromProps = ( + p: AlertProps, + v: AlertVariables, + siteVars: SiteVariablesPrepared, +): React.CSSProperties => { + const { colors } = siteVars + const naturalColors: NaturalColors = siteVars.naturalColors + + if (p.danger) { + return { + color: siteVars.red, + backgroundColor: siteVars.red10, + borderColor: siteVars.red08, + } + } + + if (p.info) { + return { + color: colors.grey[900], + backgroundColor: siteVars.gray09, + borderColor: siteVars.gray08, + } + } + + if (p.success) { + return { + color: colors.green[900], // $app-green-04 + backgroundColor: colors.grey[50], // $app-white + borderColor: naturalColors.lightGreen[900], // $app-green + } + } + + if (p.warning) { + return { + color: siteVars.gray03, + backgroundColor: colors.grey[50], // $app-white + borderColor: colors.yellow[900], // $app-yellow + } + } + + return { + color: v.color, + backgroundColor: v.backgroundColor, + borderColor: v.borderColor, + } +} + +const alertStyles: ComponentSlotStylesInput = { + root: ({ props: p, variables: v, theme: { siteVariables } }): ICSSInJSStyle => ({ + display: 'flex', + alignItems: 'center', + position: 'relative', + width: '100%', + boxSizing: 'border-box', + border: v.border, + borderRadius: v.borderRadius, + minHeight: v.minHeight, + padding: v.padding, + fontWeight: v.fontWeight, + + ...getIntentColorsFromProps(p, v, siteVariables), + + ...((p.attached === 'top' || p.attached === true) && { + borderRadius: `${v.borderRadius} ${v.borderRadius} 0 0`, + }), + + ...(p.attached === 'bottom' && { borderRadius: `0 0 ${v.borderRadius} ${v.borderRadius}` }), + }), + + content: (): ICSSInJSStyle => ({ + flexGrow: 1, + }), + + action: ({ props: p, variables: v, theme: { siteVariables } }): ICSSInJSStyle => ({ + height: v.actionSize, + minWidth: v.actionSize, + color: v.actionColor || 'currentColor', + ...(p.info && { color: siteVariables.gray02 }), + }), +} + +export default alertStyles diff --git a/packages/react/src/themes/teams/components/Alert/alertVariables.ts b/packages/react/src/themes/teams/components/Alert/alertVariables.ts new file mode 100644 index 0000000000..ee1c5ed1c9 --- /dev/null +++ b/packages/react/src/themes/teams/components/Alert/alertVariables.ts @@ -0,0 +1,37 @@ +import { FontWeightProperty } from 'csstype' + +import { pxToRem } from '../../../../lib' +import { SiteVariablesPrepared } from 'src/themes/types' + +export interface AlertVariables { + border: string + borderRadius: string + backgroundColor: string + borderColor: string + color: string + fontWeight: FontWeightProperty + minHeight: string + padding: string + + actionSize: string + actionColor: string +} + +export default (siteVars: SiteVariablesPrepared): AlertVariables => { + const alertHeight = 28 + const borderSize = 1 + + return { + border: `${pxToRem(borderSize)} solid`, + borderRadius: pxToRem(3), + backgroundColor: siteVars.colors.grey[50], // $app-white + borderColor: siteVars.gray06, + color: siteVars.gray02, + fontWeight: siteVars.fontWeightSemibold, + minHeight: pxToRem(alertHeight), + padding: `0 0 0 ${pxToRem(16)}`, + + actionSize: pxToRem(alertHeight - 2 * borderSize), + actionColor: undefined, + } +} diff --git a/packages/react/src/themes/teams/siteVariables.ts b/packages/react/src/themes/teams/siteVariables.ts index 5225ae026f..36f46dec74 100644 --- a/packages/react/src/themes/teams/siteVariables.ts +++ b/packages/react/src/themes/teams/siteVariables.ts @@ -25,7 +25,10 @@ export const brand08 = '#8B8CC7' // no mapping color export const magenta = '#B24782' // no mapping color export const orchid = '#943670' // no mapping color -export const red08 = '#F3D6DB' // no mapping color + +export const red = colors.red[900] // no mapping color $app-red +export const red08 = '#F3D6DB' // no mapping color $app-red-08 +export const red10 = '#FCF4F6' // no mapping color $app-red-10 // // SHADOW LEVELS diff --git a/packages/react/src/themes/types.ts b/packages/react/src/themes/types.ts index 6983bad295..9a0aed8ef9 100644 --- a/packages/react/src/themes/types.ts +++ b/packages/react/src/themes/types.ts @@ -294,6 +294,7 @@ export interface ThemeComponentStylesInput { [key: string]: ComponentSlotStylesInput | undefined Accordion?: ComponentSlotStylesInput + Alert?: ComponentSlotStylesInput Animation?: ComponentSlotStylesInput Attachment?: ComponentSlotStylesInput Avatar?: ComponentSlotStylesInput @@ -337,6 +338,7 @@ export interface ThemeComponentStylesPrepared { [key: string]: ComponentSlotStylesPrepared | undefined Accordion?: ComponentSlotStylesPrepared + Alert?: ComponentSlotStylesPrepared Animation?: ComponentSlotStylesPrepared Attachment?: ComponentSlotStylesPrepared Avatar?: ComponentSlotStylesPrepared @@ -380,6 +382,7 @@ export interface ThemeComponentVariablesInput { [key: string]: any Accordion?: ComponentVariablesInput + Alert?: ComponentVariablesInput Animation?: ComponentVariablesInput Attachment?: ComponentVariablesInput Avatar?: ComponentVariablesInput @@ -421,6 +424,7 @@ export interface ThemeComponentVariablesPrepared { [key: string]: any Accordion?: ComponentVariablesPrepared + Alert?: ComponentVariablesPrepared Animation?: ComponentVariablesPrepared Attachment?: ComponentVariablesPrepared Avatar?: ComponentVariablesPrepared diff --git a/packages/react/test/specs/components/Alert/Alert-test.tsx b/packages/react/test/specs/components/Alert/Alert-test.tsx new file mode 100644 index 0000000000..91956a688f --- /dev/null +++ b/packages/react/test/specs/components/Alert/Alert-test.tsx @@ -0,0 +1,14 @@ +import { isConformant, implementsShorthandProp } from 'test/specs/commonTests' + +import Alert from 'src/components/Alert/Alert' +import Box from 'src/components/Box/Box' +import Button from 'src/components/Button/Button' + +const alertImplementsShorthandProp = implementsShorthandProp(Alert) + +describe('Alert', () => { + isConformant(Alert) + + alertImplementsShorthandProp('action', Button, { mapsValueToProp: 'content' }) + alertImplementsShorthandProp('content', Box, { mapsValueToProp: 'children' }) +}) diff --git a/packages/react/test/specs/components/Label/Label-test.tsx b/packages/react/test/specs/components/Label/Label-test.tsx index 77e0ea915a..a6ec7f2ba7 100644 --- a/packages/react/test/specs/components/Label/Label-test.tsx +++ b/packages/react/test/specs/components/Label/Label-test.tsx @@ -1,9 +1,8 @@ -import { isConformant } from 'test/specs/commonTests' +import { isConformant, implementsShorthandProp } from 'test/specs/commonTests' import Label from 'src/components/Label/Label' import Icon from 'src/components/Icon/Icon' import Image from 'src/components/Image/Image' -import { implementsShorthandProp } from '../../commonTests' const labelImplementsShorthandProp = implementsShorthandProp(Label)