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
This commit is contained in:
Brandy Carney
2020-01-14 11:51:28 -05:00
committed by Liam DeBeasi
parent 8e11f79fcc
commit dc66ce48e1
27 changed files with 1278 additions and 481 deletions

View File

@ -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

View File

@ -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
// --------------------------------------------------
@media (any-hover: hover) {
:host(:hover:not(.segment-button-checked)) {
background: var(--background-hover);
}
.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: Activated
// Segment Button: Checked Borders
// --------------------------------------------------
:host(.activated) {
background: var(--background-activated);
:host(.segment-button-checked)::before,
:host(.segment-button-after-checked)::before {
opacity: 0;
}
// Segment: Checked & Activated
// Segment Button: Checked
// --------------------------------------------------
:host(.segment-button-checked.activated) {
background: var(--background-checked);
color: var(--color-checked);
:host(.segment-button-checked) {
z-index: -1;
}
// Segment Button: Toolbar
// --------------------------------------------------
// 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;
}

View File

@ -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 - 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 on hover
$segment-button-ios-opacity-hover: .1 !default;
/// @prop - Opacity of the segment button when pressed
$segment-button-ios-opacity-activated: .16 !default;
$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;
$segment-button-ios-background-color-hover: transparent !default;
/// @prop - Background of the activated segment button
$segment-button-ios-background-color-activated: ion-color(primary, base, $segment-button-ios-opacity-activated) !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;

View File

@ -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
// --------------------------------------------------
@media (any-hover: hover) {
:host(:hover) {
background: var(--background-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))};
}
// 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)};
}

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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<void>;
@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 {
<slot></slot>
{mode === 'md' && <ion-ripple-effect></ion-ripple-effect>}
</button>
<div class="segment-button-indicator"></div>
<div
part="indicator"
class={{
'segment-button-indicator': true,
'segment-button-indicator-animated': true
}}
>
<div part="indicator-background" class="segment-button-indicator-background"></div>
</div>
</Host>
);
}