From caf6dee95995e151de6305b51cdef9f81e7b8846 Mon Sep 17 00:00:00 2001 From: Brandy Carney Date: Fri, 30 Jun 2017 14:40:33 -0400 Subject: [PATCH] feat(segment): add segment component --- .../segment-button/segment-button.tsx | 125 ++++++++++ .../src/components/segment/segment.ios.scss | 231 ++++++++++++++++++ .../src/components/segment/segment.md.scss | 145 +++++++++++ .../core/src/components/segment/segment.scss | 28 +++ .../core/src/components/segment/segment.tsx | 130 ++++++++++ .../src/components/segment/segment.wp.scss | 133 ++++++++++ packages/core/src/utils/interfaces.ts | 7 + packages/core/stencil.config.js | 1 + 8 files changed, 800 insertions(+) create mode 100644 packages/core/src/components/segment-button/segment-button.tsx create mode 100644 packages/core/src/components/segment/segment.ios.scss create mode 100644 packages/core/src/components/segment/segment.md.scss create mode 100644 packages/core/src/components/segment/segment.scss create mode 100644 packages/core/src/components/segment/segment.tsx create mode 100644 packages/core/src/components/segment/segment.wp.scss diff --git a/packages/core/src/components/segment-button/segment-button.tsx b/packages/core/src/components/segment-button/segment-button.tsx new file mode 100644 index 0000000000..b260a43ec1 --- /dev/null +++ b/packages/core/src/components/segment-button/segment-button.tsx @@ -0,0 +1,125 @@ +import { Component, h, Ionic, Prop, State } from '@stencil/core'; + +import { CssClassObject } from '../../util/interfaces'; +import { createThemedClasses } from '../../util/theme'; + + +/** + * @name SegmentButton + * @description + * The child buttons of the `ion-segment` component. Each `ion-segment-button` must have a value. + * + * @usage + * + * ```html + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * Friends + * + * + * Enemies + * + * + * + * ``` + * + * + * @demo /docs/demos/src/segment/ + * @see {@link /docs/components#segment Segment Component Docs} + * @see {@link /docs/api/components/segment/Segment/ Segment API Docs} + */ +@Component({ + tag: 'ion-segment-button' +}) +export class SegmentButton { + styleTmr: any; + + mode: string; + color: string; + + @State() activated: boolean = false; + + /* + * @input {boolean} If true, the button is selected. Default false. + */ + @Prop({ state: true }) checked: boolean = false; + + /* + * @input {boolean} If true, the user cannot interact with this element. Default false. + */ + @Prop({ state: true }) disabled: boolean = false; + + /** + * @input {string} the value of the segment button. Required. + */ + @Prop({ state: true }) value: string; + + segmentButtonClick(ev: UIEvent) { + ev.preventDefault(); + ev.stopPropagation(); + + console.log('in segment button click'); + this.emitClick(); + } + + /** + * Emit the click event to the parent segment + */ + private emitClick() { + clearTimeout(this.styleTmr); + + this.styleTmr = setTimeout(() => { + Ionic.emit(this, 'ionClick', { + detail: { + 'segmentButton': this, + } + }); + }); + } + + /** + * @hidden + * Get the element classes to add to the child element + */ + getElementClassList() { + let classList = [].concat( + this.disabled ? 'segment-button-disabled' : [], + this.activated ? 'segment-activated' : [], + ); + + return classList; + } + + render() { + const segmentButtonCss = createThemedClasses(this.mode, this.color, 'segment-button'); + + var segmentButtonClasses: CssClassObject = [] + .concat( + this.getElementClassList() + ) + .reduce((prevValue, cssClass) => { + prevValue[cssClass] = true; + return prevValue; + }, {}); + + segmentButtonClasses = Object.assign(segmentButtonClasses, segmentButtonCss); + + return [ + + ]; + } +} diff --git a/packages/core/src/components/segment/segment.ios.scss b/packages/core/src/components/segment/segment.ios.scss new file mode 100644 index 0000000000..7819dc5474 --- /dev/null +++ b/packages/core/src/components/segment/segment.ios.scss @@ -0,0 +1,231 @@ +@import "../../themes/ionic.globals.ios"; +@import "./segment"; + +// iOS Segment +// -------------------------------------------------- + +/// @prop - Background of the segment button +$segment-button-ios-background-color: transparent !default; + +/// @prop - Background of the activated segment button +$segment-button-ios-background-color-activated: $toolbar-ios-active-color !default; + +/// @prop - Text color of the segment button +$segment-button-ios-text-color: color-contrast($colors-ios, $segment-button-ios-background-color-activated) !default; + +/// @prop - Transition of the activated segment button +$segment-button-ios-transition-activated: 100ms all linear !default; + +/// @prop - Transition of the segment button on hover +$segment-button-ios-transition-hover: 100ms all linear !default; + +/// @prop - Transition of the segment button when pressed +$segment-button-ios-transition-active: 100ms all linear !default; + +/// @prop - Opacity of the segment button on hover +$segment-button-ios-opacity-hover: .1 !default; + +/// @prop - Opacity of the segment button when pressed +$segment-button-ios-opacity-active: .16 !default; + +/// @prop - Opacity of the activated segment button +$segment-button-ios-opacity-activated: 1 !default; + +/// @prop - Opacity of the disabled segment button +$segment-button-ios-opacity-disabled: .3 !default; + +/// @prop - Border width of the segment button +$segment-button-ios-border-width: 1px !default; + +/// @prop - Height of the segment button +$segment-button-ios-height: 3.2rem !default; + +/// @prop - Line height of the segment button +$segment-button-ios-line-height: 2.8rem !default; + +/// @prop - Font size of the segment button +$segment-button-ios-font-size: 1.3rem !default; + +/// @prop - Border radius of the segment button +$segment-button-ios-border-radius: 4px !default; + +/// @prop - Size of an icon in the segment button +$segment-button-ios-icon-size: 2.6rem !default; + +/// @prop - Line height of an icon in the segment button +$segment-button-ios-icon-line-height: 2.8rem !default; + +/// @prop - Max width of the segment button in a toolbar +$segment-button-ios-toolbar-button-max-width: 100px !default; + +/// @prop - Height of the segment button in a toolbar +$segment-button-ios-toolbar-button-height: 2.6rem !default; + +/// @prop - Line height of the segment button in a toolbar +$segment-button-ios-toolbar-line-height: 2.2rem !default; + +/// @prop - Font size of the segment button in a toolbar +$segment-button-ios-toolbar-font-size: 1.2rem !default; + +/// @prop - Size of an icon in the segment button in a toolbar +$segment-button-ios-toolbar-icon-size: 2.2rem !default; + +/// @prop - Line height of an icon in the segment button in a toolbar +$segment-button-ios-toolbar-icon-line-height: 2.4rem !default; + +.segment-ios ion-segment-button { + display: flex; + flex: 1; + + width: 0; + + &:first-of-type .segment-button { + @include border-radius($segment-button-ios-border-radius, 0, 0, $segment-button-ios-border-radius); + @include margin-horizontal(null, 0); + } + + &:not(:first-of-type) .segment-button { + border-left-width: 0; + } + + &:last-of-type .segment-button { + @include border-radius(0, $segment-button-ios-border-radius, $segment-button-ios-border-radius, 0); + @include margin-horizontal(0, null); + + border-left-width: 0; + } +} + +.segment-ios .segment-button { + flex: 1; + + height: $segment-button-ios-height; + + border-width: $segment-button-ios-border-width; + border-style: solid; + border-color: $segment-button-ios-background-color-activated; + + font-size: $segment-button-ios-font-size; + line-height: $segment-button-ios-line-height; + + color: $segment-button-ios-background-color-activated; + background-color: $segment-button-ios-background-color; + + ion-icon { + font-size: $segment-button-ios-icon-size; + line-height: $segment-button-ios-icon-line-height; + } + + &.segment-activated { + color: $segment-button-ios-text-color; + background-color: $segment-button-ios-background-color-activated; + opacity: $segment-button-ios-opacity-activated; + transition: $segment-button-ios-transition-activated; + } + + &:hover:not(.segment-activated) { + background-color: rgba($segment-button-ios-background-color-activated, $segment-button-ios-opacity-hover); + transition: $segment-button-ios-transition-hover; + } + + &:active:not(.segment-activated) { + background-color: rgba($segment-button-ios-background-color-activated, $segment-button-ios-opacity-active); + transition: $segment-button-ios-transition-active; + } +} + +[dir="rtl"] .segment-ios ion-segment-button { + &:first-of-type .segment-button { + border-left-width: 0; + } + + &:last-of-type .segment-button { + border-left-width: $segment-button-ios-border-width; + } +} + + +.segment-ios.segment-disabled { + opacity: .4; + + pointer-events: none; +} + +.segment-ios .segment-button-disabled { + color: rgba($segment-button-ios-background-color-activated, $segment-button-ios-opacity-disabled); + + pointer-events: none; +} + + +// iOS Segment in Toolbar +// -------------------------------------------------- + +.toolbar-ios .segment-ios { + @include position(0, 0, 0, 0); + + position: absolute; +} + +.toolbar-ios ion-segment-button { + max-width: $segment-button-ios-toolbar-button-max-width; +} + +.toolbar-ios .segment-button { + height: $segment-button-ios-toolbar-button-height; + + font-size: $segment-button-ios-toolbar-font-size; + line-height: $segment-button-ios-toolbar-line-height; + + ion-icon { + font-size: $segment-button-ios-toolbar-icon-size; + line-height: $segment-button-ios-toolbar-icon-line-height; + } +} + + +// iOS Segment Button Mixin +// -------------------------------------------------- + +@mixin ios-segment-button($color-name, $color-base, $color-contrast) { + + .segment-ios-#{$color-name} { + + .segment-button { + border-color: $color-base; + color: $color-base; + + &:hover:not(.segment-activated) { + background-color: rgba($color-base, $segment-button-ios-opacity-hover); + } + + &:active:not(.segment-activated) { + background-color: rgba($color-base, $segment-button-ios-opacity-active); + } + + &.segment-activated { + color: $color-contrast; + background-color: $color-base; + } + } + + .segment-button-disabled { + color: rgba($color-base, $segment-button-ios-opacity-disabled); + } + + } + +} + + +// iOS Segment Color Generation +// -------------------------------------------------- + +@each $color-name, $color-base, $color-contrast in get-colors($colors-ios) { + + @include ios-segment-button($color-name, $color-base, $color-contrast); + + .toolbar-ios-#{$color-name} .segment-ios .segment-button.segment-activated { + color: $color-base; + } +} \ No newline at end of file diff --git a/packages/core/src/components/segment/segment.md.scss b/packages/core/src/components/segment/segment.md.scss new file mode 100644 index 0000000000..b701f1aef7 --- /dev/null +++ b/packages/core/src/components/segment/segment.md.scss @@ -0,0 +1,145 @@ +@import "../../themes/ionic.globals.md"; +@import "./segment"; + +// Material Design Segment +// -------------------------------------------------- + +/// @prop - Text color of the activated segment button +$segment-button-md-text-color-activated: $toolbar-md-active-color !default; + +/// @prop - Border color of the activated segment button +$segment-button-md-border-color-activated: $toolbar-md-active-color !default; + +/// @prop - Width of the bottom border on the segment button +$segment-button-md-border-bottom-width: 2px !default; + +/// @prop - Color of the bottom border on the segment button +$segment-button-md-border-bottom-color: rgba(#000, .10) !default; + +/// @prop - Opacity of the segment button +$segment-button-md-opacity: .7 !default; + +/// @prop - Opacity of the activated segment button +$segment-button-md-opacity-activated: 1 !default; + +/// @prop - Opacity of the disabled segment button +$segment-button-md-opacity-disabled: .3 !default; + +// deprecated +$segment-button-md-padding: null !default; + +/// @prop - Padding top of the segment button +$segment-button-md-padding-top: 0 !default; + +/// @prop - Padding end of the segment button +$segment-button-md-padding-end: 6px !default; + +/// @prop - Padding bottom of the segment button +$segment-button-md-padding-bottom: $segment-button-md-padding-top !default; + +/// @prop - Padding start of the segment button +$segment-button-md-padding-start: $segment-button-md-padding-end !default; + +/// @prop - Height of the segment button +$segment-button-md-height: 4.2rem !default; + +/// @prop - Line height of the segment button +$segment-button-md-line-height: 4rem !default; + +/// @prop - Font size of the segment button +$segment-button-md-font-size: 1.2rem !default; + +/// @prop - Size of an icon in the segment button +$segment-button-md-icon-size: 2.6rem !default; + +/// @prop - Line height of an icon in the segment button +$segment-button-md-icon-line-height: $segment-button-md-line-height !default; + + +.segment-md ion-segment-button { + display: flex; + flex: 1; +} + +.segment-md .segment-button { + flex: 1; + + width: 0; + height: $segment-button-md-height; + + border-bottom-width: $segment-button-md-border-bottom-width; + border-bottom-style: solid; + border-bottom-color: $segment-button-md-border-bottom-color; + + font-size: $segment-button-md-font-size; + font-weight: 500; + line-height: $segment-button-md-line-height; + text-transform: uppercase; + + color: $segment-button-md-text-color-activated; + background-color: transparent; + opacity: $segment-button-md-opacity; + transition: 100ms all linear; + + @include deprecated-variable(padding, $segment-button-md-padding) { + @include padding($segment-button-md-padding-top, $segment-button-md-padding-end, $segment-button-md-padding-bottom, $segment-button-md-padding-start); + } + + ion-icon { + font-size: $segment-button-md-icon-size; + line-height: $segment-button-md-icon-line-height; + } + + &.activated, + &.segment-activated { + border-color: $segment-button-md-border-color-activated; + opacity: $segment-button-md-opacity-activated; + } +} + +.segment-md.segment-disabled, +.segment-md .segment-button-disabled { + opacity: $segment-button-md-opacity-disabled; + + pointer-events: none; +} + +.toolbar { + + .segment-md { + @include margin(0, auto); + } + + .segment-md .segment-button.activated, + .segment-md .segment-button.segment-activated { + opacity: 1; + } + +} + + +// Material Design Segment Button Mixin +// -------------------------------------------------- + +@mixin md-segment-button($color-name, $color-base, $color-contrast) { + + .segment-md-#{$color-name} .segment-button { + color: $color-base; + + &.activated, + &.segment-activated { + border-color: $color-base; + color: $color-base; + opacity: 1; + } + } + +} + + +// Material Design Segment Color Generation +// -------------------------------------------------- + +@each $color-name, $color-base, $color-contrast in get-colors($colors-md) { + @include md-segment-button($color-name, $color-base, $color-contrast); +} \ No newline at end of file diff --git a/packages/core/src/components/segment/segment.scss b/packages/core/src/components/segment/segment.scss new file mode 100644 index 0000000000..7303cafe26 --- /dev/null +++ b/packages/core/src/components/segment/segment.scss @@ -0,0 +1,28 @@ +@import "../../themes/ionic.globals"; + +// Segment +// -------------------------------------------------- + + +ion-segment { + display: flex; + + flex: 1; + align-items: center; + justify-content: center; + + width: 100%; +} + +.segment-button { + @include margin-horizontal(0); + @include text-align(center); + + position: relative; + display: block; + overflow: hidden; + + text-overflow: ellipsis; + white-space: nowrap; + cursor: pointer; +} \ No newline at end of file diff --git a/packages/core/src/components/segment/segment.tsx b/packages/core/src/components/segment/segment.tsx new file mode 100644 index 0000000000..dee82bba2a --- /dev/null +++ b/packages/core/src/components/segment/segment.tsx @@ -0,0 +1,130 @@ +import { Component, h, Listen, Prop, PropDidChange } from '@stencil/core'; +import { SegmentButtonEvent, VNodeData } from '../../util/interfaces'; + + +/** + * @name Segment + * @description + * A Segment is a group of buttons, sometimes known as Segmented Controls, that allow the user to interact with a compact group of a number of controls. + * Segments provide functionality similar to tabs, selecting one will unselect all others. You should use a tab bar instead of a segmented control when you want to let the user move back and forth between distinct pages in your app. + * You could use Angular's `ngModel` or `FormBuilder` API. For an overview on how `FormBuilder` works, checkout [Angular Forms](http://learnangular2.com/forms/), or [Angular FormBuilder](https://angular.io/docs/ts/latest/api/forms/index/FormBuilder-class.html) + * + * + * ```html + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * Friends + * + * + * Enemies + * + * + * + * + *
+ * + * + * Standard + * + * + * Hybrid + * + * + * Satellite + * + * + *
+ *
+ * ``` + * + * + * @demo /docs/demos/src/segment/ + * @see {@link /docs/components#segment Segment Component Docs} + * @see [Angular Forms](http://learnangular2.com/forms/) + */ +@Component({ + tag: 'ion-segment', + styleUrls: { + ios: 'segment.ios.scss', + md: 'segment.md.scss', + wp: 'segment.wp.scss' + }, + host: { + theme: 'segment' + } +}) +export class Segment { + buttons: any; + $el: any; + + @Prop({ state: true }) disabled: boolean = false; + + @Prop({ state: true }) value: string; + + @PropDidChange('value') + changed(val: string) { + this.selectButton(val); + } + + ionViewDidLoad() { + this.buttons = this.$el.querySelectorAll('ion-segment-button'); + + for (var i = 0; i < this.buttons.length; i++) { + const button = this.buttons[i].$instance; + + button.activated = (button.value === this.value); + + // If there is no value set on the segment and a button + // is checked we should activate it + if (!this.value && button.checked) { + button.activated = button.checked; + } + } + } + + @Listen('ionClick') + segmentClick(ev: SegmentButtonEvent) { + let selectedButton = ev.detail.segmentButton; + + this.value = selectedButton.value; + this.selectButton(this.value); + } + + selectButton(val: string) { + for (var i = 0; i < this.buttons.length; i++) { + const button = this.buttons[i].$instance; + button.activated = (button.value === val); + } + + // returning true tells the renderer to queue an update + return true; + } + + hostData(): VNodeData { + return { + class: { + 'segment-disabled': this.disabled + } + }; + } + + render() { + return ; + } +} diff --git a/packages/core/src/components/segment/segment.wp.scss b/packages/core/src/components/segment/segment.wp.scss new file mode 100644 index 0000000000..20b93f9665 --- /dev/null +++ b/packages/core/src/components/segment/segment.wp.scss @@ -0,0 +1,133 @@ +@import "../../themes/ionic.globals.wp"; +@import "./segment"; + +// Windows Segment +// -------------------------------------------------- + +/// @prop - Background of the segment button +$segment-button-wp-background-color: transparent !default; + +/// @prop - Text color of the activated segment button +$segment-button-wp-text-color-activated: $toolbar-wp-text-color !default; + +// deprecated +$segment-button-wp-padding: null !default; + +/// @prop - Padding top of the segment button +$segment-button-wp-padding-top: 0 !default; + +/// @prop - Padding end of the segment button +$segment-button-wp-padding-end: 6px !default; + +/// @prop - Padding bottom of the segment button +$segment-button-wp-padding-bottom: $segment-button-wp-padding-top !default; + +/// @prop - Padding start of the segment button +$segment-button-wp-padding-start: $segment-button-wp-padding-end !default; + +/// @prop - Height of the segment button +$segment-button-wp-height: 4rem !default; + +/// @prop - Line height of the segment button +$segment-button-wp-line-height: 4rem !default; + +/// @prop - Font size of the segment button +$segment-button-wp-font-size: 1.3rem !default; + +/// @prop - Text transform of the segment button +$segment-button-wp-text-transform: uppercase !default; + +/// @prop - Font weight of the segment button +$segment-button-wp-font-weight: bold !default; + +/// @prop - Opacity of the segment button +$segment-button-wp-opacity: .5 !default; + +/// @prop - Opacity of the activated segment button +$segment-button-wp-opacity-activated: 1 !default; + +/// @prop - Opacity of the disabled segment button +$segment-button-wp-opacity-disabled: .3 !default; + +/// @prop - Size of an icon in the segment button +$segment-button-wp-icon-size: 2.6rem !default; + +/// @prop - Line height of an icon in the segment button +$segment-button-wp-icon-line-height: $segment-button-wp-line-height !default; + +/// @prop - Position of the buttons in the segment +$segment-button-wp-buttons-justify-content: flex-start !default; + + +.segment-wp { + justify-content: $segment-button-wp-buttons-justify-content; +} + +.segment-wp .segment-button { + height: $segment-button-wp-height; + + font-size: $segment-button-wp-font-size; + font-weight: $segment-button-wp-font-weight; + line-height: $segment-button-wp-line-height; + + text-transform: $segment-button-wp-text-transform; + + color: $segment-button-wp-text-color-activated; + background-color: $segment-button-wp-background-color; + opacity: $segment-button-wp-opacity; + + @include deprecated-variable(padding, $segment-button-wp-padding) { + @include padding($segment-button-wp-padding-top, $segment-button-wp-padding-end, $segment-button-wp-padding-bottom, $segment-button-wp-padding-start); + } + + &.segment-activated { + opacity: $segment-button-wp-opacity-activated; + } + + ion-icon { + font-size: $segment-button-wp-icon-size; + line-height: $segment-button-wp-icon-line-height; + } +} + +.segment-wp.segment-disabled, +.segment-wp .segment-button-disabled { + opacity: $segment-button-wp-opacity-disabled; + + pointer-events: none; +} + +.toolbar { + + .segment-wp { + @include margin(0, auto); + } + +} + + +// Windows Segment Button Mixin +// -------------------------------------------------- + +@mixin wp-segment-button($color-name, $color-base) { + + .segment-wp-#{$color-name} .segment-button { + color: $color-base; + + &.activated, + &.segment-activated { + border-color: $color-base; + color: $color-base; + opacity: $segment-button-wp-opacity-activated; + } + } + +} + + +// Windows Segment Color Generation +// -------------------------------------------------- + +@each $color-name, $color-base, $color-contrast in get-colors($colors-wp) { + @include wp-segment-button($color-name, $color-base); +} \ No newline at end of file diff --git a/packages/core/src/utils/interfaces.ts b/packages/core/src/utils/interfaces.ts index 15e516b7a7..e00d3feba7 100644 --- a/packages/core/src/utils/interfaces.ts +++ b/packages/core/src/utils/interfaces.ts @@ -211,6 +211,13 @@ export interface GestureCallback { } +export interface SegmentButtonEvent { + detail: { + segmentButton: any; + }; +} + + export interface ScrollDetail extends GestureDetail { scrollTop?: number; scrollLeft?: number; diff --git a/packages/core/stencil.config.js b/packages/core/stencil.config.js index f401c0211e..7e3ed2ed4c 100644 --- a/packages/core/stencil.config.js +++ b/packages/core/stencil.config.js @@ -11,6 +11,7 @@ exports.config = { { components: ['ion-loading', 'ion-loading-controller'] }, { components: ['ion-menu'], priority: 'low' }, { components: ['ion-modal', 'ion-modal-controller'] }, + { components: ['ion-segment', 'ion-segment-button'] }, { components: ['ion-slides', 'ion-slide'] }, { components: ['ion-spinner'] }, { components: ['ion-toggle'] }