From 5776fa43c7329c79b36f9010cbeb22fdd6f58e38 Mon Sep 17 00:00:00 2001 From: mmart1n Date: Thu, 28 Oct 2021 18:32:29 +0300 Subject: [PATCH 1/5] feat(stepper): add component styles Co-authored-by: Marin Popov --- .../stepper/_stepper-component.scss | 101 ++ .../components/stepper/_stepper-theme.scss | 986 ++++++++++++++++++ .../src/lib/core/styles/themes/_core.scss | 1 + .../src/lib/core/styles/themes/_index.scss | 5 + .../styles/themes/schemas/dark/_index.scss | 9 + .../styles/themes/schemas/dark/_stepper.scss | 87 ++ .../styles/themes/schemas/light/_index.scss | 6 + .../styles/themes/schemas/light/_stepper.scss | 466 +++++++++ .../themes/schemas/round-light/_index.scss | 3 + .../styles/themes/schemas/shape/_stepper.scss | 38 + .../themes/schemas/square-light/_index.scss | 3 + .../core/styles/typography/_typography.scss | 2 + 12 files changed, 1707 insertions(+) create mode 100644 projects/igniteui-angular/src/lib/core/styles/components/stepper/_stepper-component.scss create mode 100644 projects/igniteui-angular/src/lib/core/styles/components/stepper/_stepper-theme.scss create mode 100644 projects/igniteui-angular/src/lib/core/styles/themes/schemas/dark/_stepper.scss create mode 100644 projects/igniteui-angular/src/lib/core/styles/themes/schemas/light/_stepper.scss create mode 100644 projects/igniteui-angular/src/lib/core/styles/themes/schemas/shape/_stepper.scss diff --git a/projects/igniteui-angular/src/lib/core/styles/components/stepper/_stepper-component.scss b/projects/igniteui-angular/src/lib/core/styles/components/stepper/_stepper-component.scss new file mode 100644 index 00000000000..64b13c6a917 --- /dev/null +++ b/projects/igniteui-angular/src/lib/core/styles/components/stepper/_stepper-component.scss @@ -0,0 +1,101 @@ +//// +/// @group components +/// @author Marin Popov +/// @requires {mixin} bem-block +/// @requires {mixin} bem-elem +/// @requires {mixin} bem-mod +//// +@include b(igx-stepper) { + $block: bem--selector-to-string(&); + @include register-component($block); + + @extend %stepper-display !optional; + + @include e(header) { + @extend %igx-stepper__header !optional; + } + + @include e(body) { + @extend %igx-stepper__body !optional; + } + + @include e(step) { + @extend %igx-stepper__step !optional; + } + + @include e(step, $m: simple) { + @extend %igx-stepper__step--simple !optional; + } + + @include e(step, $m: completed) { + @extend %igx-stepper__step--completed !optional; + } + + @include e(step, $m: disabled) { + @extend %igx-stepper__step--disabled !optional; + } + + @include e(step-header) { + @extend %igx-stepper__step-header !optional; + } + + @include e(step-header, $m: current) { + @extend %igx-stepper__step-header--current !optional; + } + + @include e(step-header, $m: invalid) { + @extend %igx-stepper__step-header--invalid !optional; + } + + @include e(step-content) { + @extend %igx-stepper__step-content !optional; + } + + @include e(step-content-wrapper) { + @extend %igx-stepper__step-content-wrapper !optional; + } + + @include e(step-indicator) { + @extend %igx-stepper__step-indicator !optional; + } + + @include e(step-title-wrapper) { + @extend %igx-stepper__step-title-wrapper !optional; + } + + @include e(step-title) { + @extend %igx-stepper__step-title !optional; + } + + @include e(step-subtitle) { + @extend %igx-stepper__step-subtitle !optional; + } + + @include e(step, $m: top) { + @extend %igx-stepper__step--top !optional; + } + + @include e(step, $m: bottom) { + @extend %igx-stepper__step--bottom !optional; + } + + @include e(step, $m: start) { + @extend %igx-stepper__step--start !optional; + } + + @include e(step, $m: end) { + @extend %igx-stepper__step--end !optional; + } + + @include m(horizontal) { + @extend %igx-stepper--horizontal !optional; + + @include e(body-content) { + @extend %igx-stepper__body-content !optional; + } + + @include e(body-content, $m: active) { + @extend %igx-stepper__body-content--active !optional; + } + } +} diff --git a/projects/igniteui-angular/src/lib/core/styles/components/stepper/_stepper-theme.scss b/projects/igniteui-angular/src/lib/core/styles/components/stepper/_stepper-theme.scss new file mode 100644 index 00000000000..c1e0875b874 --- /dev/null +++ b/projects/igniteui-angular/src/lib/core/styles/components/stepper/_stepper-theme.scss @@ -0,0 +1,986 @@ +@use 'sass:math'; + +//// +/// @group themes +/// @access public +/// @author Marin Popov +//// + +/// @param {Map} $palette [null] - The palette used as basis for styling the component. +/// @param {Map} $schema [$light-schema] - The schema used as basis for styling the component. +/// +/// @param {Color} $step-background [null] - The background of the step header. +/// @param {Color} $step-hover-background [null] - The background of the step header on hover. +/// @param {Color} $step-focus-background [null] - The background of the step header on focus. +/// @param {Color} $title-color [null] - The color of the step title. +/// @param {Color} $title-hover-color [null] - The color of the step title on hover. +/// @param {Color} $title-focus-color [null] - The color of the step title on focus. +/// @param {Color} $subtitle-color [null] - The color of the step subtitle. +/// @param {Color} $subtitle-hover-color [null] - The color of the step subtitle on hover. +/// @param {Color} $subtitle-focus-color [null] - The color of the step subtitle on focus. +/// @param {Color} $indicator-color [null] - The text color of the step indicator. +/// @param {Color} $indicator-background [null] - The background color of the step indicator. +/// @param {Color} $indicator-outline [null] - The outline color of the step indicator. +/// +/// @param {Color} $invalid-step-background [null] - The background of the invalid step header. +/// @param {Color} $invalid-step-hover-background [null] - The background of the invalid step header on hover. +/// @param {Color} $invalid-step-focus-background [null] - The background of the invalid step header on focus. +/// @param {Color} $invalid-title-color [null] - The color of the invalid step title. +/// @param {Color} $invalid-title-hover-color [null] - The color of the invalid step title on hover. +/// @param {Color} $invalid-title-focus-color [null] - The color of the invalid step title on focus. +/// @param {Color} $invalid-subtitle-color [null] - The color of the invalid step subtitle. +/// @param {Color} $invalid-subtitle-hover-color [null] - The color of the invalid step subtitle on hover. +/// @param {Color} $invalid-subtitle-focus-color [null] - The color of the invalid step subtitle on focus. +/// @param {Color} $invalid-indicator-color [null] - The color of the invalid step indicator. +/// @param {Color} $invalid-indicator-background [null] - The background color of the invalid step indicator. +/// @param {Color} $invalid-indicator-outline [null] - The outline color of the invalid step indicator. +/// +/// @param {Color} $current-step-background [null] - The background of the current step header. +/// @param {Color} $current-step-hover-background [null] - The background of the current step header on hover. +/// @param {Color} $current-step-focus-background [null] - The background of the current step header on focus. +/// @param {Color} $current-title-color [null] - The color of the current step title. +/// @param {Color} $current-title-hover-color [null] - The color of the current step title on hover. +/// @param {Color} $current-title-focus-color [null] - The color of the current step title on focus. +/// @param {Color} $current-subtitle-color [null] - The color of the current step subtitle. +/// @param {Color} $current-subtitle-hover-color [null] - The color of the current step subtitle on hover. +/// @param {Color} $current-subtitle-focus-color [null] - The color of the current step subtitle on focus. +/// @param {Color} $current-indicator-color [null] - The color of the current step indicator. +/// @param {Color} $current-indicator-background [null] - The background color of the current step indicator. +/// @param {Color} $current-indicator-outline [null] - The outline color of the current step indicator. +/// +/// @param {Color} $complete-step-background [null] - The background of the complete step header. +/// @param {Color} $complete-step-hover-background [null] - The background of the complete step header on hover. +/// @param {Color} $complete-step-focus-background [null] - The background of the complete step header on focus. +/// @param {Color} $complete-title-color [null] - The color of the complete step title. +/// @param {Color} $complete-title-hover-color [null] - The color of the complete step title on hover. +/// @param {Color} $complete-title-focus-color [null] - The color of the complete step title on focus. +/// @param {Color} $complete-subtitle-color [null] - The color of the complete step subtitle. +/// @param {Color} $complete-subtitle-hover-color [null] - The color of the complete step subtitle on hover. +/// @param {Color} $complete-subtitle-focus-color [null] - The color of the complete step subtitle on focus. +/// @param {Color} $complete-indicator-color [null] - The color of the completed step indicator. +/// @param {Color} $complete-indicator-background [null] - The background color of the completed step indicator. +/// @param {Color} $complete-indicator-outline [null] - The outline color of the completed step indicator. +/// +/// @param {Color} $disabled-title-color [null] - The title color of the disabled step. +/// @param {Color} $disabled-subtitle-color [null] - The subtitle color of the disabled step. +/// @param {Color} $disabled-indicator-color [null] - The indicator color of the disabled step. +/// @param {Color} $disabled-indicator-background [null] - The indicator background of the disabled step. +/// @param {Color} $disabled-indicator-outline [null] - The indicator outline color of the disabled step. +/// +/// @param {Color} $step-separator-color [null] - The separator border-color of between the steps. +/// @param {Color} $complete-step-separator-color [null] - The separator border-color between the completed steps. +/// +/// @param {Color} $step-separator-style [null] - The separator border-style of between the steps. +/// @param {Color} $complete-step-separator-style [null] - The separator border-style between the completed steps. +/// +/// @param {Color} $border-radius-indicator [null] - The border-radius of the step indicator. +/// @param {Color} $border-radius-step-header [null] - The border-radius of the step header. +/// +/// @requires $default-palette +/// @requires $light-schema +/// @requires apply-palette +/// @requires extend +/// +/// @example scss Set custom track and thumb on colors +/// $my-stepper-theme: igx-stepper-theme(); +/// @include igx-stepper($my-stepper-theme); +/// +/// @example scss Set custom steppet colors +/// $my-stepper-theme: igx-stepper-theme($step-hover-background: red); +/// // Pass the theme to the igx-stepper component mixin +/// @include igx-stepper($my-stepper-theme); +@function igx-stepper-theme( + $palette: null, + $schema: $light-schema, + + $step-background: null, + $step-hover-background: null, + $step-focus-background: null, + + $invalid-step-background: null, + $invalid-step-hover-background: null, + $invalid-step-focus-background: null, + + $current-step-background: null, + $current-step-hover-background: null, + $current-step-focus-background: null, + + $complete-step-background: null, + $complete-step-hover-background: null, + $complete-step-focus-background: null, + + // Incomplete + $indicator-color: null, + $indicator-background: null, + $indicator-outline: null, + + $title-color: null, + $title-hover-color: null, + $title-focus-color: null, + + $subtitle-color: null, + $subtitle-hover-color: null, + $subtitle-focus-color: null, + + // Invalid + $invalid-indicator-color: null, + $invalid-indicator-background: null, + $invalid-indicator-outline: null, + + $invalid-title-color: null, + $invalid-title-hover-color: null, + $invalid-title-focus-color: null, + + $invalid-subtitle-color: null, + $invalid-subtitle-hover-color: null, + $invalid-subtitle-focus-color: null, + + // Current + $current-indicator-color: null, + $current-indicator-background: null, + $current-indicator-outline: null, + + $current-title-color: null, + $current-title-hover-color: null, + $current-title-focus-color: null, + + $current-subtitle-color: null, + $current-subtitle-hover-color: null, + $current-subtitle-focus-color: null, + + // complete + $complete-indicator-color: null, + $complete-indicator-background: null, + $complete-indicator-outline: null, + + $complete-title-color: null, + $complete-title-hover-color: null, + $complete-title-focus-color: null, + + $complete-subtitle-color: null, + $complete-subtitle-hover-color: null, + $complete-subtitle-focus-color: null, + + // Disabled + $disabled-indicator-color: null, + $disabled-indicator-background: null, + $disabled-indicator-outline: null, + $disabled-title-color: null, + $disabled-subtitle-color: null, + + // Separator + $step-separator-color: null, + $complete-step-separator-color: null, + + $step-separator-style: null, + $complete-step-separator-style: null, + + // Border-radius + $border-radius-indicator: null, + $border-radius-step-header: null, +) { + $name: 'igx-stepper'; + $stepper-schema: (); + + @if map-has-key($schema, $name) { + $stepper-schema: map-get($schema, $name); + } @else { + $stepper-schema: $schema; + } + + $theme: apply-palette($stepper-schema, $palette); + + $border-radius-indicator: round-borders( + if($border-radius-indicator, $border-radius-indicator, map-get($stepper-schema, 'border-radius-indicator')), 0, 100px + ); + + $border-radius-step-header: round-borders( + if($border-radius-step-header, $border-radius-step-header, map-get($stepper-schema, 'border-radius-step-header')), 0, 100px + ); + + @if not($indicator-background) and $step-background { + $indicator-background: text-contrast($step-background); + } + + @if not($indicator-color) and $indicator-background { + $indicator-color: text-contrast($indicator-background); + } + + @if not($complete-indicator-color) and $complete-indicator-background { + $complete-indicator-color: text-contrast($complete-indicator-background); + } + + @if not($invalid-indicator-color) and $invalid-indicator-background { + $invalid-indicator-color: text-contrast($invalid-indicator-background); + } + + @if not($current-indicator-color) and $current-indicator-background { + $current-indicator-color: text-contrast($current-indicator-background); + } + + @if not($title-color) and $step-background { + $title-color: text-contrast($step-background); + } + + @if not($subtitle-color) and $step-background { + $subtitle-color: text-contrast($step-background); + } + + @if not($title-hover-color) and $step-hover-background { + $title-hover-color: text-contrast($step-hover-background); + } + + @if not($subtitle-hover-color) and $step-hover-background { + $subtitle-hover-color: text-contrast($step-hover-background); + } + + @if not($title-focus-color) and $step-focus-background { + $title-focus-color: text-contrast($step-focus-background); + } + + @if not($subtitle-focus-color) and $step-focus-background { + $subtitle-focus-color: text-contrast($step-focus-background); + } + + @return extend($theme, ( + name: $name, + palette: $palette, + + // Incomplete + step-background: $step-background, + step-hover-background: $step-hover-background, + step-focus-background: $step-focus-background, + indicator-color: $indicator-color, + indicator-background: $indicator-background, + indicator-outline: $indicator-outline, + title-color: $title-color, + title-hover-color: $title-hover-color, + title-focus-color: $title-focus-color, + subtitle-color: $subtitle-color, + subtitle-hover-color: $subtitle-hover-color, + subtitle-focus-color: $subtitle-focus-color, + + // Invalid + invalid-step-background: $invalid-step-background, + invalid-step-hover-background: $invalid-step-hover-background, + invalid-step-focus-background: $invalid-step-focus-background, + invalid-indicator-color: $invalid-indicator-color, + invalid-indicator-background: $invalid-indicator-background, + invalid-indicator-outline: $invalid-indicator-outline, + invalid-title-color: $invalid-title-color, + invalid-title-hover-color: $invalid-title-hover-color, + invalid-title-focus-color: $invalid-title-focus-color, + invalid-subtitle-color: $invalid-subtitle-color, + invalid-subtitle-hover-color: $invalid-subtitle-hover-color, + invalid-subtitle-focus-color: $invalid-subtitle-focus-color, + + // Current + current-step-background: $current-step-background, + current-step-hover-background: $current-step-hover-background, + current-step-focus-background: $current-step-focus-background, + current-indicator-color: $current-indicator-color, + current-indicator-background: $current-indicator-background, + current-indicator-outline: $current-indicator-outline, + current-title-color: $current-title-color, + current-title-hover-color: $current-title-hover-color, + current-title-focus-color: $current-title-focus-color, + current-subtitle-color: $current-subtitle-color, + current-subtitle-hover-color: $current-subtitle-hover-color, + current-subtitle-focus-color: $current-subtitle-focus-color, + + // Complete + complete-step-background: $complete-step-background, + complete-step-hover-background: $complete-step-hover-background, + complete-step-focus-background: $complete-step-focus-background, + complete-indicator-color: $complete-indicator-color, + complete-indicator-background: $complete-indicator-background, + complete-indicator-outline: $complete-indicator-outline, + complete-title-color: $complete-title-color, + complete-title-hover-color: $complete-title-hover-color, + complete-title-focus-color: $complete-title-focus-color, + complete-subtitle-color: $complete-subtitle-color, + complete-subtitle-hover-color: $complete-subtitle-hover-color, + complete-subtitle-focus-color: $complete-subtitle-focus-color, + + // Disabled + disabled-indicator-color: $disabled-indicator-color, + disabled-indicator-background: $disabled-indicator-background, + disabled-indicator-outline: $disabled-indicator-outline, + disabled-title-color: $disabled-title-color, + disabled-subtitle-color: $disabled-subtitle-color, + + // Separator + step-separator-color: $step-separator-color, + complete-step-separator-color: $complete-step-separator-color, + step-separator-style: $step-separator-style, + complete-step-separator-style: $complete-step-separator-style, + + // Border-radius + border-radius-indicator: $border-radius-indicator, + border-radius-step-header: $border-radius-step-header, + )); +} + +/// @param {Map} $theme - The theme used to style the component. +/// @requires {mixin} igx-css-vars +/// @requires em +/// @requires --var +@mixin igx-stepper($theme) { + @include igx-css-vars($theme); + + $variant: map-get($theme, variant); + + $indicator-size: map-get(( + material: rem(24px), + fluent: rem(24px), + bootstrap: rem(40px), + indigo-design: rem(24px) + ), $variant); + + $step-header-padding: map-get(( + material: rem(24px), + fluent: rem(8px), + bootstrap: rem(24px), + indigo-design: rem(16px) + ), $variant); + + $step-header-padding-simple: map-get(( + material: rem(8px), + fluent: rem(8px), + bootstrap: rem(16px), + indigo-design: rem(8px) + ), $variant); + + $step-body-padding: rem(16px); + $title-gap: rem(8px); + $indicator-gap: rem(4px); + $indicator-padding: rem(2px); + $v-line-indent: calc(#{$step-header-padding} + (#{$indicator-size} / 2)); + $separator-position: 50%; + + $outline-width: map-get(( + material: rem(1px), + fluent: rem(1px), + bootstrap: rem(1px), + indigo-design: rem(2px) + ), $variant); + + $separator-size: map-get(( + material: rem(1px), + fluent: rem(1px), + bootstrap: rem(8px), + indigo-design: rem(2px) + ), $variant); + + $separator-title-top: calc(100% - ((#{$indicator-size} / 2) + #{$step-header-padding} + (#{$separator-size} / 2))); + $separator-title-bottom: calc((#{$indicator-size} / 2) + #{$step-header-padding} - (#{$separator-size} / 2)); + + $left: if-ltr(left, right); + $right: if-ltr(right, left); + + %stepper-display, + %igx-stepper__header, + %igx-stepper__body, + %igx-stepper__step { + display: flex; + } + + %stepper-display { + flex-direction: column; + width: 100%; + } + + %igx-stepper__header { + white-space: nowrap; + flex-direction: column; + width: 100%; + } + + %igx-stepper__body { + position: relative + } + + %stepper-display, + %igx-stepper__body, + %igx-stepper__step-header, + %igx-stepper__step-title-wrapper { + overflow: hidden; + } + + %igx-stepper__step-title { + color: --var($theme, 'title-color'); + } + + %igx-stepper__step-subtitle { + color: --var($theme, 'subtitle-color'); + } + + %igx-stepper__step { + position: relative; + flex-direction: column; + align-content: center; + justify-content: center; + min-width: rem(100px); + + &:focus { + outline: none; + + %igx-stepper__step-title { + color: --var($theme, 'title-focus-color'); + } + + %igx-stepper__step-subtitle { + color: --var($theme, 'subtitle-focus-color'); + } + + %igx-stepper__step-header { + background: --var($theme, 'step-focus-background'); + color: --var($theme, 'title-focus-color'); + + @if $variant == 'bootstrap' { + box-shadow: inset 0 0 0 $outline-width --var($theme, 'indicator-outline'); + } + } + + %igx-stepper__step-header--current { + background: --var($theme, 'current-step-focus-background') !important; + + %igx-stepper__step-title { + color: --var($theme, 'current-title-focus-color'); + } + + %igx-stepper__step-subtitle { + color: --var($theme, 'current-subtitle-focus-color'); + } + } + + %igx-stepper__step-header--invalid { + background: --var($theme, 'invalid-step-focus-background'); + + %igx-stepper__step-title { + color: --var($theme, 'invalid-title-focus-color'); + } + + %igx-stepper__step-subtitle { + color: --var($theme, 'invalid-subtitle-focus-color'); + } + } + } + + &:first-of-type { + %igx-stepper__step-header { + &::before { + visibility: hidden; + } + } + } + + &:last-of-type { + %igx-stepper__step-content-wrapper { + &::before { + display: none; + } + } + + %igx-stepper__step-header { + &::after { + visibility: hidden; + } + } + } + } + + %igx-stepper__step-header { + display: flex; + padding: $step-header-padding; + position: relative; + line-height: normal; + flex-direction: column; + align-items: flex-start; + gap: $title-gap; + cursor: pointer; + background: --var($theme, 'step-background'); + border-radius: --var($theme, 'border-radius-step-header'); + + &:hover { + background: --var($theme, 'step-hover-background'); + color: --var($theme, 'title-hover-color'); + } + + @if $variant != material { + .igx-ripple__inner { + display: none; + } + } + } + + %igx-stepper__step-indicator { + display: flex; + align-items: center; + justify-content: center; + position: relative; + font-size: rem(12px); + height: $indicator-size; + width: $indicator-size; + white-space: nowrap; + border-radius: --var($theme, 'border-radius-indicator'); + color: --var($theme, 'indicator-color'); + background: --var($theme, 'indicator-background'); + box-shadow: 0 0 0 $outline-width --var($theme, 'indicator-outline'); + + @if $variant != 'bootstrap' { + > igx-icon { + width: calc(#{$indicator-size} - #{rem(6px)}); + height: calc(#{$indicator-size} - #{rem(6px)}); + font-size: calc(#{$indicator-size} - #{rem(6px)}); + color: inherit; + } + } + + div > igx-icon, + div > igx-avatar, + div > igx-circular-bar { + max-height: $indicator-size; + max-width: $indicator-size; + } + } + + %igx-stepper__step-header--current { + background: --var($theme, 'current-step-background') !important; + color: --var($theme, 'current-title-color'); + + %igx-stepper__step-indicator { + color: --var($theme, 'current-indicator-color') !important; + background: --var($theme, 'current-indicator-background') !important; + box-shadow: 0 0 0 $outline-width --var($theme, 'current-indicator-outline') !important; + } + + %igx-stepper__step-title { + color: --var($theme, 'current-title-color'); + } + + %igx-stepper__step-subtitle { + color: --var($theme, 'current-subtitle-color'); + } + + &:hover { + background: --var($theme, 'current-step-hover-background') !important; + + %igx-stepper__step-title { + color: --var($theme, 'current-title-hover-color'); + } + + %igx-stepper__step-subtitle { + color: --var($theme, 'current-subtitle-hover-color'); + } + } + } + + %igx-stepper__step--disabled { + color: --var($theme, 'disabled-title-color'); + pointer-events: none; + cursor: default; + + %igx-stepper__step-indicator { + color: --var($theme, 'disabled-indicator-color'); + background: --var($theme, 'disabled-indicator-background'); + box-shadow: 0 0 0 $outline-width --var($theme, 'disabled-indicator-outline'); + } + + %igx-stepper__step-title { + color: --var($theme, 'disabled-title-color'); + } + + %igx-stepper__step-subtitle { + color: --var($theme, 'disabled-subtitle-color'); + } + } + + %igx-stepper__step-header--invalid { + background: --var($theme, 'invalid-step-background'); + color: --var($theme, 'invalid-title-color'); + + %igx-stepper__step-indicator { + color: --var($theme, 'invalid-indicator-color'); + background: --var($theme, 'invalid-indicator-background'); + box-shadow: 0 0 0 $outline-width --var($theme, 'invalid-indicator-outline'); + } + + %igx-stepper__step-title { + color: --var($theme, 'invalid-title-color'); + } + + %igx-stepper__step-subtitle { + color: --var($theme, 'invalid-subtitle-color'); + } + + &:hover { + background: --var($theme, 'invalid-step-hover-background'); + + %igx-stepper__step-title { + color: --var($theme, 'invalid-title-hover-color'); + } + + %igx-stepper__step-subtitle { + color: --var($theme, 'invalid-subtitle-hover-color'); + } + } + } + + %igx-stepper__body-content { + display: block; + position: absolute; + top: 0; + #{$left}: 0; + #{$right}: 0; + bottom: 0; + width: 100%; + height: 100%; + overflow-y: auto; + overflow-x: hidden; + z-index: -1; + } + + %igx-stepper__step-content-wrapper, + %igx-stepper__body-content { + padding: $step-body-padding; + } + + %igx-stepper__body-content--active { + z-index: 1; + position: relative; + } + + %igx-stepper__step-content-wrapper { + margin-#{$left}: $v-line-indent; + position: relative; + min-height: rem(32px); + + &::before { + content: ''; + position: absolute; + #{$left}: calc(-#{$separator-size} / 2); + top: calc(-#{$step-header-padding} + #{$title-gap}); + bottom: calc(-#{$step-header-padding} + #{$title-gap}); + width: $separator-size; + border-#{$left}: $separator-size unquote(--var($theme, 'step-separator-style')) --var($theme, 'step-separator-color'); + } + } + + %igx-stepper__step-title-wrapper { + white-space: nowrap; + text-overflow: ellipsis; + min-width: rem(32px); + + &:empty { + display: none; + } + + > * { + display: block; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + } + } + + %igx-stepper__step--start, + %igx-stepper__step--end { + %igx-stepper__step-header { + flex-direction: row; + align-items: center; + //gap: $title-gap--horizontal; + } + } + + %igx-stepper__step--start, + %igx-stepper__step--top { + %igx-stepper__step-title-wrapper { + order: -1; + } + } + + %igx-stepper__step--completed { + + %igx-stepper__step-header { + background: --var($theme, 'complete-step-background'); + + &:hover { + background: --var($theme, 'complete-step-hover-background'); + %igx-stepper__step-title { + color: --var($theme, 'complete-title-hover-color'); + } + + %igx-stepper__step-subtitle { + color: --var($theme, 'complete-subtitle-hover-color'); + } + } + + &::after { + border-top-color: --var($theme, 'complete-step-separator-color') !important; + border-top-style: unquote(--var($theme, 'complete-step-separator-style')) !important; + } + } + + %igx-stepper__step-indicator { + color: --var($theme, 'complete-indicator-color'); + background: --var($theme, 'complete-indicator-background'); + box-shadow: 0 0 0 $outline-width --var($theme, 'complete-indicator-outline'); + } + + %igx-stepper__step-title { + color: --var($theme, 'complete-title-color'); + } + + %igx-stepper__step-subtitle { + color: --var($theme, 'complete-subtitle-color'); + } + + &:focus { + %igx-stepper__step-header { + background: --var($theme, 'complete-step-focus-background'); + + %igx-stepper__step-title { + color: --var($theme, 'complete-title-focus-color'); + } + + %igx-stepper__step-subtitle { + color: --var($theme, 'complete-subtitle-focus-color'); + } + } + } + + %igx-stepper__step-content-wrapper { + &::before { + border-#{$left}-style: unquote(--var($theme, 'complete-step-separator-style')); + border-#{$left}-color: --var($theme, 'complete-step-separator-color'); + } + } + } + + %igx-stepper__step--completed + %igx-stepper__step { + &::before { + border-top-color: --var($theme, 'complete-step-separator-color') !important; + border-top-style: unquote(--var($theme, 'complete-step-separator-style')) !important; + } + + %igx-stepper__step-header { + &::before { + border-top-color: --var($theme, 'complete-step-separator-color') !important; + border-top-style: unquote(--var($theme, 'complete-step-separator-style')) !important; + } + } + } + + %igx-stepper__step--simple { + %igx-stepper__step-indicator { + min-width: $indicator-size; + min-height: $indicator-size; + width: initial; + height: initial; + + div > igx-icon, + div > igx-avatar, + div > igx-circular-bar { + max-width: initial; + max-height: initial; + } + } + } + + // HORIZONTAL MODE START + %igx-stepper--horizontal { + %igx-stepper__header { + flex-direction: row; + } + + %igx-stepper__step { + overflow: hidden; + flex-direction: row; + flex-grow: 1; + + &::before { + content: ''; + width: auto; + min-width: rem(10px); + height: $separator-size; + flex: 1; + position: relative; + top: $separator-title-bottom; + border-top: $separator-size unquote(--var($theme, 'step-separator-style')) --var($theme, 'step-separator-color'); + } + + &:first-of-type { + flex-grow: 0; + min-width: 0; + + &::before { + display: none; + } + } + } + + %igx-stepper__step-header { + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + + &::before, + &::after { + content: ''; + position: absolute; + height: $separator-size; + width: calc(50% - (#{$indicator-size} - #{$indicator-gap})); + top: $separator-title-bottom; + flex: 1; + border-top: $separator-size unquote(--var($theme, 'step-separator-style')) --var($theme, 'step-separator-color'); + } + + &::before { + #{$left}: 0; + } + + &::after { + #{$right}: 0; + } + } + + %igx-stepper__step--simple { + text-align: center; + + %igx-stepper__step-header { + align-self: center; + padding: $step-header-padding-simple; + height: auto; + + &::before, + &::after { + display: none; + } + } + + &%igx-stepper__step { + &::before { + top: calc(50% - (#{$separator-size} / 2)); + } + } + } + + %igx-stepper__step-title-wrapper { + width: 100%; + } + + %igx-stepper__step--top { + %igx-stepper__step-header { + justify-content: flex-end; + + &::before, + &::after { + top: $separator-title-top; + } + } + + &%igx-stepper__step { + &::before { + border-top: $separator-size unquote(--var($theme, 'step-separator-style')) --var($theme, 'step-separator-color'); + top: $separator-title-top; + } + } + } + + %igx-stepper__step--bottom { + %igx-stepper__step-header { + justify-content: flex-start; + } + } + + %igx-stepper__step--top, + %igx-stepper__step--bottom { + %igx-stepper__step-title-wrapper { + text-align: center; + } + + %igx-stepper__step-header { + flex-direction: column; + } + } + + %igx-stepper__step--start { + %igx-stepper__step-title-wrapper { + text-align: #{$right}; + } + } + + %igx-stepper__step--start, + %igx-stepper__step--end { + %igx-stepper__step-indicator { + flex: 1 0 auto; + } + + %igx-stepper__step-header { + @if $variant != 'fluent' { + padding: calc(#{$step-header-padding} / 2); + } + + &::before, + &::after { + display: none; + } + } + + &%igx-stepper__step { + &::before { + top: calc(50% - (#{$separator-size} / 2)); + } + } + } + + %igx-stepper__step-content { + &:focus { + outline: none; + } + + &::before { + display: none; + } + } + + %igx-stepper__step-content-wrapper { + text-align: center; + } + } + // HORIZONTAL MODE END +} + +/// Adds typography styles for the igx-stepper component. +/// Uses the 'body-2' category from the typographic scale. +/// @group typography +/// @param {Map} $type-scale - A typographic scale as produced by igx-type-scale. +/// @param {Map} $categories [(title: 'body-2')] - The categories from the typographic scale used for type styles. +/// @requires {mixin} igx-type-style +@mixin igx-stepper-typography($type-scale, $categories: (title: 'body-2', subtitle: 'caption')) { + $title: map-get($categories, 'title'); + $subtitle: map-get($categories, 'subtitle'); + + %igx-stepper__step-title { + @include igx-type-style($type-scale, $title) { + margin-top: 0; + margin-bottom: 0; + } + } + + %igx-stepper__step-subtitle { + @include igx-type-style($type-scale, $subtitle) { + margin-top: 0; + margin-bottom: 0; + } + } + + %igx-stepper__step-header--current { + %igx-stepper__step-title { + font-weight: 600; + } + } +} + diff --git a/projects/igniteui-angular/src/lib/core/styles/themes/_core.scss b/projects/igniteui-angular/src/lib/core/styles/themes/_core.scss index 6ee4b013c61..c67a9387807 100644 --- a/projects/igniteui-angular/src/lib/core/styles/themes/_core.scss +++ b/projects/igniteui-angular/src/lib/core/styles/themes/_core.scss @@ -70,6 +70,7 @@ @import '../components/splitter/splitter-component'; @import '../components/snackbar/snackbar-component'; @import '../components/switch/switch-component'; +@import '../components/stepper/stepper-component'; @import '../components/tabs/tabs-component'; @import '../components/toast/toast-component'; @import '../components/tooltip/tooltip-component'; diff --git a/projects/igniteui-angular/src/lib/core/styles/themes/_index.scss b/projects/igniteui-angular/src/lib/core/styles/themes/_index.scss index e9f15f97bba..a179f339930 100644 --- a/projects/igniteui-angular/src/lib/core/styles/themes/_index.scss +++ b/projects/igniteui-angular/src/lib/core/styles/themes/_index.scss @@ -50,6 +50,7 @@ @import '../components/tabs/tabs-theme'; @import '../components/scrollbar/scrollbar-theme'; @import '../components/switch/switch-theme'; +@import '../components/stepper/stepper-theme'; @import '../components/snackbar/snackbar-theme'; @import '../components/slider/slider-theme'; @import '../components/splitter/splitter-theme'; @@ -445,6 +446,10 @@ )); } + @if not(index($exclude, 'igx-stepper')) { + @include igx-stepper(igx-stepper-theme($schema: $schema)); + } + @if not(index($exclude, 'igx-tabs')) { @include igx-tabs(igx-tabs-theme( $schema: $schema, diff --git a/projects/igniteui-angular/src/lib/core/styles/themes/schemas/dark/_index.scss b/projects/igniteui-angular/src/lib/core/styles/themes/schemas/dark/_index.scss index aea995689c5..e12f406a412 100644 --- a/projects/igniteui-angular/src/lib/core/styles/themes/schemas/dark/_index.scss +++ b/projects/igniteui-angular/src/lib/core/styles/themes/schemas/dark/_index.scss @@ -55,6 +55,7 @@ @import './sparkline'; @import './splitter'; @import './switch'; +@import './stepper'; @import './tabs'; @import './time-picker'; @import './toast'; @@ -118,6 +119,7 @@ /// @property {Map} sparkline [$_dark-sparkline] /// @property {Map} igx-splitter [$_dark-splitter] /// @property {Map} igx-switch [$_dark-switch] +/// @property {Map} igx-stepper [$_dark-stepper] /// @property {Map} igx-tabs [$_dark-tabs] /// @property {Map} igx-time-picker [$_dark-time-picker] /// @property {Map} igx-toast [$_dark-toast] @@ -177,6 +179,7 @@ $dark-schema: ( sparkline: $_dark-sparkline, igx-splitter: $_dark-splitter, igx-switch: $_dark-switch, + igx-stepper: $_dark-stepper, igx-tabs: $_dark-tabs, igx-time-picker: $_dark-time-picker, igx-toast: $_dark-toast, @@ -246,6 +249,7 @@ $dark-material-schema: $dark-schema; /// @property {Map} sparkline [$_dark-fluent-sparkline], /// @property {Map} igx-splitter [$_dark-fluent-splitter], /// @property {Map} igx-switch [$_dark-fluent-switch], +/// @property {Map} igx-stepper [$_dark-fluent-stepper], /// @property {Map} igx-tabs [$_dark-fluent-tabs], /// @property {Map} igx-time-picker [$_dark-fluent-time-picker], /// @property {Map} igx-toast [$_dark-fluent-toast], @@ -305,6 +309,7 @@ $dark-fluent-schema: ( sparkline: $_dark-fluent-sparkline, igx-splitter: $_dark-fluent-splitter, igx-switch: $_dark-fluent-switch, + igx-stepper: $_dark-fluent-stepper, igx-tabs: $_dark-fluent-tabs, igx-time-picker: $_dark-fluent-time-picker, igx-toast: $_dark-fluent-toast, @@ -369,6 +374,7 @@ $dark-fluent-schema: ( /// @property {Map} sparkline [$_dark-bootstrap-sparkline], /// @property {Map} igx-splitter [$_dark-bootstrap-splitter], /// @property {Map} igx-switch [$_dark-bootstrap-switch], +/// @property {Map} igx-stepper [$_dark-bootstrap-stepper], /// @property {Map} igx-tabs [$_dark-bootstrap-tabs], /// @property {Map} igx-time-picker [$_dark-bootstrap-time-picker], /// @property {Map} igx-toast [$_dark-bootstrap-toast], @@ -428,6 +434,7 @@ $dark-bootstrap-schema: ( sparkline: $_dark-bootstrap-sparkline, igx-splitter: $_dark-bootstrap-splitter, igx-switch: $_dark-bootstrap-switch, + igx-stepper: $_dark-bootstrap-stepper, igx-tabs: $_dark-bootstrap-tabs, igx-time-picker: $_dark-bootstrap-time-picker, igx-toast: $_dark-bootstrap-toast, @@ -492,6 +499,7 @@ $dark-bootstrap-schema: ( /// @property {Map} sparkline [$_dark-indigo-sparkline] /// @property {Map} igx-splitter [$_dark-indigo-splitter] /// @property {Map} igx-switch [$_dark-indigo-switch] +/// @property {Map} igx-stepper [$_dark-indigo-stepper] /// @property {Map} igx-tabs [$_dark-indigo-tabs] /// @property {Map} igx-time-picker [$_dark-indigo-time-picker] /// @property {Map} igx-toast [$_dark-indigo-toast] @@ -551,6 +559,7 @@ $dark-indigo-schema: ( sparkline: $_dark-indigo-sparkline, igx-splitter: $_dark-indigo-splitter, igx-switch: $_dark-indigo-switch, + igx-stepper: $_dark-indigo-stepper, igx-tabs: $_dark-indigo-tabs, igx-time-picker: $_dark-indigo-time-picker, igx-toast: $_dark-indigo-toast, diff --git a/projects/igniteui-angular/src/lib/core/styles/themes/schemas/dark/_stepper.scss b/projects/igniteui-angular/src/lib/core/styles/themes/schemas/dark/_stepper.scss new file mode 100644 index 00000000000..e21419ebcab --- /dev/null +++ b/projects/igniteui-angular/src/lib/core/styles/themes/schemas/dark/_stepper.scss @@ -0,0 +1,87 @@ +@import '../light/stepper'; + +//// +/// @group schemas +/// @access public +/// @author Marin Popov +//// + +/// Generates a base dark stepper schema. +/// @property {Map} current-indicator-color [igx-color: ('grays', 900)] - The color of the current step indicator. +/// @property {Map} invalid-indicator-color [igx-color: ('grays', 900)] - The color of the invalid step indicator. +/// @type {Map} +$_base-stepper: ( + current-indicator-color: ( + igx-color: ('grays', 900) + ), + + invalid-indicator-color: ( + igx-color: ('grays', 900) + ), +); + +/// Generates a dark stepper schema. +/// @type {Map} +/// @requires {function} extend +/// @requires $_light-stepper +/// @requires $_base-stepper +/// @see $default-palette +$_dark-stepper: extend( + $_light-stepper, + $_base-stepper +); + +/// Generates a dark fluent stepper schema +/// @type {Map} +/// @requires {function} extend +/// @requires $_fluent-stepper +/// @requires $_base-stepper +/// @see $default-palette +$_dark-fluent-stepper: extend( + $_fluent-stepper, + $_base-stepper +); + +/// Generates a dark bootstrap stepper schema. +/// @type {Map} +/// @property {Map} indicator-outline [igx-color: (igx-color: ('grays', 600)] - The outline color of the incomplete step indicator. +/// @property {Map} disabled-indicator-outline [igx-color: ('grays', 300)] - The outline color of the disabled step indicator. +/// @requires {function} extend +/// @requires $_bootstrap-stepper +/// @requires $_base-stepper +/// @see $default-palette +$_dark-bootstrap-stepper: extend( + $_bootstrap-stepper, + $_base-stepper, + ( + indicator-outline: ( + igx-color: ('grays', 600) + ), + + disabled-indicator-outline: ( + igx-color: ('grays', 300) + ), + ) +); + +/// Generates a dark indigo stepper schema. +/// @type {Map} +/// @property {Map} indicator-outline [igx-color: (igx-color: ('grays', 600)] - The outline color of the incomplete step indicator. +/// @property {Map} disabled-indicator-outline [igx-color: ('grays', 300)] - The outline color of the disabled step indicator. +/// @requires {function} extend +/// @requires $_indigo-stepper +/// @requires $_base-stepper +/// @see $default-palette +$_dark-indigo-stepper: extend( + $_indigo-stepper, + $_base-stepper, + ( + indicator-outline: ( + igx-color: ('grays', 600) + ), + + disabled-indicator-outline: ( + igx-color: ('grays', 300) + ), + ) +); diff --git a/projects/igniteui-angular/src/lib/core/styles/themes/schemas/light/_index.scss b/projects/igniteui-angular/src/lib/core/styles/themes/schemas/light/_index.scss index b776bc62c36..f2c9d1acdc8 100644 --- a/projects/igniteui-angular/src/lib/core/styles/themes/schemas/light/_index.scss +++ b/projects/igniteui-angular/src/lib/core/styles/themes/schemas/light/_index.scss @@ -55,6 +55,7 @@ @import './sparkline'; @import './splitter'; @import './switch'; +@import './stepper'; @import './tabs'; @import './time-picker'; @import './toast'; @@ -118,6 +119,7 @@ /// @property {Map} sparkline [$_light-sparkline] /// @property {Map} igx-splitter [$_light-splitter] /// @property {Map} igx-switch [$_light-switch] +/// @property {Map} igx-stepper [$_light-stepper] /// @property {Map} igx-tabs [$_light-tabs] /// @property {Map} igx-time-picker [$_light-time-picker] /// @property {Map} igx-toast [$_light-toast] @@ -177,6 +179,7 @@ $light-schema: ( sparkline: $_light-sparkline, igx-splitter: $_light-splitter, igx-switch: $_light-switch, + igx-stepper: $_light-stepper, igx-tabs: $_light-tabs, igx-time-picker: $_light-time-picker, igx-toast: $_light-toast, @@ -247,6 +250,7 @@ $light-fluent-schema: ( sparkline: $_fluent-sparkline, igx-splitter: $_fluent-splitter, igx-switch: $_fluent-switch, + igx-stepper: $_fluent-stepper, igx-tabs: $_fluent-tabs, igx-time-picker: $_fluent-time-picker, igx-toast: $_fluent-toast, @@ -312,6 +316,7 @@ $light-bootstrap-schema: ( sparkline: $_bootstrap-sparkline, igx-splitter: $_bootstrap-splitter, igx-switch: $_bootstrap-switch, + igx-stepper: $_bootstrap-stepper, igx-tabs: $_bootstrap-tabs, igx-time-picker: $_bootstrap-time-picker, igx-toast: $_bootstrap-toast, @@ -377,6 +382,7 @@ $light-indigo-schema: ( sparkline: $_indigo-sparkline, igx-splitter: $_indigo-splitter, igx-switch: $_indigo-switch, + igx-stepper: $_indigo-stepper, igx-tabs: $_indigo-tabs, igx-time-picker: $_indigo-time-picker, igx-toast: $_indigo-toast, diff --git a/projects/igniteui-angular/src/lib/core/styles/themes/schemas/light/_stepper.scss b/projects/igniteui-angular/src/lib/core/styles/themes/schemas/light/_stepper.scss new file mode 100644 index 00000000000..3f4b2a0d76c --- /dev/null +++ b/projects/igniteui-angular/src/lib/core/styles/themes/schemas/light/_stepper.scss @@ -0,0 +1,466 @@ +@import '../shape/stepper'; + +/// @group schemas +/// @access public +/// @author Marin Popov +//// + +/// Generates a light stepper schema. +/// @type {Map} +/// +/// @property {Color} step-background [transparent] - The background of the step header. +/// @property {Map} step-hover-background [igx-color: 'grays', 50)] - The background of the step header on hover. +/// @property {Map} step-focus-background [igx-color: 'grays', 100)] - The background of the step header on focus. +/// @property {Map} title-color [igx-color: ('grays', 900)] - The text color of the step title. +/// @property {Map} title-hover-color [igx-color: ('grays', 900)] - The text color of the step title on hover. +/// @property {Map} title-focus-color [igx-color: ('grays', 900)] - The text color of the step title on focus. +/// @property {Map} subtitle-color [igx-color: ('grays', 700)] - The text color of the step subtitle. +/// @property {Map} subtitle-hover-color [igx-color: ('grays', 700)] - The text color of the step subtitle on hover. +/// @property {Map} subtitle-focus-color [igx-color: ('grays', 700)] - The text color of the step subtitle on focus. +/// @property {Map} indicator-color [igx-color: (igx-contrast-color: ('grays', 400)] - The text color of the incomplete step indicator. +/// @property {Map} indicator-background [igx-color: ('grays', 400)] - The background color of the incomplete step indicator. +/// @property {Map} indicator-outline [igx-color: ('grays', 400)] - The outline color of the incomplete step indicator. +/// +/// @property {Color} invalid-step-background [transparent] - The background of the invalid step header. +/// @property {Map} invalid-step-hover-background [igx-color: 'grays', 50)] - The background of the invalid step header on hover. +/// @property {Map} invalid-step-focus-background [igx-color: 'grays', 100)] - The background of the invalid step header on focus. +/// @property {Map} invalid-title-color [igx-color: ('error')] - The color of the invalid step title. +/// @property {Map} invalid-title-hover-color [igx-color: ('error')] - The color of the invalid step title on hover. +/// @property {Map} invalid-title-focus-color [igx-color: ('error')] - The color of the invalid step title on focus. +/// @property {Map} invalid-subtitle-color [igx-color: ('error')] - The text of the invalid step subtitle. +/// @property {Map} invalid-subtitle-hover-color [igx-color: ('error')] - The color of the invalid step subtitle on hover. +/// @property {Map} invalid-subtitle-focus-color [igx-color: ('error')] - The color of the invalid step subtitle on focus. +/// @property {Map} invalid-indicator-color [igx-contrast-color: ('grays', 900)] - The color of the invalid step indicator. +/// @property {Map} invalid-indicator-background [igx-color: ('error')] - The background color of the invalid step indicator. +/// @property {Map} invalid-indicator-outline [igx-color: ('error')] - The outline color of the invalid step indicator. +/// +/// @property {Color} current-step-background [transparent] - The background of the current step header. +/// @property {Map} current-step-hover-background [igx-color: 'grays', 50)] - The background of the current step header on hover. +/// @property {Map} current-step-focus-background [igx-color: 'grays', 100)] - The background of the current step header on focus. +/// @property {Map} current-title-color [igx-color: ('grays', 900)] - The color of the current step title. +/// @property {Map} current-title-hover-color [igx-color: ('grays', 900)] - The color of the current step title on hover. +/// @property {Map} current-title-focus-color [igx-color: ('grays', 900)] - The color of the current step title on focus. +/// @property {Map} current-subtitle-color [igx-color: ('grays', 700)] - The color of the current step subtitle. +/// @property {Map} current-subtitle-hover-color [igx-color: ('grays', 700)] - The color of the current step subtitle on hover. +/// @property {Map} current-subtitle-focus-color [igx-color: ('grays', 700)] - The color of the current step subtitle on focus. +/// @property {Map} current-indicator-color [igx-contrast-color: ('grays', 900)] - The color of the current step indicator. +/// @property {Map} current-indicator-background [igx-color: ('primary', 500)] - The background color of the current step indicator. +/// @property {Map} current-indicator-outline [igx-color: ('primary', 500)] - The outline color of the current step indicator. +/// +/// @property {Color} complete-step-background [transparent] - The background of the complete step header. +/// @property {Map} complete-step-hover-background [igx-color: 'grays', 50)] - The background of the complete step header on hover. +/// @property {Map} complete-step-focus-background [igx-color: 'grays', 100)] - The background of the complete step header on focus. +/// @property {Map} complete-title-color [igx-color: ('grays', 900)] - The color of the complete step title. +/// @property {Map} complete-title-hover-color [igx-color: ('grays', 900)] - The color of the complete step title on hover. +/// @property {Map} complete-title-focus-color [igx-color: ('grays', 900)] - The color of the complete step title on focus. +/// @property {Map} complete-subtitle-color [igx-color: ('grays', 700)] - The color of the complete step subtitle. +/// @property {Map} complete-subtitle-hover-color [igx-color: ('grays', 700)] - The color of the complete step subtitle on hover. +/// @property {Map} complete-subtitle-focus-color [igx-color: ('grays', 700)] - The color of the complete step subtitle on focus. +/// @property {Map} complete-indicator-color [igx-contrast-color: ('grays', 900)] - The color of the completed step indicator. +/// @property {Map} complete-indicator-background [igx-color: ('grays', 900)] - The background color of the completed step indicator. +/// @property {Map} complete-indicator-outline [igx-color: ('grays', 900)] - The outline color of the completed step indicator. +/// +/// @property {Map} disabled-title-color [igx-color: ('grays', 500)] - The title color of the disabled step. +/// @property {Map} disabled-subtitle-color [igx-color: ('grays', 500)] - The subtitle color of the disabled step. +/// @property {Map} disabled-indicator-color [igx-color: ('grays', 500)] - The color of the disabled step indicator, title, and subtitle. +/// @property {Map} disabled-indicator-background [igx-color: ('grays', 200)] - The background color of the disabled step indicator. +/// @property {Map} disabled-indicator-outline [igx-color: ('grays', 200)] - The outline color of the disabled step indicator. +/// +/// @property {Map} step-separator-color [igx-color: ('grays', 300)] - The separator border-color of between the steps. +/// @property {Map} complete-step-separator-color [igx-color: ('grays', 900)] - The separator border-color between the completed steps. +/// @property {String} step-separator-style ['dashed'] - The separator border-style of between the steps. +/// @property {String} complete-step-separator-style ['solid'] - The separator border-style between the completed steps. +/// +/// @requires {function} extend +/// @requires {Map} $_default-shape-stepper +/// @see $default-palette +$_light-stepper: extend( + $_default-shape-stepper, + ( + variant: 'material', + + // Step incomplete + step-background: transparent, + step-hover-background: ( + igx-color: ('grays', 50) + ), + step-focus-background: ( + igx-color: ('grays', 100) + ), + + indicator-background: ( + igx-color: ('grays', 400) + ), + indicator-outline: ( + igx-color: ('grays', 400) + ), + indicator-color: ( + igx-contrast-color: ('grays', 400) + ), + + title-color: ( + igx-color: ('grays', 900) + ), + title-hover-color: ( + igx-color: ('grays', 900) + ), + title-focus-color: ( + igx-color: ('grays', 900) + ), + + subtitle-color: ( + igx-color: ('grays', 700) + ), + subtitle-hover-color: ( + igx-color: ('grays', 700) + ), + subtitle-focus-color: ( + igx-color: ('grays', 700) + ), + + // Complete + complete-step-background: transparent, + complete-step-hover-background: ( + igx-color: ('grays', 50) + ), + complete-step-focus-background: ( + igx-color: ('grays', 100) + ), + + complete-indicator-background: ( + igx-color: ('grays', 900) + ), + complete-indicator-outline: ( + igx-color: ('grays', 900) + ), + complete-indicator-color: ( + igx-contrast-color: ('grays', 900) + ), + + complete-title-color: ( + igx-color: ('grays', 900) + ), + complete-title-hover-color: ( + igx-color: ('grays', 900) + ), + complete-title-focus-color: ( + igx-color: ('grays', 900) + ), + + complete-subtitle-color: ( + igx-color: ('grays', 700) + ), + complete-subtitle-hover-color: ( + igx-color: ('grays', 700) + ), + complete-subtitle-focus-color: ( + igx-color: ('grays', 700) + ), + + // Current + current-step-background: transparent, + current-step-hover-background: ( + igx-color: ('grays', 50) + ), + current-step-focus-background: ( + igx-color: ('grays', 100) + ), + + current-indicator-background: ( + igx-color: ('primary', 500) + ), + + current-indicator-outline: ( + igx-color: ('primary', 500) + ), + + current-indicator-color: ( + igx-contrast-color: ('grays', 900) + ), + + current-title-color: ( + igx-color: ('grays', 900) + ), + current-title-hover-color: ( + igx-color: ('grays', 900) + ), + current-title-focus-color: ( + igx-color: ('grays', 900) + ), + + current-subtitle-color: ( + igx-color: ('grays', 700) + ), + current-subtitle-hover-color: ( + igx-color: ('grays', 700) + ), + current-subtitle-focus-color: ( + igx-color: ('grays', 700) + ), + + // Invalid + invalid-step-background: transparent, + invalid-step-hover-background: ( + igx-color: ('grays', 50) + ), + invalid-step-focus-background: ( + igx-color: ('grays', 100) + ), + + invalid-indicator-background: ( + igx-color: ('error') + ), + invalid-indicator-outline: ( + igx-color: ('error') + ), + invalid-indicator-color: ( + igx-contrast-color: ('grays', 900) + ), + + invalid-title-color: ( + igx-color: ('error') + ), + invalid-title-hover-color: ( + igx-color: ('error') + ), + invalid-title-focus-color: ( + igx-color: ('error') + ), + + invalid-subtitle-color: ( + igx-color: ('error') + ), + invalid-subtitle-hover-color: ( + igx-color: ('error') + ), + invalid-subtitle-focus-color: ( + igx-color: ('error') + ), + + // Disabled + disabled-indicator-color: ( + igx-color: ('grays', 500) + ), + disabled-indicator-background: ( + igx-color: ('grays', 200) + ), + disabled-indicator-outline: ( + igx-color: ('grays', 200) + ), + disabled-title-color: ( + igx-color: ('grays', 500) + ), + disabled-subtitle-color: ( + igx-color: ('grays', 500) + ), + + // Separator + step-separator-color: ( + igx-color: ('grays', 300) + ), + complete-step-separator-color: ( + igx-color: ('grays', 900) + ), + step-separator-style: 'dashed', + complete-step-separator-style: 'solid', + ) +); + +/// Generates a fluent stepper schema. +/// @type {Map} +/// @property {Color} indicator-background [transparent] - The background color of the incomplete step indicator. +/// @property {Color} indicator-outline [transparent] - The outline color of the incomplete step indicator. +/// @property {Map} indicator-color [igx-color: ('grays', 900)] - The text color of the incomplete step indicator. +/// @property {Map} complete-indicator-background [igx-color: ('grays', 200)] - The background color of the completed step indicator. +/// @property {Color} complete-indicator-outline [transparent] - The outline color of the completed step indicator. +/// @property {Map} complete-indicator-color [igx-contrast-color: ('grays', 200)] - The text color of the completed step indicator. +/// @property {Map} complete-step-separator-color [igx-color: ('primary', 500)] - The separator border-color between the completed steps. +/// @property {Color} disabled-indicator-background [transparent] - The background color of the disabled step indicator. +/// @property {Color} disabled-indicator-outline [transparent] - The outline color of the disabled step indicator. +/// +/// @requires {function} extend +/// @requires {Map} $_light-stepper +/// @requires {Map} $_fluent-shape-stepper +$_fluent-stepper: extend( + $_light-stepper, + $_fluent-shape-stepper, + ( + variant: 'fluent', + + indicator-background: transparent, + indicator-outline: transparent, + indicator-color: ( + igx-color: ('grays', 900) + ), + + // Complete + complete-indicator-background: ( + igx-color: ('grays', 200) + ), + complete-indicator-outline: transparent, + complete-indicator-color: ( + igx-contrast-color: ('grays', 200) + ), + complete-step-separator-color: ( + igx-color: ('primary', 500) + ), + + // Disabled + disabled-indicator-background: transparent, + disabled-indicator-outline: transparent, + ) +); + +/// Generates a bootstrap stepper schema. +/// @type {Map} +/// @property {Color} indicator-color [igx-color: ('primary', 500)] - The text color of the incomplete step indicator. +/// @property {Color} indicator-background [transparent] - The background color of the incomplete step indicator. +/// @property {Map} indicator-outline [igx-color: ('grays', 300)] - The outline color of the incomplete step indicator. +/// +/// @property {Map} current-title-color [igx-color: ('primary', 500)] - The color of the current step title. +/// @property {Map} current-title-hover-color [igx-color: ('primary', 500)] - The color of the current step title on hover. +/// @property {Map} current-title-focus-color [igx-color: ('primary', 500)] - The color of the current step title on focus. +/// @property {Map} current-subtitle-color [igx-color: ('primary', 500)] - The color of the current step subtitle. +/// @property {Map} current-subtitle-hover-color [igx-color: ('primary', 500)] - The color of the current step subtitle on hover. +/// @property {Map} current-subtitle-focus-color [igx-color: ('primary', 500)] - The color of the current step subtitle on focus. +/// +/// @property {Color} disabled-indicator-background [transparent] - The background color of the disabled step indicator. +/// @property {Map} disabled-indicator-outline [igx-color: ('grays', 300)] - The outline color of the disabled step indicator. +/// +/// @property {Map} complete-indicator-background [igx-color: ('grays', 300)] - The background color of the completed step indicator. +/// @property {Map} complete-indicator-outline [igx-color: ('grays', 300)] - The outline color of the completed step indicator. +/// @property {Map} complete-indicator-color [igx-color: ('grays', 300)] - The text color of the completed step indicator. +/// +/// @property {Map} step-separator-color [igx-color: ('grays', 200)] - The separator border-color of between the steps. +/// @property {Map} complete-step-separator-color [igx-color: ('primary', 500)] - The separator border-color between the completed steps. +/// @property {String} step-separator-style ['solid'] - The separator border-style of between the steps. +/// @property {String} complete-step-separator-style ['solid'] - The separator border-style between the completed steps. +/// +/// @requires {function} extend +/// @requires {Map} $_light-stepper +/// @requires {Map} $_bootstrap-shape-stepper +$_bootstrap-stepper: extend( + $_light-stepper, + $_bootstrap-shape-stepper, + ( + variant: 'bootstrap', + + indicator-background: transparent, + indicator-outline: ( + igx-color: ('grays', 300) + ), + indicator-color: ( + igx-color: ('primary', 500) + ), + + // Current + current-title-color: ( + igx-color: ('primary', 500) + ), + current-title-hover-color: ( + igx-color: ('primary', 500) + ), + current-title-focus-color: ( + igx-color: ('primary', 500) + ), + current-subtitle-color: ( + igx-color: ('primary', 500) + ), + current-subtitle-hover-color: ( + igx-color: ('primary', 500) + ), + current-subtitle-focus-color: ( + igx-color: ('primary', 500) + ), + + // Complete + complete-indicator-background: ( + igx-color: ('grays', 300) + ), + complete-indicator-outline: ( + igx-color: ('grays', 300) + ), + complete-indicator-color: ( + igx-contrast-color: ('grays', 300) + ), + + // Disabled + disabled-indicator-background: transparent, + disabled-indicator-outline: ( + igx-color: ('grays', 300) + ), + + // Separator + step-separator-color: ( + igx-color: ('grays', 200) + ), + complete-step-separator-color: ( + igx-color: ('primary', 500) + ), + step-separator-style: 'solid', + complete-step-separator-style: 'solid', + ) +); + +/// Generates an indigo stepper schema. +/// @type {Map} +/// @property {Map} indicator-color [igx-color: ('primary', 500)] - The text color of the incomplete step indicator. +/// @property {Color} indicator-background [transparent] - The background color of the incomplete step indicator. +/// @property {Map} indicator-outline [igx-color: ('grays', 300)] - The outline color of the incomplete step indicator. +/// +/// @property {Map} complete-indicator-color [igx-color: ('primary', 500)] - The color of the completed step indicator. +/// @property {Map} complete-indicator-background [igx-color: ('primary', 100)] - The background color of the completed step indicator. +/// @property {Map} complete-indicator-outline [igx-color: ('primary', 100)] - The outline color of the completed step indicator. +/// +/// @property {Color} disabled-indicator-background [transparent] - The background color of the disabled step indicator. +/// @property {Map} disabled-indicator-outline [igx-color: ('grays', 200)] - The outline color of the disabled step indicator. +/// +/// @property {Map} step-separator-color [igx-color: ('grays', 300)] - The separator border-color of between the steps. +/// @property {Map} complete-step-separator-color [igx-color: ('primary', 500)] - The separator border-color between the completed steps. +/// @property {String} step-separator-style ['solid'] - The separator border-style of between the steps. +/// @property {String} complete-step-separator-style ['solid'] - The separator border-style between the completed steps. +/// +/// @requires {function} extend +/// @requires $_light-stepper +$_indigo-stepper: extend( + $_light-stepper, + ( + variant: 'indigo-design', + + indicator-background: transparent, + indicator-outline: ( + igx-color: ('grays', 300) + ), + indicator-color: ( + igx-color: ('primary', 500) + ), + + // Complete + complete-indicator-background: ( + igx-color: ('primary', 100) + ), + complete-indicator-outline: ( + igx-color: ('primary', 100) + ), + complete-indicator-color: ( + igx-color: ('primary', 500) + ), + + // Disabled + disabled-indicator-background: transparent, + disabled-indicator-outline: ( + igx-color: ('grays', 200) + ), + + // Separator + step-separator-color: ( + igx-color: ('grays', 300) + ), + complete-step-separator-color: ( + igx-color: ('primary', 500) + ), + step-separator-style: 'solid', + complete-step-separator-style: 'solid', + ) +); + diff --git a/projects/igniteui-angular/src/lib/core/styles/themes/schemas/round-light/_index.scss b/projects/igniteui-angular/src/lib/core/styles/themes/schemas/round-light/_index.scss index 4f95169691f..347ef9757b1 100644 --- a/projects/igniteui-angular/src/lib/core/styles/themes/schemas/round-light/_index.scss +++ b/projects/igniteui-angular/src/lib/core/styles/themes/schemas/round-light/_index.scss @@ -40,6 +40,7 @@ @import '../light/slider'; @import '../light/snackbar'; @import '../light/switch'; +@import '../light/stepper'; @import '../light/tabs'; @import '../light/time-picker'; @import '../light/toast'; @@ -85,6 +86,7 @@ /// @property {Map} igx-slider [$_light-slider] /// @property {Map} igx-snackbar [$_light-snackbar] /// @property {Map} igx-switch [$_light-switch] +/// @property {Map} igx-stepper [$_light-stepper] /// @property {Map} igx-tabs [$_light-tabs] /// @property {Map} igx-time-picker [$_light-time-picker] /// @property {Map} igx-toast [$_light-toast] @@ -112,6 +114,7 @@ $light-round-schema: ( igx-circular-bar: extend($_light-progress-circular, $_default-shape-progress), igx-snackbar: extend($_light-snackbar, $_round-shape-snackbar), igx-switch: extend($_light-switch, $_round-shape-switch), + igx-stepper: extend($_light-stepper, $_round-shape-stepper), igx-tabs: extend($_light-tabs, $_round-shape-tabs), igx-time-picker: extend($_light-time-picker, $_round-shape-time-picker), igx-toast: extend($_light-toast, $_round-shape-toast), diff --git a/projects/igniteui-angular/src/lib/core/styles/themes/schemas/shape/_stepper.scss b/projects/igniteui-angular/src/lib/core/styles/themes/schemas/shape/_stepper.scss new file mode 100644 index 00000000000..cf2eab72913 --- /dev/null +++ b/projects/igniteui-angular/src/lib/core/styles/themes/schemas/shape/_stepper.scss @@ -0,0 +1,38 @@ +//// +/// @group schemas +/// @access public +/// @author Marin Popov +//// + +/// @type Map +/// @property {Number} border-radius-indicator [1] - The border radius used for stepper indicator. Can be a fraction between 0 and 1, pixels, or percent. +/// @property {Number} border-radius-step-header [0] - The border radius used for stepper step-header. Can be a fraction between 0 and 1, pixels, or percent. +$_default-shape-stepper: ( + border-radius-indicator: 1, + border-radius-step-header: 0 +); + +$_round-shape-stepper: ( + border-radius-indicator: 1, + border-radius-step-header: 32px +); + +$_square-shape-stepper: ( + border-radius-indicator: 0, + border-radius-step-header: 0 +); + +/// @type Map +$_fluent-shape-stepper: ( + border-radius-indicator: 2px, + border-radius-step-header: 2px +); + +/// @type Map +$_bootstrap-shape-stepper: ( + border-radius-indicator: 2px, + border-radius-step-header: 2px +); + +/// @type Map +$_indigo-shape-stepper: extend($_default-shape-stepper); diff --git a/projects/igniteui-angular/src/lib/core/styles/themes/schemas/square-light/_index.scss b/projects/igniteui-angular/src/lib/core/styles/themes/schemas/square-light/_index.scss index a9d2a5f9cd3..4cf4a46debf 100644 --- a/projects/igniteui-angular/src/lib/core/styles/themes/schemas/square-light/_index.scss +++ b/projects/igniteui-angular/src/lib/core/styles/themes/schemas/square-light/_index.scss @@ -40,6 +40,7 @@ @import '../light/slider'; @import '../light/snackbar'; @import '../light/switch'; +@import '../light/stepper'; @import '../light/tabs'; @import '../light/time-picker'; @import '../light/toast'; @@ -85,6 +86,7 @@ /// @property {Map} igx-slider [$_light-slider] /// @property {Map} igx-snackbar [$_light-snackbar] /// @property {Map} igx-switch [$_light-switch] +/// @property {Map} igx-stepper [$_light-stepper] /// @property {Map} igx-tabs [$_light-tabs] /// @property {Map} igx-time-picker [$_light-time-picker] /// @property {Map} igx-toast [$_light-toast] @@ -112,6 +114,7 @@ $light-square-schema: ( igx-circular-bar: extend($_light-progress-circular, $_default-shape-progress), igx-snackbar: extend($_light-snackbar, $_square-shape-snackbar), igx-switch: extend($_light-switch, $_square-shape-switch), + igx-stepper: extend($_light-stepper, $_square-shape-stepper), igx-tabs: extend($_light-tabs, $_square-shape-tabs), igx-time-picker: extend($_light-time-picker, $_square-shape-time-picker), igx-toast: extend($_light-toast, $_square-shape-toast), diff --git a/projects/igniteui-angular/src/lib/core/styles/typography/_typography.scss b/projects/igniteui-angular/src/lib/core/styles/typography/_typography.scss index c066145e58b..3b5786574bb 100644 --- a/projects/igniteui-angular/src/lib/core/styles/typography/_typography.scss +++ b/projects/igniteui-angular/src/lib/core/styles/typography/_typography.scss @@ -33,6 +33,7 @@ @import '../components/slider/slider-theme'; @import '../components/snackbar/snackbar-theme'; @import '../components/switch/switch-theme'; +@import '../components/stepper/stepper-theme'; @import '../components/tabs/tabs-theme'; @import '../components/time-picker/time-picker-theme'; @import '../components/toast/toast-theme'; @@ -72,6 +73,7 @@ @include igx-slider-typography($type-scale); @include igx-snackbar-typography($type-scale); @include igx-switch-typography($type-scale); + @include igx-stepper-typography($type-scale); @include igx-tabs-typography($type-scale); @include igx-time-picker-typography($type-scale); @include igx-toast-typography($type-scale); From 9c2d929a11af4f183a0230db0646f776ee81b7ff Mon Sep 17 00:00:00 2001 From: mmart1n Date: Thu, 28 Oct 2021 18:33:21 +0300 Subject: [PATCH 2/5] feat(stepper): add component implementation Co-authored-by: Teodosia Hristodorova --- CHANGELOG.md | 19 +- README.md | 1 + .../toggle-animation-component.ts | 3 +- .../src/lib/stepper/README.md | 123 ++++ .../src/lib/stepper/public_api.ts | 3 + .../src/lib/stepper/step/step.component.html | 36 ++ .../src/lib/stepper/step/step.component.ts | 557 +++++++++++++++++ .../src/lib/stepper/stepper.common.ts | 148 +++++ .../src/lib/stepper/stepper.component.html | 17 + .../src/lib/stepper/stepper.component.ts | 564 ++++++++++++++++++ .../src/lib/stepper/stepper.directive.ts | 85 +++ .../src/lib/stepper/stepper.service.ts | 163 +++++ projects/igniteui-angular/src/public_api.ts | 1 + src/app/shared/shared.module.ts | 2 + 14 files changed, 1720 insertions(+), 2 deletions(-) create mode 100644 projects/igniteui-angular/src/lib/stepper/README.md create mode 100644 projects/igniteui-angular/src/lib/stepper/public_api.ts create mode 100644 projects/igniteui-angular/src/lib/stepper/step/step.component.html create mode 100644 projects/igniteui-angular/src/lib/stepper/step/step.component.ts create mode 100644 projects/igniteui-angular/src/lib/stepper/stepper.common.ts create mode 100644 projects/igniteui-angular/src/lib/stepper/stepper.component.html create mode 100644 projects/igniteui-angular/src/lib/stepper/stepper.component.ts create mode 100644 projects/igniteui-angular/src/lib/stepper/stepper.directive.ts create mode 100644 projects/igniteui-angular/src/lib/stepper/stepper.service.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 12ec37eef84..4f326bbdc78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,21 @@ All notable changes for each version of this project will be documented in this ## 13.0.0 ### New Features +- Added `IgxStepper` component + - Highly customizable component that visualizes content as a process and shows its progress by dividing the content into chronological `igx-steps`. + - Exposed API to control features like step validation, styling, orientation, and easy-to-use keyboard navigation. + - Code example below: + + ```html + + + ... + + + ``` + + - For more information, check out the [README](https://github.com/IgniteUI/igniteui-angular/blob/master/projects/igniteui-angular/src/lib/stepper/README.md), [specification](https://github.com/IgniteUI/igniteui-angular/wiki/Stepper-Specification) and [official documentation](https://www.infragistics.com/products/ignite-ui-angular/angular/components/stepper). + - `IgxCsvExporterService`, `IgxExcelExporterService` - Exporter services are no longer required to be provided in the application since they are now injected on a root level. - `IgxGridToolbarPinningComponent`, `IgxGridToolbarHidingComponent` @@ -34,7 +49,9 @@ All notable changes for each version of this project will be documented in this Use `IgxGridToolbarComponent`, `IgxGridToolbarHidingComponent`, `IgxGridToolbarPinningComponent` instead. - `IgxColumnActionsComponent` - **Breaking Change** - The following input has been removed - - Input `columns`. Use `igxGrid` `columns` input instead. + - Input `columns`. Use `igxGrid` `columns` input instead. +- `IgxCarousel` + - **Breaking Changes** -The carousel animation type `CarouselAnimationType` is renamed to `HorizontalAnimationType`. ## 12.2.3 diff --git a/README.md b/README.md index ad12d325d9f..5bca238553b 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,7 @@ Some of the Angular chart types included are: [Polar chart](https://www.infragis |select|:white_check_mark:|[Readme](https://github.com/IgniteUI/igniteui-angular/blob/master/projects/igniteui-angular/src/lib/select/README.md)|[Docs](https://www.infragistics.com/products/ignite-ui-angular/angular/components/select)||||| |slider|:white_check_mark:|[Readme](https://github.com/IgniteUI/igniteui-angular/blob/master/projects/igniteui-angular/src/lib/slider/README.md)|[Docs](https://www.infragistics.com/products/ignite-ui-angular/angular/components/slider/slider)||||| |snackbar|:white_check_mark:|[Readme](https://github.com/IgniteUI/igniteui-angular/blob/master/projects/igniteui-angular/src/lib/snackbar/README.md)|[Docs](https://www.infragistics.com/products/ignite-ui-angular/angular/components/snackbar)||||| +|stepper|:white_check_mark:|[Readme](https://github.com/IgniteUI/igniteui-angular/blob/master/projects/igniteui-angular/src/lib/stepper/README.md)|[Docs](https://www.infragistics.com/products/ignite-ui-angular/angular/components/stepper)| |switch|:white_check_mark:|[Readme](https://github.com/IgniteUI/igniteui-angular/blob/master/projects/igniteui-angular/src/lib/switch/README.md)|[Docs](https://www.infragistics.com/products/ignite-ui-angular/angular/components/switch)||||| |tabs|:white_check_mark:|[Readme](https://github.com/IgniteUI/igniteui-angular/blob/master/projects/igniteui-angular/src/lib/tabs/tabs/README.md)|[Docs](https://www.infragistics.com/products/ignite-ui-angular/angular/components/tabs)||||| |time picker|:white_check_mark:|[Readme](https://github.com/IgniteUI/igniteui-angular/blob/master/projects/igniteui-angular/src/lib/time-picker/README.md)|[Docs](https://www.infragistics.com/products/ignite-ui-angular/angular/components/time-picker)||||| diff --git a/projects/igniteui-angular/src/lib/expansion-panel/toggle-animation-component.ts b/projects/igniteui-angular/src/lib/expansion-panel/toggle-animation-component.ts index 081f2c97e7c..7a6cf5852f2 100644 --- a/projects/igniteui-angular/src/lib/expansion-panel/toggle-animation-component.ts +++ b/projects/igniteui-angular/src/lib/expansion-panel/toggle-animation-component.ts @@ -133,7 +133,8 @@ export abstract class ToggleAnimationPlayer implements ToggleAnimationOwner, OnD if (opposite) { if (opposite.hasStarted()) { // .getPosition() still returns 0 sometimes, regardless of the fix for https://github.com/angular/angular/issues/18891; - oppositePosition = (opposite as any)._renderer.engine.players[0].getPosition(); + const renderer = (opposite as any)._renderer; + oppositePosition = renderer.engine.players[renderer.engine.players.length - 1].getPosition(); } this.cleanUpPlayer(oppositeType); diff --git a/projects/igniteui-angular/src/lib/stepper/README.md b/projects/igniteui-angular/src/lib/stepper/README.md new file mode 100644 index 00000000000..725712c6178 --- /dev/null +++ b/projects/igniteui-angular/src/lib/stepper/README.md @@ -0,0 +1,123 @@ +# IgxStepperComponent + +## Description +_**IgxStepperComponent** is a collection of **IgxStepComponent**s that delivers a wizard-like workflow:_ + +A complete walkthrough of how to get started can be found [here](https://www.infragistics.com/products/ignite-ui-angular/angular/components/stepper). +The specification for the stepper can be found [here](https://github.com/IgniteUI/igniteui-angular/wiki/Stepper-Specification) + +---------- + +## Usage +```html + + + + {{step.indicator}} + + +

+ {{step.title}} +

+ +
+ ... +
+
+
+``` + +---------- + +## Keyboard Navigation + +The keyboard can be used to navigate through all steps in the stpper. + +_Disabled steps are not counted as visible steps for the purpose of keyboard navigation._ + +|Keys |Description| +|---------------|-----------| +| ARROW DOWN | Focuses the next step header in a vertical stepper. | +| ARROW UP | Focuses the previous step header in a vertical stepper. | +| TAB | Moves the focus to the next tabbable element. | +| SHIFT + TAB | Moves the focus to the previous tabbable element. | +| HOME | Moves the focus to the header of the FIRST enabled step in the _igx-stepper_ | +| END | Moves the focus to the header of the LAST enabled step in the _igx-stepper_ | +| ARROW RIGHT | Moves the focus to the header of the next accessible step in both orientations. | +| ARROW LEFT | Moves the focus to the header of the previous accessible step in both orientations. | +| ENTER / SPACE | Activates the currently focused step. | +| CLICK | Activates the currently focused step. | + +_By design when the user presses the **Tab** key over the step header the focus will move to the step content container. In case the container should be skipped the developer should set the content container [tabIndex]="-1"_ + +---------- + +## API Summary + +### IgxStepperComponent + +#### Accessors + +**Get** + + | Name | Description | Type | + |----------------|------------------------------------------------------------------------------|---------------------| + | steps | Gets the steps that are rendered in the stepper. | `IgxStepComponent[]` | + + +#### Properties + + | Name | Description | Type | + |----------------|------------------------------------------------------------------------------|----------------------------------------| + | id | The id of the stepper. Bound to attr.id | `string` | + | orientation | Gets/sets the orientation of the stepper. Default is `horizontal`. | `IgxStepperOrientation` | + | stepType| Gets/sets the type of the steps in the stepper. Default value is `full` | `IgxStepType` | + | titlePosition | Gets/sets the position of the titles in the stepper. Default value is `bottom` when the stepper is horizontally orientated and `end` when the layout is set to vertical. | `IgxStepperTitlePosition` | + | linear | Whether the validity of previous steps should be checked and only in case, it's valid to be able to move forward or not. Default value is `false`. | `boolean` | + | contentTop| Whether the steps content should be displayed above the steps header when the stepper orientation is Horizontal. Default value is `false`. | `boolean` | + | verticalAnimationType | Gets/sets the animation type of the stepper when the orientation direction is vertical. Default value is `grow`. | `VerticalAnimationType` | + | horizontalAnimationType | Gets/sets the animation type of the stepper when the orientation direction is horizontal. Default value is `slide`. |`HorizontalAnimationType` | + | animationDuration | 320 | `number` | + +#### Methods + | Name | Description | Parameters | Returns | + |-----------------|----------------------------|-------------------------|--------| + | navigateTo | Activates the step given by index. | `index: number` | `void` | + | next | Activates the next enabled step. | | `void` | + | prev | Activates the previous enabled step. | | `void` | + | reset | Resets the stepper to its initial state. | | `void` | + +#### Events + + | Name | Description | Cancelable | Arguments | + |----------------|-------------------------------------------------------------------------|------------|------------| + | activeStepChanging | Emitted when the active step is about to change. | true | `{ oldIndex: number, newIndex: number, owner: IgxStepperComponent, cancel: boolean }` | + | activeStepChanged | Emitted when the active step is changed. | false | `{ index: number, owner: IgxStepperComponent }` | +### IgxStepComponent + +#### Accessors + +**Get** + + | Name | Description | Type | + |-----------------|-------------------------------------------------------------------------------|---------------------| + | index | Gets the step index inside of the stepper. | `number` | + +#### Properties + + | Name | Description | Type | + |-----------------|-------------------------------------------------------------------------------|---------------------| + | id | The id of the step. Bound to attr.id | `string` | + | disabled | Gets/sets whether the step is interactable. | `boolean` | + | active | Gets/sets whether the step is activе. Two-way data binding. | `boolean` | + | optional | Gets/sets whether the step is optional. | `boolean` | + | complete | Gets/sets whether the step is completed. | `boolean` | + | isValid | Gets/sets whether the step is valid. Default value is `true`. | `boolean` | + +#### Events + + | Name | Description | Cancelable | Parameters | + |-----------------|-------------------------------------------------------------------------------|------------|---------| + | activeChange | Emitted when the step's active property changes | false | `boolean` | + + diff --git a/projects/igniteui-angular/src/lib/stepper/public_api.ts b/projects/igniteui-angular/src/lib/stepper/public_api.ts new file mode 100644 index 00000000000..ca991bac1e5 --- /dev/null +++ b/projects/igniteui-angular/src/lib/stepper/public_api.ts @@ -0,0 +1,3 @@ +export * from './stepper.component'; +export * from './step/step.component'; +export * from './stepper.common'; diff --git a/projects/igniteui-angular/src/lib/stepper/step/step.component.html b/projects/igniteui-angular/src/lib/stepper/step/step.component.html new file mode 100644 index 00000000000..c5e83f938a0 --- /dev/null +++ b/projects/igniteui-angular/src/lib/stepper/step/step.component.html @@ -0,0 +1,36 @@ + + + + + + +
+ +
+
+ + + {{ index + 1 }} + + + + + + +
+ +
+ +
+ +
+ +
+
+ + +
+ +
+
diff --git a/projects/igniteui-angular/src/lib/stepper/step/step.component.ts b/projects/igniteui-angular/src/lib/stepper/step/step.component.ts new file mode 100644 index 00000000000..5be601bdae0 --- /dev/null +++ b/projects/igniteui-angular/src/lib/stepper/step/step.component.ts @@ -0,0 +1,557 @@ +import { AnimationBuilder } from '@angular/animations'; +import { + AfterViewInit, + ChangeDetectorRef, Component, ContentChild, ElementRef, + EventEmitter, forwardRef, HostBinding, HostListener, Inject, Input, OnDestroy, Output, Renderer2, TemplateRef, ViewChild +} from '@angular/core'; +import { takeUntil } from 'rxjs/operators'; +import { HorizontalAnimationType, Direction, IgxSlideComponentBase } from '../../carousel/carousel-base'; +import { PlatformUtil } from '../../core/utils'; +import { ToggleAnimationPlayer, ToggleAnimationSettings } from '../../expansion-panel/toggle-animation-component'; +import { IgxDirectionality } from '../../services/direction/directionality'; +import { IgxStep, IgxStepper, IgxStepperOrientation, IgxStepType, IGX_STEPPER_COMPONENT, IGX_STEP_COMPONENT } from '../stepper.common'; +import { IgxStepContentDirective, IgxStepIndicatorDirective } from '../stepper.directive'; +import { IgxStepperService } from '../stepper.service'; + +let NEXT_ID = 0; + +/** + * The IgxStepComponent is used within the `igx-stepper` element and it holds the content of each step. + * It also supports custom indicators, title and subtitle. + * + * @igxModule IgxStepperModule + * + * @igxKeywords step + * + * @example + * ```html + * + * ... + * + * ... + * + * ... + * + * ``` + */ +@Component({ + selector: 'igx-step', + templateUrl: 'step.component.html', + providers: [ + { provide: IGX_STEP_COMPONENT, useExisting: IgxStepComponent } + ] +}) +export class IgxStepComponent extends ToggleAnimationPlayer implements IgxStep, AfterViewInit, OnDestroy, IgxSlideComponentBase { + + /** + * Get/Set the `id` of the step component. + * Default value is `"igx-step-0"`; + * ```html + * + * ``` + * ```typescript + * const stepId = this.step.id; + * ``` + */ + @HostBinding('attr.id') + @Input() + public id = `igx-step-${NEXT_ID++}`; + + /** + * Get/Set whether the step is interactable. + * + * ```html + * + * ... + * + * ... + * + * ``` + * + * ```typescript + * this.stepper.steps[1].disabled = true; + * ``` + */ + @Input() + public set disabled(value: boolean) { + this._disabled = value; + if (this.stepper.linear) { + this.stepperService.calculateLinearDisabledSteps(); + } + } + + public get disabled(): boolean { + return this._disabled; + } + + /** + * Get/Set whether the step is completed. + * + * @remarks + * When set to `true` the following separator is styled `solid`. + * + * ```html + * + * ... + * + * ... + * + * ``` + * + * ```typescript + * this.stepper.steps[1].completed = true; + * ``` + */ + @Input() + @HostBinding('class.igx-stepper__step--completed') + public completed = false; + + /** + * Get/Set whether the step is valid. + *```html + * + * ... + *
+ *
+ * ... + *
+ *
+ *
+ * ``` + */ + @Input() + public get isValid(): boolean { + return this._valid; + } + + public set isValid(value: boolean) { + this._valid = value; + if (this.stepper.linear && this.index !== undefined) { + this.stepperService.calculateLinearDisabledSteps(); + } + } + + /** + * Get/Set whether the step is optional. + * + * @remarks + * Optional steps validity does not affect the default behavior when the stepper is in linear mode i.e. + * if optional step is invalid the user could still move to the next step. + * + * ```html + * + * ``` + * ```typescript + * this.stepper.steps[1].optional = true; + * ``` + */ + @Input() + public optional = false; + + /** + * Get/Set the active state of the step + * + * ```html + * + * ``` + * + * ```typescript + * this.stepper.steps[1].active = true; + * ``` + * + * @param value: boolean + */ + @HostBinding('attr.aria-selected') + @Input() + public set active(value: boolean) { + if (value) { + this.stepperService.expandThroughApi(this); + } else { + this.stepperService.collapse(this); + } + } + + public get active(): boolean { + return this.stepperService.activeStep === this; + } + + /** @hidden @internal */ + @HostBinding('attr.tabindex') + @Input() + public set tabIndex(value: number) { + this._tabIndex = value; + } + + public get tabIndex(): number { + return this._tabIndex; + } + + /** @hidden @internal **/ + @HostBinding('attr.role') + public role = 'tab'; + + /** @hidden @internal */ + @HostBinding('attr.aria-controls') + public get contentId(): string { + return this.content?.id; + } + + /** @hidden @internal */ + @HostBinding('class.igx-stepper__step') + public cssClass = true; + + /** @hidden @internal */ + @HostBinding('class.igx-stepper__step--disabled') + public get generalDisabled(): boolean { + return this.disabled || this.linearDisabled; + } + + /** @hidden @internal */ + @HostBinding('class') + public get titlePositionTop(): string { + if (this.stepper.stepType !== IgxStepType.Full) { + return 'igx-stepper__step--simple'; + } + + return `igx-stepper__step--${this.titlePosition}`; + } + + /** + * Emitted when the step's `active` property changes. Can be used for two-way binding. + * + * ```html + * + * + * ``` + * + * ```typescript + * const step: IgxStepComponent = this.stepper.step[0]; + * step.activeChange.subscribe((e: boolean) => console.log("Step active state change to ", e)) + * ``` + */ + @Output() + public activeChange = new EventEmitter(); + + /** @hidden @internal */ + @ViewChild('contentTemplate', { static: true }) + public contentTemplate: TemplateRef; + + /** @hidden @internal */ + @ViewChild('customIndicator', { static: true }) + public customIndicatorTemplate: TemplateRef; + + /** @hidden @internal */ + @ViewChild('contentContainer') + public contentContainer: ElementRef; + + /** @hidden @internal */ + @ContentChild(forwardRef(() => IgxStepIndicatorDirective)) + public indicator: IgxStepIndicatorDirective; + + /** @hidden @internal */ + @ContentChild(forwardRef(() => IgxStepContentDirective)) + public content: IgxStepContentDirective; + + /** + * Get the step index inside of the stepper. + * + * ```typescript + * const step = this.stepper.steps[1]; + * const stepIndex: number = step.index; + * ``` + */ + public get index(): number { + return this._index; + } + + /** @hidden @internal */ + public get indicatorTemplate(): TemplateRef { + if (this.active && this.stepper.activeIndicatorTemplate) { + return this.stepper.activeIndicatorTemplate; + } + + if (!this.isValid && this.stepper.invalidIndicatorTemplate) { + return this.stepper.invalidIndicatorTemplate; + } + + if (this.completed && this.stepper.completedIndicatorTemplate) { + return this.stepper.completedIndicatorTemplate; + } + + if (this.indicator) { + return this.customIndicatorTemplate; + } + + return null; + } + + /** @hidden @internal */ + public get direction(): Direction { + return this.stepperService.previousActiveStep + && this.stepperService.previousActiveStep.index > this.index + ? Direction.PREV + : Direction.NEXT; + } + + /** @hidden @internal */ + public get isAccessible(): boolean { + return !this.disabled && !this.linearDisabled; + } + + /** @hidden @internal */ + public get isHorizontal(): boolean { + return this.stepper.orientation === IgxStepperOrientation.Horizontal; + } + + /** @hidden @internal */ + public get isTitleVisible(): boolean { + return this.stepper.stepType !== IgxStepType.Indicator; + } + + /** @hidden @internal */ + public get isIndicatorVisible(): boolean { + return this.stepper.stepType !== IgxStepType.Title; + } + + /** @hidden @internal */ + public get titlePosition(): string { + return this.stepper.titlePosition ? this.stepper.titlePosition : this.stepper._defaultTitlePosition; + } + + /** @hidden @internal */ + public get linearDisabled(): boolean { + return this.stepperService.linearDisabledSteps.has(this); + } + + /** @hidden @internal */ + public get collapsing(): boolean { + return this.stepperService.collapsingSteps.has(this); + } + + /** @hidden @internal */ + public get animationSettings(): ToggleAnimationSettings { + return this.stepper.verticalAnimationSettings; + } + + /** @hidden @internal */ + public get contentClasses(): any { + if (this.isHorizontal) { + return { 'igx-stepper__body-content': true, 'igx-stepper__body-content--active': this.active }; + } else { + return 'igx-stepper__step-content'; + } + } + + /** @hidden @internal */ + public get stepHeaderClasses(): any { + return { + 'igx-stepper__step--optional': this.optional, + 'igx-stepper__step-header--current': this.active, + 'igx-stepper__step-header--invalid': !this.isValid + && this.stepperService.visitedSteps.has(this) && !this.active && this.isAccessible + }; + } + + /** @hidden @internal */ + public get nativeElement(): HTMLElement { + return this.element.nativeElement; + } + /** @hidden @internal */ + public previous: boolean; + /** @hidden @internal */ + public _index: number; + private _tabIndex = -1; + private _valid = true; + private _focused = false; + private _disabled = false; + + constructor( + @Inject(IGX_STEPPER_COMPONENT) public stepper: IgxStepper, + public cdr: ChangeDetectorRef, + public renderer: Renderer2, + protected platform: PlatformUtil, + protected stepperService: IgxStepperService, + protected builder: AnimationBuilder, + private element: ElementRef, + private dir: IgxDirectionality + ) { + super(builder); + } + + /** @hidden @internal */ + @HostListener('focus') + public onFocus(): void { + this._focused = true; + this.stepperService.focusedStep = this; + if (this.stepperService.focusedStep !== this.stepperService.activeStep) { + this.stepperService.activeStep.tabIndex = -1; + } + } + + /** @hidden @internal */ + @HostListener('blur') + public onBlur(): void { + this._focused = false; + this.stepperService.activeStep.tabIndex = 0; + } + + /** @hidden @internal */ + @HostListener('keydown', ['$event']) + public handleKeydown(event: KeyboardEvent): void { + if (!this._focused) { + return; + } + const key = event.key; + if (this.stepper.orientation === IgxStepperOrientation.Horizontal) { + if (key === this.platform.KEYMAP.ARROW_UP || key === this.platform.KEYMAP.ARROW_DOWN) { + return; + } + } + if (!(this.platform.isNavigationKey(key) || this.platform.isActivationKey(event))) { + return; + } + event.preventDefault(); + this.handleNavigation(key); + } + + /** @hidden @internal */ + public ngAfterViewInit(): void { + this.openAnimationDone.pipe(takeUntil(this.destroy$)).subscribe( + () => { + if (this.stepperService.activeStep === this) { + this.stepper.activeStepChanged.emit({ owner: this.stepper, index: this.index }); + } + } + ); + this.closeAnimationDone.pipe(takeUntil(this.destroy$)).subscribe(() => { + this.stepperService.collapse(this); + this.cdr.markForCheck(); + }); + } + + /** @hidden @internal */ + public ngOnDestroy(): void { + super.ngOnDestroy(); + } + + /** @hidden @internal */ + public onPointerDown(event: MouseEvent): void { + event.stopPropagation(); + if (this.isHorizontal) { + this.changeHorizontalActiveStep(); + } else { + this.changeVerticalActiveStep(); + } + } + + /** @hidden @internal */ + public handleNavigation(key: string): void { + switch (key) { + case this.platform.KEYMAP.HOME: + this.stepper.steps.filter(s => s.isAccessible)[0]?.nativeElement.focus(); + break; + case this.platform.KEYMAP.END: + this.stepper.steps.filter(s => s.isAccessible).pop()?.nativeElement.focus(); + break; + case this.platform.KEYMAP.ARROW_UP: + this.previousStep?.nativeElement.focus(); + break; + case this.platform.KEYMAP.ARROW_LEFT: + if (this.dir.rtl && this.stepper.orientation === IgxStepperOrientation.Horizontal) { + this.nextStep?.nativeElement.focus(); + } else { + this.previousStep?.nativeElement.focus(); + } + break; + case this.platform.KEYMAP.ARROW_DOWN: + this.nextStep?.nativeElement.focus(); + break; + case this.platform.KEYMAP.ARROW_RIGHT: + if (this.dir.rtl && this.stepper.orientation === IgxStepperOrientation.Horizontal) { + this.previousStep?.nativeElement.focus(); + } else { + this.nextStep?.nativeElement.focus(); + } + break; + case this.platform.KEYMAP.SPACE: + case this.platform.KEYMAP.ENTER: + if (this.isHorizontal) { + this.changeHorizontalActiveStep(); + } else { + this.changeVerticalActiveStep(); + } + break; + default: + return; + } + } + + /** @hidden @internal */ + public changeHorizontalActiveStep(): void { + if (this.stepper.animationType === HorizontalAnimationType.none && this.stepperService.activeStep !== this) { + const argsCanceled = this.stepperService.emitActivatingEvent(this); + if (argsCanceled) { + return; + } + + this.active = true; + this.stepper.activeStepChanged.emit({ owner: this.stepper, index: this.index }); + return; + } + this.stepperService.expand(this); + if (this.stepper.animationType === HorizontalAnimationType.fade) { + if (this.stepperService.collapsingSteps.has(this.stepperService.previousActiveStep)) { + this.stepperService.previousActiveStep.active = false; + } + } + } + + private get nextStep(): IgxStepComponent | null { + const focusedStep = this.stepperService.focusedStep; + if (focusedStep) { + if (focusedStep.index === this.stepper.steps.length - 1) { + return this.stepper.steps.find(s => s.isAccessible); + } + + const nextAccessible = this.stepper.steps.find((s, i) => i > focusedStep.index && s.isAccessible); + return nextAccessible ? nextAccessible : this.stepper.steps.find(s => s.isAccessible); + } + + return null; + } + + private get previousStep(): IgxStepComponent | null { + const focusedStep = this.stepperService.focusedStep; + if (focusedStep) { + if (focusedStep.index === 0) { + return this.stepper.steps.filter(s => s.isAccessible).pop(); + } + + let prevStep; + for (let i = focusedStep.index - 1; i >= 0; i--) { + const step = this.stepper.steps[i]; + if (step.isAccessible) { + prevStep = step; + break; + } + } + + return prevStep ? prevStep : this.stepper.steps.filter(s => s.isAccessible).pop(); + + } + + return null; + } + + private changeVerticalActiveStep(): void { + this.stepperService.expand(this); + + if (!this.animationSettings.closeAnimation) { + this.stepperService.previousActiveStep.openAnimationPlayer?.finish(); + } + + if (!this.animationSettings.openAnimation) { + this.stepperService.activeStep.closeAnimationPlayer?.finish(); + } + } +} diff --git a/projects/igniteui-angular/src/lib/stepper/stepper.common.ts b/projects/igniteui-angular/src/lib/stepper/stepper.common.ts new file mode 100644 index 00000000000..57dc57356b9 --- /dev/null +++ b/projects/igniteui-angular/src/lib/stepper/stepper.common.ts @@ -0,0 +1,148 @@ +import { ChangeDetectorRef, ElementRef, EventEmitter, InjectionToken, TemplateRef } from '@angular/core'; +import { IBaseCancelableBrowserEventArgs, IBaseEventArgs } from '../core/utils'; +import { IgxStepperComponent } from './stepper.component'; +import { IgxStepComponent } from './step/step.component'; +import { + IgxStepActiveIndicatorDirective, IgxStepCompletedIndicatorDirective, IgxStepContentDirective, + IgxStepIndicatorDirective, IgxStepInvalidIndicatorDirective +} from './stepper.directive'; +import { Direction, HorizontalAnimationType, IgxCarouselComponentBase } from '../carousel/carousel-base'; +import { ToggleAnimationPlayer, ToggleAnimationSettings } from '../expansion-panel/toggle-animation-component'; + +// Component interfaces +export interface IgxStepper extends IgxCarouselComponentBase { + steps: IgxStepComponent[]; + /** @hidden @internal */ + nativeElement: HTMLElement; + /** @hidden @internal */ + invalidIndicatorTemplate: TemplateRef; + /** @hidden @internal */ + completedIndicatorTemplate: TemplateRef; + /** @hidden @internal */ + activeIndicatorTemplate: TemplateRef; + verticalAnimationType: VerticalAnimationType; + horizontalAnimationType: HorizontalAnimationType; + animationDuration: number; + linear: boolean; + orientation: IgxStepperOrientation; + stepType: IgxStepType; + contentTop: boolean; + titlePosition: IgxStepperTitlePosition; + /** @hidden @internal */ + verticalAnimationSettings: ToggleAnimationSettings; + /** @hidden @internal */ + _defaultTitlePosition: IgxStepperTitlePosition; + activeStepChanging: EventEmitter; + activeStepChanged: EventEmitter; + navigateTo(index: number): void; + next(): void; + prev(): void; + reset(): void; + /** @hidden @internal */ + playHorizontalAnimations(): void; +} + +// Item interfaces + +export interface IgxStep extends ToggleAnimationPlayer { + id: string; + /** @hidden @internal */ + contentTemplate: TemplateRef; + /** @hidden @internal */ + customIndicatorTemplate: TemplateRef; + /** @hidden @internal */ + contentContainer: ElementRef; + /** @hidden @internal */ + indicator: IgxStepIndicatorDirective; + /** @hidden @internal */ + content: IgxStepContentDirective; + /** @hidden @internal */ + indicatorTemplate: TemplateRef; + index: number; + disabled: boolean; + completed: boolean; + isValid: boolean; + optional: boolean; + active: boolean; + tabIndex: number; + /** @hidden @internal */ + contentId: string; + /** @hidden @internal */ + generalDisabled: boolean; + /** @hidden @internal */ + titlePositionTop: string; + /** @hidden @internal */ + direction: Direction; + /** @hidden @internal */ + isAccessible: boolean; + /** @hidden @internal */ + isHorizontal: boolean; + /** @hidden @internal */ + isTitleVisible: boolean; + /** @hidden @internal */ + isIndicatorVisible: boolean; + /** @hidden @internal */ + titlePosition: string; + /** @hidden @internal */ + linearDisabled: boolean; + /** @hidden @internal */ + collapsing: boolean; + /** @hidden @internal */ + animationSettings: ToggleAnimationSettings; + /** @hidden @internal */ + contentClasses: any; + /** @hidden @internal */ + stepHeaderClasses: any; + /** @hidden @internal */ + nativeElement: HTMLElement; + /** @hidden @internal */ + previous: boolean; + cdr: ChangeDetectorRef; + activeChange: EventEmitter; +} + +// Events +export interface IStepChangingEventArgs extends IBaseEventArgs, IBaseCancelableBrowserEventArgs { + newIndex: number; + oldIndex: number; + owner: IgxStepper; +} + +export interface IStepChangedEventArgs extends IBaseEventArgs { + // Provides the index of the current active step within the stepper steps + index: number; + owner: IgxStepper; +} + +// Enums +export const IgxStepperOrientation = { + Horizontal: 'horizontal', + Vertical: 'vertical' +}; +export type IgxStepperOrientation = (typeof IgxStepperOrientation)[keyof typeof IgxStepperOrientation]; + +export const IgxStepType = { + Indicator: 'indicator', + Title: 'title', + Full: 'full' +}; +export type IgxStepType = (typeof IgxStepType)[keyof typeof IgxStepType]; + +export const IgxStepperTitlePosition = { + Bottom: 'bottom', + Top: 'top', + End: 'end', + Start: 'start' +}; +export type IgxStepperTitlePosition = (typeof IgxStepperTitlePosition)[keyof typeof IgxStepperTitlePosition]; + +export const VerticalAnimationType = { + Grow: 'grow', + Fade: 'fade', + None: 'none' +}; +export type VerticalAnimationType = (typeof VerticalAnimationType)[keyof typeof VerticalAnimationType]; + +// Token +export const IGX_STEPPER_COMPONENT = new InjectionToken('IgxStepperToken'); +export const IGX_STEP_COMPONENT = new InjectionToken('IgxStepToken'); diff --git a/projects/igniteui-angular/src/lib/stepper/stepper.component.html b/projects/igniteui-angular/src/lib/stepper/stepper.component.html new file mode 100644 index 00000000000..d0da6d322f6 --- /dev/null +++ b/projects/igniteui-angular/src/lib/stepper/stepper.component.html @@ -0,0 +1,17 @@ +
+ +
+ +
+ + + +
+ +
+ +
+ + + + diff --git a/projects/igniteui-angular/src/lib/stepper/stepper.component.ts b/projects/igniteui-angular/src/lib/stepper/stepper.component.ts new file mode 100644 index 00000000000..24824aa0ab3 --- /dev/null +++ b/projects/igniteui-angular/src/lib/stepper/stepper.component.ts @@ -0,0 +1,564 @@ +import { AnimationBuilder, AnimationReferenceMetadata, useAnimation } from '@angular/animations'; +import { CommonModule } from '@angular/common'; +import { + Component, HostBinding, OnDestroy, OnInit, + Input, Output, EventEmitter, ContentChildren, QueryList, ElementRef, + NgModule, OnChanges, SimpleChanges, TemplateRef, ContentChild, AfterContentInit +} from '@angular/core'; +import { Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; +import { growVerIn, growVerOut } from '../animations/grow'; +import { fadeIn } from '../animations/main'; +import { HorizontalAnimationType, IgxCarouselComponentBase } from '../carousel/carousel-base'; +import { IgxRippleModule } from '../directives/ripple/ripple.directive'; +import { ToggleAnimationSettings } from '../expansion-panel/toggle-animation-component'; +import { + IgxStepper, IgxStepperTitlePosition, IgxStepperOrientation, + IgxStepType, IGX_STEPPER_COMPONENT, IStepChangedEventArgs, IStepChangingEventArgs, VerticalAnimationType +} from './stepper.common'; +import { + IgxStepActiveIndicatorDirective, + IgxStepCompletedIndicatorDirective, + IgxStepContentDirective, + IgxStepIndicatorDirective, IgxStepInvalidIndicatorDirective, + IgxStepSubTitleDirective, IgxStepTitleDirective +} from './stepper.directive'; +import { IgxStepComponent } from './step/step.component'; +import { IgxStepperService } from './stepper.service'; + + +// TODO: common interface between IgxCarouselComponentBase and ToggleAnimationPlayer? + +/** + * IgxStepper provides a wizard-like workflow by dividing content into logical steps. + * + * @igxModule IgxStepperModule + * + * @igxKeywords stepper + * + * @igxGroup Layouts + * + * @remarks + * The Ignite UI for Angular Stepper component allows the user to navigate between multiple steps. + * It supports horizontal and vertical orientation as well as keyboard navigation and provides API methods to control the active step. + * The component offers keyboard navigation and API to control the active step. + * + * @example + * ```html + * + * + * home + *

Home

+ *
+ * ... + *
+ *
+ * + *
+ * ... + *
+ *
+ * + *
+ * ... + *
+ *
+ *
+ * ``` + */ +@Component({ + selector: 'igx-stepper', + templateUrl: 'stepper.component.html', + providers: [ + IgxStepperService, + { provide: IGX_STEPPER_COMPONENT, useExisting: IgxStepperComponent }, + ] +}) +export class IgxStepperComponent extends IgxCarouselComponentBase implements IgxStepper, OnChanges, OnInit, AfterContentInit, OnDestroy { + + /** + * Get/Set the animation type of the stepper when the orientation direction is vertical. + * + * @remarks + * Default value is `grow`. Other possible values are `fade` and `none`. + * + * ```html + * + * + * ``` + */ + @Input() + public get verticalAnimationType(): VerticalAnimationType { + return this._verticalAnimationType; + } + + public set verticalAnimationType(value: VerticalAnimationType) { + // TODO: activeChange event is not emitted for the collapsing steps (loop through collapsing steps and emit) + this.stepperService.collapsingSteps.clear(); + this._verticalAnimationType = value; + + switch (value) { + case 'grow': + this.verticalAnimationSettings = this.updateVerticalAnimationSettings(growVerIn, growVerOut); + break; + case 'fade': + this.verticalAnimationSettings = this.updateVerticalAnimationSettings(fadeIn, null); + break; + case 'none': + this.verticalAnimationSettings = this.updateVerticalAnimationSettings(null, null); + break; + } + } + + /** + * Get/Set the animation type of the stepper when the orientation direction is horizontal. + * + * @remarks + * Default value is `grow`. Other possible values are `fade` and `none`. + * + * ```html + * + * + * ``` + */ + @Input() + public get horizontalAnimationType(): HorizontalAnimationType { + return this.animationType; + } + + public set horizontalAnimationType(value: HorizontalAnimationType) { + // TODO: activeChange event is not emitted for the collapsing steps (loop through collapsing steps and emit) + this.stepperService.collapsingSteps.clear(); + this.animationType = value; + } + + /** + * Get/Set the animation duration. + * ```html + * + * + * ``` + */ + @Input() + public get animationDuration(): number { + return this.defaultAnimationDuration; + } + + public set animationDuration(value: number) { + if (value && value > 0) { + this.defaultAnimationDuration = value; + return; + } + this.defaultAnimationDuration = this._defaultAnimationDuration; + } + + /** + * Get/Set whether the stepper is linear. + * + * @remarks + * If the stepper is in linear mode and if the active step is valid only then the user is able to move forward. + * + * ```html + * + * ``` + */ + @Input() + public get linear(): boolean { + return this._linear; + } + + public set linear(value: boolean) { + this._linear = value; + if (this._linear && this.steps.length > 0) { + // when the stepper is in linear mode we should calculate which steps should be disabled + // and which are visited i.e. their validity should be correctly displayed. + this.stepperService.calculateVisitedSteps(); + this.stepperService.calculateLinearDisabledSteps(); + } else { + this.stepperService.linearDisabledSteps.clear(); + } + } + + /** + * Get/Set the stepper orientation. + * + * ```typescript + * this.stepper.orientation = IgxStepperOrientation.Vertical; + * ``` + */ + @HostBinding('attr.aria-orientation') + @Input() + public get orientation(): IgxStepperOrientation { + return this._orientation; + } + + public set orientation(value: IgxStepperOrientation) { + if (this._orientation === value) { + return; + } + + // TODO: activeChange event is not emitted for the collapsing steps + this.stepperService.collapsingSteps.clear(); + this._orientation = value; + this._defaultTitlePosition = this._orientation === IgxStepperOrientation.Horizontal ? + IgxStepperTitlePosition.Bottom : IgxStepperTitlePosition.End; + } + + /** + * Get/Set the type of the steps. + * + * ```typescript + * this.stepper.stepType = IgxStepType.Indicator; + * ``` + */ + @Input() + public stepType: IgxStepType = IgxStepType.Full; + + /** + * Get/Set whether the content is displayed above the steps. + * + * @remarks + * Default value is `false` and the content is below the steps. + * + * ```typescript + * this.stepper.contentTop = true; + * ``` + */ + @Input() + public contentTop = false; + + /** + * Get/Set the position of the steps title. + * + * @remarks + * The default value when the stepper is horizontally orientated is `bottom`. + * In vertical layout the default title position is `end`. + * + * ```typescript + * this.stepper.titlePosition = IgxStepperTitlePosition.Top; + * ``` + */ + @Input() + public titlePosition: IgxStepperTitlePosition = null; + + /** @hidden @internal **/ + @HostBinding('class.igx-stepper') + public cssClass = 'igx-stepper'; + + /** @hidden @internal **/ + @HostBinding('attr.role') + public role = 'tablist'; + + /** @hidden @internal **/ + @HostBinding('class.igx-stepper--horizontal') + public get directionClass() { + return this.orientation === IgxStepperOrientation.Horizontal; + } + + /** + * Emitted when the stepper's active step is changing. + * + *```html + * + * + * ``` + * + *```typescript + * public handleActiveStepChanging(event: IStepTogglingEventArgs) { + * if (event.newIndex < event.oldIndex) { + * event.cancel = true; + * } + * } + *``` + */ + @Output() + public activeStepChanging = new EventEmitter(); + + /** + * Emitted when the active step is changed. + * + * @example + * ``` + * + * ``` + */ + @Output() + public activeStepChanged = new EventEmitter(); + + /** @hidden @internal */ + @ContentChild(IgxStepInvalidIndicatorDirective, { read: TemplateRef }) + public invalidIndicatorTemplate: TemplateRef; + + /** @hidden @internal */ + @ContentChild(IgxStepCompletedIndicatorDirective, { read: TemplateRef }) + public completedIndicatorTemplate: TemplateRef; + + /** @hidden @internal */ + @ContentChild(IgxStepActiveIndicatorDirective, { read: TemplateRef }) + public activeIndicatorTemplate: TemplateRef; + + /** @hidden @internal */ + @ContentChildren(IgxStepComponent, { descendants: false }) + private _steps: QueryList; + + /** + * Get all steps. + * + * ```typescript + * const steps: IgxStepComponent[] = this.stepper.steps; + * ``` + */ + public get steps(): IgxStepComponent[] { + return this._steps?.toArray() || []; + } + + /** @hidden @internal */ + public get nativeElement(): HTMLElement { + return this.element.nativeElement; + } + + /** @hidden @internal */ + public verticalAnimationSettings: ToggleAnimationSettings = { + openAnimation: growVerIn, + closeAnimation: growVerOut, + }; + /** @hidden @internal */ + public _defaultTitlePosition: IgxStepperTitlePosition = IgxStepperTitlePosition.Bottom; + private destroy$ = new Subject(); + private _orientation: IgxStepperOrientation = IgxStepperOrientation.Horizontal; + private _verticalAnimationType: VerticalAnimationType = VerticalAnimationType.Grow; + private _linear = false; + private readonly _defaultAnimationDuration = 350; + + constructor( + private animBuilder: AnimationBuilder, + private stepperService: IgxStepperService, + private element: ElementRef) { + super(animBuilder); + this.stepperService.stepper = this; + } + + /** @hidden @internal */ + public ngOnChanges(changes: SimpleChanges): void { + if (changes['animationDuration']) { + this.verticalAnimationType = this._verticalAnimationType; + } + } + + /** @hidden @internal */ + public ngOnInit(): void { + this.enterAnimationDone.pipe(takeUntil(this.destroy$)).subscribe(() => { + this.activeStepChanged.emit({ owner: this, index: this.stepperService.activeStep.index }); + }); + this.leaveAnimationDone.pipe(takeUntil(this.destroy$)).subscribe(() => { + if (this.stepperService.collapsingSteps.size === 1) { + this.stepperService.collapse(this.stepperService.previousActiveStep); + } else { + Array.from(this.stepperService.collapsingSteps).slice(0, this.stepperService.collapsingSteps.size - 1) + .forEach(step => this.stepperService.collapse(step)); + } + }); + + + } + + /** @hidden @internal */ + public ngAfterContentInit(): void { + let activeStep; + this.steps.forEach((step, index) => { + this.updateStepAria(step, index); + if (!activeStep && step.active) { + activeStep = step; + } + }); + if (!activeStep) { + this.activateFirstStep(true); + } + + this.handleStepChanges(); + } + + /** @hidden @internal */ + public ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } + + /** + * Activates the step at a given index. + * + *```typescript + * this.stepper.navigateTo(1); + *``` + */ + public navigateTo(index: number): void { + const step = this.steps[index]; + if (!step || this.stepperService.activeStep === step) { + return; + } + this.activateStep(step); + } + + /** + * Activates the next enabled step. + * + *```typescript + * this.stepper.next(); + *``` + */ + public next(): void { + this.moveToNextStep(); + } + + /** + * Activates the previous enabled step. + * + *```typescript + * this.stepper.prev(); + *``` + */ + public prev(): void { + this.moveToNextStep(false); + } + + /** + * Resets the stepper to its initial state i.e. activates the first step. + * + * @remarks + * The steps' content will not be automatically reset. + *```typescript + * this.stepper.reset(); + *``` + */ + public reset(): void { + this.stepperService.visitedSteps.clear(); + const activeStep = this.steps.find(s => !s.disabled); + if (activeStep) { + this.activateStep(activeStep); + } + } + + /** @hidden @internal */ + public playHorizontalAnimations(): void { + this.previousItem = this.stepperService.previousActiveStep; + this.currentItem = this.stepperService.activeStep; + this.triggerAnimations(); + } + + protected getPreviousElement(): HTMLElement { + return this.stepperService.previousActiveStep?.contentContainer.nativeElement; + } + + protected getCurrentElement(): HTMLElement { + return this.stepperService.activeStep.contentContainer.nativeElement; + } + + private updateVerticalAnimationSettings( + openAnimation: AnimationReferenceMetadata, + closeAnimation: AnimationReferenceMetadata): ToggleAnimationSettings { + const customCloseAnimation = useAnimation(closeAnimation, { + params: { + duration: this.animationDuration + 'ms' + } + }); + const customOpenAnimation = useAnimation(openAnimation, { + params: { + duration: this.animationDuration + 'ms' + } + }); + + return { + openAnimation: openAnimation ? customOpenAnimation : null, + closeAnimation: closeAnimation ? customCloseAnimation : null + }; + } + + private updateStepAria(step: IgxStepComponent, index: number): void { + step._index = index; + step.renderer.setAttribute(step.nativeElement, 'aria-setsize', (this.steps.length).toString()); + step.renderer.setAttribute(step.nativeElement, 'aria-posinset', (index + 1).toString()); + } + + private handleStepChanges(): void { + this._steps.changes.pipe(takeUntil(this.destroy$)).subscribe(steps => { + Promise.resolve().then(() => { + steps.forEach((step, index) => { + this.updateStepAria(step, index); + }); + + // when the active step is removed + const hasActiveStep = this.steps.find(s => s === this.stepperService.activeStep); + if (!hasActiveStep) { + this.activateFirstStep(); + } + // TO DO: mark step added before the active as visited? + if (this.linear) { + this.stepperService.calculateLinearDisabledSteps(); + } + }); + }); + } + + private activateFirstStep(activateInitially = false) { + const firstEnabledStep = this.steps.find(s => !s.disabled); + if (firstEnabledStep) { + firstEnabledStep.active = true; + if (activateInitially) { + firstEnabledStep.activeChange.emit(true); + this.activeStepChanged.emit({ owner: this, index: firstEnabledStep.index }); + } + } + } + + private activateStep(step: IgxStepComponent) { + if (this.orientation === IgxStepperOrientation.Horizontal) { + step.changeHorizontalActiveStep(); + } else { + this.stepperService.expand(step); + } + } + + private moveToNextStep(next = true) { + let steps: IgxStepComponent[] = this.steps; + let activeStepIndex = this.stepperService.activeStep.index; + if (!next) { + steps = this.steps.reverse(); + activeStepIndex = steps.findIndex(s => s === this.stepperService.activeStep); + } + + const nextStep = steps.find((s, i) => i > activeStepIndex && s.isAccessible); + if (nextStep) { + this.activateStep(nextStep); + } + } +} + +@NgModule({ + imports: [ + CommonModule, + IgxRippleModule + ], + declarations: [ + IgxStepComponent, + IgxStepperComponent, + IgxStepTitleDirective, + IgxStepSubTitleDirective, + IgxStepIndicatorDirective, + IgxStepContentDirective, + IgxStepActiveIndicatorDirective, + IgxStepCompletedIndicatorDirective, + IgxStepInvalidIndicatorDirective, + ], + exports: [ + IgxStepComponent, + IgxStepperComponent, + IgxStepTitleDirective, + IgxStepSubTitleDirective, + IgxStepIndicatorDirective, + IgxStepContentDirective, + IgxStepActiveIndicatorDirective, + IgxStepCompletedIndicatorDirective, + IgxStepInvalidIndicatorDirective, + ] +}) +export class IgxStepperModule { } diff --git a/projects/igniteui-angular/src/lib/stepper/stepper.directive.ts b/projects/igniteui-angular/src/lib/stepper/stepper.directive.ts new file mode 100644 index 00000000000..b623013c82d --- /dev/null +++ b/projects/igniteui-angular/src/lib/stepper/stepper.directive.ts @@ -0,0 +1,85 @@ +import { Directive, ElementRef, HostBinding, Inject, Input } from '@angular/core'; +import { IGX_STEP_COMPONENT } from './stepper.common'; +import { IgxStepComponent } from './step/step.component'; +import { IgxStepperService } from './stepper.service'; + +@Directive({ + selector: '[igxStepActiveIndicator]' +}) +export class IgxStepActiveIndicatorDirective { } + +@Directive({ + selector: '[igxStepCompletedIndicator]' +}) +export class IgxStepCompletedIndicatorDirective { } + +@Directive({ + selector: '[igxStepInvalidIndicator]' +}) +export class IgxStepInvalidIndicatorDirective { } + +@Directive({ + selector: '[igxStepIndicator]' +}) +export class IgxStepIndicatorDirective { } + +@Directive({ + selector: '[igxStepTitle]' +}) +export class IgxStepTitleDirective { + @HostBinding('class.igx-stepper__step-title') + public defaultClass = true; +} + +@Directive({ + selector: '[igxStepSubTitle]' +}) +export class IgxStepSubTitleDirective { + @HostBinding('class.igx-stepper__step-subtitle') + public defaultClass = true; +} + +@Directive({ + selector: '[igxStepContent]' +}) +export class IgxStepContentDirective { + private get target(): IgxStepComponent { + return this.step; + } + + @HostBinding('class.igx-stepper__step-content') + public defaultClass = true; + + @HostBinding('attr.role') + public role = 'tabpanel'; + + @HostBinding('attr.aria-labelledby') + public get stepId(): string { + return this.target.id; + } + + @HostBinding('attr.id') + @Input() + public id = this.target.id.replace('step', 'content'); + + @HostBinding('attr.tabindex') + @Input() + public get tabIndex(): number { + if (this._tabIndex !== null) { + return this._tabIndex; + } + + return this.stepperService.activeStep === this.target ? 0 : -1; + } + + public set tabIndex(val: number) { + this._tabIndex = val; + } + + private _tabIndex = null; + + constructor(@Inject(IGX_STEP_COMPONENT) private step: IgxStepComponent, + private stepperService: IgxStepperService, + public elementRef: ElementRef) { + } +} diff --git a/projects/igniteui-angular/src/lib/stepper/stepper.service.ts b/projects/igniteui-angular/src/lib/stepper/stepper.service.ts new file mode 100644 index 00000000000..93186fdc260 --- /dev/null +++ b/projects/igniteui-angular/src/lib/stepper/stepper.service.ts @@ -0,0 +1,163 @@ +import { Injectable } from '@angular/core'; +import { IgxStepper, IgxStepperOrientation, IStepChangingEventArgs } from './stepper.common'; +import { IgxStepComponent } from './step/step.component'; + +/** @hidden @internal */ +@Injectable() +export class IgxStepperService { + public activeStep: IgxStepComponent; + public previousActiveStep: IgxStepComponent; + public focusedStep: IgxStepComponent; + + public collapsingSteps: Set = new Set(); + public linearDisabledSteps: Set = new Set(); + public visitedSteps: Set = new Set(); + public stepper: IgxStepper; + + /** + * Activates the step, fires the steps change event and plays animations. + */ + public expand(step: IgxStepComponent): void { + if (this.activeStep === step) { + return; + } + + const cancel = this.emitActivatingEvent(step); + if (cancel) { + return; + } + + this.collapsingSteps.delete(step); + + this.previousActiveStep = this.activeStep; + this.activeStep = step; + this.activeStep.activeChange.emit(true); + + this.collapsingSteps.add(this.previousActiveStep); + this.visitedSteps.add(this.activeStep); + + if (this.stepper.orientation === IgxStepperOrientation.Vertical) { + this.previousActiveStep.playCloseAnimation( + this.previousActiveStep.contentContainer + ); + this.activeStep.cdr.detectChanges(); + + this.activeStep.playOpenAnimation( + this.activeStep.contentContainer + ); + } else { + this.activeStep.cdr.detectChanges(); + this.stepper.playHorizontalAnimations(); + } + } + + /** + * Activates the step and fires the steps change event without playing animations. + */ + public expandThroughApi(step: IgxStepComponent): void { + if (this.activeStep === step) { + return; + } + + this.collapsingSteps.clear(); + + this.previousActiveStep = this.activeStep; + this.activeStep = step; + + if (this.previousActiveStep) { + this.previousActiveStep.tabIndex = -1; + } + this.activeStep.tabIndex = 0; + this.visitedSteps.add(this.activeStep); + + this.activeStep.cdr.detectChanges(); + this.previousActiveStep?.cdr.detectChanges(); + + this.activeStep.activeChange.emit(true); + this.previousActiveStep?.activeChange.emit(false); + } + + /** + * Collapses the currently active step and fires the change event. + */ + public collapse(step: IgxStepComponent): void { + if (this.activeStep === step) { + return; + } + step.activeChange.emit(false); + this.collapsingSteps.delete(step); + } + + /** + * Determines the steps that should be marked as visited based on the active step. + */ + public calculateVisitedSteps(): void { + this.stepper.steps.forEach(step => { + if (step.index <= this.activeStep.index) { + this.visitedSteps.add(step); + } else { + this.visitedSteps.delete(step); + } + }); + } + + /** + * Determines the steps that should be disabled in linear mode based on the validity of the active step. + */ + public calculateLinearDisabledSteps(): void { + if (!this.activeStep) { + return; + } + + if (this.activeStep.isValid) { + const firstRequiredIndex = this.getNextRequiredStep(); + if (firstRequiredIndex !== -1) { + this.updateLinearDisabledSteps(firstRequiredIndex); + } else { + this.linearDisabledSteps.clear(); + } + } else { + this.stepper.steps.forEach(s => { + if (s.index > this.activeStep.index) { + this.linearDisabledSteps.add(s); + } + }); + } + } + + public emitActivatingEvent(step: IgxStepComponent): boolean { + const args: IStepChangingEventArgs = { + owner: this.stepper, + newIndex: step.index, + oldIndex: this.activeStep.index, + cancel: false + }; + + this.stepper.activeStepChanging.emit(args); + return args.cancel; + } + + /** + * Updates the linearDisabled steps from the current active step to the next required invalid step. + * + * @param toIndex the index of the last step that should be enabled. + */ + private updateLinearDisabledSteps(toIndex: number): void { + this.stepper.steps.forEach(s => { + if (s.index > this.activeStep.index) { + if (s.index <= toIndex) { + this.linearDisabledSteps.delete(s); + } else { + this.linearDisabledSteps.add(s); + } + } + }); + } + + private getNextRequiredStep(): number { + if (!this.activeStep) { + return; + } + return this.stepper.steps.findIndex(s => s.index > this.activeStep.index && !s.optional && !s.disabled && !s.isValid); + } +} diff --git a/projects/igniteui-angular/src/public_api.ts b/projects/igniteui-angular/src/public_api.ts index 24bb1da6b55..d12f1fcaf8b 100644 --- a/projects/igniteui-angular/src/public_api.ts +++ b/projects/igniteui-angular/src/public_api.ts @@ -100,6 +100,7 @@ export * from './lib/select/public_api'; export * from './lib/splitter/splitter-pane/splitter-pane.component'; export * from './lib/splitter/splitter.component'; export * from './lib/splitter/splitter.module'; +export * from './lib/stepper/public_api'; export * from './lib/date-range-picker/public_api'; export * from './lib/date-common/public_api'; diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index 617a4d78c74..2b300c5fe8e 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -33,6 +33,7 @@ import { IgxRippleModule, IgxSliderModule, IgxSnackbarModule, + IgxStepperModule, IgxSwitchModule, IgxSplitterModule, IgxTabsModule, @@ -80,6 +81,7 @@ const igniteModules = [ IgxRippleModule, IgxSliderModule, IgxSnackbarModule, + IgxStepperModule, IgxSwitchModule, IgxSplitterModule, IgxTreeModule, From 715e060811514bad75d9a9a76c2c4ec97e50d95f Mon Sep 17 00:00:00 2001 From: mmart1n Date: Thu, 28 Oct 2021 18:33:41 +0300 Subject: [PATCH 3/5] feat(stepper): add component sample --- src/app/app.component.ts | 5 + src/app/app.module.ts | 4 +- src/app/app.routing.ts | 5 + src/app/routing.ts | 5 + src/app/stepper/stepper.sample.html | 224 ++++++++++++++++++++++++++++ src/app/stepper/stepper.sample.scss | 42 ++++++ src/app/stepper/stepper.sample.ts | 171 +++++++++++++++++++++ 7 files changed, 455 insertions(+), 1 deletion(-) create mode 100644 src/app/stepper/stepper.sample.html create mode 100644 src/app/stepper/stepper.sample.scss create mode 100644 src/app/stepper/stepper.sample.ts diff --git a/src/app/app.component.ts b/src/app/app.component.ts index d4100bff3c6..13260d3399e 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -449,6 +449,11 @@ export class AppComponent implements OnInit { icon: 'feedback', name: 'Snackbar' }, + { + link: '/stepper', + icon: 'format_list_bulleted', + name: 'Stepper' + }, { link: '/tabs', icon: 'tab', diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 2aafc090715..f4e87a04e83 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -154,6 +154,7 @@ import { GridLocalizationSampleComponent } from './grid-localization/grid-locali import { TreeGridGroupBySampleComponent } from './tree-grid-groupby/tree-grid-groupby.sample'; import { PaginationSampleComponent } from './pagination/pagination.component'; import { GridCellAPISampleComponent } from './grid-cell-api/grid-cell-api.sample'; +import { IgxStepperSampleComponent } from './stepper/stepper.sample'; const components = [ AccordionSampleComponent, @@ -288,7 +289,8 @@ const components = [ GridNestedPropsSampleComponent, IgxColumnGroupingDirective, GridColumnTypesSampleComponent, - GridLocalizationSampleComponent + GridLocalizationSampleComponent, + IgxStepperSampleComponent ]; @NgModule({ diff --git a/src/app/app.routing.ts b/src/app/app.routing.ts index bf008995af1..43e61f20dd1 100644 --- a/src/app/app.routing.ts +++ b/src/app/app.routing.ts @@ -95,6 +95,7 @@ import { GridLocalizationSampleComponent } from './grid-localization/grid-locali import { TreeGridGroupBySampleComponent } from './tree-grid-groupby/tree-grid-groupby.sample'; import { PaginationSampleComponent } from './pagination/pagination.component'; import { GridCellAPISampleComponent } from './grid-cell-api/grid-cell-api.sample'; +import { IgxStepperSampleComponent } from './stepper/stepper.sample'; const appRoutes = [ { @@ -446,6 +447,10 @@ const appRoutes = [ { path: 'pagination', Comment: PaginationSampleComponent + }, + { + path: 'stepper', + component: IgxStepperSampleComponent } ]; diff --git a/src/app/routing.ts b/src/app/routing.ts index 2bb4e35eaa4..52ce8e27d16 100644 --- a/src/app/routing.ts +++ b/src/app/routing.ts @@ -125,6 +125,7 @@ import { GridLocalizationSampleComponent } from './grid-localization/grid-locali import { TreeGridGroupBySampleComponent } from './tree-grid-groupby/tree-grid-groupby.sample'; import { PaginationSampleComponent } from './pagination/pagination.component'; import { GridCellAPISampleComponent } from './grid-cell-api/grid-cell-api.sample'; +import { IgxStepperSampleComponent as StepperSampleComponent } from './stepper/stepper.sample'; const appRoutes = [ { @@ -598,6 +599,10 @@ const appRoutes = [ },{ path: 'pagination', component: PaginationSampleComponent + }, + { + path: 'stepper', + component: StepperSampleComponent } ]; diff --git a/src/app/stepper/stepper.sample.html b/src/app/stepper/stepper.sample.html new file mode 100644 index 00000000000..00b7bae344a --- /dev/null +++ b/src/app/stepper/stepper.sample.html @@ -0,0 +1,224 @@ +
+ +
+

Vertical Animation Type

+ + +
+
+

Horizontal Animation Type

+ + +
+ + + + ms + +
+
+ Set Title Position +
+

Title Position

+ + +
+
+

Step Type

+ +
+
+ +
+ + + + edit + + + + + + + + shopping_cart + Shopping Card + {{stepper.linear === true ? '(Required)' : 'Optional'}} +
+
+ + + + + + + + + + + +
+
+
+ + place + Delivery Address + (Required) +
+ Lorem ipsum dolor sit amet consectetur adipisicing elit. Cum eos asperiores, doloribus reiciendis, esse + earum, iusto expedita rem quo mollitia voluptatem quas saepe quia! Rem illum quo officia eum quisquam? +
+
+ + + + + +
+ + +
+ + User Details + (Requred) +
+
+ + + + + + + + + + + + + + + + +
+
+
+ + + attach_money + Payment method + Currently you can pay only cash +
+
+ + + + + + + + + + + + +
+
+
+ + + notes + Additional notes + {{stepper.linear === true ? '(Required)' : 'Optional'}} +
+
+
+ + + + + person + + + + + + + email + + + + + Apple + Orange + Grapes + Banana + +
+ +
+
+
+ + + receipt_long + Finish order + (#12542653) +
+
+
+ + + +359 + + + + phone + + Ex.: +359 888 123 456 + +
+ +
+
+
+
+
+ +
+ + + + Linear + Display Step +
diff --git a/src/app/stepper/stepper.sample.scss b/src/app/stepper/stepper.sample.scss new file mode 100644 index 00000000000..d21544f68ad --- /dev/null +++ b/src/app/stepper/stepper.sample.scss @@ -0,0 +1,42 @@ +.sample-actions { + margin: 20px 0; + display: flex; + gap: 25px; + + p { + margin-top: 0; + margin-bottom: 4px; + font-size: 14px; + } + + button { + align-self: center; + } + + &__btg { + width: 250px; + } +} + +igx-badge { + position: absolute; + top: -6px; + right: -6px; + width: 16px; + height: 16px; + min-width: 0; +} + +.nav-buttons { + margin: 40px 0; + + > * { + margin-right: 16px; + } +} + +.sample-stepper-wrapper { + margin: 16px 0; + padding: 16px; + border: 1px solid rgba(54, 54, 54, .43); +} diff --git a/src/app/stepper/stepper.sample.ts b/src/app/stepper/stepper.sample.ts new file mode 100644 index 00000000000..bee6bc8ba81 --- /dev/null +++ b/src/app/stepper/stepper.sample.ts @@ -0,0 +1,171 @@ +import { AfterViewInit, ChangeDetectorRef, Component, ViewChild } from '@angular/core'; +import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms'; +import { HorizontalAnimationType } from 'projects/igniteui-angular/src/lib/carousel/carousel-base'; +import { + IgxStepperTitlePosition, IgxStepperOrientation, IgxStepType, VerticalAnimationType +} from 'projects/igniteui-angular/src/lib/stepper/stepper.common'; +import { IgxStepperComponent } from 'projects/igniteui-angular/src/lib/stepper/stepper.component'; + +@Component({ + templateUrl: 'stepper.sample.html', + styleUrls: ['stepper.sample.scss'] +}) +export class IgxStepperSampleComponent implements AfterViewInit { + @ViewChild('stepper', { static: true }) public stepper: IgxStepperComponent; + public displayStep = false; + public horizontalAnimationType: HorizontalAnimationType = 'slide'; + public verticalAnimationType: VerticalAnimationType = 'grow'; + public animationDuration = 320; + public stepType: IgxStepType = IgxStepType.Full; + public titlePos: IgxStepperTitlePosition = IgxStepperTitlePosition.Bottom; + public setTitlePos = false; + public stepTypes = []; + public titlePositions = []; + public horizontalAnimationTypes = []; + public verticalAnimationTypes = []; + + public user = { + firstName: 'John', + lastName: 'Doe' + }; + + public user1 = { + password: '1337s3cr3t', + phone: '', + dateOfBirth: new Date('07 July, 1987') + }; + + public user2 = { + firstName: 'Sam', + lastName: '' + }; + + public user3: FormGroup; + public user4: FormGroup; + + constructor(private cdr: ChangeDetectorRef, fb: FormBuilder) { + this.stepTypes = [ + { + label: 'Indicator', stepType: IgxStepType.Indicator, + selected: this.stepType === IgxStepType.Indicator, togglable: true + }, + { + label: 'Title', stepType: IgxStepType.Title, + selected: this.stepType === IgxStepType.Title, togglable: true + }, + { + label: 'Full', stepType: IgxStepType.Full, + selected: this.stepType === IgxStepType.Full, togglable: true + } + ]; + + this.titlePositions = [ + { + label: 'Bottom', titlePos: IgxStepperTitlePosition.Bottom, + selected: this.titlePos === IgxStepperTitlePosition.Bottom, togglable: true + }, + { + label: 'Top', titlePos: IgxStepperTitlePosition.Top, + selected: this.titlePos === IgxStepperTitlePosition.Top, togglable: true + }, + { + label: 'End', titlePos: IgxStepperTitlePosition.End, + selected: this.titlePos === IgxStepperTitlePosition.End, togglable: true + }, + { + label: 'Start', titlePos: IgxStepperTitlePosition.Start, + selected: this.titlePos === IgxStepperTitlePosition.Start, togglable: true + } + ]; + + this.horizontalAnimationTypes = [ + { + label: 'slide', horizontalAnimationType: HorizontalAnimationType.slide, + selected: this.horizontalAnimationType === HorizontalAnimationType.slide, togglable: true + }, + { + label: 'fade', horizontalAnimationType: HorizontalAnimationType.fade, + selected: this.horizontalAnimationType === HorizontalAnimationType.fade, togglable: true + }, + { + label: 'none', horizontalAnimationType: HorizontalAnimationType.none, + selected: this.horizontalAnimationType === HorizontalAnimationType.none, togglable: true + } + ]; + + this.verticalAnimationTypes = [ + { + label: 'grow', verticalAnimationType: VerticalAnimationType.Grow, + selected: this.verticalAnimationType === VerticalAnimationType.Grow, togglable: true + }, + { + label: 'fade', verticalAnimationType: VerticalAnimationType.Fade, + selected: this.verticalAnimationType === VerticalAnimationType.Fade, togglable: true + }, + { + label: 'none', verticalAnimationType: VerticalAnimationType.None, + selected: this.verticalAnimationType === VerticalAnimationType.None, togglable: true + } + ]; + + this.user3 = fb.group({ + fullName: new FormControl('', Validators.required), + email: ['', Validators.required] + }); + + this.user4 = fb.group({ + phone: ['', Validators.required], + dateTime: [''] + }); + } + + public ngAfterViewInit() { + } + + public toggleStepTypes(event) { + this.stepType = this.stepTypes[event.index].stepType; + } + + public toggleHorizontalAnimations(event) { + this.horizontalAnimationType = this.horizontalAnimationTypes[event.index].horizontalAnimationType; + } + + public toggleVerticalAnimations(event) { + this.verticalAnimationType = this.verticalAnimationTypes[event.index].verticalAnimationType; + } + + public setTitlePosition(event) { + this.setTitlePos = event.checked; + this.stepper.titlePosition = event.checked ? this.titlePos : null; + } + + public toggleTitlePos(event) { + this.titlePos = this.titlePositions[event.index].titlePos; + if (this.setTitlePos) { + this.stepper.titlePosition = this.titlePos; + } + } + + public activeChanged(event) { + console.log('GOLQM ACTIVE CHANGED'); + // console.log(event); + } + + public activeStepChange(ev) { + console.log('MALUK CHANGE', ev); + } + + public activeStepChanging(ev) { + console.log('ACTIVE STEP CHANGING'); + // ev.cancel = true; + console.log(ev); + } + + public changeOrientation() { + if (this.stepper.orientation === IgxStepperOrientation.Horizontal) { + this.stepper.orientation = IgxStepperOrientation.Vertical; + } else { + this.stepper.orientation = IgxStepperOrientation.Horizontal; + } + } +} From e37a358b90e7d3f6f0f51e49c5736d36dff82c30 Mon Sep 17 00:00:00 2001 From: mmart1n Date: Thu, 28 Oct 2021 18:34:19 +0300 Subject: [PATCH 4/5] refactor(carousel): add migration for CarouselAnimationType - add leaveAnimationDone and enterAnimationDone events - rename animationDuration Co-authored-by: MonikaKirkova --- .../migrations/migration-collection.json | 5 ++ .../update-13_0_0/changes/classes.json | 8 +++ .../migrations/update-13_0_0/index.spec.ts | 70 +++++++++++++++++++ .../migrations/update-13_0_0/index.ts | 11 +++ .../src/lib/carousel/README.md | 2 +- .../src/lib/carousel/carousel-base.ts | 26 ++++--- .../lib/carousel/carousel.component.spec.ts | 8 +-- .../src/lib/carousel/carousel.component.ts | 12 ++-- 8 files changed, 123 insertions(+), 19 deletions(-) create mode 100644 projects/igniteui-angular/migrations/update-13_0_0/changes/classes.json create mode 100644 projects/igniteui-angular/migrations/update-13_0_0/index.spec.ts create mode 100644 projects/igniteui-angular/migrations/update-13_0_0/index.ts diff --git a/projects/igniteui-angular/migrations/migration-collection.json b/projects/igniteui-angular/migrations/migration-collection.json index b00301e1fdc..b4206ea4131 100644 --- a/projects/igniteui-angular/migrations/migration-collection.json +++ b/projects/igniteui-angular/migrations/migration-collection.json @@ -105,6 +105,11 @@ "version": "12.1.0", "description": "Updates Ignite UI for Angular from v11.1.x to v12.1.0", "factory": "./update-12_1_0" + }, + "migration-22": { + "version": "13.0.0", + "description": "Updates Ignite UI for Angular from v12.2.x to v13.0.0", + "factory": "./update-13_0_0" } } } diff --git a/projects/igniteui-angular/migrations/update-13_0_0/changes/classes.json b/projects/igniteui-angular/migrations/update-13_0_0/changes/classes.json new file mode 100644 index 00000000000..ec58fda9821 --- /dev/null +++ b/projects/igniteui-angular/migrations/update-13_0_0/changes/classes.json @@ -0,0 +1,8 @@ +{ + "$schema": "../../common/schema/class.schema.json", + "changes": [{ + "name": "CarouselAnimationType", + "replaceWith": "HorizontalAnimationType" + } + ] +} diff --git a/projects/igniteui-angular/migrations/update-13_0_0/index.spec.ts b/projects/igniteui-angular/migrations/update-13_0_0/index.spec.ts new file mode 100644 index 00000000000..f6e3398bf8b --- /dev/null +++ b/projects/igniteui-angular/migrations/update-13_0_0/index.spec.ts @@ -0,0 +1,70 @@ +import * as path from 'path'; + +import { EmptyTree } from '@angular-devkit/schematics'; +import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing'; + +const version = '13.0.0'; + +describe(`Update to ${version}`, () => { + let appTree: UnitTestTree; + const schematicRunner = new SchematicTestRunner('ig-migrate', path.join(__dirname, '../migration-collection.json')); + const configJson = { + defaultProject: 'testProj', + projects: { + testProj: { + sourceRoot: '/testSrc' + } + }, + schematics: { + '@schematics/angular:component': { + prefix: 'appPrefix' + } + } + }; + + const migrationName = 'migration-22'; + + beforeEach(() => { + appTree = new UnitTestTree(new EmptyTree()); + appTree.create('/angular.json', JSON.stringify(configJson)); + }); + + it('should rename CarouselAnimationType to HorizontalAnimationType', async () => { + appTree.create( + '/testSrc/appPrefix/component/test.component.ts', + `import { Component, ViewChild } from '@angular/core'; + import { CarouselAnimationType } from 'igniteui-angular'; + + @Component({ + selector: 'animationType', + templateUrl: './test.component.html', + styleUrls: ['./test.component.scss'] + }) + export class AnimationType { + public animationType: CarouselAnimationType = CarouselAnimationType.slide; + } + `); + const tree = await schematicRunner + .runSchematicAsync(migrationName, {}, appTree) + .toPromise(); + + const expectedContent = `import { Component, ViewChild } from '@angular/core'; + import { HorizontalAnimationType } from 'igniteui-angular'; + + @Component({ + selector: 'animationType', + templateUrl: './test.component.html', + styleUrls: ['./test.component.scss'] + }) + export class AnimationType { + public animationType: HorizontalAnimationType = HorizontalAnimationType.slide; + } + `; + + expect( + tree.readContent( + '/testSrc/appPrefix/component/test.component.ts' + ) + ).toEqual(expectedContent); + }); +}); diff --git a/projects/igniteui-angular/migrations/update-13_0_0/index.ts b/projects/igniteui-angular/migrations/update-13_0_0/index.ts new file mode 100644 index 00000000000..ab7fddb6b1c --- /dev/null +++ b/projects/igniteui-angular/migrations/update-13_0_0/index.ts @@ -0,0 +1,11 @@ +import { Rule, SchematicContext, Tree } from '@angular-devkit/schematics'; +import { UpdateChanges } from '../common/UpdateChanges'; + +const version = '13.0.0'; + +export default (): Rule => (host: Tree, context: SchematicContext) => { + context.logger.info(`Applying migration for Ignite UI for Angular to version ${version}`); + + const update = new UpdateChanges(__dirname, host, context); + update.applyChanges(); +}; diff --git a/projects/igniteui-angular/src/lib/carousel/README.md b/projects/igniteui-angular/src/lib/carousel/README.md index 54d009d4a73..b1433017bc2 100644 --- a/projects/igniteui-angular/src/lib/carousel/README.md +++ b/projects/igniteui-angular/src/lib/carousel/README.md @@ -17,7 +17,7 @@ A walkthrough of how to get started can be found [here](https://www.infragistics | `gesturesSupport` | boolean | Controls should the gestures should be supported. Defaults to `true`. | | `maximumIndicatorsCount` | number | The number of visible indicators. Defaults to `5`. | | `indicatorsOrientation` | CarouselIndicatorsOrientation | Controls whether the indicators should be previewed on top or on bottom of carousel. Defaults to `bottom`. | -| `animationType` | CarouselAnimationType | Controls what animation should be played when slides are changing. Defaults to `slide`. | +| `animationType` | HorizontalAnimationType | Controls what animation should be played when slides are changing. Defaults to `slide`. | | `total` | number | The number of slides the carousel currently has. | | `current` | number | The index of the slide currently showing. | | `isPlaying` | boolean | Returns whether the carousel is paused/playing. | diff --git a/projects/igniteui-angular/src/lib/carousel/carousel-base.ts b/projects/igniteui-angular/src/lib/carousel/carousel-base.ts index 87f27b56c83..3de3a208164 100644 --- a/projects/igniteui-angular/src/lib/carousel/carousel-base.ts +++ b/projects/igniteui-angular/src/lib/carousel/carousel-base.ts @@ -1,16 +1,17 @@ import { AnimationBuilder, AnimationPlayer, AnimationReferenceMetadata, useAnimation } from '@angular/animations'; +import { EventEmitter } from '@angular/core'; import { fadeIn } from '../animations/fade'; import { slideInLeft } from '../animations/slide'; import { mkenum } from '../core/utils'; export enum Direction { NONE, NEXT, PREV } -export const CarouselAnimationType = mkenum({ +export const HorizontalAnimationType = mkenum({ none: 'none', slide: 'slide', fade: 'fade' }); -export type CarouselAnimationType = (typeof CarouselAnimationType)[keyof typeof CarouselAnimationType]; +export type HorizontalAnimationType = (typeof HorizontalAnimationType)[keyof typeof HorizontalAnimationType]; export interface CarouselAnimationSettings { enterAnimation: AnimationReferenceMetadata; @@ -26,7 +27,12 @@ export interface IgxSlideComponentBase { /** @hidden */ export abstract class IgxCarouselComponentBase { /** @hidden */ - public animationType = CarouselAnimationType.slide; + public animationType: HorizontalAnimationType = HorizontalAnimationType.slide; + + /** @hidden @internal */ + public enterAnimationDone = new EventEmitter(); + /** @hidden @internal */ + public leaveAnimationDone = new EventEmitter(); /** @hidden */ protected currentItem: IgxSlideComponentBase; @@ -37,7 +43,7 @@ export abstract class IgxCarouselComponentBase { /** @hidden */ protected leaveAnimationPlayer?: AnimationPlayer; /** @hidden */ - protected animationDuration = 320; + protected defaultAnimationDuration = 320; /** @hidden */ protected animationPosition = 0; /** @hidden */ @@ -48,7 +54,7 @@ export abstract class IgxCarouselComponentBase { /** @hidden */ protected triggerAnimations() { - if (this.animationType !== CarouselAnimationType.none) { + if (this.animationType !== HorizontalAnimationType.none) { if (this.animationStarted(this.leaveAnimationPlayer) || this.animationStarted(this.enterAnimationPlayer)) { requestAnimationFrame(() => { this.resetAnimations(); @@ -74,10 +80,12 @@ export abstract class IgxCarouselComponentBase { private resetAnimations() { if (this.animationStarted(this.leaveAnimationPlayer)) { this.leaveAnimationPlayer.reset(); + this.leaveAnimationDone.emit(); } if (this.animationStarted(this.enterAnimationPlayer)) { this.enterAnimationPlayer.reset(); + this.enterAnimationDone.emit(); } } @@ -86,11 +94,11 @@ export abstract class IgxCarouselComponentBase { if (this.newDuration) { duration = this.animationPosition ? this.animationPosition * this.newDuration : this.newDuration; } else { - duration = this.animationPosition ? this.animationPosition * this.animationDuration : this.animationDuration; + duration = this.animationPosition ? this.animationPosition * this.defaultAnimationDuration : this.defaultAnimationDuration; } switch (this.animationType) { - case CarouselAnimationType.slide: + case HorizontalAnimationType.slide: const trans = this.animationPosition ? this.animationPosition * 100 : 100; return { enterAnimation: useAnimation(slideInLeft, @@ -116,7 +124,7 @@ export abstract class IgxCarouselComponentBase { } }) }; - case CarouselAnimationType.fade: + case HorizontalAnimationType.fade: return { enterAnimation: useAnimation(fadeIn, { params: { duration: `${duration}ms`, startOpacity: `${this.animationPosition}` } }), @@ -146,6 +154,7 @@ export abstract class IgxCarouselComponentBase { this.animationPosition = 0; this.newDuration = 0; this.previousItem.previous = false; + this.enterAnimationDone.emit(); }); this.previousItem.previous = true; this.enterAnimationPlayer.play(); @@ -167,6 +176,7 @@ export abstract class IgxCarouselComponentBase { } this.animationPosition = 0; this.newDuration = 0; + this.leaveAnimationDone.emit(); }); this.leaveAnimationPlayer.play(); } diff --git a/projects/igniteui-angular/src/lib/carousel/carousel.component.spec.ts b/projects/igniteui-angular/src/lib/carousel/carousel.component.spec.ts index 59cf590d689..eddc5445e45 100644 --- a/projects/igniteui-angular/src/lib/carousel/carousel.component.spec.ts +++ b/projects/igniteui-angular/src/lib/carousel/carousel.component.spec.ts @@ -11,7 +11,7 @@ import { UIInteractions, wait } from '../test-utils/ui-interactions.spec'; import { configureTestSuite } from '../test-utils/configure-suite'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { IgxSlideComponent } from './slide.component'; -import { CarouselAnimationType } from './carousel-base'; +import { HorizontalAnimationType } from './carousel-base'; describe('Carousel', () => { configureTestSuite(); @@ -589,7 +589,7 @@ describe('Carousel', () => { await wait(); expect(carousel.get(0).active).toBeTruthy(); expect(carousel.get(0).nativeElement.classList.contains(HelperTestFunctions.ACTIVE_SLIDE_CLASS)).toBeTruthy(); - expect(carousel.animationType).toBe(CarouselAnimationType.slide); + expect(carousel.animationType).toBe(HorizontalAnimationType.slide); carousel.next(); fixture.detectChanges(); await wait(200); @@ -616,12 +616,12 @@ describe('Carousel', () => { it('Test fade animation', async () => { await wait(); - carousel.animationType = CarouselAnimationType.fade; + carousel.animationType = HorizontalAnimationType.fade; fixture.detectChanges(); expect(carousel.get(0).active).toBeTruthy(); expect(carousel.get(0).nativeElement.classList.contains(HelperTestFunctions.ACTIVE_SLIDE_CLASS)).toBeTruthy(); - expect(carousel.animationType).toBe(CarouselAnimationType.fade); + expect(carousel.animationType).toBe(HorizontalAnimationType.fade); carousel.next(); fixture.detectChanges(); await wait(200); diff --git a/projects/igniteui-angular/src/lib/carousel/carousel.component.ts b/projects/igniteui-angular/src/lib/carousel/carousel.component.ts index 1f1912c7677..d55a083864e 100644 --- a/projects/igniteui-angular/src/lib/carousel/carousel.component.ts +++ b/projects/igniteui-angular/src/lib/carousel/carousel.component.ts @@ -30,7 +30,7 @@ import { IgxSlideComponent } from './slide.component'; import { ICarouselResourceStrings } from '../core/i18n/carousel-resources'; import { CurrentResourceStrings } from '../core/i18n/resources'; import { HammerGestureConfig, HAMMER_GESTURE_CONFIG } from '@angular/platform-browser'; -import { CarouselAnimationType, Direction, IgxCarouselComponentBase } from './carousel-base'; +import { HorizontalAnimationType, Direction, IgxCarouselComponentBase } from './carousel-base'; let NEXT_ID = 0; @@ -227,7 +227,7 @@ export class IgxCarouselComponent extends IgxCarouselComponentBase implements On * * @memberOf IgxSlideComponent */ - @Input() public animationType = CarouselAnimationType.slide; + @Input() public animationType: HorizontalAnimationType = HorizontalAnimationType.slide; /** * The custom template, if any, that should be used when rendering carousel indicators @@ -647,18 +647,18 @@ export class IgxCarouselComponent extends IgxCarouselComponentBase implements On this.incomingSlide.direction = event.deltaX < 0 ? Direction.NEXT : Direction.PREV; this.incomingSlide.previous = false; - this.animationPosition = this.animationType === CarouselAnimationType.fade ? + this.animationPosition = this.animationType === HorizontalAnimationType.fade ? deltaX / slideWidth : (slideWidth - deltaX) / slideWidth; if (velocity > 1) { - this.newDuration = this.animationDuration / velocity; + this.newDuration = this.defaultAnimationDuration / velocity; } this.incomingSlide.active = true; } else { this.currentItem.direction = event.deltaX > 0 ? Direction.NEXT : Direction.PREV; this.previousItem = this.incomingSlide; this.previousItem.previous = true; - this.animationPosition = this.animationType === CarouselAnimationType.fade ? + this.animationPosition = this.animationType === HorizontalAnimationType.fade ? Math.abs((slideWidth - deltaX) / slideWidth) : deltaX / slideWidth; this.playAnimations(); } @@ -912,7 +912,7 @@ export class IgxCarouselComponent extends IgxCarouselComponentBase implements On } this.incomingSlide.previous = true; - if (this.animationType === CarouselAnimationType.fade) { + if (this.animationType === HorizontalAnimationType.fade) { this.currentItem.nativeElement.style.opacity = `${Math.abs(offset) / slideWidth}`; } else { this.currentItem.nativeElement.style.transform = `translateX(${deltaX}px)`; From 80de0e8ab702884e92be7a6d44a7580995749034 Mon Sep 17 00:00:00 2001 From: ddaribo Date: Thu, 23 Sep 2021 16:59:38 +0300 Subject: [PATCH 5/5] test(stepper): add component tests Co-authored-by: Bozhidara Pachilova --- .../src/lib/stepper/stepper.component.spec.ts | 1293 +++++++++++++++++ 1 file changed, 1293 insertions(+) create mode 100644 projects/igniteui-angular/src/lib/stepper/stepper.component.spec.ts diff --git a/projects/igniteui-angular/src/lib/stepper/stepper.component.spec.ts b/projects/igniteui-angular/src/lib/stepper/stepper.component.spec.ts new file mode 100644 index 00000000000..5f459fc2ad9 --- /dev/null +++ b/projects/igniteui-angular/src/lib/stepper/stepper.component.spec.ts @@ -0,0 +1,1293 @@ +import { Component, ViewChild } from '@angular/core'; +import { fakeAsync, ComponentFixture, TestBed, waitForAsync, tick } from '@angular/core/testing'; +import { configureTestSuite } from '../test-utils/configure-suite'; +import { IgxStepperComponent, IgxStepperModule } from './stepper.component'; +import { By } from '@angular/platform-browser'; +import { UIInteractions } from '../test-utils/ui-interactions.spec'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { + IgxStepperOrientation, + IgxStepperTitlePosition, + IgxStepType, + IStepChangedEventArgs, + IStepChangingEventArgs +} from './stepper.common'; +import { take } from 'rxjs/operators'; +import { IgxIconModule } from '../icon/public_api'; +import { IgxStepperService } from './stepper.service'; +import { AnimationMetadata, AnimationOptions } from '@angular/animations'; +import { Direction } from '../services/direction/directionality'; +import { IgxStepComponent } from './step/step.component'; + +const STEPPER_CLASS = 'igx-stepper'; +const STEPPER_HEADER = 'igx-stepper__header'; +const STEPPER_BODY = 'igx-stepper__body'; +const STEP_TAG = 'IGX-STEP'; +const STEP_HEADER = 'igx-stepper__step-header'; +const STEP_INDICATOR_CLASS = 'igx-stepper__step-indicator'; +const STEP_TITLE_CLASS = 'igx-stepper__step-title'; +const STEP_SUBTITLE_CLASS = 'igx-stepper__step-subtitle'; +const INVALID_CLASS = 'igx-stepper__step-header--invalid'; +const DISABLED_CLASS = 'igx-stepper__step--disabled'; +const COMPLETED_CLASS = 'igx-stepper__step--completed'; +const CURRENT_CLASS = 'igx-stepper__step-header--current'; + +const getHeaderElements = (stepper: IgxStepperComponent, stepIndex: number): Map => { + const elementsMap = new Map(); + elementsMap.set('indicator', stepper.steps[stepIndex].nativeElement.querySelector(`div.${STEP_INDICATOR_CLASS}`)); + elementsMap.set('title', stepper.steps[stepIndex].nativeElement.querySelector(`.${STEP_TITLE_CLASS}`)); + elementsMap.set('subtitle', stepper.steps[stepIndex].nativeElement.querySelector(`.${STEP_SUBTITLE_CLASS}`)); + return elementsMap; +}; + +const getStepperPositions = (): string[] => { + const positions = []; + Object.values(IgxStepperTitlePosition).forEach((position: IgxStepperTitlePosition) => { + positions.push(position); + }); + return positions; +}; + +const testAnimationBehvior = ( + val: any, + fix: ComponentFixture, + isHorAnimTypeInvalidTest: boolean +): void => { + const stepper = fix.componentInstance.stepper; + stepper.steps[0].active = true; + fix.detectChanges(); + const previousActiveStep = stepper.steps[0]; + const activeChangeSpy = spyOn(previousActiveStep.activeChange, 'emit'); + activeChangeSpy.calls.reset(); + stepper.next(); + fix.detectChanges(); + tick(1000); + if (!isHorAnimTypeInvalidTest) { + expect(previousActiveStep.activeChange.emit).withContext(val).toHaveBeenCalledOnceWith(false); + } else { + expect(previousActiveStep.activeChange.emit).withContext(val).not.toHaveBeenCalled(); + } + activeChangeSpy.calls.reset(); +}; + +describe('Rendering Tests', () => { + configureTestSuite(); + let fix: ComponentFixture; + let stepper: IgxStepperComponent; + + beforeAll( + waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [ + IgxStepperSampleTestComponent + ], + imports: [ + NoopAnimationsModule, + IgxStepperModule, + IgxIconModule + ], + providers: [] + }).compileComponents(); + }) + ); + beforeEach(() => { + fix = TestBed.createComponent(IgxStepperSampleTestComponent); + fix.detectChanges(); + stepper = fix.componentInstance.stepper; + }); + + describe('General', () => { + it('should render a stepper containing a sequence of steps', () => { + const stepperElement: HTMLElement = fix.debugElement.queryAll(By.css(`${STEPPER_CLASS}`))[0].nativeElement; + const stepperHeader = stepperElement.querySelector(`.${STEPPER_HEADER}`); + const steps = Array.from(stepperHeader.children); + expect(steps.length).toBe(5); + for (const step of steps) { + expect(step.tagName === STEP_TAG).toBeTruthy(); + } + }); + + it('should not allow activating a step with next/prev methods when disabled is set to true', fakeAsync(() => { + const serviceExpandSpy = spyOn((stepper as any).stepperService, 'expand').and.callThrough(); + const serviceCollapseSpy = spyOn((stepper as any).stepperService, 'collapse').and.callThrough(); + stepper.orientation = IgxStepperOrientation.Horizontal; + stepper.steps[0].active = true; + stepper.steps[1].disabled = true; + fix.detectChanges(); + tick(); + + expect(stepper.steps[1].nativeElement).toHaveClass('igx-stepper__step--disabled'); + + stepper.next(); + fix.detectChanges(); + tick(350); + + expect(stepper.steps[1].active).toBeFalsy(); + expect(stepper.steps[2].isAccessible).toBeTruthy(); + expect(stepper.steps[2].active).toBeTruthy(); + expect(serviceExpandSpy).toHaveBeenCalledOnceWith(stepper.steps[2]); + expect(serviceCollapseSpy).toHaveBeenCalledOnceWith(stepper.steps[0]); + + serviceExpandSpy.calls.reset(); + serviceCollapseSpy.calls.reset(); + + stepper.orientation = IgxStepperOrientation.Vertical; + stepper.steps[0].active = true; + stepper.steps[1].disabled = true; + fix.detectChanges(); + tick(); + + stepper.next(); + fix.detectChanges(); + tick(350); + + expect(stepper.steps[1].active).toBeFalsy(); + expect(stepper.steps[2].isAccessible).toBeTruthy(); + expect(stepper.steps[2].active).toBeTruthy(); + expect(serviceExpandSpy).toHaveBeenCalledOnceWith(stepper.steps[2]); + expect(serviceCollapseSpy).toHaveBeenCalledOnceWith(stepper.steps[0]); + })); + + it('should not allow moving forward to next step in linear mode if the previous step is invalid', fakeAsync(() => { + const serviceExpandSpy = spyOn((stepper as any).stepperService, 'expand').and.callThrough(); + stepper.orientation = IgxStepperOrientation.Horizontal; + stepper.linear = true; + stepper.steps[0].isValid = false; + fix.detectChanges(); + + stepper.next(); + fix.detectChanges(); + tick(); + + expect(stepper.steps[1].active).toBeFalsy(); + expect(stepper.steps[0].active).toBeTruthy(); + expect(stepper.steps[1].linearDisabled).toBeTruthy(); + expect(stepper.steps[2].linearDisabled).toBeTruthy(); + expect(serviceExpandSpy).not.toHaveBeenCalled(); + + stepper.orientation = IgxStepperOrientation.Vertical; + fix.detectChanges(); + + stepper.next(); + fix.detectChanges(); + tick(); + + expect(stepper.steps[1].active).toBeFalsy(); + expect(stepper.steps[0].active).toBeTruthy(); + expect(stepper.steps[1].linearDisabled).toBeTruthy(); + expect(serviceExpandSpy).not.toHaveBeenCalled(); + + // if the step after the active and valid step is disabled, + // the following accessible one should not be linear disabled + stepper.steps[0].isValid = true; + fix.detectChanges(); + expect(stepper.steps[1].linearDisabled).toBeFalsy(); + + stepper.steps[1].disabled = true; + stepper.steps[1].isValid = false; + fix.detectChanges(); + + expect(stepper.steps[1].linearDisabled).toBeFalsy(); + expect(stepper.steps[2].isAccessible).toBeTruthy(); + expect(stepper.steps[2].linearDisabled).toBeFalsy(); + expect(stepper.steps[2].isValid).toBeTruthy(); + + // in case the disabled step ([1]) becomes enabled and invalid, + // the following step becomes linear disabled + stepper.steps[1].disabled = false; + fix.detectChanges(); + + expect(stepper.steps[2].linearDisabled).toBeTruthy(); + + stepper.steps[1].isValid = true; + fix.detectChanges(); + + expect(stepper.steps[2].linearDisabled).toBeFalsy(); + })); + + it('should emit ing and ed events when a step is activated', fakeAsync(() => { + const changingSpy = spyOn(stepper.activeStepChanging, 'emit').and.callThrough(); + const changedSpy = spyOn(stepper.activeStepChanged, 'emit').and.callThrough(); + const serviceExpandSpy = spyOn((stepper as any).stepperService, 'expand').and.callThrough(); + const serviceCollapseSpy = spyOn((stepper as any).stepperService, 'collapse').and.callThrough(); + + expect(changingSpy).not.toHaveBeenCalled(); + expect(changedSpy).not.toHaveBeenCalled(); + + const argsIng: IStepChangingEventArgs = { + newIndex: stepper.steps[1].index, + oldIndex: stepper.steps[0].index, + owner: stepper, + cancel: false + }; + const argsEd: IStepChangedEventArgs = { + index: stepper.steps[1].index, + owner: stepper, + }; + + const testValues = [null, undefined, [], {}, 'sampleString']; + + for (const val of testValues) { + stepper.navigateTo(val as any); + fix.detectChanges(); + expect(changingSpy).not.toHaveBeenCalled(); + expect(changedSpy).not.toHaveBeenCalled(); + expect(serviceExpandSpy).not.toHaveBeenCalled(); + expect(serviceCollapseSpy).not.toHaveBeenCalled(); + } + + stepper.navigateTo(1); + fix.detectChanges(); + tick(); + + expect(stepper.steps[1].active).toBeTruthy(); + expect(changingSpy).toHaveBeenCalledOnceWith(argsIng); + expect(changedSpy).toHaveBeenCalledOnceWith(argsEd); + expect(serviceExpandSpy).toHaveBeenCalledOnceWith(stepper.steps[1]); + expect(serviceCollapseSpy).toHaveBeenCalledOnceWith(stepper.steps[0]); + })); + + it('should be able to cancel the activeStepChanging event', fakeAsync(() => { + const changingSpy = spyOn(stepper.activeStepChanging, 'emit').and.callThrough(); + const serviceExpandSpy = spyOn((stepper as any).stepperService, 'expand').and.callThrough(); + const serviceCollapseSpy = spyOn((stepper as any).stepperService, 'collapse').and.callThrough(); + + expect(changingSpy).not.toHaveBeenCalled(); + + const argsIng: IStepChangingEventArgs = { + newIndex: stepper.steps[1].index, + oldIndex: stepper.steps[0].index, + owner: stepper, + cancel: true + }; + + stepper.activeStepChanging.pipe(take(1)).subscribe(e => { + e.cancel = true; + }); + + stepper.navigateTo(1); + fix.detectChanges(); + tick(); + + expect(stepper.steps[1].active).toBeFalsy(); + expect(stepper.steps[0].active).toBeTruthy(); + expect(changingSpy).toHaveBeenCalledOnceWith(argsIng); + expect(serviceExpandSpy).toHaveBeenCalledOnceWith(stepper.steps[1]); + expect(serviceCollapseSpy).not.toHaveBeenCalled(); + })); + + it('a step should emit activeChange event when its active property changes', fakeAsync(() => { + const fourthActiveChangeSpy = spyOn(stepper.steps[3].activeChange, 'emit').and.callThrough(); + const fifthActiveChangeSpy = spyOn(stepper.steps[4].activeChange, 'emit').and.callThrough(); + const serviceExpandAPISpy = spyOn((stepper as any).stepperService, 'expandThroughApi').and.callThrough(); + + expect(fourthActiveChangeSpy).not.toHaveBeenCalled(); + expect(fifthActiveChangeSpy).not.toHaveBeenCalled(); + + stepper.steps[0].active = true; + fix.detectChanges(); + expect(serviceExpandAPISpy).toHaveBeenCalledOnceWith(stepper.steps[0]); + + stepper.steps[3].active = true; + fix.detectChanges(); + tick(); + + expect(stepper.steps[3].active).toBeTruthy(); + expect(stepper.steps[3].activeChange.emit).toHaveBeenCalledOnceWith(true); + expect(fifthActiveChangeSpy).not.toHaveBeenCalled(); + expect(serviceExpandAPISpy.calls.mostRecent().args[0]).toBe(stepper.steps[3]); + + fourthActiveChangeSpy.calls.reset(); + serviceExpandAPISpy.calls.reset(); + + stepper.steps[4].active = true; + fix.detectChanges(); + tick(); + + expect(stepper.steps[4].active).toBeTruthy(); + expect(stepper.steps[3].active).toBeFalsy(); + expect(fifthActiveChangeSpy).toHaveBeenCalledOnceWith(true); + expect(fourthActiveChangeSpy).toHaveBeenCalledOnceWith(false); + expect(serviceExpandAPISpy).toHaveBeenCalledOnceWith(stepper.steps[4]); + })); + }); + + describe('Appearance', () => { + it('should apply the appropriate class to a stepper in horizontal mode', () => { + stepper.orientation = IgxStepperOrientation.Horizontal; + fix.detectChanges(); + + expect(stepper.nativeElement).toHaveClass('igx-stepper--horizontal'); + // no css class is applied when the stepper is in vertical mode + }); + + it('should indicate the currently active step', () => { + const step0Header = stepper.steps[0].nativeElement.querySelector(`.${STEP_HEADER}`); + const step1Header = stepper.steps[1].nativeElement.querySelector(`.${STEP_HEADER}`); + const serviceExpandSpy = spyOn((stepper as any).stepperService, 'expand').and.callThrough(); + + stepper.steps[0].active = true; + fix.detectChanges(); + + expect(step0Header).toHaveClass(CURRENT_CLASS); + + stepper.steps[1].active = true; + stepper.steps[1].nativeElement.focus(); + fix.detectChanges(); + + UIInteractions.triggerKeyDownEvtUponElem(' ', stepper.steps[1].nativeElement); + fix.detectChanges(); + + expect(step0Header).not.toHaveClass(CURRENT_CLASS); + expect(step1Header).toHaveClass(CURRENT_CLASS); + expect(serviceExpandSpy).toHaveBeenCalledOnceWith(stepper.steps[1]); + }); + + it('should indicate that a step is completed', () => { + stepper.steps[0].active = true; + fix.detectChanges(); + + expect(stepper.steps[0].completed).toBeFalsy(); + expect(stepper.steps[0].nativeElement).not.toHaveClass(COMPLETED_CLASS); + + stepper.steps[0].completed = true; + fix.detectChanges(); + + expect(stepper.steps[0].nativeElement).toHaveClass(COMPLETED_CLASS); + + stepper.steps[1].completed = true; + fix.detectChanges(); + + expect(stepper.steps[1].nativeElement).toHaveClass(COMPLETED_CLASS); + }); + + it('should indicate that a step is invalid', () => { + const step0Header = stepper.steps[0].nativeElement.querySelector(`.${STEP_HEADER}`); + stepper.steps[0].isValid = true; + fix.detectChanges(); + + expect(step0Header).not.toHaveClass(INVALID_CLASS); + + stepper.steps[0].isValid = false; + fix.detectChanges(); + + expect(step0Header).not.toHaveClass(INVALID_CLASS); + + stepper.steps[1].active = true; + fix.detectChanges(); + + expect(step0Header).toHaveClass(INVALID_CLASS); + + //indicate that a step is disabled without indicating that it is also invalid + stepper.steps[0].disabled = true; + fix.detectChanges(); + + expect(step0Header).not.toHaveClass(INVALID_CLASS); + expect(stepper.steps[0].nativeElement).toHaveClass(DISABLED_CLASS); + }); + + it('should render the visual step element according to the specified stepType', () => { + stepper.stepType = IgxStepType.Full; + fix.detectChanges(); + + for (let i = 0; i < stepper.steps.length; i++) { + const elementsMap = getHeaderElements(stepper, i); + + expect(elementsMap.get('indicator')).not.toBeNull(); + expect(stepper.steps[i].isIndicatorVisible).toBeTruthy(); + if (i === 3) { + expect(elementsMap.get('title')).toBeNull(); + expect(elementsMap.get('subtitle')).toBeNull(); + continue; + } + expect(elementsMap.get('title')).not.toBeNull(); + expect(elementsMap.get('subtitle')).not.toBeNull(); + expect(stepper.steps[i].isTitleVisible).toBeTruthy(); + } + + stepper.stepType = IgxStepType.Indicator; + fix.detectChanges(); + + for (let i = 0; i < stepper.steps.length; i++) { + const elementsMap = getHeaderElements(stepper, i); + + expect(elementsMap.get('indicator')).not.toBeNull(); + expect(stepper.steps[i].isIndicatorVisible).toBeTruthy(); + expect(elementsMap.get('title')).toBeNull(); + expect(elementsMap.get('subtitle')).toBeNull(); + expect(stepper.steps[i].isTitleVisible).toBeFalsy(); + } + + stepper.stepType = IgxStepType.Title; + fix.detectChanges(); + + for (let i = 0; i < stepper.steps.length; i++) { + const elementsMap = getHeaderElements(stepper, i); + + expect(elementsMap.get('indicator')).toBeNull(); + expect(stepper.steps[i].isIndicatorVisible).toBeFalsy(); + if (i === 3) { + expect(elementsMap.get('title')).toBeNull(); + expect(elementsMap.get('subtitle')).toBeNull(); + continue; + } + expect(elementsMap.get('title')).not.toBeNull(); + expect(elementsMap.get('subtitle')).not.toBeNull(); + expect(stepper.steps[i].isTitleVisible).toBeTruthy(); + } + }); + + it('should place the title in the step element according to the specified titlePosition when stepType is set to "full"', () => { + stepper.orientation = IgxStepperOrientation.Horizontal; + stepper.stepType = IgxStepType.Full; + stepper.titlePosition = null; + fix.detectChanges(); + + //test default title positions + for (const step of stepper.steps) { + expect(step.titlePosition).toBe(stepper._defaultTitlePosition); + expect(step.titlePosition).toBe(IgxStepperTitlePosition.Bottom); + expect(step.nativeElement).toHaveClass(`igx-stepper__step--${stepper._defaultTitlePosition}`); + } + + const positions = getStepperPositions(); + positions.forEach((pos: IgxStepperTitlePosition) => { + stepper.titlePosition = pos; + fix.detectChanges(); + + for (const step of stepper.steps) { + expect(step.nativeElement).toHaveClass(`igx-stepper__step--${pos}`); + } + }); + + stepper.orientation = IgxStepperOrientation.Vertical; + stepper.titlePosition = null; + fix.detectChanges(); + + //test default title positions + for (const step of stepper.steps) { + expect(step.titlePosition).toBe(stepper._defaultTitlePosition); + expect(step.titlePosition).toBe(IgxStepperTitlePosition.End); + expect(step.nativeElement).toHaveClass(`igx-stepper__step--${stepper._defaultTitlePosition}`); + } + + positions.forEach((pos: IgxStepperTitlePosition) => { + stepper.titlePosition = pos; + fix.detectChanges(); + + for (const step of stepper.steps) { + expect(step.nativeElement).toHaveClass(`igx-stepper__step--${pos}`); + } + }); + }); + + it('should indicate steps with a number when igxStepIndicator is not set and stepType is "indicator" or "full"', () => { + stepper.stepType = IgxStepType.Full; + fix.detectChanges(); + + let indicatorElement5 = stepper.steps[4].nativeElement.querySelector(`div.${STEP_INDICATOR_CLASS}`); + + expect(stepper.steps[4].isIndicatorVisible).toBeTruthy(); + expect(indicatorElement5).not.toBeNull(); + expect(indicatorElement5.textContent).toBe((stepper.steps[4].index + 1).toString()); + + stepper.stepType = IgxStepType.Indicator; + fix.detectChanges(); + + indicatorElement5 = stepper.steps[4].nativeElement.querySelector(`div.${STEP_INDICATOR_CLASS}`); + + expect(indicatorElement5).not.toBeNull(); + expect(indicatorElement5.textContent).toBe((stepper.steps[4].index + 1).toString()); + }); + + it('should allow overriding the default invalid, completed and active indicators', () => { + const step0Header = stepper.steps[0].nativeElement.querySelector(`.${STEP_HEADER}`); + let indicatorElement = step0Header.querySelector(`.${STEP_INDICATOR_CLASS}`).children[0]; + + expect(step0Header).not.toHaveClass(INVALID_CLASS); + expect(step0Header).toHaveClass(CURRENT_CLASS); + expect(stepper.steps[0].nativeElement).not.toHaveClass(COMPLETED_CLASS); + expect(indicatorElement.tagName).toBe('IGX-ICON'); + expect(indicatorElement.textContent).toBe('edit'); + + stepper.steps[0].isValid = false; + fix.detectChanges(); + stepper.steps[1].active = true; + fix.detectChanges(); + + indicatorElement = step0Header.querySelector(`.${STEP_INDICATOR_CLASS}`).children[0]; + + expect(step0Header).toHaveClass(INVALID_CLASS); + expect(step0Header).not.toHaveClass(CURRENT_CLASS); + expect(stepper.steps[0].nativeElement).not.toHaveClass(COMPLETED_CLASS); + expect(indicatorElement.tagName).toBe('IGX-ICON'); + expect(indicatorElement.textContent).toBe('error'); + + stepper.steps[0].isValid = true; + stepper.steps[0].completed = true; + fix.detectChanges(); + + indicatorElement = step0Header.querySelector(`.${STEP_INDICATOR_CLASS}`).children[0]; + + expect(step0Header).not.toHaveClass(INVALID_CLASS); + expect(step0Header).not.toHaveClass(CURRENT_CLASS); + expect(stepper.steps[0].nativeElement).toHaveClass(COMPLETED_CLASS); + expect(indicatorElement.tagName).toBe('IGX-ICON'); + expect(indicatorElement.textContent).toBe('check'); + }); + + it('should be able to display the steps\' content above the steps headers when the stepper is horizontally orientated', () => { + stepper.orientation = IgxStepperOrientation.Horizontal; + fix.detectChanges(); + expect(stepper.contentTop).toBeFalsy(); + + expect(stepper.nativeElement.children[0]).toHaveClass(STEPPER_HEADER); + expect(stepper.nativeElement.children[1]).toHaveClass(STEPPER_BODY); + + stepper.contentTop = true; + fix.detectChanges(); + + expect(stepper.nativeElement.children[0]).toHaveClass(STEPPER_BODY); + expect(stepper.nativeElement.children[1]).toHaveClass(STEPPER_HEADER); + }); + + it('should allow modifying animationSettings that are used for transitioning between steps ', fakeAsync(() => { + const numericTestValues = [100, 1000]; + + for (const val of numericTestValues) { + fix.componentInstance.animationDuration = val as any; + testAnimationBehvior(val, fix, false); + } + + const fallbackToDefaultValues = [-1, 0, null, undefined, 'sampleString', [], {}]; + for (const val of fallbackToDefaultValues) { + fix.componentInstance.animationDuration = val as any; + fix.detectChanges(); + expect(stepper.animationDuration) + .toBe((stepper as any)._defaultAnimationDuration); + testAnimationBehvior(val, fix, false); + } + + fix.componentInstance.animationDuration = 300; + stepper.orientation = IgxStepperOrientation.Horizontal; + fix.detectChanges(); + + const horAnimTypeValidValues = ['slide', 'fade', 'none']; + for (const val of horAnimTypeValidValues) { + fix.componentInstance.horizontalAnimationType = val as any; + testAnimationBehvior(val, fix, false); + } + + const horAnimTypeTestValues = ['sampleString', null, undefined, 0, [], {}]; + for (const val of horAnimTypeTestValues) { + fix.componentInstance.horizontalAnimationType = val as any; + testAnimationBehvior(val, fix, true); + } + + stepper.orientation = IgxStepperOrientation.Vertical; + fix.detectChanges(); + + const vertAnimTypeTestValues = ['fade', 'grow', 'none', 'sampleString', null, undefined, 0, [], {}]; + for (const val of vertAnimTypeTestValues) { + fix.componentInstance.verticalAnimationType = val as any; + testAnimationBehvior(val, fix, false); + } + })); + + it('should render dynamically added step and properly set the linear disabled steps with its addition', fakeAsync(() => { + const stepsLength = stepper.steps.length; + expect(stepsLength).toBe(5); + + fix.componentInstance.displayHiddenStep = true; + fix.detectChanges(); + + expect(stepper.steps.length).toBe(stepsLength + 1); + + const titleElement = stepper.steps[2].nativeElement.querySelector(`.${STEP_TITLE_CLASS}`); + expect(titleElement.textContent).toBe('Hidden step'); + + // should set the first accessible step as active when the active step is dynamically removed + stepper.steps[2].active = true; + fix.detectChanges(); + tick(300); + fix.componentInstance.displayHiddenStep = false; + fix.detectChanges(); + tick(300); + + let firstAccessibleStepIdx = stepper.steps.findIndex(step => step.isAccessible); + expect(stepper.steps[firstAccessibleStepIdx].active).toBeTruthy(); + + fix.componentInstance.displayHiddenStep = true; + fix.detectChanges(); + tick(300); + stepper.steps[2].active = true; + stepper.steps[0].disabled = true; + fix.detectChanges(); + tick(300); + expect(stepper.steps[0].isAccessible).toBeFalsy(); + + fix.componentInstance.displayHiddenStep = false; + fix.detectChanges(); + tick(300); + + firstAccessibleStepIdx = stepper.steps.findIndex(step => step.isAccessible); + expect(firstAccessibleStepIdx).toBe(1); + expect(stepper.steps[firstAccessibleStepIdx].active).toBeTruthy(); + + // if the dynamically added step's position is before the active step in linear mode, + // it should not be linear disabled + stepper.linear = true; + stepper.steps[4].active = true; + for (let index = 0; index <= 4; index++) { + const step = stepper.steps[index]; + step.isValid = true; + } + fix.detectChanges(); + fix.componentInstance.displayHiddenStep = true; + fix.detectChanges(); + + for (let index = 0; index <= 5; index++) { + const step = stepper.steps[index]; + expect(step.linearDisabled).toBeFalsy(); + } + + fix.componentInstance.displayHiddenStep = false; + fix.detectChanges(); + + // if the dynamically added step's position is after the active step in linear mode, + // and the latter is not valid, the added step should be linear disabled + stepper.steps[0].isValid = true; + stepper.steps[1].isValid = false; + stepper.steps[1].active = true; + fix.detectChanges(); + fix.componentInstance.displayHiddenStep = true; + fix.detectChanges(); + tick(300); + + expect(stepper.steps[2].linearDisabled).toBeTruthy(); + + for (let index = 3; index <= 5; index++) { + const step = stepper.steps[index]; + expect(step.linearDisabled).toBeTruthy(); + } + })); + + it('should activate the first accessible step and clear the visited steps collection when the stepper is reset', fakeAsync(() => { + // "visit" some steps + stepper.steps[0].active = true; + fix.detectChanges(); + stepper.steps[1].active = true; + fix.detectChanges(); + stepper.steps[2].active = true; + fix.detectChanges(); + + expect((stepper as any).stepperService.visitedSteps.size).toBe(3); + + stepper.reset(); + fix.detectChanges(); + + const firstAccessibleStepIdx = stepper.steps.findIndex(step => step.isAccessible); + expect(stepper.steps[firstAccessibleStepIdx].active).toBeTruthy(); + + expect((stepper as any).stepperService.visitedSteps.size).toBe(1); + expect((stepper as any).stepperService.visitedSteps).toContain(stepper.steps[firstAccessibleStepIdx]); + })); + + it('should properly collapse the previously active step in horizontal orientation and animation type \'fade\'', fakeAsync(() => { + stepper.orientation = IgxStepperOrientation.Horizontal; + stepper.horizontalAnimationType = 'fade'; + testAnimationBehvior('fade', fix, false); + })); + }); + + describe('Keyboard navigation', () => { + it('should navigate to first/last step on Home/End key press', fakeAsync(() => { + const serviceExpandSpy = spyOn((stepper as any).stepperService, 'expand').and.callThrough(); + const serviceCollapseSpy = spyOn((stepper as any).stepperService, 'collapse').and.callThrough(); + + stepper.steps[3].active = true; + stepper.steps[3].nativeElement.focus(); + fix.detectChanges(); + + expect(document.activeElement).toBe(stepper.steps[3].nativeElement as Element); + + UIInteractions.triggerKeyDownEvtUponElem('Home', stepper.steps[3].nativeElement); + fix.detectChanges(); + + expect(stepper.steps[0].nativeElement as Element).toBe(document.activeElement); + expect(serviceExpandSpy).not.toHaveBeenCalled(); + expect(serviceCollapseSpy).not.toHaveBeenCalled(); + + UIInteractions.triggerKeyDownEvtUponElem('End', stepper.steps[0].nativeElement); + fix.detectChanges(); + + expect(stepper.steps[4].nativeElement as Element).toBe(document.activeElement); + expect(serviceExpandSpy).not.toHaveBeenCalled(); + expect(serviceCollapseSpy).not.toHaveBeenCalled(); + })); + + it('should activate the currently focused step on Enter/Space key press', fakeAsync(() => { + const serviceExpandSpy = spyOn((stepper as any).stepperService, 'expand').and.callThrough(); + stepper.steps[0].active = true; + fix.detectChanges(); + + expect(stepper.steps[3].active).toBeFalsy(); + + stepper.steps[3].nativeElement.focus(); + fix.detectChanges(); + + expect(document.activeElement).toBe(stepper.steps[3].nativeElement as Element); + + UIInteractions.triggerKeyDownEvtUponElem('Enter', stepper.steps[3].nativeElement); + fix.detectChanges(); + + expect(stepper.steps[3].nativeElement as Element).toBe(document.activeElement); + expect(stepper.steps[3].active).toBeTruthy(); + expect(serviceExpandSpy).toHaveBeenCalledOnceWith(stepper.steps[3]); + + stepper.steps[4].nativeElement.focus(); + fix.detectChanges(); + + expect(document.activeElement).toBe(stepper.steps[4].nativeElement as Element); + expect(stepper.steps[4].active).toBeFalsy(); + + UIInteractions.triggerKeyDownEvtUponElem(' ', stepper.steps[4].nativeElement); + fix.detectChanges(); + + expect(stepper.steps[4].active).toBeTruthy(); + expect(serviceExpandSpy.calls.mostRecent().args[0]).toBe(stepper.steps[4]); + })); + + it('should navigate to the next/previous step in horizontal orientation on Arrow Right/Left key press', fakeAsync(() => { + stepper.orientation = IgxStepperOrientation.Horizontal; + stepper.steps[0].active = true; + fix.detectChanges(); + + expect(stepper.steps[1].active).toBeFalsy(); + + stepper.steps[0].nativeElement.focus(); + fix.detectChanges(); + + expect(document.activeElement).toBe(stepper.steps[0].nativeElement as Element); + + UIInteractions.triggerKeyDownEvtUponElem('ArrowRight', stepper.steps[0].nativeElement); + fix.detectChanges(); + + expect(stepper.steps[1].nativeElement as Element).toBe(document.activeElement); + expect(stepper.steps[1].active).toBeFalsy(); + + UIInteractions.triggerKeyDownEvtUponElem('ArrowLeft', stepper.steps[1].nativeElement); + fix.detectChanges(); + + expect(stepper.steps[0].nativeElement as Element).toBe(document.activeElement); + })); + + it('should navigate to the next/previous step in vertical orientation on Arrow Down/Up key press', fakeAsync(() => { + stepper.orientation = IgxStepperOrientation.Vertical; + stepper.steps[0].active = true; + fix.detectChanges(); + + expect(stepper.steps[1].active).toBeFalsy(); + + stepper.steps[0].nativeElement.focus(); + fix.detectChanges(); + + expect(document.activeElement).toBe(stepper.steps[0].nativeElement as Element); + + UIInteractions.triggerKeyDownEvtUponElem('ArrowDown', stepper.steps[0].nativeElement); + fix.detectChanges(); + + expect(stepper.steps[1].nativeElement as Element).toBe(document.activeElement); + expect(stepper.steps[1].active).toBeFalsy(); + + UIInteractions.triggerKeyDownEvtUponElem('ArrowUp', stepper.steps[1].nativeElement); + fix.detectChanges(); + + expect(stepper.steps[0].nativeElement as Element).toBe(document.activeElement); + })); + + it('should specify tabIndex="0" for the active step and tabIndex="-1" for the other steps', fakeAsync(() => { + stepper.orientation = IgxStepperOrientation.Horizontal; + stepper.steps[0].active = true; + fix.detectChanges(); + + stepper.steps[0].nativeElement.focus(); + let stepContent = stepper.nativeElement.querySelector(`#${stepper.steps[0].id.replace('step', 'content')}`); + + expect(stepper.steps[0].tabIndex).toBe(0); + expect(stepContent.getAttribute('tabIndex')).toBe('0'); + + for (let i = 1; i < stepper.steps.length; i++) { + expect(stepper.steps[i].tabIndex).toBe(-1); + } + + stepper.steps[1].active = true; + fix.detectChanges(); + + expect(stepContent.getAttribute('tabIndex')).toBe('-1'); + expect(stepper.steps[1].tabIndex).toBe(0); + + stepper.steps[1].nativeElement.focus(); + UIInteractions.triggerKeyDownEvtUponElem('Enter', stepper.steps[1].nativeElement); + fix.detectChanges(); + + stepContent = stepper.nativeElement.querySelector(`#${stepper.steps[1].id.replace('step', 'content')}`); + expect(stepContent).not.toBeNull(); + expect(stepContent.getAttribute('tabIndex')).toBe('0'); + + for (let i = 0; i < stepper.steps.length; i++) { + if (i === 1) { + continue; + } + expect(stepper.steps[i].tabIndex).toBe(-1); + } + + stepper.orientation = IgxStepperOrientation.Vertical; + stepper.steps[0].active = true; + fix.detectChanges(); + + stepContent = stepper.nativeElement.querySelector(`#${stepper.steps[0].id.replace('step', 'content')}`); + stepper.steps[0].nativeElement.focus(); + + expect(stepper.steps[0].tabIndex).toBe(0); + expect(stepContent).not.toBeNull(); + expect(stepContent.getAttribute('tabIndex')).toBe('0'); + + for (let i = 1; i < stepper.steps.length; i++) { + stepContent = stepper.nativeElement.querySelector(`#${stepper.steps[i].id.replace('step', 'content')}`); + expect(stepper.steps[i].tabIndex).toBe(-1); + expect(stepContent).toBeNull(); + } + })); + }); + + describe('ARIA', () => { + it('should render proper role and orientation attributes for the stepper', () => { + expect(stepper.nativeElement.attributes['role'].value).toEqual('tablist'); + + stepper.orientation = IgxStepperOrientation.Horizontal; + fix.detectChanges(); + + expect(stepper.nativeElement.attributes['aria-orientation'].value).toEqual('horizontal'); + + stepper.orientation = IgxStepperOrientation.Vertical; + fix.detectChanges(); + + expect(stepper.nativeElement.attributes['aria-orientation'].value).toEqual('vertical'); + }); + + it('should render proper aria attributes for each step', () => { + for (let i = 0; i < stepper.steps.length; i++) { + expect(stepper.steps[i].nativeElement.attributes['role'].value) + .toEqual('tab'); + expect(stepper.steps[i].nativeElement.attributes['aria-posinset'].value) + .toEqual((i + 1).toString()); + expect(stepper.steps[i].nativeElement.attributes['aria-setsize'].value) + .toEqual(stepper.steps.length.toString()); + expect(stepper.steps[i].nativeElement.attributes['aria-controls'].value) + .toEqual(`${stepper.steps[i].id.replace('step', 'content')}`); + + if (i !== 0) { + expect(stepper.steps[i].nativeElement.attributes['aria-selected'].value).toEqual('false'); + } + + stepper.steps[i].active = true; + fix.detectChanges(); + + expect(stepper.steps[i].nativeElement.attributes['aria-selected'].value).toEqual('true'); + } + }); + }); +}); + +describe('Stepper service unit tests', () => { + configureTestSuite(); + + let stepperService: IgxStepperService; + let mockElement: any; + let mockElementRef: any; + let mockCdr: any; + let mockAnimationBuilder: any; + let mockPlatform: any; + let mockDocument: any; + let mockDir: any; + + let steps: IgxStepComponent[] = []; + let stepper: IgxStepperComponent; + + beforeEach(() => { + mockElement = { + style: { visibility: '', cursor: '', transitionDuration: '' }, + classList: { add: () => { }, remove: () => { } }, + appendChild: () => { }, + removeChild: () => { }, + addEventListener: (_type: string, _listener: (this: HTMLElement, ev: MouseEvent) => any) => { }, + removeEventListener: (_type: string, _listener: (this: HTMLElement, ev: MouseEvent) => any) => { }, + insertBefore: (_newChild: HTMLDivElement, _refChild: Node) => { }, + contains: () => { } + }; + mockElement.parent = mockElement; + mockElement.parentElement = mockElement; + mockElement.parentNode = mockElement; + mockElementRef = { nativeElement: mockElement }; + + mockAnimationBuilder = { + build: (_a: AnimationMetadata | AnimationMetadata[]) => ({ + create: (_e: any, _opt?: AnimationOptions) => ({ + onDone: (_fn: any) => { }, + onStart: (_fn: any) => { }, + onDestroy: (_fn: any) => { }, + init: () => { }, + hasStarted: () => true, + play: () => { }, + pause: () => { }, + restart: () => { }, + finish: () => { }, + destroy: () => { }, + rest: () => { }, + setPosition: (_p: any) => { }, + getPosition: () => 0, + parentPlayer: {}, + totalTime: 0, + beforeDestroy: () => { } + }) + }) + }; + + mockPlatform = { isIOS: false }; + + mockDocument = { + body: mockElement, + defaultView: mockElement, + createElement: () => mockElement, + appendChild: () => { }, + addEventListener: (_type: string, _listener: (this: HTMLElement, ev: MouseEvent) => any) => { }, + removeEventListener: (_type: string, _listener: (this: HTMLElement, ev: MouseEvent) => any) => { } + }; + + mockDir = { + value: (): Direction => 'rtl', + document: () => mockDocument, + rtl: () => true + }; + + mockCdr = { + markForCheck: (): void => { }, + detach: (): void => { }, + detectChanges: (): void => { }, + checkNoChanges: (): void => { }, + reattach: (): void => { }, + }; + + stepperService = new IgxStepperService(); + stepper = new IgxStepperComponent(mockAnimationBuilder, stepperService, mockElementRef); + steps = []; + for (let index = 0; index < 4; index++) { + const newStep = new IgxStepComponent(stepper, mockCdr, null, + mockPlatform, stepperService, mockAnimationBuilder, mockElementRef, mockDir); + newStep._index = index; + steps.push(newStep); + } + }); + + it('should expand a step by activating it and firing the step\'s activeChange event', () => { + spyOnProperty(stepper, 'orientation', 'get').and.returnValue(IgxStepperOrientation.Horizontal); + spyOnProperty(stepper, 'steps', 'get').and.returnValue(steps); + + stepperService.activeStep = steps[0]; + + steps[0].contentContainer = mockElementRef; + steps[1].contentContainer = mockElementRef; + + spyOn(steps[0].activeChange, 'emit').and.callThrough(); + spyOn(steps[1].activeChange, 'emit').and.callThrough(); + + stepperService.expand(steps[1]); + expect(stepperService.activeStep).toBe(steps[1]); + expect(steps[1].activeChange.emit).toHaveBeenCalledTimes(1); + expect(steps[1].activeChange.emit).toHaveBeenCalledWith(true); + + spyOnProperty(stepper, 'orientation', 'get').and.returnValue(IgxStepperOrientation.Vertical); + stepperService.expand(steps[0]); + + expect(stepperService.activeStep).toBe(steps[0]); + expect(steps[0].activeChange.emit).toHaveBeenCalledOnceWith(true); + + const testValues = [null, undefined, [], {}, 'sampleString']; + + for (const val of testValues) { + expect(() => { + stepperService.expand(val as any); + }).toThrow(); + } + }); + + it('should expand a step through API by activating it and firing the step\'s activeChange event', () => { + spyOnProperty(stepper, 'orientation', 'get').and.returnValue(IgxStepperOrientation.Horizontal); + spyOnProperty(stepper, 'steps', 'get').and.returnValue(steps); + + stepperService.activeStep = steps[0]; + + spyOn(steps[0].activeChange, 'emit'); + spyOn(steps[1].activeChange, 'emit'); + + stepperService.expandThroughApi(steps[1]); + + expect(stepperService.activeStep).toBe(steps[1]); + expect(steps[0].activeChange.emit).toHaveBeenCalledOnceWith(false); + expect(steps[1].activeChange.emit).toHaveBeenCalledOnceWith(true); + + spyOnProperty(stepper, 'orientation', 'get').and.returnValue(IgxStepperOrientation.Vertical); + stepperService.expandThroughApi(steps[0]); + + expect(stepperService.activeStep).toBe(steps[0]); + expect(steps[1].activeChange.emit).toHaveBeenCalledTimes(2); + expect(steps[1].activeChange.emit).toHaveBeenCalledWith(false); + expect(steps[0].activeChange.emit).toHaveBeenCalledTimes(2); + expect(steps[0].activeChange.emit).toHaveBeenCalledWith(true); + + const testValues = [null, undefined, [], {}, 'sampleString']; + + for (const val of testValues) { + expect(() => { + stepperService.expandThroughApi(val as any); + }).toThrow(); + } + }); + + it('should collapse the currently active step and fire the change event', () => { + spyOnProperty(stepper, 'orientation', 'get').and.returnValue(IgxStepperOrientation.Horizontal); + spyOnProperty(stepper, 'steps', 'get').and.returnValue(steps); + + stepperService.previousActiveStep = steps[0]; + stepperService.activeStep = steps[1]; + stepperService.collapsingSteps.add(stepperService.previousActiveStep); + + expect(stepperService.collapsingSteps).toContain(steps[0]); + expect(stepperService.collapsingSteps).not.toContain(steps[1]); + + spyOn(steps[0].activeChange, 'emit'); + spyOn(steps[1].activeChange, 'emit'); + + stepperService.collapse(steps[0]); + + expect(stepperService.collapsingSteps).not.toContain(steps[0]); + expect(stepperService.activeStep).not.toBe(steps[0]); + expect(stepperService.activeStep).toBe(steps[1]); + expect(steps[0].activeChange.emit).toHaveBeenCalledOnceWith(false); + expect(steps[1].activeChange.emit).not.toHaveBeenCalled(); + + spyOnProperty(stepper, 'orientation', 'get').and.returnValue(IgxStepperOrientation.Vertical); + + stepperService.previousActiveStep = steps[1]; + stepperService.activeStep = steps[0]; + + stepperService.collapsingSteps.add(stepperService.previousActiveStep); + expect(stepperService.collapsingSteps).toContain(steps[1]); + expect(stepperService.collapsingSteps).not.toContain(steps[0]); + + stepperService.collapse(steps[1]); + + expect(stepperService.collapsingSteps).not.toContain(steps[1]); + expect(stepperService.activeStep).not.toBe(steps[1]); + expect(stepperService.activeStep).toBe(steps[0]); + + expect(steps[1].activeChange.emit).toHaveBeenCalledOnceWith(false); + expect(steps[0].activeChange.emit).not.toHaveBeenCalledTimes(2); + + const testValues = [null, undefined, [], {}, 'sampleString']; + + for (const val of testValues) { + expect(() => { + stepperService.collapse(val as any); + }).toThrow(); + } + }); + + it('should determine the steps that are marked as visited based on the active step', () => { + spyOnProperty(stepper, 'steps', 'get').and.returnValue(steps); + let sampleSet: Set; + + stepperService.activeStep = steps[0]; + stepperService.calculateVisitedSteps(); + expect(stepperService.visitedSteps.size).toEqual(1); + sampleSet = new Set([steps[0]]); + expect(stepperService.visitedSteps).toEqual(sampleSet); + + stepperService.activeStep = steps[1]; + stepperService.calculateVisitedSteps(); + expect(stepperService.visitedSteps.size).toEqual(2); + sampleSet = new Set([steps[0], steps[1]]); + expect(stepperService.visitedSteps).toEqual(sampleSet); + + stepperService.activeStep = steps[2]; + stepperService.calculateVisitedSteps(); + expect(stepperService.visitedSteps.size).toEqual(3); + sampleSet = new Set([steps[0], steps[1], steps[2]]); + expect(stepperService.visitedSteps).toEqual(sampleSet); + }); + + it('should determine the steps that should be disabled in linear mode based on the validity of the active step', () => { + spyOnProperty(stepper, 'orientation', 'get').and.returnValue(IgxStepperOrientation.Horizontal); + spyOnProperty(stepper, 'steps').and.returnValue(steps); + + for (const step of steps) { + spyOnProperty(step, 'isValid').and.returnValue(false); + } + spyOnProperty(stepper, 'linear').and.returnValue(true); + stepperService.activeStep = steps[0]; + spyOnProperty(steps[0], 'active').and.returnValue(true); + + expect(stepperService.linearDisabledSteps.size).toBe(0); + stepperService.calculateLinearDisabledSteps(); + expect(stepperService.linearDisabledSteps.size).toBe(3); + let sampleSet = new Set([steps[1], steps[2], steps[3]]); + expect(stepperService.linearDisabledSteps).toEqual(sampleSet); + + spyOnProperty(steps[0], 'isValid').and.returnValue(true); + stepperService.calculateLinearDisabledSteps(); + sampleSet = new Set([steps[2], steps[3]]); + expect(stepperService.linearDisabledSteps.size).toBe(2); + expect(stepperService.linearDisabledSteps).toEqual(sampleSet); + + spyOnProperty(steps[1], 'active').and.returnValue(true); + spyOnProperty(steps[1], 'isValid').and.returnValue(false); + stepperService.calculateLinearDisabledSteps(); + expect(stepperService.linearDisabledSteps.size).toBe(2); + expect(stepperService.linearDisabledSteps).toEqual(sampleSet); + + spyOnProperty(steps[1], 'isValid').and.returnValue(true); + stepperService.activeStep = steps[1]; + sampleSet = new Set([steps[3]]); + stepperService.calculateLinearDisabledSteps(); + expect(stepperService.linearDisabledSteps.size).toBe(1); + expect(stepperService.linearDisabledSteps).toEqual(sampleSet); + expect(stepperService.linearDisabledSteps).toContain(steps[3]); + + spyOnProperty(steps[2], 'isValid').and.returnValue(true); + spyOnProperty(steps[3], 'isValid').and.returnValue(true); + stepperService.activeStep = steps[3]; + stepperService.calculateLinearDisabledSteps(); + expect(stepperService.linearDisabledSteps.size).toBe(0); + expect(stepperService.linearDisabledSteps).not.toContain(steps[3]); + }); + + it('should emit activating event', () => { + spyOnProperty(stepper, 'orientation', 'get').and.returnValue(IgxStepperOrientation.Horizontal); + spyOnProperty(stepper, 'steps').and.returnValue(steps); + const activeChangingSpy = spyOn(stepper.activeStepChanging, 'emit'); + stepperService.activeStep = steps[0]; + + let activeChangingEventArgs: any = { + owner: stepper, + newIndex: steps[1].index, + oldIndex: steps[0].index, + cancel: false + }; + + let result: boolean = stepperService.emitActivatingEvent(steps[1]); + expect(result).toEqual(false); + expect(activeChangingSpy).toHaveBeenCalledOnceWith(activeChangingEventArgs); + + activeChangingSpy.calls.reset(); + + stepperService.activeStep = steps[1]; + stepperService.previousActiveStep = steps[0]; + + result = stepperService.emitActivatingEvent(steps[0]); + expect(result).toEqual(false); + expect(activeChangingSpy).toHaveBeenCalledTimes(1); + expect(activeChangingSpy).not.toHaveBeenCalledWith(activeChangingEventArgs); + + activeChangingEventArgs = { + owner: stepper, + newIndex: steps[0].index, + oldIndex: steps[1].index, + cancel: false + }; + + expect(activeChangingSpy).toHaveBeenCalledWith(activeChangingEventArgs); + }); +}); + + +@Component({ + template: ` + + + + error + + + + check + + + + edit + + + + 1 + Step No 1 + Step SubTitle +
+ +
+
+ + + 2 + Step No 2 + Step SubTitle +
+

Test step 2

+
+
+ + + * + Hidden step + Step SubTitle +
+

Test hidden step

+
+
+ + + 3 + Step No 3 + Step SubTitle +
+

Test step 3

+
+
+ + + 4 +
+

Test step 4

+
+
+ + + Step No 5 + Step SubTitle +
+

Test step 5

+
+
+
+
+ ` +}) +export class IgxStepperSampleTestComponent { + @ViewChild(IgxStepperComponent) public stepper: IgxStepperComponent; + + public horizontalAnimationType = 'slide'; + public verticalAnimationType = 'grow'; + public animationDuration = 300; + public displayHiddenStep = false; + +}