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

@ -986,13 +986,14 @@ ion-searchbar,css-prop,--placeholder-font-style
ion-searchbar,css-prop,--placeholder-font-weight ion-searchbar,css-prop,--placeholder-font-weight
ion-searchbar,css-prop,--placeholder-opacity ion-searchbar,css-prop,--placeholder-opacity
ion-segment,scoped ion-segment,shadow
ion-segment,prop,color,string | undefined,undefined,false,false ion-segment,prop,color,string | undefined,undefined,false,false
ion-segment,prop,disabled,boolean,false,false,false ion-segment,prop,disabled,boolean,false,false,false
ion-segment,prop,mode,"ios" | "md",undefined,false,false ion-segment,prop,mode,"ios" | "md",undefined,false,false
ion-segment,prop,scrollable,boolean,false,false,false ion-segment,prop,scrollable,boolean,false,false,false
ion-segment,prop,value,null | string | undefined,undefined,false,false ion-segment,prop,value,null | string | undefined,undefined,false,false
ion-segment,event,ionChange,SegmentChangeEventDetail,true ion-segment,event,ionChange,SegmentChangeEventDetail,true
ion-segment,css-prop,--background
ion-segment-button,shadow ion-segment-button,shadow
ion-segment-button,prop,checked,boolean,false,false,false 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,prop,value,string,'ion-sb-' + (ids++),false,false
ion-segment-button,event,ionSelect,void,true ion-segment-button,event,ionSelect,void,true
ion-segment-button,css-prop,--background 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-checked
ion-segment-button,css-prop,--background-disabled
ion-segment-button,css-prop,--background-hover ion-segment-button,css-prop,--background-hover
ion-segment-button,css-prop,--border-color ion-segment-button,css-prop,--border-color
ion-segment-button,css-prop,--border-radius ion-segment-button,css-prop,--border-radius
ion-segment-button,css-prop,--border-style ion-segment-button,css-prop,--border-style
ion-segment-button,css-prop,--border-width ion-segment-button,css-prop,--border-width
ion-segment-button,css-prop,--color 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
ion-segment-button,css-prop,--color-checked-disabled
ion-segment-button,css-prop,--color-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
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-bottom
ion-segment-button,css-prop,--margin-end ion-segment-button,css-prop,--margin-end
ion-segment-button,css-prop,--margin-start ion-segment-button,css-prop,--margin-start

View File

@ -2231,7 +2231,7 @@ export namespace Components {
*/ */
'mode'?: "ios" | "md"; '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; 'scrollable': boolean;
/** /**
@ -5492,7 +5492,7 @@ declare namespace LocalJSX {
*/ */
'onIonChange'?: (event: CustomEvent<SegmentChangeEventDetail>) => void; 'onIonChange'?: (event: CustomEvent<SegmentChangeEventDetail>) => 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; 'scrollable'?: boolean;
/** /**

View File

@ -125,6 +125,7 @@ ion-icon {
// Menu Button in Toolbar: Global Theming // 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)) { :host-context(ion-toolbar:not(.ion-color)) {
color: #{var(--ion-toolbar-color, var(--color))}; color: #{var(--ion-toolbar-color, var(--color))};
} }

View File

@ -639,32 +639,33 @@ export const SegmentButtonExample: React.FC = () => (
## CSS Custom Properties ## CSS Custom Properties
| Name | Description | | Name | Description |
| --------------------------- | ----------------------------------------------------------------------------------------------------------------- | | ------------------------ | ----------------------------------------------------------------------------------------------------------------- |
| `--background` | Background of the segment button | | `--background` | Background of the segment button |
| `--background-activated` | Background of the segment button when pressed | | `--background-checked` | Background of the checked 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 | | `--background-hover` | Background of the segment button on hover |
| `--border-color` | Color of the segment button border | | `--border-color` | Color of the segment button border |
| `--border-radius` | Radius of the segment button border | | `--border-radius` | Radius of the segment button border |
| `--border-style` | Style of the segment button border | | `--border-style` | Style of the segment button border |
| `--border-width` | Width of the segment button border | | `--border-width` | Width of the segment button border |
| `--color` | Color of the segment button | | `--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` | Color of the checked segment button | | `--color-disabled` | Color of the disabled segment button |
| `--color-checked-disabled` | Color of the checked & disabled segment button | | `--color-hover` | Color of the segment button on hover |
| `--color-disabled` | Color of the disabled segment button | | `--indicator-box-shadow` | Box shadow on the indicator for the checked segment button |
| `--indicator-color` | Color of the indicator (highlight) under the segment button | | `--indicator-color` | Color of the indicator for the checked segment button |
| `--indicator-color-checked` | Color of the indicator (highlight) under the checked segment button | | `--indicator-transform` | Transform of the indicator for the checked segment button |
| `--margin-bottom` | Bottom margin of the segment button | | `--indicator-transition` | Transition of the indicator for the checked 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-bottom` | Bottom margin 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-end` | Right margin if direction is left-to-right, and left margin if direction is right-to-left of the segment button |
| `--margin-top` | Top margin 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 |
| `--padding-bottom` | Bottom padding of the segment button | | `--margin-top` | Top margin 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-bottom` | Bottom padding 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-end` | Right padding if direction is left-to-right, and left padding if direction is right-to-left of the segment button |
| `--padding-top` | Top padding 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 |
| `--transition` | Transition of the segment button | | `--padding-top` | Top padding of the segment button |
| `--transition` | Transition of the segment button |
## Dependencies ## Dependencies

View File

@ -5,24 +5,73 @@
// -------------------------------------------------- // --------------------------------------------------
:host { :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-radius: #{$segment-button-ios-border-radius};
--border-width: #{$segment-button-ios-border-width}; --border-width: #{$segment-button-ios-border-width};
--border-color: #{$segment-button-ios-border-color};
--border-style: solid; --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}; --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-size: #{$segment-button-ios-font-size};
font-weight: 450;
line-height: #{$segment-button-ios-line-height}; line-height: #{$segment-button-ios-line-height};
} }
// Segment Button: Indicator // Segment Button: Borders
// -------------------------------------------------- // --------------------------------------------------
.segment-button-indicator { :host::before {
display: none; @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) { .segment-button-indicator {
:host(:hover:not(.segment-button-checked)) { @include padding(null, $segment-button-ios-margin);
background: var(--background-hover); @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) { :host(.segment-button-checked)::before,
background: var(--background-activated); :host(.segment-button-after-checked)::before {
opacity: 0;
} }
// Segment: Checked & Activated // Segment Button: Checked
// -------------------------------------------------- // --------------------------------------------------
:host(.segment-button-checked.activated) { :host(.segment-button-checked) {
background: var(--background-checked); z-index: -1;
color: var(--color-checked); }
// 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; $segment-button-ios-background-color: transparent !default;
/// @prop - Background of the checked segment button /// @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 /// @prop - Opacity of the segment button on hover
$segment-button-ios-opacity-hover: .1 !default; $segment-button-ios-opacity-hover: .5 !default;
/// @prop - Opacity of the segment button when pressed
$segment-button-ios-opacity-activated: .16 !default;
/// @prop - Opacity of the disabled segment button /// @prop - Opacity of the disabled segment button
$segment-button-ios-opacity-disabled: .3 !default; $segment-button-ios-opacity-disabled: .3 !default;
/// @prop - Background of the segment button on hover /// @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 /// @prop - Box shadow of the checked segment button
$segment-button-ios-background-color-activated: ion-color(primary, base, $segment-button-ios-opacity-activated) !default; $segment-button-ios-box-shadow-checked: 0 0 5px rgba(0, 0, 0, 0.16) !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 - Border width of the segment button /// @prop - Border width of the segment button
$segment-button-ios-border-width: 1px !default; $segment-button-ios-border-width: 1px !default;
/// @prop - Height of the segment button /// @prop - Border color of the segment button
$segment-button-ios-height: 24px !default; $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 /// @prop - Line height of the segment button
$segment-button-ios-line-height: 37px !default; $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; $segment-button-ios-font-size: 13px !default;
/// @prop - Border radius of the segment button /// @prop - Border radius of the segment button
$segment-button-ios-border-radius: 4px !default; $segment-button-ios-border-radius: 7px !default;
/// @prop - Border color of the segment button
$segment-button-ios-border-color: ion-color(primary, base) !default;
/// @prop - Size of an icon in the segment button /// @prop - Size of an icon in the segment button
$segment-button-ios-icon-size: 24px !default; $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 /// @prop - Transition of the segment button
$segment-button-ios-transition: 100ms all linear !default; $segment-button-ios-transition: 100ms all linear !default;
/// @prop - Max width of the segment button in a toolbar /// @prop - Transition of the animated segment button
$segment-button-ios-toolbar-button-max-width: 100px !default; $segment-button-ios-transition-animated: transform 260ms cubic-bezier(0.4, 0, 0.2, 1) !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;

View File

@ -5,6 +5,15 @@
// -------------------------------------------------- // --------------------------------------------------
:host { :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-top: #{$segment-button-md-padding-top};
--padding-end: #{$segment-button-md-padding-end}; --padding-end: #{$segment-button-md-padding-end};
--padding-bottom: #{$segment-button-md-padding-bottom}; --padding-bottom: #{$segment-button-md-padding-bottom};
@ -16,6 +25,10 @@
max-width: $segment-button-md-max-width; max-width: $segment-button-md-max-width;
min-height: $segment-button-md-min-height; 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-size: $segment-button-md-font-size;
font-weight: $segment-button-md-font-weight; font-weight: $segment-button-md-font-weight;
@ -26,15 +39,6 @@
text-transform: uppercase; 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 // Segment Button: Disabled
// -------------------------------------------------- // --------------------------------------------------
@ -93,20 +97,26 @@
@include margin(12px, null, 12px, null); @include margin(12px, null, 12px, null);
} }
// Segment Button: Indicator
// Segment: Checked & Activated
// -------------------------------------------------- // --------------------------------------------------
:host(.segment-button-checked.activated) { .segment-button-indicator {
color: var(--color-checked); @include position(null, 0, 0, 0);
} }
.segment-button-indicator-background {
height: 2px;
// Segment Button: Hover background: var(--indicator-color);
// -------------------------------------------------- }
@media (any-hover: hover) { // Segment button indicator color should use the background checked variable with
:host(:hover) { // a fallback to the default value of --indicator-color
background: var(--background-hover); :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 /// @prop - Background of the hovered segment button
$segment-button-md-background-hover: ion-color(primary, base, .04) !default; $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 /// @prop - Width of the bottom border on the segment button
$segment-button-md-border-bottom-width: 2px !default; $segment-button-md-border-bottom-width: 2px !default;
/// @prop - Color of the bottom border on the segment button /// @prop - Color of the bottom border on the segment button
$segment-button-md-border-bottom-color: transparent !default; $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; $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 /// @prop - Opacity of the disabled segment button
$segment-button-md-opacity-disabled: .3 !default; $segment-button-md-opacity-disabled: .3 !default;
@ -75,6 +66,9 @@ $segment-button-md-font-weight: 500 !default;
/// @prop - Transition of the segment button /// @prop - Transition of the segment button
$segment-button-md-transition: color .15s linear 0s, opacity .15s linear 0s !default; $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 /// @prop - Size of an icon in the segment button
$segment-button-md-icon-size: 24px !default; $segment-button-md-icon-size: 24px !default;

View File

@ -6,15 +6,14 @@
:host { :host {
/** /**
* @prop --background: Background of the segment button * @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-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: 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-checked: Color of the checked segment button
* @prop --color-disabled: Color of the disabled 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-radius: Radius of the segment button border
* @prop --border-color: Color 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 --transition: Transition of the segment button
* *
* @prop --indicator-color: Color of the indicator (highlight) under the segment button * @prop --indicator-box-shadow: Box shadow on the indicator for the checked segment button
* @prop --indicator-color-checked: Color of the indicator (highlight) under 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-start: 0;
--padding-end: 0; --padding-end: 0;
--padding-top: 0; --padding-top: 0;
--padding-bottom: 0; --padding-bottom: 0;
@include border-radius(var(--border-radius));
display: flex; display: flex;
flex: 1 0 auto; position: relative;
flex: 1 1 auto;
flex-direction: column; flex-direction: column;
height: auto; height: auto;
border-width: var(--border-width);
border-style: var(--border-style);
border-color: var(--border-color);
background: var(--background); background: var(--background);
color: var(--color); color: var(--color);
@ -62,32 +66,15 @@
white-space: nowrap; white-space: nowrap;
overflow: hidden;
font-kerning: none; 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 { .button-native {
@include border-radius(inherit); @include border-radius(0);
@include text-inherit(); @include text-inherit();
@include margin(var(--margin-top), var(--margin-end), var(--margin-bottom), var(--margin-start)); @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 padding(var(--padding-top), var(--padding-end), var(--padding-bottom), var(--padding-start));
@include transform(translate3d(0,0,0));
display: flex; display: flex;
position: relative; position: relative;
@ -116,22 +103,8 @@
contain: content; contain: content;
cursor: pointer; cursor: pointer;
}
z-index: 2;
// Segment Button: Indicator
// --------------------------------------------------
.segment-button-indicator {
align-self: flex-end;
width: 100%;
height: 2px;
background-color: var(--indicator-color);
opacity: 1;
} }
@ -143,30 +116,28 @@
color: var(--color-checked); 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 // Segment Button: Disabled
// -------------------------------------------------- // --------------------------------------------------
:host(.segment-button-disabled) { :host(.segment-button-disabled) {
background: var(--background-disabled);
color: var(--color-disabled); color: var(--color-disabled);
} }
// Segment Button: Checked & Disabled
// Segment Button: Hover
// -------------------------------------------------- // --------------------------------------------------
:host(.segment-button-disabled.segment-button-checked) { @media (any-hover: hover) {
color: var(--color-checked-disabled); :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) { ::slotted(ion-icon) {
flex-shrink: 0;
order: -1; order: -1;
pointer-events: none;
} }
@ -193,12 +168,19 @@
white-space: nowrap; white-space: nowrap;
box-sizing: border-box; box-sizing: border-box;
pointer-events: none;
} }
// Segment Button Layout // Segment Button Layout
// -------------------------------------------------- // --------------------------------------------------
// Layout: icon top
:host(.segment-button-layout-icon-top) .button-native {
flex-direction: column;
}
// Layout: icon start // Layout: icon start
:host(.segment-button-layout-icon-start) .button-native { :host(.segment-button-layout-icon-start) .button-native {
flex-direction: row; flex-direction: row;
@ -230,3 +212,49 @@
ion-ripple-effect { ion-ripple-effect {
color: var(--ripple-color, var(--color-checked)); 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 { getIonMode } from '../../global/ionic-global';
import { SegmentButtonLayout } from '../../interface'; import { SegmentButtonLayout } from '../../interface';
import { ButtonInterface } from '../../utils/element-interface'; import { ButtonInterface } from '../../utils/element-interface';
import { hostContext } from '../../utils/theme';
let ids = 0; let ids = 0;
@ -18,7 +19,6 @@ let ids = 0;
shadow: true shadow: true
}) })
export class SegmentButton implements ComponentInterface, ButtonInterface { export class SegmentButton implements ComponentInterface, ButtonInterface {
@Element() el!: HTMLElement; @Element() el!: HTMLElement;
/** /**
@ -52,8 +52,10 @@ export class SegmentButton implements ComponentInterface, ButtonInterface {
@Event() ionSelect!: EventEmitter<void>; @Event() ionSelect!: EventEmitter<void>;
@Watch('checked') @Watch('checked')
checkedChanged(checked: boolean, prev: boolean) { checkedChanged(newValue: boolean, oldValue: boolean) {
if (checked && !prev) { // If the segment button is not already checked
// emit the ionSelect event
if (newValue && !oldValue) {
this.ionSelect.emit(); this.ionSelect.emit();
} }
} }
@ -79,6 +81,8 @@ export class SegmentButton implements ComponentInterface, ButtonInterface {
aria-disabled={disabled ? 'true' : null} aria-disabled={disabled ? 'true' : null}
class={{ class={{
[mode]: true, [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-label': hasLabel,
'segment-button-has-icon': hasIcon, 'segment-button-has-icon': hasIcon,
'segment-button-has-label-only': hasLabel && !hasIcon, 'segment-button-has-label-only': hasLabel && !hasIcon,
@ -99,7 +103,16 @@ export class SegmentButton implements ComponentInterface, ButtonInterface {
<slot></slot> <slot></slot>
{mode === 'md' && <ion-ripple-effect></ion-ripple-effect>} {mode === 'md' && <ion-ripple-effect></ion-ripple-effect>}
</button> </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> </Host>
); );
} }

View File

@ -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` | | `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` | | `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` | | `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` | | `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<SegmentChangeEventDetail>` | | `ionChange` | Emitted when the value property has changed. | `CustomEvent<SegmentChangeEventDetail>` |
## CSS Custom Properties
| Name | Description |
| -------------- | -------------------------------- |
| `--background` | Background of the segment button |
---------------------------------------------- ----------------------------------------------
*Built with [StencilJS](https://stenciljs.com/)* *Built with [StencilJS](https://stenciljs.com/)*

View File

@ -5,92 +5,78 @@
// -------------------------------------------------- // --------------------------------------------------
:host { :host {
--background: #{$segment-button-ios-background-color}; --background: #{$segment-ios-background-color};
--background-hover: #{$segment-button-ios-background-color-hover};
--background-activated: #{$segment-button-ios-background-color-activated}; @include border-radius($segment-ios-border-radius);
--background-checked: #{$segment-button-ios-background-color-checked};
--color: #{$segment-button-ios-text-color}; overflow: hidden;
--color-checked: #{$segment-button-ios-text-color-checked};
--color-disabled: #{ion-color(primary, base, $segment-button-ios-opacity-disabled)}; z-index: 0;
--color-checked-disabled: #{ion-color(primary, contrast, $segment-button-ios-opacity-disabled)};
--border-color: #{$segment-button-ios-border-color};
--indicator-color: transparent;
} }
:host(.segment-disabled) { :host(.segment-disabled) {
opacity: $segment-ios-opacity-disabled; opacity: $segment-button-ios-opacity-disabled;
} }
// Segment: Color // Segment: Color
// -------------------------------------------------- // --------------------------------------------------
:host(.ion-color)::slotted(ion-segment-button) { :host(.ion-color) {
--border-color: #{current-color(base)}; background: #{current-color(base, 0.065)};
background: transparent;
color: #{current-color(base)};
} }
:host(.ion-color)::slotted(.activated) { :host(.ion-color) ::slotted(.segment-button-checked) {
background: #{current-color(base, .16)}; color: #000;
color: #{current-color(base)};
} }
: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) { // Segment: Activated
color: #{current-color(base, $segment-ios-opacity-disabled)}; // --------------------------------------------------
}
:host(.ion-color)::slotted(.segment-button-checked.segment-button-disabled) { :host(.segment-activated) ::slotted(ion-segment-button) {
color: #{current-color(contrast, $segment-ios-opacity-disabled)}; --indicator-transform: scale(0.95);
}
@media (any-hover: hover) {
:host(.ion-color)::slotted(ion-segment-button:hover:not(.segment-button-checked)) {
background: #{current-color(base, .1)};
}
} }
// Segment: Default Toolbar // Segment: Default Toolbar
// -------------------------------------------------- // --------------------------------------------------
:host-context(ion-toolbar)::slotted(ion-segment-button) { :host(.in-toolbar) {
max-width: $segment-button-ios-toolbar-button-max-width; @include margin(0, auto);
font-size: $segment-button-ios-toolbar-font-size; width: auto;
line-height: $segment-button-ios-toolbar-line-height;
} }
:host-context(ion-toolbar:not(.ion-color)):not(.ion-color)::slotted(ion-segment-button) { // Default Segment, In a Toolbar
border-color: #{var(--ion-toolbar-color-checked, var(--border-color))}; :host(.in-toolbar:not(.ion-color)) {
background: var(--ion-toolbar-segment-background, $segment-ios-background-color);
color: #{var(--ion-toolbar-color-unchecked, var(--color))}; color: var(--ion-toolbar-segment-color, var(--color));
} }
:host-context(ion-toolbar:not(.ion-color)):not(.ion-color)::slotted(.segment-button-checked) { // Default Segment, In a Toolbar, Checked
background: #{var(--ion-toolbar-color-checked, var(--background-checked))}; :host(.in-toolbar:not(.ion-color)) ::slotted(.segment-button-checked) {
color: var(--ion-toolbar-segment-color-checked, var(--color-checked));
color: #{var(--ion-toolbar-background, var(--color-checked))};
} }
// Segment: Color Toolbar // Segment: Color Toolbar
// -------------------------------------------------- // --------------------------------------------------
:host-context(ion-toolbar.ion-color):not(.ion-color)::slotted(ion-segment-button) { // Toolbar with Color, Default Segment
--color: #{current-color(contrast)}; :host(.in-toolbar-color:not(.ion-color)) {
--color-disabled: #{current-color(contrast, $segment-button-ios-opacity-disabled)}; background: #{current-color(contrast, 0.11)};
--color-checked: #{current-color(base)}; color: #{current-color(contrast)};
--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)}; // Toolbar with Color, Default Segment, Checked
--background-checked: #{current-color(contrast)}; :host(.in-toolbar-color:not(.ion-color)) ::slotted(.segment-button-checked) {
--border-color: #{current-color(contrast)}; 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)};
}
} }

View File

@ -4,5 +4,11 @@
// iOS Segment // iOS Segment
// -------------------------------------------------- // --------------------------------------------------
/// @prop - Opacity of the disabled segment /// @prop - Alpha of the segment for use in the backgrounds
$segment-ios-opacity-disabled: .3 !default; $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;

View File

@ -5,14 +5,7 @@
// -------------------------------------------------- // --------------------------------------------------
:host { :host {
--background: #{$segment-button-md-background}; --background: transparent;
--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;
} }
:host(.segment-disabled) { :host(.segment-disabled) {
@ -22,25 +15,20 @@
// Segment: Color // Segment: Color
// -------------------------------------------------- // --------------------------------------------------
:host(.ion-color)::slotted(ion-segment-button) { :host(.ion-color) ::slotted(ion-segment-button) {
--background-activated: #{current-color(base, .16)};
--ripple-color: #{current-color(base)}; --ripple-color: #{current-color(base)};
--indicator-color: #{current-color(base)};
background: transparent; background: transparent;
color: $segment-button-md-text-color; color: $segment-button-md-text-color;
} }
:host(.ion-color)::slotted(.segment-button-checked) { :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) {
color: #{current-color(base)}; color: #{current-color(base)};
} }
@media (any-hover: hover) { @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)}; background: #{current-color(base, .04)};
} }
} }
@ -48,24 +36,49 @@
// Segment: Default Toolbar // Segment: Default Toolbar
// -------------------------------------------------- // --------------------------------------------------
:host-context(ion-toolbar:not(.ion-color)):not(.ion-color)::slotted(ion-segment-button) { // Default Segment, In a Toolbar
color: #{var(--ion-toolbar-color-unchecked, var(--color))}; :host(.in-toolbar:not(.ion-color)) ::slotted(ion-segment-button) {
--indicator-color: #{var(--ion-toolbar-segment-color-checked, var(--color-checked))};
background: #{var(--ion-toolbar-segment-background, var(--background))};
color: #{var(--ion-toolbar-segment-color, var(--color))};
} }
:host-context(ion-toolbar:not(.ion-color)):not(.ion-color)::slotted(.segment-button-checked) { // Default Segment, In a Toolbar, Checked
--indicator-color-checked: #{var(--ion-toolbar-color-checked, var(--color-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-color-checked, var(--color-checked))}; color: #{var(--ion-toolbar-segment-color-checked, var(--color-checked))};
} }
// Segment: Toolbar Color // Segment: Toolbar Color
// -------------------------------------------------- // --------------------------------------------------
:host-context(ion-toolbar.ion-color):not(.ion-color)::slotted(ion-segment-button) { // Default Segment, In a Toolbar with Color
--background-hover: #{current-color(contrast, .04)}; :host(.in-toolbar-color:not(.ion-color)) ::slotted(ion-segment-button) {
--background-activated: #{current-color(base)}; color: #{current-color(contrast, .6)};
--color: #{current-color(contrast, .6)};
--color-checked: #{current-color(contrast)};
--indicator-color-checked: #{current-color(contrast)};
} }
// 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;
}

View File

@ -4,22 +4,29 @@
// -------------------------------------------------- // --------------------------------------------------
:host { :host {
--indicator-color-checked: initial; /**
* @prop --background: Background of the segment button
*/
--ripple-color: currentColor; --ripple-color: currentColor;
--color-activated: initial;
@include font-smoothing(); @include font-smoothing();
display: flex; display: flex;
position: relative;
align-items: stretch; align-items: stretch;
justify-content: center; justify-content: center;
width: 100%; width: 100%;
background: var(--background);
font-family: $font-family-base; font-family: $font-family-base;
text-align: center; text-align: center;
contain: paint;
} }
@ -46,3 +53,19 @@
:host(.segment-scrollable::-webkit-scrollbar) { :host(.segment-scrollable::-webkit-scrollbar) {
display: none; 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))));
}
}

View File

@ -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 { getIonMode } from '../../global/ionic-global';
import { Color, SegmentChangeEventDetail, StyleEventDetail } from '../../interface'; 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. * @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', ios: 'segment.ios.scss',
md: 'segment.md.scss' md: 'segment.md.scss'
}, },
scoped: true shadow: true
}) })
export class Segment implements ComponentInterface { export class Segment implements ComponentInterface {
private gesture?: Gesture;
private didInit = false; private didInit = false;
private checked?: HTMLIonSegmentButtonElement;
@Element() el!: HTMLElement; @Element() el!: HTMLIonSegmentElement;
@State() activated = false;
/** /**
* The color to use from your application's color palette. * 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. * 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; @Prop() scrollable = false;
@ -43,14 +50,6 @@ export class Segment implements ComponentInterface {
*/ */
@Prop({ mutable: true }) value?: string | null; @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. * Emitted when the value property has changed.
*/ */
@ -62,10 +61,32 @@ export class Segment implements ComponentInterface {
*/ */
@Event() ionStyle!: EventEmitter<StyleEventDetail>; @Event() ionStyle!: EventEmitter<StyleEventDetail>;
@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') @Listen('ionSelect')
segmentClick(ev: CustomEvent) { segmentClick(ev: CustomEvent) {
const selectedButton = ev.target as HTMLIonSegmentButtonElement; const current = ev.target as HTMLIonSegmentButtonElement;
this.value = selectedButton.value; const previous = this.checked;
this.value = current.value;
if (previous && this.scrollable) {
this.checkButton(previous, current);
}
this.checked = current;
} }
connectedCallback() { connectedCallback() {
@ -78,11 +99,219 @@ export class Segment implements ComponentInterface {
this.emitStyle(); this.emitStyle();
} }
componentDidLoad() { componentWillLoad() {
this.emitStyle();
}
async componentDidLoad() {
this.updateButtons(); 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; 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() { private emitStyle() {
this.ionStyle.emit({ this.ionStyle.emit({
'segment': true 'segment': true
@ -102,15 +331,20 @@ export class Segment implements ComponentInterface {
render() { render() {
const mode = getIonMode(this); const mode = getIonMode(this);
return ( return (
<Host <Host
class={{ class={{
...createColorClasses(this.color), ...createColorClasses(this.color),
[mode]: true, [mode]: true,
'in-toolbar': hostContext('ion-toolbar', this.el),
'in-toolbar-color': hostContext('ion-toolbar[color]', this.el),
'segment-activated': this.activated,
'segment-disabled': this.disabled, 'segment-disabled': this.disabled,
'segment-scrollable': this.scrollable 'segment-scrollable': this.scrollable
}} }}
> >
<slot></slot>
</Host> </Host>
); );
} }

View File

@ -4,12 +4,14 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Segment - Basic</title> <title>Segment - Basic</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"> <meta name="viewport"
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet"> <link href="../../../../../css/ionic.bundle.css" rel="stylesheet">
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet"> <link href="../../../../../scripts/testing/styles.css" rel="stylesheet">
<script src="../../../../../scripts/testing/scripts.js"></script> <script src="../../../../../scripts/testing/scripts.js"></script>
<script nomodule src="../../../../../dist/ionic/ionic.js"></script> <script nomodule src="../../../../../dist/ionic/ionic.js"></script>
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script></head> <script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
</head>
<body onload="listenForEvent()"> <body onload="listenForEvent()">
<ion-app> <ion-app>
@ -22,7 +24,7 @@
<ion-toolbar> <ion-toolbar>
<ion-segment class="event-tester" value="Free"> <ion-segment class="event-tester" value="Free">
<ion-segment-button value="Paid"> <ion-segment-button value="Paid">
Paid PaidPaidPaid
</ion-segment-button> </ion-segment-button>
<ion-segment-button value="Free"> <ion-segment-button value="Free">
Free Free
@ -62,23 +64,12 @@
</ion-toolbar> </ion-toolbar>
<ion-toolbar> <ion-toolbar>
<ion-segment color="danger"> <ion-segment>
<ion-segment-button value="sunny"> <ion-segment-button value="all" checked>
Sunny All
</ion-segment-button> </ion-segment-button>
<ion-segment-button value="rainy" checked> <ion-segment-button value="missed">
Rainy Missed
</ion-segment-button>
</ion-segment>
</ion-toolbar>
<ion-toolbar color="primary">
<ion-segment color="light">
<ion-segment-button value="sunny">
Sunny
</ion-segment-button>
<ion-segment-button value="rainy" checked>
Rainy
</ion-segment-button> </ion-segment-button>
</ion-segment> </ion-segment>
</ion-toolbar> </ion-toolbar>
@ -87,26 +78,38 @@
<ion-content> <ion-content>
<div class="ion-padding"> <div class="ion-padding">
<ion-segment> <ion-segment>
<ion-segment-button><ion-label>Seg 1</ion-label></ion-segment-button> <ion-segment-button>
<ion-segment-button><ion-label>Seg 2</ion-label></ion-segment-button> <ion-label>Seg 1</ion-label>
<ion-segment-button><ion-label>Seg 3</ion-label></ion-segment-button> </ion-segment-button>
<ion-segment-button>
<ion-label>Seg 2</ion-label>
</ion-segment-button>
<ion-segment-button>
<ion-label>Seg 3</ion-label>
</ion-segment-button>
</ion-segment> </ion-segment>
<ion-segment disabled> <ion-segment class="segment-no-animate">
<ion-segment-button><ion-label>Seg 2 1</ion-label></ion-segment-button> <ion-segment-button>
<ion-segment-button checked><ion-label>Seg 2 2</ion-label></ion-segment-button> <ion-label>Animate</ion-label>
<ion-segment-button><ion-label>Seg 2 3</ion-label></ion-segment-button> </ion-segment-button>
<ion-segment-button checked>
<ion-label>Is</ion-label>
</ion-segment-button>
<ion-segment-button>
<ion-label>False</ion-label>
</ion-segment-button>
</ion-segment> </ion-segment>
<ion-segment color="dark" value="Reading List"> <ion-segment color="dark" value="Reading List">
<ion-segment-button value="Bookmarks"> <ion-segment-button value="Bookmarks">
<ion-icon name="book"></ion-icon> <ion-icon name="md-book"></ion-icon>
</ion-segment-button> </ion-segment-button>
<ion-segment-button value="Reading List"> <ion-segment-button value="Reading List">
<ion-icon name="glasses"></ion-icon> <ion-icon name="search"></ion-icon>
</ion-segment-button> </ion-segment-button>
<ion-segment-button value="Shared Links"> <ion-segment-button value="Shared Links">
<ion-icon name="at"></ion-icon> <ion-icon name="md-time"></ion-icon>
</ion-segment-button> </ion-segment-button>
</ion-segment> </ion-segment>
@ -117,13 +120,13 @@
<ion-segment-button value="440"> <ion-segment-button value="440">
<ion-label>440ml</ion-label> <ion-label>440ml</ion-label>
</ion-segment-button> </ion-segment-button>
<ion-segment-button value="500"> <ion-segment-button disabled value="500">
<ion-label>500ml</ion-label> <ion-label>500ml</ion-label>
</ion-segment-button> </ion-segment-button>
<ion-segment-button value="custom"> <ion-segment-button value="custom">
<ion-icon name="create"></ion-icon> <ion-icon name="create"></ion-icon>
</ion-segment-button> </ion-segment-button>
</ion-segment> </ion-segment>
<ion-segment name="dynamicPropDisable" disabled color="danger"> <ion-segment name="dynamicPropDisable" disabled color="danger">
<ion-segment-button value="Bookmarks"> <ion-segment-button value="Bookmarks">
@ -149,7 +152,7 @@
</ion-segment-button> </ion-segment-button>
</ion-segment> </ion-segment>
<ion-segment name="dynamicAttrElem" value="disabled"> <ion-segment name="dynamicAttrElem">
<ion-segment-button value="active"> <ion-segment-button value="active">
<ion-label>Active</ion-label> <ion-label>Active</ion-label>
</ion-segment-button> </ion-segment-button>
@ -162,7 +165,7 @@
</ion-segment> </ion-segment>
<!-- Dynamic Buttons --> <!-- Dynamic Buttons -->
<ion-segment id="dynamicButtons" color="dark"></ion-segment> <ion-segment id="dynamicButtons"></ion-segment>
</div> </div>
<div class="ion-padding-horizontal"> <div class="ion-padding-horizontal">
@ -204,7 +207,6 @@
async function listenForEvent() { async function listenForEvent() {
const ionSegmentElement = document.querySelector('ion-segment.event-tester'); const ionSegmentElement = document.querySelector('ion-segment.event-tester');
await ionSegmentElement.componentOnReady();
ionSegmentElement.addEventListener('ionChange', (event) => { ionSegmentElement.addEventListener('ionChange', (event) => {
console.log('event.target: ', event.target.value); console.log('event.target: ', event.target.value);
}); });
@ -218,13 +220,15 @@
}, 4000); }, 4000);
function updateSegmentButtons(length) { function updateSegmentButtons(length) {
dynamicButtons.innerHTML = ''; const buttonsLength = dynamicButtons.children.length;
const begin = buttonsLength === 0 ? 0 : buttonsLength;
for (var i = 0; i < length; i++) { for (var i = begin; i < length; i++) {
dynamicButtons.innerHTML += ` const button = document.createElement('ion-segment-button');
<ion-segment-button value="segment-${i}"> button.value = `segment-${i}`;
<ion-label>Btn ${i}</ion-label> button.innerHTML = `<ion-label>Btn ${i}</ion-label>`;
</ion-segment-button>`;
dynamicButtons.appendChild(button);
} }
} }
@ -236,6 +240,10 @@
margin-bottom: 10px; margin-bottom: 10px;
} }
.segment-no-animate ion-segment-button {
--indicator-transition: none;
--indicator-transform: none;
}
</style> </style>
</ion-app> </ion-app>
</body> </body>

View File

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

View File

@ -0,0 +1,178 @@
<!DOCTYPE html>
<html dir="ltr">
<head>
<meta charset="UTF-8">
<title>Segment - Colors</title>
<meta name="viewport"
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet">
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet">
<script src="../../../../../scripts/testing/scripts.js"></script>
<script nomodule src="../../../../../dist/ionic/ionic.js"></script>
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
</head>
<body>
<ion-app>
<ion-header>
<ion-toolbar>
<ion-title>Segment - Colors</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<ion-segment value="free">
<ion-segment-button value="paid">
Paid
</ion-segment-button>
<ion-segment-button value="free">
Free
</ion-segment-button>
<ion-segment-button value="top">
Top
</ion-segment-button>
</ion-segment>
<ion-segment color="primary" value="reading-list">
<ion-segment-button value="bookmarks">
Bookmarks
</ion-segment-button>
<ion-segment-button value="reading-list">
Reading List
</ion-segment-button>
<ion-segment-button value="shared-links">
Shared Links
</ion-segment-button>
</ion-segment>
<ion-segment color="secondary" value="active">
<ion-segment-button value="active">
Active
</ion-segment-button>
<ion-segment-button value="disabled" disabled="true">
Disabled
</ion-segment-button>
<ion-segment-button value="inactive" disabled="false">
Inactive
</ion-segment-button>
</ion-segment>
<ion-segment color="tertiary">
<ion-segment-button value="all" checked>
All
</ion-segment-button>
<ion-segment-button value="missed">
Missed
</ion-segment-button>
</ion-segment>
<ion-segment color="success">
<ion-segment-button checked value="330">
<ion-label>330ml</ion-label>
</ion-segment-button>
<ion-segment-button value="440">
<ion-label>440ml</ion-label>
</ion-segment-button>
<ion-segment-button value="500">
<ion-label>500ml</ion-label>
</ion-segment-button>
<ion-segment-button value="custom">
<ion-icon name="create"></ion-icon>
</ion-segment-button>
</ion-segment>
<ion-segment color="warning" value="reading-list">
<ion-segment-button value="bookmarks">
<ion-icon name="book"></ion-icon>
</ion-segment-button>
<ion-segment-button value="reading-list">
<ion-icon name="glasses"></ion-icon>
</ion-segment-button>
<ion-segment-button value="shared-links">
<ion-icon name="at"></ion-icon>
</ion-segment-button>
</ion-segment>
<ion-segment color="danger" value="bookmarks">
<ion-segment-button value="bookmarks">
<ion-label>Bookmarks</ion-label>
</ion-segment-button>
<ion-segment-button value="reading-list">
<ion-label>Reading List</ion-label>
</ion-segment-button>
<ion-segment-button value="shared-links">
<ion-label>Shared Links</ion-label>
</ion-segment-button>
</ion-segment>
<ion-segment color="light">
<ion-segment-button value="sunny">
Sunny
</ion-segment-button>
<ion-segment-button value="rainy" checked>
Rainy
</ion-segment-button>
</ion-segment>
<ion-segment color="medium">
<ion-segment-button checked>
<ion-label>Seg 1</ion-label>
</ion-segment-button>
<ion-segment-button>
<ion-label>Seg 2</ion-label>
</ion-segment-button>
<ion-segment-button>
<ion-label>Seg 3</ion-label>
</ion-segment-button>
</ion-segment>
<ion-segment color="dark">
<ion-segment-button>
<ion-label>Seg 2 1</ion-label>
</ion-segment-button>
<ion-segment-button checked>
<ion-label>Seg 2 2</ion-label>
</ion-segment-button>
<ion-segment-button>
<ion-label>Seg 2 3</ion-label>
</ion-segment-button>
</ion-segment>
<ion-segment disabled color="danger">
<ion-segment-button>
<ion-label>Seg 2 1</ion-label>
</ion-segment-button>
<ion-segment-button checked>
<ion-label>Seg 2 2</ion-label>
</ion-segment-button>
<ion-segment-button>
<ion-label>Seg 2 3</ion-label>
</ion-segment-button>
</ion-segment>
<ion-segment disabled color="medium">
<ion-segment-button>
<ion-label>Seg 2 1</ion-label>
</ion-segment-button>
<ion-segment-button checked>
<ion-label>Seg 2 2</ion-label>
</ion-segment-button>
<ion-segment-button>
<ion-label>Seg 2 3</ion-label>
</ion-segment-button>
</ion-segment>
</ion-content>
<style>
ion-content ion-segment {
margin-bottom: 10px;
}
</style>
</ion-app>
</body>
</html>

View File

@ -28,17 +28,31 @@
</ion-segment> </ion-segment>
</ion-toolbar> </ion-toolbar>
<ion-toolbar>
<ion-segment value="Free" class="custom-checked">
<ion-segment-button value="Paid">
<ion-label>Paid</ion-label>
</ion-segment-button>
<ion-segment-button value="Free">
<ion-label>Free</ion-label>
</ion-segment-button>
<ion-segment-button value="Top">
<ion-label>Top</ion-label>
</ion-segment-button>
</ion-segment>
</ion-toolbar>
<ion-toolbar> <ion-toolbar>
<ion-segment class="custom-icon" value="instagram"> <ion-segment class="custom-icon" value="instagram">
<ion-segment-button class="segment-facebook" value="facebook"> <ion-segment-button class="segment-button-facebook" value="facebook">
<ion-label>Facebook</ion-label> <ion-label>Facebook</ion-label>
<ion-icon name="logo-facebook"></ion-icon> <ion-icon name="logo-facebook"></ion-icon>
</ion-segment-button> </ion-segment-button>
<ion-segment-button class="segment-instagram" value="instagram"> <ion-segment-button class="segment-button-instagram" value="instagram">
<ion-label>Instagram</ion-label> <ion-label>Instagram</ion-label>
<ion-icon name="logo-instagram"></ion-icon> <ion-icon name="logo-instagram"></ion-icon>
</ion-segment-button> </ion-segment-button>
<ion-segment-button class="segment-slack" value="slack"> <ion-segment-button class="segment-button-slack" value="slack">
<ion-label>Slack</ion-label> <ion-label>Slack</ion-label>
<ion-icon name="logo-slack"></ion-icon> <ion-icon name="logo-slack"></ion-icon>
</ion-segment-button> </ion-segment-button>
@ -61,11 +75,39 @@
</ion-segment-button> </ion-segment-button>
</ion-segment> </ion-segment>
</ion-toolbar> </ion-toolbar>
<ion-toolbar>
<ion-segment class="custom-states-color" value="disabled">
<ion-segment-button value="inactive">
<ion-label>Inactive</ion-label>
</ion-segment-button>
<ion-segment-button value="inactive2">
<ion-label>Inactive</ion-label>
</ion-segment-button>
<ion-segment-button value="disabled" disabled>
<ion-label>Disabled</ion-label>
</ion-segment-button>
</ion-segment>
</ion-toolbar>
<ion-toolbar>
<ion-segment class="custom-states-background" value="checked">
<ion-segment-button value="checked">
<ion-label>Checked</ion-label>
</ion-segment-button>
<ion-segment-button value="inactive">
<ion-label>Inactive</ion-label>
</ion-segment-button>
<ion-segment-button value="disabled" disabled>
<ion-label>Disabled</ion-label>
</ion-segment-button>
</ion-segment>
</ion-toolbar>
</ion-header> </ion-header>
<ion-content> <ion-content class="ion-padding-top">
<ion-segment class="custom" value="active"> <ion-segment class="custom-active" value="active">
<ion-segment-button value="active"> <ion-segment-button value="active">
<ion-label>Active</ion-label> <ion-label>Active</ion-label>
</ion-segment-button> </ion-segment-button>
@ -77,16 +119,16 @@
</ion-segment-button> </ion-segment-button>
</ion-segment> </ion-segment>
<ion-segment class="custom-icon" value="instagram"> <ion-segment class="custom-icon" value="slack">
<ion-segment-button class="segment-facebook" value="facebook"> <ion-segment-button class="segment-button-facebook" value="facebook">
<ion-label>Facebook</ion-label> <ion-label>Facebook</ion-label>
<ion-icon name="logo-facebook"></ion-icon> <ion-icon name="logo-facebook"></ion-icon>
</ion-segment-button> </ion-segment-button>
<ion-segment-button class="segment-instagram" value="instagram"> <ion-segment-button class="segment-button-instagram" value="instagram">
<ion-label>Instagram</ion-label> <ion-label>Instagram</ion-label>
<ion-icon name="logo-instagram"></ion-icon> <ion-icon name="logo-instagram"></ion-icon>
</ion-segment-button> </ion-segment-button>
<ion-segment-button class="segment-slack" value="slack"> <ion-segment-button class="segment-button-slack" value="slack">
<ion-label>Slack</ion-label> <ion-label>Slack</ion-label>
<ion-icon name="logo-slack"></ion-icon> <ion-icon name="logo-slack"></ion-icon>
</ion-segment-button> </ion-segment-button>
@ -107,34 +149,30 @@
</ion-segment-button> </ion-segment-button>
</ion-segment> </ion-segment>
<h2>Activated</h2> <ion-segment class="custom-states-color" value="checked">
<ion-segment-button value="checked">
<ion-segment> <ion-label>Checked</ion-label>
<ion-segment-button class="activated" value="Paid">
<ion-label>Paid</ion-label>
</ion-segment-button> </ion-segment-button>
<ion-segment-button class="activated" value="Free"> <ion-segment-button value="inactive">
<ion-label>Free</ion-label> <ion-label>Inactive</ion-label>
</ion-segment-button> </ion-segment-button>
<ion-segment-button class="activated" value="Top"> <ion-segment-button value="disabled" disabled>
<ion-label>Top</ion-label> <ion-label>Disabled</ion-label>
</ion-segment-button> </ion-segment-button>
</ion-segment> </ion-segment>
<ion-segment class="custom-icon"> <ion-segment class="custom-states-background" value="checked">
<ion-segment-button class="activated segment-facebook" value="facebook"> <ion-segment-button value="checked">
<ion-label>Facebook</ion-label> <ion-label>Checked</ion-label>
<ion-icon name="logo-facebook"></ion-icon>
</ion-segment-button> </ion-segment-button>
<ion-segment-button class="activated segment-instagram" value="instagram"> <ion-segment-button value="inactive">
<ion-label>Instagram</ion-label> <ion-label>Inactive</ion-label>
<ion-icon name="logo-instagram"></ion-icon>
</ion-segment-button> </ion-segment-button>
<ion-segment-button class="activated segment-slack" value="slack"> <ion-segment-button value="disabled" disabled>
<ion-label>Slack</ion-label> <ion-label>Disabled</ion-label>
<ion-icon name="logo-slack"></ion-icon>
</ion-segment-button> </ion-segment-button>
</ion-segment> </ion-segment>
</ion-content> </ion-content>
</ion-app> </ion-app>
@ -153,107 +191,153 @@
margin-bottom: 0; margin-bottom: 0;
} }
pre { /*
border: 1px solid #e6e9ee; * Custom Checked Segment (Paid, Free, Top)
background: white; *
margin: 10px; * This tests that the colors are able to be overridden on
padding: 4px; * a segment inside of a toolbar
line-height: 24px; *
* 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;
} }
code { .ios .custom-checked ion-segment-button {
display: block; --color-checked: white;
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 { .themed {
--ion-toolbar-background: #3880ff; --ion-toolbar-background: #3880ff;
--ion-toolbar-color: #fff; --ion-toolbar-color: #fff;
--ion-toolbar-color-activated: #fff;
--ion-toolbar-color-unchecked: rgba(255, 255, 255, .6); /* Segment */
--ion-toolbar-color-checked: #fff; --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: papayawhip;
--background-checked: navy;
--border-color: navy; --color: purple;
--border-color-checked: navy;
--border-color-disabled: navy;
--color: navy;
--color-activated: purple;
--color-checked: papayawhip;
--color-disabled: rgba(0, 0, 0, 0.3); --color-disabled: rgba(0, 0, 0, 0.3);
} }
/* Custom Icon Segment MD */ .ios .custom-active {
.md .custom-icon { --color-checked: papayawhip;
--indicator-color: lightgray; --indicator-color: navy;
} }
.md .segment-facebook { .md .custom-active {
--background-hover: rgba(59, 89, 153, .04); --color-checked: navy;
--color-activated: #3b5999; --indicator-color: navy;
--color-checked: #3b5999;
}
.md .segment-instagram {
--background-hover: rgba(228, 64, 95, .04);
--color-activated: #e4405f;
--color-checked: #e4405f;
}
.md .segment-slack {
--background-hover: rgba(58, 175, 133, .04);
--color-activated: #3aaf85;
--color-checked: #3aaf85;
}
/* Custom Icon Segment iOS */
.ios .custom-icon ion-segment-button {
--border-width: 0;
}
.ios .segment-facebook {
--color: #3b5999;
--color-checked: #ffffff;
--background-hover: rgba(59, 89, 153, .1);
--background-activated: rgba(59, 89, 153, .16);
--background-checked: #3b5999;
}
.ios .segment-instagram {
--color: #e4405f;
--color-checked: #ffffff;
--background-hover: rgba(228, 64, 95, .1);
--background-activated: rgba(228, 64, 95, .16);
--background-checked: #e4405f;
}
.ios .segment-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 Segment (Facebook, Instagram, Slack)
*/
.custom-icon ion-icon { .custom-icon ion-icon {
font-size: 44px; font-size: 44px;
} }
/*
* 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-button-instagram {
--background-hover: rgb(228, 64, 95, .04);
--color-checked: #e4405f;
--indicator-color: #e4405f;
}
.md .segment-button-slack {
--background-hover: rgb(58, 175, 133, .04);
--color-checked: #3aaf85;
--indicator-color: #3aaf85;
}
/*
* iOS Custom Icon Segment (Facebook, Instagram, Slack)
*/
.ios .segment-button-facebook {
--color: #3a3d46;
--color-checked: #ffffff;
--indicator-color: #3a3d46;
}
.ios .segment-button-instagram {
--color: #e4405f;
--color-checked: #ffffff;
--indicator-color: #e4405f;
}
.ios .segment-button-slack {
--color: #3aaf85;
--color-checked: #ffffff;
--indicator-color: #3aaf85;
}
/* This CSS should not apply */ /* This CSS should not apply */
.custom-color { .custom-color {
--background: purple; --background: purple;
--color: blue; --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;
}
</style> </style>
</html> </html>

View File

@ -11,7 +11,7 @@
<script nomodule src="../../../../../dist/ionic/ionic.js"></script> <script nomodule src="../../../../../dist/ionic/ionic.js"></script>
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script></head> <script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script></head>
<body onLoad="setLayout()"> <body>
<ion-app> <ion-app>
<ion-header> <ion-header>
<ion-toolbar color="tertiary"> <ion-toolbar color="tertiary">
@ -38,7 +38,7 @@
<ion-content> <ion-content>
<!-- Label only --> <!-- Label only -->
<ion-segment> <ion-segment id="multi-color">
<ion-segment-button checked> <ion-segment-button checked>
<ion-label>Item One</ion-label> <ion-label>Item One</ion-label>
</ion-segment-button> </ion-segment-button>
@ -96,7 +96,7 @@
</ion-segment> </ion-segment>
<!-- Icon start --> <!-- Icon start -->
<ion-segment scrollable> <ion-segment>
<ion-segment-button checked layout="icon-start"> <ion-segment-button checked layout="icon-start">
<ion-label>Item One</ion-label> <ion-label>Item One</ion-label>
<ion-icon name="call"></ion-icon> <ion-icon name="call"></ion-icon>
@ -112,7 +112,7 @@
</ion-segment> </ion-segment>
<!-- Icon end --> <!-- Icon end -->
<ion-segment scrollable> <ion-segment>
<ion-segment-button checked layout="icon-end"> <ion-segment-button checked layout="icon-end">
<ion-icon name="call"></ion-icon> <ion-icon name="call"></ion-icon>
<ion-label>Item One</ion-label> <ion-label>Item One</ion-label>
@ -144,7 +144,7 @@
</ion-segment> </ion-segment>
<!-- Color --> <!-- Color -->
<ion-segment scrollable color="secondary"> <ion-segment color="secondary">
<ion-segment-button checked layout="icon-end"> <ion-segment-button checked layout="icon-end">
<ion-icon name="call"></ion-icon> <ion-icon name="call"></ion-icon>
<ion-label>Item One</ion-label> <ion-label>Item One</ion-label>
@ -188,14 +188,30 @@
<style> <style>
ion-content { ion-content {
--background: #e5e5e5;
--padding-top: 16px; --padding-top: 16px;
} }
ion-content ion-segment { ion-content ion-segment {
background: white;
margin-bottom: 12px; margin-bottom: 12px;
} }
.md ion-content {
--background: #e5e5e5;
}
.md ion-content ion-segment {
background: white;
}
#multi-color ion-segment-button:first-of-type {
--indicator-color: rgba(255, 0, 0, 0.5);
}
#multi-color ion-segment-button:nth-of-type(2) {
--indicator-color: rgba(0, 255, 0, 0.5);
}
#multi-color ion-segment-button:nth-of-type(3) {
--indicator-color: rgba(0, 0, 255, 0.5);
}
</style> </style>
<script> <script>
@ -209,6 +225,11 @@
segmentButtons[i].layout = (mode === 'ios') ? 'icon-hide' : 'icon-top'; segmentButtons[i].layout = (mode === 'ios') ? 'icon-hide' : 'icon-top';
} }
} }
document.addEventListener('DOMContentLoaded', () => {
requestAnimationFrame(() => {
setLayout();
});
});
</script> </script>
</ion-app> </ion-app>

View File

@ -4,14 +4,16 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Segment - Standalone</title> <title>Segment - Standalone</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"> <meta name="viewport"
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
<link href="../../../../../css/core.css" rel="stylesheet"> <link href="../../../../../css/core.css" rel="stylesheet">
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet"> <link href="../../../../../scripts/testing/styles.css" rel="stylesheet">
<script src="../../../../../scripts/testing/scripts.js"></script> <script src="../../../../../scripts/testing/scripts.js"></script>
<script nomodule src="../../../../../dist/ionic/ionic.js"></script> <script nomodule src="../../../../../dist/ionic/ionic.js"></script>
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script></head> <script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
</head>
<body> <body onLoad="onLoad()">
<ion-toolbar> <ion-toolbar>
<ion-segment value="Free"> <ion-segment value="Free">
<ion-segment-button value="Paid"> <ion-segment-button value="Paid">
@ -77,15 +79,15 @@
</ion-segment> </ion-segment>
<ion-segment class="custom-icon" value="instagram"> <ion-segment class="custom-icon" value="instagram">
<ion-segment-button class="segment-facebook" value="facebook"> <ion-segment-button class="segment-button-facebook" value="facebook">
<ion-label>Facebook</ion-label> <ion-label>Facebook</ion-label>
<ion-icon name="logo-facebook"></ion-icon> <ion-icon name="logo-facebook"></ion-icon>
</ion-segment-button> </ion-segment-button>
<ion-segment-button class="segment-instagram" value="instagram"> <ion-segment-button class="segment-button-instagram" value="instagram">
<ion-label>Instagram</ion-label> <ion-label>Instagram</ion-label>
<ion-icon name="logo-instagram"></ion-icon> <ion-icon name="logo-instagram"></ion-icon>
</ion-segment-button> </ion-segment-button>
<ion-segment-button class="segment-slack" value="slack"> <ion-segment-button class="segment-button-slack" value="slack">
<ion-label>Slack</ion-label> <ion-label>Slack</ion-label>
<ion-icon name="logo-slack"></ion-icon> <ion-icon name="logo-slack"></ion-icon>
</ion-segment-button> </ion-segment-button>
@ -145,6 +147,29 @@
results.innerHTML = value.charAt(0).toUpperCase() + value.slice(1); results.innerHTML = value.charAt(0).toUpperCase() + value.slice(1);
segment.value = value; 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`);
}
}
</script> </script>
<style> <style>
@ -188,23 +213,64 @@
--color: navy; --color: navy;
} }
.custom-icon { /*
--indicator-color: lightgray; * Custom Icon Segment (Facebook, Instagram, Slack)
*/
.custom-icon ion-icon {
font-size: 44px;
} }
.segment-facebook { /*
--background-hover: rgba(59, 89, 153, .04); * MD Custom Icon Segment (Facebook, Instagram, Slack)
--color-checked: #3b5999; */
.md .segment-facebook-checked .segment-button-facebook {
--color-checked: #3a3d46;
--indicator-color: #3a3d46;
} }
.segment-instagram { .md .segment-instagram-checked .segment-button-instagram {
--background-hover: rgba(228, 64, 95, .04);
--color-checked: #e4405f; --color-checked: #e4405f;
--indicator-color: #e4405f;
} }
.segment-slack { .md .segment-slack-checked .segment-button-slack {
--background-hover: rgba(58, 175, 133, .04);
--color-checked: #3aaf85; --color-checked: #3aaf85;
--indicator-color: #3aaf85;
}
/*
* iOS Custom Icon Segment (Facebook, Instagram, Slack)
*/
.ios .custom-icon {
--indicator-color: transparent;
--indicator-transition: none;
}
.ios .segment-facebook-checked .segment-button-facebook {
--background-checked: #3a3d46;
--color-checked: #ffffff;
}
.ios .segment-instagram-checked .segment-button-instagram {
--background-checked: #e4405f;
--color-checked: #ffffff;
}
.ios .segment-slack-checked .segment-button-slack {
--background-checked: #3aaf85;
--color-checked: #ffffff;
}
.ios .segment-button-facebook {
--color: #3a3d46;
}
.ios .segment-button-instagram {
--color: #e4405f;
}
.ios .segment-button-slack {
--color: #3aaf85;
} }
.custom-indicator { .custom-indicator {
@ -215,6 +281,7 @@
.large-icon ion-icon { .large-icon ion-icon {
font-size: 44px; font-size: 44px;
} }
</style> </style>
</html> </html>

View File

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

View File

@ -16,7 +16,7 @@
<ion-header> <ion-header>
<ion-toolbar> <ion-toolbar>
<ion-title>Segment - Colors</ion-title> <ion-title>Segment - Toolbar</ion-title>
</ion-toolbar> </ion-toolbar>
<ion-toolbar> <ion-toolbar>
@ -114,6 +114,17 @@
</ion-segment> </ion-segment>
</ion-toolbar> </ion-toolbar>
<ion-toolbar color="primary">
<ion-segment color="light">
<ion-segment-button value="sunny">
Sunny
</ion-segment-button>
<ion-segment-button value="rainy" checked>
Rainy
</ion-segment-button>
</ion-segment>
</ion-toolbar>
<!-- Label only --> <!-- Label only -->
<ion-toolbar color="primary"> <ion-toolbar color="primary">
<ion-segment> <ion-segment>
@ -264,13 +275,17 @@
} }
.ios .themed { .ios .themed {
--ion-toolbar-color-unchecked: #fff; --ion-toolbar-segment-background: rgba(255, 255, 255, 0.11);
--ion-toolbar-color-checked: #3880ff; --ion-toolbar-segment-color: #fff;
--ion-toolbar-segment-color-checked: #3880ff;
} }
.md .themed { .md .themed {
--ion-toolbar-color-unchecked: rgba(255, 255, 255, 0.6); --ion-toolbar-segment-color: rgba(255, 255, 255, 0.6);
--ion-toolbar-color-checked: #fff; --ion-toolbar-segment-color-checked: #fff;
--ion-toolbar-segment-indicator-color: #fff;
} }
</style> </style>

View File

@ -29,11 +29,10 @@
// Toolbar: Segment // Toolbar: Segment
// -------------------------------------------------- // --------------------------------------------------
:host(.toolbar-segment) { :host(.toolbar-segment) .toolbar-content {
--min-height: auto; display: inline-flex;
} }
// Toolbar: Searchbar // Toolbar: Searchbar
// -------------------------------------------------- // --------------------------------------------------

View File

@ -1,60 +1,54 @@
:root { :root {
/* Custom Toolbar CSS (optional) */ --ion-color-primary: #428cff;
/* --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-rgb: 66,140,255; --ion-color-primary-rgb: 66,140,255;
--ion-color-primary-contrast: #ffffff; --ion-color-primary-contrast: #ffffff;
--ion-color-primary-contrast-rgb: 255,255,255; --ion-color-primary-contrast-rgb: 255,255,255;
--ion-color-primary-shade: #3a7be0; --ion-color-primary-shade: #3a7be0;
--ion-color-primary-tint: #5598ff; --ion-color-primary-tint: #5598ff;
--ion-color-secondary: #50C8FF; --ion-color-secondary: #50c8ff;
--ion-color-secondary-rgb: 80,200,255; --ion-color-secondary-rgb: 80,200,255;
--ion-color-secondary-contrast: #ffffff; --ion-color-secondary-contrast: #ffffff;
--ion-color-secondary-contrast-rgb: 255,255,255; --ion-color-secondary-contrast-rgb: 255,255,255;
--ion-color-secondary-shade: #46b0e0; --ion-color-secondary-shade: #46b0e0;
--ion-color-secondary-tint: #62ceff; --ion-color-secondary-tint: #62ceff;
--ion-color-tertiary: #6A64FF; --ion-color-tertiary: #6a64ff;
--ion-color-tertiary-rgb: 106,100,255; --ion-color-tertiary-rgb: 106,100,255;
--ion-color-tertiary-contrast: #ffffff; --ion-color-tertiary-contrast: #ffffff;
--ion-color-tertiary-contrast-rgb: 255,255,255; --ion-color-tertiary-contrast-rgb: 255,255,255;
--ion-color-tertiary-shade: #5d58e0; --ion-color-tertiary-shade: #5d58e0;
--ion-color-tertiary-tint: #7974ff; --ion-color-tertiary-tint: #7974ff;
--ion-color-success: #2FDF75; --ion-color-success: #2fdf75;
--ion-color-success-rgb: 47,223,117; --ion-color-success-rgb: 47,223,117;
--ion-color-success-contrast: #000000; --ion-color-success-contrast: #000000;
--ion-color-success-contrast-rgb: 0,0,0; --ion-color-success-contrast-rgb: 0,0,0;
--ion-color-success-shade: #29c467; --ion-color-success-shade: #29c467;
--ion-color-success-tint: #44e283; --ion-color-success-tint: #44e283;
--ion-color-warning: #FFD534; --ion-color-warning: #ffd534;
--ion-color-warning-rgb: 255,213,52; --ion-color-warning-rgb: 255,213,52;
--ion-color-warning-contrast: #000000; --ion-color-warning-contrast: #000000;
--ion-color-warning-contrast-rgb: 0,0,0; --ion-color-warning-contrast-rgb: 0,0,0;
--ion-color-warning-shade: #e0bb2e; --ion-color-warning-shade: #e0bb2e;
--ion-color-warning-tint: #ffd948; --ion-color-warning-tint: #ffd948;
--ion-color-danger: #FF4961; --ion-color-danger: #ff4961;
--ion-color-danger-rgb: 255,73,97; --ion-color-danger-rgb: 255,73,97;
--ion-color-danger-contrast: #ffffff; --ion-color-danger-contrast: #ffffff;
--ion-color-danger-contrast-rgb: 255,255,255; --ion-color-danger-contrast-rgb: 255,255,255;
--ion-color-danger-shade: #e04055; --ion-color-danger-shade: #e04055;
--ion-color-danger-tint: #ff5b71; --ion-color-danger-tint: #ff5b71;
--ion-color-dark: #F4F5F8; --ion-color-dark: #f4f5f8;
--ion-color-dark-rgb: 244,245,248; --ion-color-dark-rgb: 244,245,248;
--ion-color-dark-contrast: #000000; --ion-color-dark-contrast: #000000;
--ion-color-dark-contrast-rgb: 0,0,0; --ion-color-dark-contrast-rgb: 0,0,0;
--ion-color-dark-shade: #d7d8da; --ion-color-dark-shade: #d7d8da;
--ion-color-dark-tint: #f5f6f9; --ion-color-dark-tint: #f5f6f9;
--ion-color-medium: #989AA2; --ion-color-medium: #989aa2;
--ion-color-medium-rgb: 152,154,162; --ion-color-medium-rgb: 152,154,162;
--ion-color-medium-contrast: #000000; --ion-color-medium-contrast: #000000;
--ion-color-medium-contrast-rgb: 0,0,0; --ion-color-medium-contrast-rgb: 0,0,0;
@ -69,7 +63,6 @@
--ion-color-light-tint: #383a3e; --ion-color-light-tint: #383a3e;
} }
/* Customize the Toolbar Segment by Mode */
.ios { .ios {
--ion-background-color: #000000; --ion-background-color: #000000;
--ion-background-color-rgb: 0,0,0; --ion-background-color-rgb: 0,0,0;
@ -100,8 +93,7 @@
--ion-item-background: #1c1c1c; --ion-item-background: #1c1c1c;
--ion-item-background-activated: #313131; --ion-item-background-activated: #313131;
/* --ion-toolbar-color-unchecked: limegreen; --ion-toolbar-background: #000;
--ion-toolbar-color-checked: limegreen; */
} }
.md { .md {
@ -133,7 +125,5 @@
--ion-color-step-900: #e7e7e7; --ion-color-step-900: #e7e7e7;
--ion-color-step-950: #f3f3f3; --ion-color-step-950: #f3f3f3;
--ion-item-background: #1A1B1E; --ion-item-background: #1a1b1e;
/* --ion-toolbar-color-unchecked: rgba(255, 255, 255, .6);
--ion-toolbar-color-checked: #fff; */
} }

View File

@ -78,4 +78,20 @@
--ion-item-background: #fff; --ion-item-background: #fff;
--ion-item-border-color: #5bff76; --ion-item-border-color: #5bff76;
--ion-item-background-activated: #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);
} }