From dc66ce48e1f210ca57cecae5c89e5dc3b7e95de5 Mon Sep 17 00:00:00 2001 From: Brandy Carney Date: Tue, 14 Jan 2020 11:51:28 -0500 Subject: [PATCH] feat(segment): implement iOS 13 segment with animation (#19036) Changes Closes #18663 * Converts Segment to shadow * Enables gesture to swipe between segment buttons * Adds indicator transition to slide the indicator between buttons * Updates global theme variables * Removes activated state, now handled by the gesture * Updates iOS to latest iOS 13 UI * Ensures customization is working for the buttons and indicator * Updates the e2e tests --- core/api.txt | 12 +- core/src/components.d.ts | 4 +- .../components/menu-button/menu-button.scss | 1 + core/src/components/segment-button/readme.md | 53 ++-- .../segment-button/segment-button.ios.scss | 105 +++++-- .../segment-button.ios.vars.scss | 62 ++-- .../segment-button/segment-button.md.scss | 48 ++-- .../segment-button.md.vars.scss | 14 +- .../segment-button/segment-button.scss | 148 ++++++---- .../segment-button/segment-button.tsx | 21 +- core/src/components/segment/readme.md | 9 +- core/src/components/segment/segment.ios.scss | 100 +++---- .../components/segment/segment.ios.vars.scss | 10 +- core/src/components/segment/segment.md.scss | 71 +++-- core/src/components/segment/segment.scss | 27 +- core/src/components/segment/segment.tsx | 266 ++++++++++++++++-- .../components/segment/test/basic/index.html | 88 +++--- .../src/components/segment/test/colors/e2e.ts | 23 ++ .../components/segment/test/colors/index.html | 178 ++++++++++++ .../components/segment/test/custom/index.html | 258 +++++++++++------ .../components/segment/test/spec/index.html | 35 ++- .../segment/test/standalone/index.html | 97 ++++++- .../components/segment/test/toolbar/e2e.ts | 23 ++ .../segment/test/toolbar/index.html | 25 +- core/src/components/toolbar/toolbar.ios.scss | 5 +- .../themes/test/css-variables/css/dark.css | 30 +- .../themes/test/css-variables/css/vibrant.css | 16 ++ 27 files changed, 1263 insertions(+), 466 deletions(-) create mode 100644 core/src/components/segment/test/colors/e2e.ts create mode 100644 core/src/components/segment/test/colors/index.html create mode 100644 core/src/components/segment/test/toolbar/e2e.ts diff --git a/core/api.txt b/core/api.txt index d4ed718e484..b8760bfe529 100644 --- a/core/api.txt +++ b/core/api.txt @@ -986,13 +986,14 @@ ion-searchbar,css-prop,--placeholder-font-style ion-searchbar,css-prop,--placeholder-font-weight ion-searchbar,css-prop,--placeholder-opacity -ion-segment,scoped +ion-segment,shadow ion-segment,prop,color,string | undefined,undefined,false,false ion-segment,prop,disabled,boolean,false,false,false ion-segment,prop,mode,"ios" | "md",undefined,false,false ion-segment,prop,scrollable,boolean,false,false,false ion-segment,prop,value,null | string | undefined,undefined,false,false ion-segment,event,ionChange,SegmentChangeEventDetail,true +ion-segment,css-prop,--background ion-segment-button,shadow ion-segment-button,prop,checked,boolean,false,false,false @@ -1003,20 +1004,21 @@ ion-segment-button,prop,type,"button" | "reset" | "submit",'button',false,false ion-segment-button,prop,value,string,'ion-sb-' + (ids++),false,false ion-segment-button,event,ionSelect,void,true ion-segment-button,css-prop,--background -ion-segment-button,css-prop,--background-activated ion-segment-button,css-prop,--background-checked +ion-segment-button,css-prop,--background-disabled ion-segment-button,css-prop,--background-hover ion-segment-button,css-prop,--border-color ion-segment-button,css-prop,--border-radius ion-segment-button,css-prop,--border-style ion-segment-button,css-prop,--border-width ion-segment-button,css-prop,--color -ion-segment-button,css-prop,--color-activated ion-segment-button,css-prop,--color-checked -ion-segment-button,css-prop,--color-checked-disabled ion-segment-button,css-prop,--color-disabled +ion-segment-button,css-prop,--color-hover +ion-segment-button,css-prop,--indicator-box-shadow ion-segment-button,css-prop,--indicator-color -ion-segment-button,css-prop,--indicator-color-checked +ion-segment-button,css-prop,--indicator-transform +ion-segment-button,css-prop,--indicator-transition ion-segment-button,css-prop,--margin-bottom ion-segment-button,css-prop,--margin-end ion-segment-button,css-prop,--margin-start diff --git a/core/src/components.d.ts b/core/src/components.d.ts index 42a79e4939a..11824a3b9ec 100644 --- a/core/src/components.d.ts +++ b/core/src/components.d.ts @@ -2231,7 +2231,7 @@ export namespace Components { */ 'mode'?: "ios" | "md"; /** - * If `true`, the segment buttons will overflow and the user can swipe to see them. + * If `true`, the segment buttons will overflow and the user can swipe to see them. In addition, this will disable the gesture to drag the indicator between the buttons in order to swipe to see hidden buttons. */ 'scrollable': boolean; /** @@ -5492,7 +5492,7 @@ declare namespace LocalJSX { */ 'onIonChange'?: (event: CustomEvent) => void; /** - * If `true`, the segment buttons will overflow and the user can swipe to see them. + * If `true`, the segment buttons will overflow and the user can swipe to see them. In addition, this will disable the gesture to drag the indicator between the buttons in order to swipe to see hidden buttons. */ 'scrollable'?: boolean; /** diff --git a/core/src/components/menu-button/menu-button.scss b/core/src/components/menu-button/menu-button.scss index 7a322e64b7d..fcb07eb2107 100644 --- a/core/src/components/menu-button/menu-button.scss +++ b/core/src/components/menu-button/menu-button.scss @@ -125,6 +125,7 @@ ion-icon { // Menu Button in Toolbar: Global Theming // -------------------------------------------------- +// TODO this will not work in Safari - component is shadow not scoped :host-context(ion-toolbar:not(.ion-color)) { color: #{var(--ion-toolbar-color, var(--color))}; } diff --git a/core/src/components/segment-button/readme.md b/core/src/components/segment-button/readme.md index 2c1cd3a5772..be1c77f05cc 100644 --- a/core/src/components/segment-button/readme.md +++ b/core/src/components/segment-button/readme.md @@ -639,32 +639,33 @@ export const SegmentButtonExample: React.FC = () => ( ## CSS Custom Properties -| Name | Description | -| --------------------------- | ----------------------------------------------------------------------------------------------------------------- | -| `--background` | Background of the segment button | -| `--background-activated` | Background of the segment button when pressed | -| `--background-checked` | Background of the checked segment button | -| `--background-hover` | Background of the segment button on hover | -| `--border-color` | Color of the segment button border | -| `--border-radius` | Radius of the segment button border | -| `--border-style` | Style of the segment button border | -| `--border-width` | Width of the segment button border | -| `--color` | Color of the segment button | -| `--color-activated` | Color of the segment button when pressed | -| `--color-checked` | Color of the checked segment button | -| `--color-checked-disabled` | Color of the checked & disabled segment button | -| `--color-disabled` | Color of the disabled segment button | -| `--indicator-color` | Color of the indicator (highlight) under the segment button | -| `--indicator-color-checked` | Color of the indicator (highlight) under the checked segment button | -| `--margin-bottom` | Bottom margin of the segment button | -| `--margin-end` | Right margin if direction is left-to-right, and left margin if direction is right-to-left of the segment button | -| `--margin-start` | Left margin if direction is left-to-right, and right margin if direction is right-to-left of the segment button | -| `--margin-top` | Top margin of the segment button | -| `--padding-bottom` | Bottom padding of the segment button | -| `--padding-end` | Right padding if direction is left-to-right, and left padding if direction is right-to-left of the segment button | -| `--padding-start` | Left padding if direction is left-to-right, and right padding if direction is right-to-left of the segment button | -| `--padding-top` | Top padding of the segment button | -| `--transition` | Transition of the segment button | +| Name | Description | +| ------------------------ | ----------------------------------------------------------------------------------------------------------------- | +| `--background` | Background of the segment button | +| `--background-checked` | Background of the checked segment button | +| `--background-disabled` | Background of the disabled segment button | +| `--background-hover` | Background of the segment button on hover | +| `--border-color` | Color of the segment button border | +| `--border-radius` | Radius of the segment button border | +| `--border-style` | Style of the segment button border | +| `--border-width` | Width of the segment button border | +| `--color` | Color of the segment button | +| `--color-checked` | Color of the checked segment button | +| `--color-disabled` | Color of the disabled segment button | +| `--color-hover` | Color of the segment button on hover | +| `--indicator-box-shadow` | Box shadow on the indicator for the checked segment button | +| `--indicator-color` | Color of the indicator for the checked segment button | +| `--indicator-transform` | Transform of the indicator for the checked segment button | +| `--indicator-transition` | Transition of the indicator for the checked segment button | +| `--margin-bottom` | Bottom margin of the segment button | +| `--margin-end` | Right margin if direction is left-to-right, and left margin if direction is right-to-left of the segment button | +| `--margin-start` | Left margin if direction is left-to-right, and right margin if direction is right-to-left of the segment button | +| `--margin-top` | Top margin of the segment button | +| `--padding-bottom` | Bottom padding of the segment button | +| `--padding-end` | Right padding if direction is left-to-right, and left padding if direction is right-to-left of the segment button | +| `--padding-start` | Left padding if direction is left-to-right, and right padding if direction is right-to-left of the segment button | +| `--padding-top` | Top padding of the segment button | +| `--transition` | Transition of the segment button | ## Dependencies diff --git a/core/src/components/segment-button/segment-button.ios.scss b/core/src/components/segment-button/segment-button.ios.scss index 3c90aa23b9c..6301e3c9a5f 100644 --- a/core/src/components/segment-button/segment-button.ios.scss +++ b/core/src/components/segment-button/segment-button.ios.scss @@ -5,24 +5,73 @@ // -------------------------------------------------- :host { + --background: #{$segment-button-ios-background-color}; + --background-checked: #{$segment-button-ios-background-color-checked}; + --background-hover: #{$segment-button-ios-background-color-hover}; --border-radius: #{$segment-button-ios-border-radius}; --border-width: #{$segment-button-ios-border-width}; + --border-color: #{$segment-button-ios-border-color}; --border-style: solid; + --indicator-box-shadow: #{$segment-button-ios-box-shadow-checked}; + --indicator-color: #{$segment-button-ios-indicator-color}; + --indicator-transition: #{$segment-button-ios-transition-animated}; + --indicator-transform: none; --transition: #{$segment-button-ios-transition}; + --padding-top: 0; + --padding-end: 13px; + --padding-bottom: 0; + --padding-start: 13px; - min-height: #{$segment-button-ios-height}; + @include margin($segment-button-ios-margin, null, $segment-button-ios-margin, null); + + position: relative; + + flex-basis: 0; + flex-direction: row; + + min-width: #{$segment-button-ios-min-width}; + + min-height: #{$segment-button-ios-min-height}; + + // Necessary for the z-index to work properly + transform: translate3d(0, 0, 0); font-size: #{$segment-button-ios-font-size}; + font-weight: 450; + line-height: #{$segment-button-ios-line-height}; } -// Segment Button: Indicator +// Segment Button: Borders // -------------------------------------------------- -.segment-button-indicator { - display: none; +:host::before { + @include margin(5px, 0); + + transition: 160ms opacity ease-in-out; + + transition-delay: 100ms; + + border-left: var(--border-width) var(--border-style) var(--border-color); + + content: ""; + opacity: 1; + + will-change: opacity; +} + +:host(:first-of-type)::before { + border-left-color: transparent; +} + + +// Segment Button: Disabled +// -------------------------------------------------- + +:host(.segment-button-disabled) { + opacity: $segment-button-ios-opacity-disabled; } @@ -48,28 +97,52 @@ } -// Segment Button: Hover +// Segment Button: Checked Indicator +// -------------------------------------------------- + +.segment-button-indicator { + @include padding(null, $segment-button-ios-margin); + @include position(0, 0, 0, 0); +} + +.segment-button-indicator-background { + @include border-radius($segment-button-ios-border-radius); + + background: var(--indicator-color); +} + +.segment-button-indicator-background { + transition: var(--indicator-transition); +} + + +// Segment Button: Checked Borders // -------------------------------------------------- -@media (any-hover: hover) { - :host(:hover:not(.segment-button-checked)) { - background: var(--background-hover); - } +:host(.segment-button-checked)::before, +:host(.segment-button-after-checked)::before { + opacity: 0; } -// Segment Button: Activated +// Segment Button: Checked // -------------------------------------------------- -:host(.activated) { - background: var(--background-activated); +:host(.segment-button-checked) { + z-index: -1; } -// Segment: Checked & Activated +// Segment Button: Toolbar // -------------------------------------------------- -:host(.segment-button-checked.activated) { - background: var(--background-checked); - color: var(--color-checked); +// Segment button indicator color should use the global variable with +// a fallback to the local variable +:host(.in-toolbar) .segment-button-indicator-background { + background: #{var(--ion-toolbar-segment-indicator-color, var(--indicator-color))}; +} + +// Do not use the global or local CSS variable if the toolbar has a color +:host(.in-toolbar-color) .segment-button-indicator-background { + background: #fff; } diff --git a/core/src/components/segment-button/segment-button.ios.vars.scss b/core/src/components/segment-button/segment-button.ios.vars.scss index d629b2f2560..7d749031514 100644 --- a/core/src/components/segment-button/segment-button.ios.vars.scss +++ b/core/src/components/segment-button/segment-button.ios.vars.scss @@ -7,37 +7,37 @@ $segment-button-ios-background-color: transparent !default; /// @prop - Background of the checked segment button -$segment-button-ios-background-color-checked: ion-color(primary, base) !default; +$segment-button-ios-background-color-checked: transparent !default; -/// @prop - Opacity of the segment button on hover -$segment-button-ios-opacity-hover: .1 !default; +/// @prop - Background of the checked segment button indicator +$segment-button-ios-indicator-color: var(--ion-color-step-350, $background-color) !default; + +/// @prop - Margin of the segment button +$segment-button-ios-margin: 2px !default; -/// @prop - Opacity of the segment button when pressed -$segment-button-ios-opacity-activated: .16 !default; +/// @prop - Opacity of the segment button on hover +$segment-button-ios-opacity-hover: .5 !default; /// @prop - Opacity of the disabled segment button $segment-button-ios-opacity-disabled: .3 !default; /// @prop - Background of the segment button on hover -$segment-button-ios-background-color-hover: ion-color(primary, base, $segment-button-ios-opacity-hover) !default; - -/// @prop - Background of the activated segment button -$segment-button-ios-background-color-activated: ion-color(primary, base, $segment-button-ios-opacity-activated) !default; +$segment-button-ios-background-color-hover: transparent !default; -/// @prop - Background of the disabled segment button -$segment-button-ios-background-color-disabled: ion-color(primary, base, $segment-button-ios-opacity-disabled) !default; - -/// @prop - Text color of the segment button -$segment-button-ios-text-color: ion-color(primary, base) !default; - -/// @prop - Text color of the checked segment button -$segment-button-ios-text-color-checked: ion-color(primary, contrast) !default; +/// @prop - Box shadow of the checked segment button +$segment-button-ios-box-shadow-checked: 0 0 5px rgba(0, 0, 0, 0.16) !default; /// @prop - Border width of the segment button $segment-button-ios-border-width: 1px !default; -/// @prop - Height of the segment button -$segment-button-ios-height: 24px !default; +/// @prop - Border color of the segment button +$segment-button-ios-border-color: rgba($text-color-rgb, 0.12) !default; + +/// @prop - Minimum width of the segment button +$segment-button-ios-min-width: 70px !default; + +/// @prop - Minimum height of the segment button +$segment-button-ios-min-height: 28px !default; /// @prop - Line height of the segment button $segment-button-ios-line-height: 37px !default; @@ -46,31 +46,13 @@ $segment-button-ios-line-height: 37px !default; $segment-button-ios-font-size: 13px !default; /// @prop - Border radius of the segment button -$segment-button-ios-border-radius: 4px !default; - -/// @prop - Border color of the segment button -$segment-button-ios-border-color: ion-color(primary, base) !default; +$segment-button-ios-border-radius: 7px !default; /// @prop - Size of an icon in the segment button $segment-button-ios-icon-size: 24px !default; -/// @prop - Line height of an icon in the segment button -$segment-button-ios-icon-line-height: 28px !default; - /// @prop - Transition of the segment button $segment-button-ios-transition: 100ms all linear !default; -/// @prop - Max width of the segment button in a toolbar -$segment-button-ios-toolbar-button-max-width: 100px !default; - -/// @prop - Line height of the segment button in a toolbar -$segment-button-ios-toolbar-line-height: 22px !default; - -/// @prop - Font size of the segment button in a toolbar -$segment-button-ios-toolbar-font-size: 12px !default; - -/// @prop - Size of an icon in the segment button in a toolbar -$segment-button-ios-toolbar-icon-size: 22px !default; - -/// @prop - Line height of an icon in the segment button in a toolbar -$segment-button-ios-toolbar-icon-line-height: 24px !default; +/// @prop - Transition of the animated segment button +$segment-button-ios-transition-animated: transform 260ms cubic-bezier(0.4, 0, 0.2, 1) !default; diff --git a/core/src/components/segment-button/segment-button.md.scss b/core/src/components/segment-button/segment-button.md.scss index de33a7a1232..1f56568331b 100644 --- a/core/src/components/segment-button/segment-button.md.scss +++ b/core/src/components/segment-button/segment-button.md.scss @@ -5,6 +5,15 @@ // -------------------------------------------------- :host { + --background: #{$segment-button-md-background}; + --background-checked: #{$segment-button-md-background-checked}; + --background-hover: #{$segment-button-md-background-hover}; + --color: #{$segment-button-md-text-color}; + --color-checked: #{$segment-button-md-text-color-checked}; + --indicator-box-shadow: none; + --indicator-color: var(--color-checked); + --indicator-transition: #{$segment-button-md-transition-animated}; + --indicator-transform: none; --padding-top: #{$segment-button-md-padding-top}; --padding-end: #{$segment-button-md-padding-end}; --padding-bottom: #{$segment-button-md-padding-bottom}; @@ -16,6 +25,10 @@ max-width: $segment-button-md-max-width; min-height: $segment-button-md-min-height; + border-width: var(--border-width); + border-style: var(--border-style); + border-color: var(--border-color); + font-size: $segment-button-md-font-size; font-weight: $segment-button-md-font-weight; @@ -26,15 +39,6 @@ text-transform: uppercase; } -// Segment Button: Checked -// -------------------------------------------------- - -:host(.activated), -:host(.segment-button-checked) { - --border-color: #{$segment-button-md-border-bottom-color-activated}; - - opacity: $segment-button-md-opacity-activated; -} // Segment Button: Disabled // -------------------------------------------------- @@ -93,20 +97,26 @@ @include margin(12px, null, 12px, null); } - -// Segment: Checked & Activated +// Segment Button: Indicator // -------------------------------------------------- -:host(.segment-button-checked.activated) { - color: var(--color-checked); +.segment-button-indicator { + @include position(null, 0, 0, 0); } +.segment-button-indicator-background { + height: 2px; -// Segment Button: Hover -// -------------------------------------------------- + background: var(--indicator-color); +} + +// Segment button indicator color should use the background checked variable with +// a fallback to the default value of --indicator-color +:host(.in-toolbar) .segment-button-indicator-background { + background: #{var(--ion-toolbar-segment-indicator-color, var(--indicator-color))}; +} -@media (any-hover: hover) { - :host(:hover) { - background: var(--background-hover); - } +// Do not use the global or local CSS variable if the toolbar has a color +:host(.in-toolbar-color) .segment-button-indicator-background { + background: #{current-color(contrast)}; } diff --git a/core/src/components/segment-button/segment-button.md.vars.scss b/core/src/components/segment-button/segment-button.md.vars.scss index 0457b1c7ff4..d01883ec2df 100644 --- a/core/src/components/segment-button/segment-button.md.vars.scss +++ b/core/src/components/segment-button/segment-button.md.vars.scss @@ -18,24 +18,15 @@ $segment-button-md-background-checked: $segment-button-md-backgro /// @prop - Background of the hovered segment button $segment-button-md-background-hover: ion-color(primary, base, .04) !default; -/// @prop - Background of the activated segment button -$segment-button-md-background-activated: ion-color(primary, base, .16) !default; - /// @prop - Width of the bottom border on the segment button $segment-button-md-border-bottom-width: 2px !default; /// @prop - Color of the bottom border on the segment button $segment-button-md-border-bottom-color: transparent !default; -/// @prop - Text color of the activated segment button +/// @prop - Text color of the checked segment button $segment-button-md-text-color-checked: ion-color(primary, base) !default; -/// @prop - Border color of the activated segment button -$segment-button-md-border-bottom-color-activated: ion-color(primary, base) !default; - -/// @prop - Opacity of the activated segment button -$segment-button-md-opacity-activated: 1 !default; - /// @prop - Opacity of the disabled segment button $segment-button-md-opacity-disabled: .3 !default; @@ -75,6 +66,9 @@ $segment-button-md-font-weight: 500 !default; /// @prop - Transition of the segment button $segment-button-md-transition: color .15s linear 0s, opacity .15s linear 0s !default; +/// @prop - Transition of the animated segment button +$segment-button-md-transition-animated: transform 250ms cubic-bezier(.4, 0, .2, 1) !default; + /// @prop - Size of an icon in the segment button $segment-button-md-icon-size: 24px !default; diff --git a/core/src/components/segment-button/segment-button.scss b/core/src/components/segment-button/segment-button.scss index f14359a6f4a..bc744998dc8 100644 --- a/core/src/components/segment-button/segment-button.scss +++ b/core/src/components/segment-button/segment-button.scss @@ -6,15 +6,14 @@ :host { /** * @prop --background: Background of the segment button - * @prop --background-hover: Background of the segment button on hover - * @prop --background-activated: Background of the segment button when pressed * @prop --background-checked: Background of the checked segment button + * @prop --background-disabled: Background of the disabled segment button + * @prop --background-hover: Background of the segment button on hover * * @prop --color: Color of the segment button - * @prop --color-activated: Color of the segment button when pressed * @prop --color-checked: Color of the checked segment button * @prop --color-disabled: Color of the disabled segment button - * @prop --color-checked-disabled: Color of the checked & disabled segment button + * @prop --color-hover: Color of the segment button on hover * * @prop --border-radius: Radius of the segment button border * @prop --border-color: Color of the segment button border @@ -33,26 +32,31 @@ * * @prop --transition: Transition of the segment button * - * @prop --indicator-color: Color of the indicator (highlight) under the segment button - * @prop --indicator-color-checked: Color of the indicator (highlight) under the checked segment button - * + * @prop --indicator-box-shadow: Box shadow on the indicator for the checked segment button + * @prop --indicator-color: Color of the indicator for the checked segment button + * @prop --indicator-transition: Transition of the indicator for the checked segment button + * @prop --indicator-transform: Transform of the indicator for the checked segment button */ + --color: initial; + --color-hover: initial; + --color-checked: var(--color); + --color-disabled: var(--color); --padding-start: 0; --padding-end: 0; --padding-top: 0; --padding-bottom: 0; + @include border-radius(var(--border-radius)); + display: flex; - flex: 1 0 auto; + position: relative; + + flex: 1 1 auto; flex-direction: column; height: auto; - border-width: var(--border-width); - border-style: var(--border-style); - border-color: var(--border-color); - background: var(--background); color: var(--color); @@ -62,32 +66,15 @@ white-space: nowrap; - overflow: hidden; font-kerning: none; } -:host(:first-of-type) { - @include border-radius(var(--border-radius), 0, 0, var(--border-radius)); -} - -:host(:not(:first-of-type)) { - @include rtl() { - border-right-width: 0; - border-left-width: var(--border-width); - } - - border-left-width: 0; -} - -:host(:last-of-type) { - @include border-radius(0, var(--border-radius), var(--border-radius), 0); -} - .button-native { - @include border-radius(inherit); + @include border-radius(0); @include text-inherit(); @include margin(var(--margin-top), var(--margin-end), var(--margin-bottom), var(--margin-start)); @include padding(var(--padding-top), var(--padding-end), var(--padding-bottom), var(--padding-start)); + @include transform(translate3d(0,0,0)); display: flex; position: relative; @@ -116,22 +103,8 @@ contain: content; cursor: pointer; -} - -// Segment Button: Indicator -// -------------------------------------------------- - -.segment-button-indicator { - align-self: flex-end; - - width: 100%; - - height: 2px; - - background-color: var(--indicator-color); - - opacity: 1; + z-index: 2; } @@ -143,30 +116,28 @@ color: var(--color-checked); } -:host(.segment-button-checked) .segment-button-indicator { - background-color: var(--indicator-color-checked, var(--color-checked)); -} - -// Segment Button: Activated -// -------------------------------------------------- - -:host(.activated) { - color: var(--color-activated, var(--color)); -} - // Segment Button: Disabled // -------------------------------------------------- :host(.segment-button-disabled) { + background: var(--background-disabled); color: var(--color-disabled); } -// Segment Button: Checked & Disabled + +// Segment Button: Hover // -------------------------------------------------- -:host(.segment-button-disabled.segment-button-checked) { - color: var(--color-checked-disabled); +@media (any-hover: hover) { + :host(:hover) { + background: var(--background-hover); + color: var(--color-hover, var(--color)); + } + + :host(.segment-button-checked:hover) { + color: var(--color-hover, var(--color-checked)); + } } @@ -174,7 +145,11 @@ // -------------------------------------------------- ::slotted(ion-icon) { + flex-shrink: 0; + order: -1; + + pointer-events: none; } @@ -193,12 +168,19 @@ white-space: nowrap; box-sizing: border-box; + + pointer-events: none; } // Segment Button Layout // -------------------------------------------------- +// Layout: icon top +:host(.segment-button-layout-icon-top) .button-native { + flex-direction: column; +} + // Layout: icon start :host(.segment-button-layout-icon-start) .button-native { flex-direction: row; @@ -230,3 +212,49 @@ ion-ripple-effect { color: var(--ripple-color, var(--color-checked)); } + +// Segment Button: Indicator +// -------------------------------------------------- + +.segment-button-indicator { + @include transform-origin(left); + + position: absolute; + + opacity: 0; + + box-sizing: border-box; + + will-change: transform, opacity; +} + +.segment-button-indicator-background { + width: 100%; + height: 100%; + + transform: var(--indicator-transform); + + box-shadow: var(--indicator-box-shadow); +} + +.segment-button-indicator-animated { + transition: var(--indicator-transition); +} + +:host(.segment-button-checked) .segment-button-indicator { + opacity: 1; +} + + +// Segment: Reduced Motion +// -------------------------------------------------- + +@media (prefers-reduced-motion: reduce) { + .segment-button-indicator-background { + transform: none; + } + + .segment-button-indicator-animated { + transition: none; + } +} \ No newline at end of file diff --git a/core/src/components/segment-button/segment-button.tsx b/core/src/components/segment-button/segment-button.tsx index 3a3b52289de..d0d8074e158 100644 --- a/core/src/components/segment-button/segment-button.tsx +++ b/core/src/components/segment-button/segment-button.tsx @@ -3,6 +3,7 @@ import { Component, ComponentInterface, Element, Event, EventEmitter, Host, Prop import { getIonMode } from '../../global/ionic-global'; import { SegmentButtonLayout } from '../../interface'; import { ButtonInterface } from '../../utils/element-interface'; +import { hostContext } from '../../utils/theme'; let ids = 0; @@ -18,7 +19,6 @@ let ids = 0; shadow: true }) export class SegmentButton implements ComponentInterface, ButtonInterface { - @Element() el!: HTMLElement; /** @@ -52,8 +52,10 @@ export class SegmentButton implements ComponentInterface, ButtonInterface { @Event() ionSelect!: EventEmitter; @Watch('checked') - checkedChanged(checked: boolean, prev: boolean) { - if (checked && !prev) { + checkedChanged(newValue: boolean, oldValue: boolean) { + // If the segment button is not already checked + // emit the ionSelect event + if (newValue && !oldValue) { this.ionSelect.emit(); } } @@ -79,6 +81,8 @@ export class SegmentButton implements ComponentInterface, ButtonInterface { aria-disabled={disabled ? 'true' : null} class={{ [mode]: true, + 'in-toolbar': hostContext('ion-toolbar', this.el), + 'in-toolbar-color': hostContext('ion-toolbar[color]', this.el), 'segment-button-has-label': hasLabel, 'segment-button-has-icon': hasIcon, 'segment-button-has-label-only': hasLabel && !hasIcon, @@ -99,7 +103,16 @@ export class SegmentButton implements ComponentInterface, ButtonInterface { {mode === 'md' && } -
+
+
+
+ ); } diff --git a/core/src/components/segment/readme.md b/core/src/components/segment/readme.md index 48de8a7d902..4221e7e6ff8 100644 --- a/core/src/components/segment/readme.md +++ b/core/src/components/segment/readme.md @@ -443,7 +443,7 @@ export const SegmentExample: React.FC = () => ( | `color` | `color` | The color to use from your application's color palette. Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`. For more information on colors, see [theming](/docs/theming/basics). | `string \| undefined` | `undefined` | | `disabled` | `disabled` | If `true`, the user cannot interact with the segment. | `boolean` | `false` | | `mode` | `mode` | The mode determines which platform styles to use. | `"ios" \| "md"` | `undefined` | -| `scrollable` | `scrollable` | If `true`, the segment buttons will overflow and the user can swipe to see them. | `boolean` | `false` | +| `scrollable` | `scrollable` | If `true`, the segment buttons will overflow and the user can swipe to see them. In addition, this will disable the gesture to drag the indicator between the buttons in order to swipe to see hidden buttons. | `boolean` | `false` | | `value` | `value` | the value of the segment. | `null \| string \| undefined` | `undefined` | @@ -454,6 +454,13 @@ export const SegmentExample: React.FC = () => ( | `ionChange` | Emitted when the value property has changed. | `CustomEvent` | +## CSS Custom Properties + +| Name | Description | +| -------------- | -------------------------------- | +| `--background` | Background of the segment button | + + ---------------------------------------------- *Built with [StencilJS](https://stenciljs.com/)* diff --git a/core/src/components/segment/segment.ios.scss b/core/src/components/segment/segment.ios.scss index 9f30b26e560..556611fe664 100644 --- a/core/src/components/segment/segment.ios.scss +++ b/core/src/components/segment/segment.ios.scss @@ -5,92 +5,78 @@ // -------------------------------------------------- :host { - --background: #{$segment-button-ios-background-color}; - --background-hover: #{$segment-button-ios-background-color-hover}; - --background-activated: #{$segment-button-ios-background-color-activated}; - --background-checked: #{$segment-button-ios-background-color-checked}; - --color: #{$segment-button-ios-text-color}; - --color-checked: #{$segment-button-ios-text-color-checked}; - --color-disabled: #{ion-color(primary, base, $segment-button-ios-opacity-disabled)}; - --color-checked-disabled: #{ion-color(primary, contrast, $segment-button-ios-opacity-disabled)}; - --border-color: #{$segment-button-ios-border-color}; - --indicator-color: transparent; + --background: #{$segment-ios-background-color}; + + @include border-radius($segment-ios-border-radius); + + overflow: hidden; + + z-index: 0; } :host(.segment-disabled) { - opacity: $segment-ios-opacity-disabled; + opacity: $segment-button-ios-opacity-disabled; } // Segment: Color // -------------------------------------------------- -:host(.ion-color)::slotted(ion-segment-button) { - --border-color: #{current-color(base)}; - - background: transparent; - color: #{current-color(base)}; +:host(.ion-color) { + background: #{current-color(base, 0.065)}; } -:host(.ion-color)::slotted(.activated) { - background: #{current-color(base, .16)}; - color: #{current-color(base)}; +:host(.ion-color) ::slotted(.segment-button-checked) { + color: #000; } -:host(.ion-color)::slotted(.segment-button-checked.activated), -:host(.ion-color)::slotted(.segment-button-checked) { - background: #{current-color(base)}; - color: #{current-color(contrast)}; -} - -:host(.ion-color)::slotted(.segment-button-disabled) { - color: #{current-color(base, $segment-ios-opacity-disabled)}; -} -:host(.ion-color)::slotted(.segment-button-checked.segment-button-disabled) { - color: #{current-color(contrast, $segment-ios-opacity-disabled)}; -} +// Segment: Activated +// -------------------------------------------------- -@media (any-hover: hover) { - :host(.ion-color)::slotted(ion-segment-button:hover:not(.segment-button-checked)) { - background: #{current-color(base, .1)}; - } +:host(.segment-activated) ::slotted(ion-segment-button) { + --indicator-transform: scale(0.95); } // Segment: Default Toolbar // -------------------------------------------------- -:host-context(ion-toolbar)::slotted(ion-segment-button) { - max-width: $segment-button-ios-toolbar-button-max-width; - - font-size: $segment-button-ios-toolbar-font-size; +:host(.in-toolbar) { + @include margin(0, auto); - line-height: $segment-button-ios-toolbar-line-height; + width: auto; } -:host-context(ion-toolbar:not(.ion-color)):not(.ion-color)::slotted(ion-segment-button) { - border-color: #{var(--ion-toolbar-color-checked, var(--border-color))}; - - color: #{var(--ion-toolbar-color-unchecked, var(--color))}; +// Default Segment, In a Toolbar +:host(.in-toolbar:not(.ion-color)) { + background: var(--ion-toolbar-segment-background, $segment-ios-background-color); + color: var(--ion-toolbar-segment-color, var(--color)); } -:host-context(ion-toolbar:not(.ion-color)):not(.ion-color)::slotted(.segment-button-checked) { - background: #{var(--ion-toolbar-color-checked, var(--background-checked))}; - - color: #{var(--ion-toolbar-background, var(--color-checked))}; +// Default Segment, In a Toolbar, Checked +:host(.in-toolbar:not(.ion-color)) ::slotted(.segment-button-checked) { + color: var(--ion-toolbar-segment-color-checked, var(--color-checked)); } + // Segment: Color Toolbar // -------------------------------------------------- -:host-context(ion-toolbar.ion-color):not(.ion-color)::slotted(ion-segment-button) { - --color: #{current-color(contrast)}; - --color-disabled: #{current-color(contrast, $segment-button-ios-opacity-disabled)}; - --color-checked: #{current-color(base)}; - --color-checked-disabled: #{current-color(contrast, $segment-button-ios-opacity-disabled)}; - --background-hover: #{current-color(contrast, $segment-button-ios-opacity-hover)}; - --background-activated: #{current-color(contrast, $segment-button-ios-opacity-activated)}; - --background-checked: #{current-color(contrast)}; - --border-color: #{current-color(contrast)}; +// Toolbar with Color, Default Segment +:host(.in-toolbar-color:not(.ion-color)) { + background: #{current-color(contrast, 0.11)}; + color: #{current-color(contrast)}; +} + +// Toolbar with Color, Default Segment, Checked +:host(.in-toolbar-color:not(.ion-color)) ::slotted(.segment-button-checked) { + color: #{current-color(base)}; +} + +@media (any-hover: hover) { + // Toolbar with Color, Default Segment, Checked / Hover + :host(.in-toolbar-color:not(.ion-color)) ::slotted(.segment-button-checked:hover) { + color: #{current-color(base)}; + } } \ No newline at end of file diff --git a/core/src/components/segment/segment.ios.vars.scss b/core/src/components/segment/segment.ios.vars.scss index ab57fc67d05..f7a9161644d 100644 --- a/core/src/components/segment/segment.ios.vars.scss +++ b/core/src/components/segment/segment.ios.vars.scss @@ -4,5 +4,11 @@ // iOS Segment // -------------------------------------------------- -/// @prop - Opacity of the disabled segment -$segment-ios-opacity-disabled: .3 !default; +/// @prop - Alpha of the segment for use in the backgrounds +$segment-ios-background-alpha: 0.065 !default; + +/// @prop - Background color of the segment +$segment-ios-background-color: rgba($text-color-rgb, $segment-ios-background-alpha) !default; + +/// @prop - Border radius of the segment +$segment-ios-border-radius: 8px !default; diff --git a/core/src/components/segment/segment.md.scss b/core/src/components/segment/segment.md.scss index da45da03ada..998bf9e0c89 100644 --- a/core/src/components/segment/segment.md.scss +++ b/core/src/components/segment/segment.md.scss @@ -5,14 +5,7 @@ // -------------------------------------------------- :host { - --background: #{$segment-button-md-background}; - --background-checked: #{$segment-button-md-background-checked}; - --background-hover: #{$segment-button-md-background-hover}; - --background-activated: #{$segment-button-md-background-activated}; - --color: #{$segment-button-md-text-color}; - --color-checked: #{$segment-button-md-text-color-checked}; - --color-checked-disabled: var(--color-checked); - --indicator-color: transparent; + --background: transparent; } :host(.segment-disabled) { @@ -22,25 +15,20 @@ // Segment: Color // -------------------------------------------------- -:host(.ion-color)::slotted(ion-segment-button) { - --background-activated: #{current-color(base, .16)}; +:host(.ion-color) ::slotted(ion-segment-button) { --ripple-color: #{current-color(base)}; + --indicator-color: #{current-color(base)}; background: transparent; color: $segment-button-md-text-color; } -:host(.ion-color)::slotted(.segment-button-checked) { - --indicator-color-checked: #{current-color(base)}; - color: #{current-color(base)}; -} - -:host(.ion-color)::slotted(.segment-button-checked.activated) { +:host(.ion-color) ::slotted(.segment-button-checked) { color: #{current-color(base)}; } @media (any-hover: hover) { - :host(.ion-color)::slotted(ion-segment-button:hover) { + :host(.ion-color) ::slotted(ion-segment-button:hover) { background: #{current-color(base, .04)}; } } @@ -48,24 +36,49 @@ // Segment: Default Toolbar // -------------------------------------------------- -:host-context(ion-toolbar:not(.ion-color)):not(.ion-color)::slotted(ion-segment-button) { - color: #{var(--ion-toolbar-color-unchecked, var(--color))}; -} +// Default Segment, In a Toolbar +:host(.in-toolbar:not(.ion-color)) ::slotted(ion-segment-button) { + --indicator-color: #{var(--ion-toolbar-segment-color-checked, var(--color-checked))}; -:host-context(ion-toolbar:not(.ion-color)):not(.ion-color)::slotted(.segment-button-checked) { - --indicator-color-checked: #{var(--ion-toolbar-color-checked, var(--color-checked))}; + background: #{var(--ion-toolbar-segment-background, var(--background))}; + color: #{var(--ion-toolbar-segment-color, var(--color))}; +} - color: #{var(--ion-toolbar-color-checked, var(--color-checked))}; +// Default Segment, In a Toolbar, Checked +:host(.in-toolbar:not(.ion-color)) ::slotted(.segment-button-checked) { + background: #{var(--ion-toolbar-segment-background-checked, var(--background-checked))}; + color: #{var(--ion-toolbar-segment-color-checked, var(--color-checked))}; } // Segment: Toolbar Color // -------------------------------------------------- -:host-context(ion-toolbar.ion-color):not(.ion-color)::slotted(ion-segment-button) { - --background-hover: #{current-color(contrast, .04)}; - --background-activated: #{current-color(base)}; - --color: #{current-color(contrast, .6)}; - --color-checked: #{current-color(contrast)}; - --indicator-color-checked: #{current-color(contrast)}; +// Default Segment, In a Toolbar with Color +:host(.in-toolbar-color:not(.ion-color)) ::slotted(ion-segment-button) { + color: #{current-color(contrast, .6)}; +} + +// Default Segment, In a Toolbar with Color, Checked +:host(.in-toolbar-color:not(.ion-color)) ::slotted(.segment-button-checked) { + color: #{current-color(contrast)}; +} + + +// Segment: Toolbar Hover +// -------------------------------------------------- + +@media (any-hover: hover) { + // Default Segment, In a Toolbar with Color, Hover + :host(.in-toolbar-color:not(.ion-color)) ::slotted(ion-segment-button:hover) { + background: #{ion-color(primary, contrast, .04)}; + } } + + +// Segment: Scrollable +// -------------------------------------------------- + +:host(.segment-scrollable) ::slotted(ion-segment-button) { + min-width: $segment-button-md-min-width; +} \ No newline at end of file diff --git a/core/src/components/segment/segment.scss b/core/src/components/segment/segment.scss index 84e8a2e8fe9..a72886b4044 100644 --- a/core/src/components/segment/segment.scss +++ b/core/src/components/segment/segment.scss @@ -4,22 +4,29 @@ // -------------------------------------------------- :host { - --indicator-color-checked: initial; + /** + * @prop --background: Background of the segment button + */ --ripple-color: currentColor; - --color-activated: initial; @include font-smoothing(); display: flex; + position: relative; + align-items: stretch; justify-content: center; width: 100%; + background: var(--background); + font-family: $font-family-base; text-align: center; + + contain: paint; } @@ -46,3 +53,19 @@ :host(.segment-scrollable::-webkit-scrollbar) { display: none; } + +// Segment Button: Hover +// -------------------------------------------------- + +@media (any-hover: hover) { + // Default Segment, In a Default Toolbar, Hover + :host(.in-toolbar:not(.ion-color)) ::slotted(ion-segment-button:hover) { + background: var(--ion-toolbar-segment-background-hover, var(--background-hover)); + color: var(--ion-toolbar-segment-color-hover, var(--color-hover, var(--ion-toolbar-segment-color, var(--color)))); + } + + // Default Segment, In a Default Toolbar, Checked / Hover + :host(.in-toolbar:not(.ion-color)) ::slotted(.segment-button-checked:hover) { + color: var(--ion-toolbar-segment-color-hover, var(--color-hover, var(--ion-toolbar-segment-color-checked, var(--color-checked)))); + } +} \ No newline at end of file diff --git a/core/src/components/segment/segment.tsx b/core/src/components/segment/segment.tsx index b73f574b068..a0272d238fb 100644 --- a/core/src/components/segment/segment.tsx +++ b/core/src/components/segment/segment.tsx @@ -1,8 +1,10 @@ -import { Component, ComponentInterface, Element, Event, EventEmitter, Host, Listen, Prop, Watch, h } from '@stencil/core'; +import { Component, ComponentInterface, Element, Event, EventEmitter, Host, Listen, Prop, State, Watch, h, writeTask } from '@stencil/core'; import { getIonMode } from '../../global/ionic-global'; import { Color, SegmentChangeEventDetail, StyleEventDetail } from '../../interface'; -import { createColorClasses } from '../../utils/theme'; +import { Gesture, GestureDetail } from '../../utils/gesture'; +import { pointerCoord } from '../../utils/helpers'; +import { createColorClasses, hostContext } from '../../utils/theme'; /** * @virtualProp {"ios" | "md"} mode - The mode determines which platform styles to use. @@ -13,13 +15,16 @@ import { createColorClasses } from '../../utils/theme'; ios: 'segment.ios.scss', md: 'segment.md.scss' }, - scoped: true + shadow: true }) export class Segment implements ComponentInterface { - + private gesture?: Gesture; private didInit = false; + private checked?: HTMLIonSegmentButtonElement; + + @Element() el!: HTMLIonSegmentElement; - @Element() el!: HTMLElement; + @State() activated = false; /** * The color to use from your application's color palette. @@ -35,6 +40,8 @@ export class Segment implements ComponentInterface { /** * If `true`, the segment buttons will overflow and the user can swipe to see them. + * In addition, this will disable the gesture to drag the indicator between the buttons + * in order to swipe to see hidden buttons. */ @Prop() scrollable = false; @@ -43,14 +50,6 @@ export class Segment implements ComponentInterface { */ @Prop({ mutable: true }) value?: string | null; - @Watch('value') - protected valueChanged(value: string | undefined) { - if (this.didInit) { - this.updateButtons(); - this.ionChange.emit({ value }); - } - } - /** * Emitted when the value property has changed. */ @@ -62,10 +61,32 @@ export class Segment implements ComponentInterface { */ @Event() ionStyle!: EventEmitter; + @Watch('value') + protected valueChanged(value: string | undefined) { + if (this.didInit) { + this.updateButtons(); + this.ionChange.emit({ value }); + } + } + + @Watch('disabled') + disabledChanged() { + if (this.gesture && !this.scrollable) { + this.gesture.enable(!this.disabled); + } + } + @Listen('ionSelect') segmentClick(ev: CustomEvent) { - const selectedButton = ev.target as HTMLIonSegmentButtonElement; - this.value = selectedButton.value; + const current = ev.target as HTMLIonSegmentButtonElement; + const previous = this.checked; + this.value = current.value; + + if (previous && this.scrollable) { + this.checkButton(previous, current); + } + + this.checked = current; } connectedCallback() { @@ -78,11 +99,219 @@ export class Segment implements ComponentInterface { this.emitStyle(); } - componentDidLoad() { + componentWillLoad() { + this.emitStyle(); + } + + async componentDidLoad() { this.updateButtons(); + this.setCheckedClasses(); + + this.gesture = (await import('../../utils/gesture')).createGesture({ + el: this.el, + gestureName: 'segment', + gesturePriority: 100, + threshold: 0, + passive: false, + onStart: ev => this.onStart(ev), + onMove: ev => this.onMove(ev), + onEnd: ev => this.onEnd(ev), + }); + this.gesture.enable(!this.scrollable); + this.disabledChanged(); + this.didInit = true; } + onStart(detail: GestureDetail) { + this.activate(detail); + } + + onMove(detail: GestureDetail) { + this.setNextIndex(detail); + } + + onEnd(detail: GestureDetail) { + this.activated = false; + + this.setNextIndex(detail, true); + + detail.event.preventDefault(); + detail.event.stopImmediatePropagation(); + + this.addRipple(detail); + } + + /** + * The gesture blocks the segment button ripple. This + * function adds the ripple based on the checked segment + * and where the cursor ended. + */ + private addRipple(detail: GestureDetail) { + const buttons = this.getButtons(); + const checked = buttons.find(button => button.checked === true); + + const ripple = checked!.shadowRoot!.querySelector('ion-ripple-effect'); + + if (!ripple) { return; } + + const { x, y } = pointerCoord(detail.event); + + ripple.addRipple(x, y).then(remove => remove()); + } + + private activate(detail: GestureDetail) { + const clicked = detail.event.target as HTMLIonSegmentButtonElement; + const buttons = this.getButtons(); + const checked = buttons.find(button => button.checked === true); + + // Make sure we are only checking for activation on a segment button + // since disabled buttons will get the click on the segment + if (clicked.tagName !== 'ION-SEGMENT-BUTTON') { + return; + } + + // If there are no checked buttons, set the current button to checked + if (!checked) { + clicked.checked = true; + } + + // If the gesture began on the clicked button with the indicator + // then we should activate the indicator + if (clicked.checked) { + this.activated = true; + } + } + + private getIndicator(button: HTMLIonSegmentButtonElement): HTMLDivElement | null { + return button.shadowRoot && button.shadowRoot.querySelector('.segment-button-indicator'); + } + + private checkButton(previous: HTMLIonSegmentButtonElement, current: HTMLIonSegmentButtonElement) { + const previousIndicator = this.getIndicator(previous); + const currentIndicator = this.getIndicator(current); + + if (previousIndicator === null || currentIndicator === null) { + return; + } + + const previousClientRect = previousIndicator.getBoundingClientRect(); + const currentClientRect = currentIndicator.getBoundingClientRect(); + + const widthDelta = previousClientRect.width / currentClientRect.width; + const xPosition = previousClientRect.left - currentClientRect.left; + + // Scale the indicator width to match the previous indicator width + // and translate it on top of the previous indicator + const transform = `translate3d(${xPosition}px, 0, 0) scaleX(${widthDelta})`; + + writeTask(() => { + // Remove the transition before positioning on top of the previous indicator + currentIndicator.classList.remove('segment-button-indicator-animated'); + currentIndicator.style.setProperty('transform', transform); + + // Force a repaint to ensure the transform happens + currentIndicator.getBoundingClientRect(); + + // Add the transition to move the indicator into place + currentIndicator.classList.add('segment-button-indicator-animated'); + + // Remove the transform to slide the indicator back to the button clicked + currentIndicator.style.setProperty('transform', ''); + }); + + current.checked = true; + this.setCheckedClasses(); + } + + private setCheckedClasses() { + const buttons = this.getButtons(); + const index = buttons.findIndex(button => button.checked === true); + const next = index + 1; + + // Keep track of the currently checked button + this.checked = buttons.find(button => button.checked === true); + + for (const button of buttons) { + button.classList.remove('segment-button-after-checked'); + } + if (next < buttons.length) { + buttons[next].classList.add('segment-button-after-checked'); + } + } + + private setNextIndex(detail: GestureDetail, isEnd = false) { + const isRTL = document.dir === 'rtl'; + const activated = this.activated; + const buttons = this.getButtons(); + const index = buttons.findIndex(button => button.checked === true); + const previous = buttons[index]; + let current; + let nextIndex; + + if (index === -1) { + return; + } + + // Get the element that the touch event started on in case + // it was the checked button, then we will move the indicator + const rect = previous.getBoundingClientRect() as DOMRect; + const left = rect.left; + const width = rect.width; + + // Get the element that the gesture is on top of based on the currentX of the + // gesture event and the Y coordinate of the starting element, since the gesture + // can move up and down off of the segment + const currentX = detail.currentX; + const previousY = rect.y; + const nextEl = document.elementFromPoint(currentX, previousY) as HTMLIonSegmentButtonElement; + + const decreaseIndex = isRTL ? currentX > (left + width) : currentX < left; + const increaseIndex = isRTL ? currentX < left : currentX > (left + width); + + // If the indicator is currently activated then we have started the gesture + // on top of the checked button so we need to slide the indicator + // by checking the button next to it as we move + if (activated && !isEnd) { + // Decrease index, move left in LTR & right in RTL + if (decreaseIndex) { + const newIndex = index - 1; + + if (newIndex >= 0) { + nextIndex = newIndex; + } + // Increase index, moves right in LTR & left in RTL + } else if (increaseIndex) { + if (activated && !isEnd) { + + const newIndex = index + 1; + + if (newIndex < buttons.length) { + nextIndex = newIndex; + } + } + } + + if (nextIndex !== undefined && !buttons[nextIndex].disabled) { + current = buttons[nextIndex]; + } + } + + // If the indicator is not activated then we will just set the indicator + // to the element where the gesture ended + if (!activated && isEnd) { + current = nextEl; + } + + if (!current) { + return; + } + + if (previous !== current) { + this.checkButton(previous, current); + } + } + private emitStyle() { this.ionStyle.emit({ 'segment': true @@ -102,15 +331,20 @@ export class Segment implements ComponentInterface { render() { const mode = getIonMode(this); + return ( + ); } diff --git a/core/src/components/segment/test/basic/index.html b/core/src/components/segment/test/basic/index.html index e7a6ac70f9b..5738394f39c 100644 --- a/core/src/components/segment/test/basic/index.html +++ b/core/src/components/segment/test/basic/index.html @@ -4,12 +4,14 @@ Segment - Basic - + - + + @@ -22,7 +24,7 @@ - Paid + PaidPaidPaid Free @@ -62,23 +64,12 @@ - - - Sunny - - - Rainy - - - - - - - - Sunny + + + All - - Rainy + + Missed @@ -87,26 +78,38 @@
- Seg 1 - Seg 2 - Seg 3 + + Seg 1 + + + Seg 2 + + + Seg 3 + - - Seg 2 1 - Seg 2 2 - Seg 2 3 + + + Animate + + + Is + + + False + - + - + - + @@ -117,13 +120,13 @@ 440ml - + 500ml - + @@ -149,7 +152,7 @@ - + Active @@ -162,7 +165,7 @@ - +
@@ -204,7 +207,6 @@ async function listenForEvent() { const ionSegmentElement = document.querySelector('ion-segment.event-tester'); - await ionSegmentElement.componentOnReady(); ionSegmentElement.addEventListener('ionChange', (event) => { console.log('event.target: ', event.target.value); }); @@ -218,13 +220,15 @@ }, 4000); function updateSegmentButtons(length) { - dynamicButtons.innerHTML = ''; + const buttonsLength = dynamicButtons.children.length; + const begin = buttonsLength === 0 ? 0 : buttonsLength; + + for (var i = begin; i < length; i++) { + const button = document.createElement('ion-segment-button'); + button.value = `segment-${i}`; + button.innerHTML = `Btn ${i}`; - for (var i = 0; i < length; i++) { - dynamicButtons.innerHTML += ` - - Btn ${i} - `; + dynamicButtons.appendChild(button); } } @@ -236,6 +240,10 @@ margin-bottom: 10px; } + .segment-no-animate ion-segment-button { + --indicator-transition: none; + --indicator-transform: none; + } diff --git a/core/src/components/segment/test/colors/e2e.ts b/core/src/components/segment/test/colors/e2e.ts new file mode 100644 index 00000000000..c026c7afb6a --- /dev/null +++ b/core/src/components/segment/test/colors/e2e.ts @@ -0,0 +1,23 @@ +import { newE2EPage } from '@stencil/core/testing'; + +test('segment: basic', async () => { + const page = await newE2EPage({ + url: '/src/components/segment/test/basic?ionic:_testing=true' + }); + + await page.waitFor(250); + + const compare = await page.compareScreenshot(); + expect(compare).toMatchScreenshot(); +}); + +test('segment:rtl: basic', async () => { + const page = await newE2EPage({ + url: '/src/components/segment/test/basic?ionic:_testing=true&rtl=true' + }); + + await page.waitFor(250); + + const compare = await page.compareScreenshot(); + expect(compare).toMatchScreenshot(); +}); diff --git a/core/src/components/segment/test/colors/index.html b/core/src/components/segment/test/colors/index.html new file mode 100644 index 00000000000..ed1aa9e40a3 --- /dev/null +++ b/core/src/components/segment/test/colors/index.html @@ -0,0 +1,178 @@ + + + + + + Segment - Colors + + + + + + + + + + + + + + Segment - Colors + + + + + + + Paid + + + Free + + + Top + + + + + + Bookmarks + + + Reading List + + + Shared Links + + + + + + Active + + + Disabled + + + Inactive + + + + + + All + + + Missed + + + + + + 330ml + + + 440ml + + + 500ml + + + + + + + + + + + + + + + + + + + + + Bookmarks + + + Reading List + + + Shared Links + + + + + + + Sunny + + + Rainy + + + + + + Seg 1 + + + Seg 2 + + + Seg 3 + + + + + + Seg 2 1 + + + Seg 2 2 + + + Seg 2 3 + + + + + + Seg 2 1 + + + Seg 2 2 + + + Seg 2 3 + + + + + + Seg 2 1 + + + Seg 2 2 + + + Seg 2 3 + + + + + + + + + diff --git a/core/src/components/segment/test/custom/index.html b/core/src/components/segment/test/custom/index.html index 0a431515e99..1f9d5516cbe 100644 --- a/core/src/components/segment/test/custom/index.html +++ b/core/src/components/segment/test/custom/index.html @@ -28,17 +28,31 @@ + + + + Paid + + + Free + + + Top + + + + - + Facebook - + Instagram - + Slack @@ -61,11 +75,39 @@ + + + + + Inactive + + + Inactive + + + Disabled + + + + + + + + Checked + + + Inactive + + + Disabled + + + - + - + Active @@ -77,16 +119,16 @@ - - + + Facebook - + Instagram - + Slack @@ -107,34 +149,30 @@ -

Activated

- - - - Paid + + + Checked - - Free + + Inactive - - Top + + Disabled - - - Facebook - + + + Checked - - Instagram - + + Inactive - - Slack - + + Disabled +
@@ -153,99 +191,124 @@

Activated

margin-bottom: 0; } - pre { - border: 1px solid #e6e9ee; - background: white; - margin: 10px; - padding: 4px; - line-height: 24px; + /* + * Custom Checked Segment (Paid, Free, Top) + * + * This tests that the colors are able to be overridden on + * a segment inside of a toolbar + * + * Indicator color / ripple color can be set on ion-segment + * + * Backgrounds / colors for the button must be set on the + * ion-segment-button + */ + .custom-checked { + --ripple-color: purple; + --indicator-color: purple; + } + + .ios .custom-checked ion-segment-button { + --color-checked: white; } - code { - display: block; - padding: 0.5em; - background: #ffffff; - word-wrap: normal; - white-space: pre; - color: #314361; + .md .custom-checked ion-segment-button { + --background-hover: rgba(17, 228, 10, 0.5); + --color-checked: purple; + --color-hover: blue; } + /* + * Custom Themed Segment (Themed to Primary) + */ .themed { --ion-toolbar-background: #3880ff; --ion-toolbar-color: #fff; - --ion-toolbar-color-activated: #fff; - --ion-toolbar-color-unchecked: rgba(255, 255, 255, .6); - --ion-toolbar-color-checked: #fff; + + /* Segment */ + --ion-toolbar-segment-indicator-color: #ffffff; } - .custom { + /* Material Design Segment */ + .md .themed { + --ion-toolbar-segment-color: rgba(255, 255, 255, .6); + --ion-toolbar-segment-color-checked: #ffffff; + --ion-toolbar-segment-background-hover: rgba(255, 255, 255, 0.04); + } + + /* iOS Segment */ + .ios .themed { + --ion-toolbar-segment-background: rgba(255, 255, 255, 0.065); + --ion-toolbar-segment-color: #ffffff; + --ion-toolbar-segment-color-checked: #3880ff; + } + + /* + * Custom Active Segment (Active, Inactive, Disabled) + */ + .custom-active { --background: papayawhip; - --background-checked: navy; - --border-color: navy; - --border-color-checked: navy; - --border-color-disabled: navy; + --color: purple; + --color-disabled: rgba(0, 0, 0, 0.3); + } - --color: navy; - --color-activated: purple; + .ios .custom-active { --color-checked: papayawhip; - --color-disabled: rgba(0, 0, 0, 0.3); + --indicator-color: navy; } - /* Custom Icon Segment MD */ - .md .custom-icon { - --indicator-color: lightgray; + .md .custom-active { + --color-checked: navy; + --indicator-color: navy; } - .md .segment-facebook { - --background-hover: rgba(59, 89, 153, .04); - --color-activated: #3b5999; - --color-checked: #3b5999; + /* + * Custom Icon Segment (Facebook, Instagram, Slack) + */ + .custom-icon ion-icon { + font-size: 44px; } - .md .segment-instagram { - --background-hover: rgba(228, 64, 95, .04); - --color-activated: #e4405f; - --color-checked: #e4405f; + /* + * MD Custom Icon Segment (Facebook, Instagram, Slack) + */ + .md .segment-button-facebook { + --background-hover: rgb(58, 61, 70, .04); + --color-checked: #3a3d46; + --indicator-color: #3a3d46; } - .md .segment-slack { - --background-hover: rgba(58, 175, 133, .04); - --color-activated: #3aaf85; - --color-checked: #3aaf85; + .md .segment-button-instagram { + --background-hover: rgb(228, 64, 95, .04); + --color-checked: #e4405f; + --indicator-color: #e4405f; } - /* Custom Icon Segment iOS */ - .ios .custom-icon ion-segment-button { - --border-width: 0; + .md .segment-button-slack { + --background-hover: rgb(58, 175, 133, .04); + --color-checked: #3aaf85; + --indicator-color: #3aaf85; } - .ios .segment-facebook { - --color: #3b5999; + /* + * iOS Custom Icon Segment (Facebook, Instagram, Slack) + */ + .ios .segment-button-facebook { + --color: #3a3d46; --color-checked: #ffffff; - --background-hover: rgba(59, 89, 153, .1); - --background-activated: rgba(59, 89, 153, .16); - --background-checked: #3b5999; + --indicator-color: #3a3d46; } - .ios .segment-instagram { + .ios .segment-button-instagram { --color: #e4405f; --color-checked: #ffffff; - --background-hover: rgba(228, 64, 95, .1); - --background-activated: rgba(228, 64, 95, .16); - --background-checked: #e4405f; + --indicator-color: #e4405f; } - .ios .segment-slack { + .ios .segment-button-slack { --color: #3aaf85; --color-checked: #ffffff; - --background-hover: rgba(58, 175, 133, .1); - --background-activated: rgba(58, 175, 133, .16); - --background-checked: #3aaf85; - } - - .custom-icon ion-icon { - font-size: 44px; + --indicator-color: #3aaf85; } /* This CSS should not apply */ @@ -254,6 +317,27 @@

Activated

--color: blue; } + .custom-states-color { + --color: red; + --color-disabled: blue; + --color-checked: indigo; + --color-hover: orange; + } + + .custom-states-background ion-segment-button { + --indicator-color: transparent; + --indicator-box-shadow: none; + --color: white; + + --background: red; + --background-disabled: blue; + --background-checked: indigo; + --background-hover: orange; + } + + .custom-states-background ion-segment-button:hover { + opacity: 1; + } diff --git a/core/src/components/segment/test/spec/index.html b/core/src/components/segment/test/spec/index.html index da0825d0f95..8299f183c38 100644 --- a/core/src/components/segment/test/spec/index.html +++ b/core/src/components/segment/test/spec/index.html @@ -11,7 +11,7 @@ - + @@ -38,7 +38,7 @@ - + Item One @@ -96,7 +96,7 @@ - + Item One @@ -112,7 +112,7 @@ - + Item One @@ -144,7 +144,7 @@ - + Item One @@ -188,14 +188,30 @@ diff --git a/core/src/components/segment/test/standalone/index.html b/core/src/components/segment/test/standalone/index.html index 4c14428fb61..71627193036 100644 --- a/core/src/components/segment/test/standalone/index.html +++ b/core/src/components/segment/test/standalone/index.html @@ -4,14 +4,16 @@ Segment - Standalone - + - + + - + @@ -77,15 +79,15 @@ - + Facebook - + Instagram - + Slack @@ -145,6 +147,29 @@ results.innerHTML = value.charAt(0).toUpperCase() + value.slice(1); segment.value = value; } + + async function onLoad() { + const customIconSegments = document.querySelectorAll('.custom-icon'); + + for (var i = 0; i < customIconSegments.length; i++) { + const customIconSegment = customIconSegments[i]; + + await customIconSegment.componentOnReady(); + addIconClass(customIconSegment, customIconSegment.value); + + customIconSegment.addEventListener('ionChange', function (ev) { + addIconClass(customIconSegment, ev.detail.value); + }); + } + } + + function addIconClass(el, value) { + console.log('adding class to', el, value); + if (value) { + el.classList.remove('segment-facebook-checked', 'segment-instagram-checked', 'segment-slack-checked'); + el.classList.add(`segment-${value}-checked`); + } + } diff --git a/core/src/components/segment/test/toolbar/e2e.ts b/core/src/components/segment/test/toolbar/e2e.ts new file mode 100644 index 00000000000..3f1af2739fe --- /dev/null +++ b/core/src/components/segment/test/toolbar/e2e.ts @@ -0,0 +1,23 @@ +import { newE2EPage } from '@stencil/core/testing'; + +test('segment: toolbar', async () => { + const page = await newE2EPage({ + url: '/src/components/segment/test/toolbar?ionic:_testing=true' + }); + + await page.waitFor(250); + + const compare = await page.compareScreenshot(); + expect(compare).toMatchScreenshot(); +}); + +test('segment:rtl: toolbar', async () => { + const page = await newE2EPage({ + url: '/src/components/segment/test/toolbar?ionic:_testing=true&rtl=true' + }); + + await page.waitFor(250); + + const compare = await page.compareScreenshot(); + expect(compare).toMatchScreenshot(); +}); diff --git a/core/src/components/segment/test/toolbar/index.html b/core/src/components/segment/test/toolbar/index.html index 86bbcee1091..d970c654042 100644 --- a/core/src/components/segment/test/toolbar/index.html +++ b/core/src/components/segment/test/toolbar/index.html @@ -16,7 +16,7 @@ - Segment - Colors + Segment - Toolbar @@ -114,6 +114,17 @@ + + + + Sunny + + + Rainy + + + + @@ -264,13 +275,17 @@ } .ios .themed { - --ion-toolbar-color-unchecked: #fff; - --ion-toolbar-color-checked: #3880ff; + --ion-toolbar-segment-background: rgba(255, 255, 255, 0.11); + --ion-toolbar-segment-color: #fff; + + --ion-toolbar-segment-color-checked: #3880ff; } .md .themed { - --ion-toolbar-color-unchecked: rgba(255, 255, 255, 0.6); - --ion-toolbar-color-checked: #fff; + --ion-toolbar-segment-color: rgba(255, 255, 255, 0.6); + --ion-toolbar-segment-color-checked: #fff; + + --ion-toolbar-segment-indicator-color: #fff; } diff --git a/core/src/components/toolbar/toolbar.ios.scss b/core/src/components/toolbar/toolbar.ios.scss index dcc675a73e5..acf6b08818a 100644 --- a/core/src/components/toolbar/toolbar.ios.scss +++ b/core/src/components/toolbar/toolbar.ios.scss @@ -29,11 +29,10 @@ // Toolbar: Segment // -------------------------------------------------- -:host(.toolbar-segment) { - --min-height: auto; +:host(.toolbar-segment) .toolbar-content { + display: inline-flex; } - // Toolbar: Searchbar // -------------------------------------------------- diff --git a/core/src/themes/test/css-variables/css/dark.css b/core/src/themes/test/css-variables/css/dark.css index 45cd19e9911..1e09d57c39b 100644 --- a/core/src/themes/test/css-variables/css/dark.css +++ b/core/src/themes/test/css-variables/css/dark.css @@ -1,60 +1,54 @@ :root { - /* Custom Toolbar CSS (optional) */ - /* --ion-toolbar-background: #000; - --ion-toolbar-border-color: #333; - --ion-toolbar-color: limegreen; - --ion-toolbar-color-activated: rgb(35, 156, 35); */ - - --ion-color-primary: #428CFF; + --ion-color-primary: #428cff; --ion-color-primary-rgb: 66,140,255; --ion-color-primary-contrast: #ffffff; --ion-color-primary-contrast-rgb: 255,255,255; --ion-color-primary-shade: #3a7be0; --ion-color-primary-tint: #5598ff; - --ion-color-secondary: #50C8FF; + --ion-color-secondary: #50c8ff; --ion-color-secondary-rgb: 80,200,255; --ion-color-secondary-contrast: #ffffff; --ion-color-secondary-contrast-rgb: 255,255,255; --ion-color-secondary-shade: #46b0e0; --ion-color-secondary-tint: #62ceff; - --ion-color-tertiary: #6A64FF; + --ion-color-tertiary: #6a64ff; --ion-color-tertiary-rgb: 106,100,255; --ion-color-tertiary-contrast: #ffffff; --ion-color-tertiary-contrast-rgb: 255,255,255; --ion-color-tertiary-shade: #5d58e0; --ion-color-tertiary-tint: #7974ff; - --ion-color-success: #2FDF75; + --ion-color-success: #2fdf75; --ion-color-success-rgb: 47,223,117; --ion-color-success-contrast: #000000; --ion-color-success-contrast-rgb: 0,0,0; --ion-color-success-shade: #29c467; --ion-color-success-tint: #44e283; - --ion-color-warning: #FFD534; + --ion-color-warning: #ffd534; --ion-color-warning-rgb: 255,213,52; --ion-color-warning-contrast: #000000; --ion-color-warning-contrast-rgb: 0,0,0; --ion-color-warning-shade: #e0bb2e; --ion-color-warning-tint: #ffd948; - --ion-color-danger: #FF4961; + --ion-color-danger: #ff4961; --ion-color-danger-rgb: 255,73,97; --ion-color-danger-contrast: #ffffff; --ion-color-danger-contrast-rgb: 255,255,255; --ion-color-danger-shade: #e04055; --ion-color-danger-tint: #ff5b71; - --ion-color-dark: #F4F5F8; + --ion-color-dark: #f4f5f8; --ion-color-dark-rgb: 244,245,248; --ion-color-dark-contrast: #000000; --ion-color-dark-contrast-rgb: 0,0,0; --ion-color-dark-shade: #d7d8da; --ion-color-dark-tint: #f5f6f9; - --ion-color-medium: #989AA2; + --ion-color-medium: #989aa2; --ion-color-medium-rgb: 152,154,162; --ion-color-medium-contrast: #000000; --ion-color-medium-contrast-rgb: 0,0,0; @@ -69,7 +63,6 @@ --ion-color-light-tint: #383a3e; } -/* Customize the Toolbar Segment by Mode */ .ios { --ion-background-color: #000000; --ion-background-color-rgb: 0,0,0; @@ -100,8 +93,7 @@ --ion-item-background: #1c1c1c; --ion-item-background-activated: #313131; - /* --ion-toolbar-color-unchecked: limegreen; - --ion-toolbar-color-checked: limegreen; */ + --ion-toolbar-background: #000; } .md { @@ -133,7 +125,5 @@ --ion-color-step-900: #e7e7e7; --ion-color-step-950: #f3f3f3; - --ion-item-background: #1A1B1E; - /* --ion-toolbar-color-unchecked: rgba(255, 255, 255, .6); - --ion-toolbar-color-checked: #fff; */ + --ion-item-background: #1a1b1e; } \ No newline at end of file diff --git a/core/src/themes/test/css-variables/css/vibrant.css b/core/src/themes/test/css-variables/css/vibrant.css index 4ce5c753918..21a4e19255d 100644 --- a/core/src/themes/test/css-variables/css/vibrant.css +++ b/core/src/themes/test/css-variables/css/vibrant.css @@ -78,4 +78,20 @@ --ion-item-background: #fff; --ion-item-border-color: #5bff76; --ion-item-background-activated: #5bff76; + + --ion-toolbar-color: #fff; +} + +.ios { + --ion-toolbar-segment-background: var(--ion-color-danger); + --ion-toolbar-segment-color: var(--ion-color-danger-contrast); + + --ion-toolbar-segment-indicator-color: var(--ion-color-danger-contrast); + --ion-toolbar-segment-color-checked: var(--ion-color-danger); +} + +.md { + --ion-toolbar-segment-color: #fff; + --ion-toolbar-segment-color-checked: var(--ion-color-danger); + --ion-toolbar-segment-background-hover: rgba(var(--ion-color-danger-contrast-rgb), .04); } \ No newline at end of file