diff --git a/.cfignore b/.cfignore new file mode 100644 index 000000000000..ab4a5d7aade6 --- /dev/null +++ b/.cfignore @@ -0,0 +1,4 @@ +* + +!Staticfile +!build \ No newline at end of file diff --git a/.gitignore b/.gitignore index 289530fd7293..865b904bcd36 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -# Build folders + # Build folders /build packages/*/build/ packages/*/examples/*/build/ @@ -53,3 +53,6 @@ results/ # Directories generated by Next.js .next + +# Contains Github personal access token +.env.local diff --git a/Staticfile b/Staticfile new file mode 100644 index 000000000000..78ab427d0e8d --- /dev/null +++ b/Staticfile @@ -0,0 +1 @@ +root: build \ No newline at end of file diff --git a/manifest.yml b/manifest.yml new file mode 100644 index 000000000000..3bdd00cadae1 --- /dev/null +++ b/manifest.yml @@ -0,0 +1,5 @@ +--- +applications: + - name: input-validation-animation + memory: 64M + buildpack: https://github.com/cloudfoundry/staticfile-buildpack.git \ No newline at end of file diff --git a/packages/components/src/components/multi-select/_multi-select.scss b/packages/components/src/components/multi-select/_multi-select.scss index 5f11bd2464db..9632beeb4d2d 100644 --- a/packages/components/src/components/multi-select/_multi-select.scss +++ b/packages/components/src/components/multi-select/_multi-select.scss @@ -103,6 +103,33 @@ } } +.bx--multi-select--invalid { + animation: shake $duration--slow-02 motion(standard, productive) both; +} + +@keyframes shake { + 10%, + 90% { + transform: translate3d(-1px, 0, 0); + } + + 20%, + 80% { + transform: translate3d(2px, 0, 0); + } + + 30%, + 50%, + 70% { + transform: translate3d(-4px, 0, 0); + } + + 40%, + 60% { + transform: translate3d(4px, 0, 0); + } +} + @include exports('multi-select') { @include multiselect; } diff --git a/packages/components/src/components/progress-indicator/_progress-indicator.scss b/packages/components/src/components/progress-indicator/_progress-indicator.scss index 8564c19342db..99ea46b5c593 100644 --- a/packages/components/src/components/progress-indicator/_progress-indicator.scss +++ b/packages/components/src/components/progress-indicator/_progress-indicator.scss @@ -16,6 +16,7 @@ /// Progress indicator styles /// @access private /// @group progress-indicator + @mixin progress-indicator { .#{$prefix}--progress { @include reset; @@ -31,6 +32,7 @@ width: rem(128px); min-width: 7rem; flex-direction: row; + border-top: 2px solid $ui-03; .#{$prefix}--tooltip__label { display: block; @@ -45,14 +47,15 @@ .#{$prefix}--progress-line { position: absolute; left: 0; - width: rem(128px); - height: 1px; - border: $progress-indicator-bar-width; + width: 0; + height: 2px; + transform: translateY(-2px); + transition: width $duration--moderate-01 motion(standard, productive) + $duration--moderate-01; } .#{$prefix}--progress--space-equal .#{$prefix}--progress-line { - width: 100%; - min-width: rem(128px); + width: 0; } .#{$prefix}--progress-step svg { @@ -130,7 +133,6 @@ display: block; width: rem(125px); - min-width: rem(115px); min-height: $carbon--spacing-06; padding: $carbon--spacing-03 $carbon--spacing-05; @@ -157,8 +159,12 @@ left: 0; margin-top: rem(28px); margin-left: $carbon--spacing-06; + animation: $duration--moderate-01 optional-text-entrance + $duration--moderate-01 motion(standard, productive) forwards; color: $text-02; + opacity: 0; text-align: start; + transform: translateY(-10px); } //CURRENT STYLING @@ -166,6 +172,12 @@ .#{$prefix}--progress-line { background-color: $interactive-04; } + + svg circle { + animation: $duration--moderate-01 progress-indicator-icon-fade + $duration--moderate-01 motion(standard, productive) forwards; + opacity: 0; + } } //INCOMPLETE STYLING @@ -182,8 +194,15 @@ //COMPLETED STYLING .#{$prefix}--progress-step--complete { .#{$prefix}--progress-line { + width: 100%; background-color: $interactive-04; } + + svg { + animation: $duration--moderate-01 progress-indicator-icon-fade + motion(standard, productive) forwards; + opacity: 0; + } } //interactive button @@ -224,6 +243,12 @@ fill: $disabled; } + svg circle { + animation: $duration--moderate-01 progress-indicator-icon-fade + $duration--moderate-01 motion(standard, productive) forwards; + opacity: 0; + } + .#{$prefix}--progress-label, .#{$prefix}--progress-label:hover { box-shadow: none; @@ -263,10 +288,20 @@ } // Vertical Variant + .#{$prefix}--progress--vertical .#{$prefix}--progress-step { + border-top: none; + } .#{$prefix}--progress--vertical { display: flex; flex-direction: column; + border-left: 2px solid $ui-03; + + .#{$prefix}--progress-step--complete { + .#{$prefix}--progress-line { + height: 100%; + } + } } .#{$prefix}--progress--vertical, @@ -315,8 +350,81 @@ position: absolute; top: 0; left: 0; - width: 1px; - height: 100%; + width: 2px; + height: 0; + background-color: $interactive; + transform: translateX(-2px); + transition: height $duration--moderate-01 cubic-bezier(0.2, 0, 0.38, 0.9); + } + + // No animations or transitions if user prefers reduced motion + @media (prefers-reduced-motion) { + .#{$prefix}--progress, + .#{$prefix}--progress-step { + border-top: none; + } + + .#{$prefix}--progress-line { + width: rem(128px); + height: 2px; + transform: none; + transition: none; + } + + .#{$prefix}--progress--space-equal .#{$prefix}--progress-line { + width: 100%; + min-width: rem(128px); + } + + .#{$prefix}--progress-step svg { + animation: none; + opacity: 1; + transform: none; + } + + .#{$prefix}--progress-step--current { + svg circle { + animation: none; + opacity: 1; + } + } + + .#{$prefix}--progress-step--complete { + svg { + animation: none; + opacity: 1; + } + } + + .#{$prefix}--progress-step--disabled { + svg circle { + animation: none; + opacity: 1; + } + } + + .#{$prefix}--progress--vertical { + border-left: none; + } + + .#{$prefix}--progress--vertical .#{$prefix}--progress-line { + transform: none; + transition: none; + } + } +} + +// Animations +@keyframes optional-text-entrance { + to { + opacity: 1; + transform: none; + } +} + +@keyframes progress-indicator-icon-fade { + to { + opacity: 1; } } diff --git a/packages/components/src/components/select/_select.scss b/packages/components/src/components/select/_select.scss index ebc21d7bb7a1..b69e24893c5b 100644 --- a/packages/components/src/components/select/_select.scss +++ b/packages/components/src/components/select/_select.scss @@ -264,6 +264,33 @@ } } +.bx--select--invalid .bx--select-input__wrapper { + animation: shake 820ms cubic-bezier(0.36, 0.07, 0.19, 0.97) both; +} + +@keyframes shake { + 10%, + 90% { + transform: translate3d(-1px, 0, 0); + } + + 20%, + 80% { + transform: translate3d(2px, 0, 0); + } + + 30%, + 50%, + 70% { + transform: translate3d(-4px, 0, 0); + } + + 40%, + 60% { + transform: translate3d(4px, 0, 0); + } +} + @include exports('select') { @include select; } diff --git a/packages/components/src/components/text-area/_text-area.scss b/packages/components/src/components/text-area/_text-area.scss index aa35058b5330..4dc42e0413cd 100644 --- a/packages/components/src/components/text-area/_text-area.scss +++ b/packages/components/src/components/text-area/_text-area.scss @@ -101,6 +101,33 @@ } } +.bx--text-area--invalid { + animation: shake 820ms cubic-bezier(0.36, 0.07, 0.19, 0.97) both; +} + +@keyframes shake { + 10%, + 90% { + transform: translate3d(-1px, 0, 0); + } + + 20%, + 80% { + transform: translate3d(2px, 0, 0); + } + + 30%, + 50%, + 70% { + transform: translate3d(-4px, 0, 0); + } + + 40%, + 60% { + transform: translate3d(4px, 0, 0); + } +} + @include exports('text-area') { @include text-area; } diff --git a/packages/components/src/components/time-picker/_time-picker.scss b/packages/components/src/components/time-picker/_time-picker.scss index 09377d37b2ef..42ab3b7bcaea 100644 --- a/packages/components/src/components/time-picker/_time-picker.scss +++ b/packages/components/src/components/time-picker/_time-picker.scss @@ -93,6 +93,34 @@ } } +.bx--time-picker--invalid .bx--time-picker__input { + z-index: 10; + animation: shake 820ms cubic-bezier(0.36, 0.07, 0.19, 0.97) both; +} + +@keyframes shake { + 10%, + 90% { + transform: translate3d(-1px, 0, 0); + } + + 20%, + 80% { + transform: translate3d(2px, 0, 0); + } + + 30%, + 50%, + 70% { + transform: translate3d(-4px, 0, 0); + } + + 40%, + 60% { + transform: translate3d(4px, 0, 0); + } +} + @include exports('time-picker') { @include time-picker; } diff --git a/packages/react/manifest.yml b/packages/react/manifest.yml index 05b6ae612d29..d8b925d6deae 100644 --- a/packages/react/manifest.yml +++ b/packages/react/manifest.yml @@ -1,8 +1,15 @@ +# --- +# applications: +# - name: carbon-storybook +# memory: 64M +# buildpack: https://github.com/cloudfoundry/staticfile-buildpack.git +# routes: +# - route: react.carbondesignsystem.com +# - route: carbon-react-storybook.mybluemix.net + + --- applications: -- name: carbon-storybook - memory: 64M - buildpack: https://github.com/cloudfoundry/staticfile-buildpack.git - routes: - - route: react.carbondesignsystem.com - - route: carbon-react-storybook.mybluemix.net + - name: input-validation-animation + memory: 64M + buildpack: https://github.com/cloudfoundry/staticfile-buildpack.git \ No newline at end of file diff --git a/packages/react/src/components/ProgressIndicator/ProgressIndicator-story.js b/packages/react/src/components/ProgressIndicator/ProgressIndicator-story.js index 702911855a52..dba375973787 100644 --- a/packages/react/src/components/ProgressIndicator/ProgressIndicator-story.js +++ b/packages/react/src/components/ProgressIndicator/ProgressIndicator-story.js @@ -6,7 +6,7 @@ */ import React from 'react'; -import { withKnobs, number, boolean, text } from '@storybook/addon-knobs'; +import { withKnobs, number, boolean } from '@storybook/addon-knobs'; import { action } from '@storybook/addon-actions'; import { ProgressIndicator, ProgressStep } from '../ProgressIndicator'; import ProgressIndicatorSkeleton from '../ProgressIndicator/ProgressIndicator.Skeleton'; @@ -28,65 +28,77 @@ export default { subcomponents: { ProgressStep, + Tooltip, }, }, }; +const tooltipTwo = () => { + return ( + +

Overflow tooltip content.

+
+ ); +}; + +const tooltipThree = () => { + return ( + +

+ Lorem ipsum dolor, sit amet consectetur adipisicing elit. Animi + consequuntur hic ratione aliquid cupiditate, nesciunt saepe iste + blanditiis cumque maxime tenetur veniam est illo deserunt sint quae + pariatur. Laboriosam, consequatur. +

+
+ ); +}; + +const stepData = [ + { + label: 'First step', + description: 'Step 1: Getting started with Carbon Design System', + secondaryLabel: 'Optional label', + }, + { + label: 'Second step with tooltip', + description: 'Step 2: Getting started with Carbon Design System', + renderLabel: tooltipTwo, + }, + { + label: 'Third step with tooltip', + description: 'Step 3: Getting started with Carbon Design System', + renderLabel: tooltipThree, + }, + { + label: 'Fourth step', + description: 'Step 4: Getting started with Carbon Design System', + invalid: true, + secondaryLabel: 'Example invalid step', + }, + { + label: 'Fifth step', + description: 'Step 5: Getting started with Carbon Design System', + disabled: true, + }, +]; + export const Default = () => ( - - ( - -

Overflow tooltip content.

-
- )} - /> - ( - -

- Lorem ipsum dolor, sit amet consectetur adipisicing elit. Animi - consequuntur hic ratione aliquid cupiditate, nesciunt saepe iste - blanditiis cumque maxime tenetur veniam est illo deserunt sint quae - pariatur. Laboriosam, consequatur. -

-
- )} - /> - - -
+ spaceEqually={boolean('Space Equally (spaceEqually)', false)} + stepData={stepData}> ); Default.parameters = { @@ -102,35 +114,8 @@ Default.parameters = { export const Interactive = () => ( - - - ( - -

- Lorem ipsum dolor, sit amet consectetur adipisicing elit. Animi - consequuntur hic ratione aliquid cupiditate, nesciunt saepe iste - blanditiis cumque maxime tenetur veniam est illo deserunt sint quae - pariatur. Laboriosam, consequatur. -

-
- )} - /> -
+ onChange={action('onChange')} + stepData={stepData}> ); Interactive.storyName = 'interactive'; diff --git a/packages/react/src/components/ProgressIndicator/ProgressIndicator.js b/packages/react/src/components/ProgressIndicator/ProgressIndicator.js index 740d8a22c37d..8bfd334c7ab3 100644 --- a/packages/react/src/components/ProgressIndicator/ProgressIndicator.js +++ b/packages/react/src/components/ProgressIndicator/ProgressIndicator.js @@ -15,7 +15,7 @@ import { CircleFilled16, } from '@carbon/icons-react'; import { keys, matches } from '../../internal/keyboard'; -import { usePrefix, PrefixContext } from '../../internal/usePrefix'; +import { PrefixContext } from '../../internal/usePrefix'; const defaultRenderLabel = (props) =>

; @@ -26,208 +26,218 @@ const defaultTranslations = { 'carbon.progress-step.invalid': 'Invalid', }; -function translateWithId(messageId) { +const translateWithId = (messageId) => { return defaultTranslations[messageId]; -} +}; -export function ProgressStep({ - label, - description, - className, - current, - complete, - invalid, - secondaryLabel, - disabled, - onClick, - renderLabel: ProgressStepLabel, - translateWithId: t, - ...rest -}) { - const prefix = usePrefix(); - const classes = classnames({ - [`${prefix}--progress-step`]: true, - [`${prefix}--progress-step--current`]: current, - [`${prefix}--progress-step--complete`]: complete, - [`${prefix}--progress-step--incomplete`]: !complete && !current, - [`${prefix}--progress-step--disabled`]: disabled, - [className]: className, - }); - - const handleKeyDown = (e) => { - if (matches(e, [keys.Enter, keys.Space])) { - onClick(); - } +export class ProgressStep extends React.PureComponent { + constructor(props) { + super(props); + } + + static propTypes = { + /** + * Provide an optional className to be applied to the containing `

  • ` node + */ + className: PropTypes.string, + + /** + * Specify whether the step has been completed + */ + complete: PropTypes.bool, + + /** + * Specify whether the step is the current step + */ + current: PropTypes.bool, + + /** + * Provide a description for the + */ + description: PropTypes.string, + + /** + * Specify whether the step is disabled + */ + disabled: PropTypes.bool, + + /** + * Index of the current step within the ProgressIndicator + */ + index: PropTypes.number, + + /** + * Specify whether the step is invalid + */ + invalid: PropTypes.bool, + + /** + * Provide the label for the + */ + label: PropTypes.node.isRequired, + + /** + * A callback called if the step is clicked or the enter key is pressed + */ + onClick: PropTypes.func, + + /** + * Provide the props that describe a progress step tooltip + */ + overflowTooltipProps: PropTypes.object, + + /* + * An optional parameter to allow for overflow content to be rendered in a + * tooltip. + */ + renderLabel: PropTypes.func, + + /** + * Provide an optional secondary label + */ + secondaryLabel: PropTypes.string, + + /** + * The ID of the tooltip content. + */ + tooltipId: PropTypes.string, + + /** + * Optional method that takes in a message id and returns an + * internationalized string. + */ + translateWithId: PropTypes.func, }; - // eslint-disable-next-line react/prop-types - const SVGIcon = ({ complete, current, description, invalid, prefix }) => { - if (invalid) { + static contextType = PrefixContext; + + static defaultProps = { + renderLabel: defaultRenderLabel, + translateWithId, + }; + + render() { + const prefix = this.context; + const { + label, + description, + className, + secondaryLabel, + onClick, + renderLabel: ProgressStepLabel, + current, + complete, + invalid, + disabled, + translateWithId: t, + ...rest + } = this.props; + + const classes = classnames({ + [`${prefix}--progress-step`]: true, + [`${prefix}--progress-step--current`]: current, + [`${prefix}--progress-step--complete`]: complete, + [`${prefix}--progress-step--incomplete`]: !complete && !current, + [`${prefix}--progress-step--disabled`]: disabled, + [className]: className, + }); + + const handleKeyDown = (e) => { + if (matches(e, [keys.Enter, keys.Space])) { + onClick(); + } + }; + + // eslint-disable-next-line react/prop-types + const SVGIcon = ({ complete, current, description, invalid, prefix }) => { + if (invalid) { + return ( + + {description} + + ); + } + if (current) { + return ( + + {description} + + ); + } + if (complete) { + return ( + + {description} + + ); + } return ( - + {description} - + ); - } + }; + + let message = t('carbon.progress-step.incomplete'); + if (current) { - return ( - - {description} - - ); + message = t('carbon.progress-step.current'); } + if (complete) { - return ( - - {description} - - ); + message = t('carbon.progress-step.complete'); } - return ( - - {description} - - ); - }; - - let message = t('carbon.progress-step.incomplete'); - if (current) { - message = t('carbon.progress-step.current'); - } - - if (complete) { - message = t('carbon.progress-step.complete'); - } - - if (invalid) { - message = t('carbon.progress-step.invalid'); - } + if (invalid) { + message = t('carbon.progress-step.invalid'); + } - return ( -
  • - -
  • - ); + + + + ); + } } -ProgressStep.propTypes = { - /** - * Provide an optional className to be applied to the containing `
  • ` node - */ - className: PropTypes.string, - - /** - * Specify whether the step has been completed - */ - complete: PropTypes.bool, - - /** - * Specify whether the step is the current step - */ - current: PropTypes.bool, - - /** - * Provide a description for the - */ - description: PropTypes.string, - - /** - * Specify whether the step is disabled - */ - disabled: PropTypes.bool, - - /** - * Index of the current step within the ProgressIndicator - */ - index: PropTypes.number, - - /** - * Specify whether the step is invalid - */ - invalid: PropTypes.bool, - - /** - * Provide the label for the - */ - label: PropTypes.node.isRequired, - - /** - * A callback called if the step is clicked or the enter key is pressed - */ - onClick: PropTypes.func, - - /** - * Provide the props that describe a progress step tooltip - */ - overflowTooltipProps: PropTypes.object, - - /* - * An optional parameter to allow for overflow content to be rendered in a - * tooltip. - */ - renderLabel: PropTypes.func, - - /** - * Provide an optional secondary label - */ - secondaryLabel: PropTypes.string, - - /** - * The ID of the tooltip content. - */ - tooltipId: PropTypes.string, - - /** - * Optional method that takes in a message id and returns an - * internationalized string. - */ - translateWithId: PropTypes.func, -}; - -ProgressStep.defaultProps = { - renderLabel: defaultRenderLabel, - translateWithId, -}; - export class ProgressIndicator extends Component { state = {}; + // static contextType = PrefixContext; + static propTypes = { /** * Provide components to be rendered in the * */ - children: PropTypes.node, /** * Provide an optional className to be applied to the containing node @@ -248,6 +258,27 @@ export class ProgressIndicator extends Component { * Specify whether the progress steps should be split equally in size in the div */ spaceEqually: PropTypes.bool, + + /** + * Pass in data needed to render the ProgressSteps + */ + stepData: PropTypes.arrayOf( + PropTypes.shape({ + className: PropTypes.string, + complete: PropTypes.bool, + current: PropTypes.bool, + description: PropTypes.string, + disabled: PropTypes.bool, + index: PropTypes.number, + invalid: PropTypes.bool, + label: PropTypes.node.isRequired, + onClick: PropTypes.func, + renderLabel: PropTypes.func, + secondaryLabel: PropTypes.string, + translateWithId: PropTypes.func, + }) + ), + /** * Determines whether or not the ProgressIndicator should be rendered vertically. */ @@ -270,46 +301,72 @@ export class ProgressIndicator extends Component { }; } - renderSteps = () => { + renderSteps = (stepData) => { const { onChange } = this.props; - return React.Children.map(this.props.children, (child, index) => { + return stepData.map((step, index) => { // only setup click handlers if onChange event is passed const onClick = onChange ? () => onChange(index) : undefined; + + let isCurrent; + let isComplete; + + const { + className, + description, + disabled, + invalid, + label, + renderLabel, + secondaryLabel, + translateWithId, + } = step; + if (index === this.state.currentIndex) { - return React.cloneElement(child, { - current: true, - index, - onClick, - }); + isCurrent = true; } + if (index < this.state.currentIndex) { - return React.cloneElement(child, { - complete: true, - index, - onClick, - }); + if (step.complete != true) { + isComplete = true; + } } + if (index > this.state.currentIndex) { - return React.cloneElement(child, { - complete: child.props.complete || false, - index, - onClick, - }); + isComplete = step.complete || false; } - return null; + + return ( + + ); }); }; render() { + const prefix = this.context; const { className, currentIndex, // eslint-disable-line no-unused-vars vertical, spaceEqually, + stepData, ...other } = this.props; - const prefix = this.context; + const classes = classnames({ [`${prefix}--progress`]: true, [`${prefix}--progress--vertical`]: vertical, @@ -318,7 +375,7 @@ export class ProgressIndicator extends Component { }); return (
      - {this.renderSteps()} + {this.renderSteps(stepData)}
    ); } diff --git a/packages/react/src/components/TextInput/TextInput.js b/packages/react/src/components/TextInput/TextInput.js index 8ce8d7b0f3e9..1fe2bd3f2dd8 100644 --- a/packages/react/src/components/TextInput/TextInput.js +++ b/packages/react/src/components/TextInput/TextInput.js @@ -170,9 +170,11 @@ const TextInput = React.forwardRef(function TextInput( )} {input} + {isFluid &&
    } {isFluid && !inline && normalizedProps.validation} + {!isFluid && !inline && (normalizedProps.validation || helper)}