From b3a7298a522498b4fc3d4b27addb6aaa37a00944 Mon Sep 17 00:00:00 2001 From: Adam Bradley Date: Wed, 20 Jan 2016 21:41:54 -0600 Subject: [PATCH] refactor(input): place inputs inside of ion-item --- ionic/animations/scroll-to.ts | 30 +- ionic/components.ts | 4 +- ionic/components/card/card.ios.scss | 2 +- ionic/components/card/card.md.scss | 2 +- ionic/components/checkbox/checkbox.ios.scss | 96 +-- ionic/components/checkbox/checkbox.md.scss | 100 ++- ionic/components/checkbox/checkbox.ts | 108 ++-- ionic/components/checkbox/test/basic/e2e.ts | 2 +- .../components/checkbox/test/basic/main.html | 42 +- ionic/components/input/input.ios.scss | 11 +- ionic/components/input/input.md.scss | 15 +- ionic/components/input/input.scss | 90 ++- ionic/components/input/input.ts | 612 ++++++++++-------- .../text-input.ts => input/native-input.ts} | 92 +-- .../input/test/fixed-inline-labels/index.ts | 8 +- .../input/test/fixed-inline-labels/main.html | 103 +-- .../input/test/floating-labels/main.html | 80 +-- .../input/test/form-inputs/main.html | 72 +-- .../input/test/inline-labels/main.html | 92 +-- .../input/test/input-focus/main.html | 100 +-- .../input/test/inset-inputs/main.html | 48 +- .../input/test/placeholder-labels/main.html | 64 +- .../input/test/stacked-labels/main.html | 84 +-- ionic/components/item/item-media.scss | 10 + ionic/components/item/item-sliding.ts | 6 +- ionic/components/item/item.ios.scss | 6 +- ionic/components/item/item.md.scss | 6 +- ionic/components/item/item.scss | 8 +- ionic/components/item/item.ts | 105 ++- ionic/components/item/test/dividers/main.html | 12 +- ionic/components/item/test/text/main.html | 12 +- ionic/components/label/label.ios.scss | 22 +- ionic/components/label/label.md.scss | 20 +- ionic/components/label/label.scss | 25 +- ionic/components/label/label.ts | 76 ++- ionic/components/list/list.md.scss | 2 +- ionic/components/option/option.ts | 20 +- ionic/components/radio/radio-button.ts | 140 ++++ ionic/components/radio/radio-group.ts | 167 +++++ ...s~3d61141c0391dbefcc6565ad94ab918ba2fff603 | 167 +++++ ionic/components/radio/radio-group.ts~HEAD | 167 +++++ ionic/components/radio/radio.ios.scss | 98 +-- ionic/components/radio/radio.md.scss | 110 ++-- ionic/components/radio/radio.ts | 261 -------- ionic/components/radio/test/basic/e2e.ts | 2 +- ionic/components/radio/test/basic/main.html | 58 +- ionic/components/select/select.ios.scss | 10 + ionic/components/select/select.md.scss | 10 + ionic/components/select/select.scss | 42 +- ionic/components/select/select.ts | 132 ++-- .../select/test/multiple-value/main.html | 48 +- .../select/test/single-value/main.html | 103 ++- ionic/components/toggle/test/basic/main.html | 42 +- ionic/components/toggle/toggle.ios.scss | 155 +++-- ionic/components/toggle/toggle.md.scss | 89 +-- ionic/components/toggle/toggle.ts | 228 ++++--- ionic/config/directives.ts | 9 +- ionic/util/form.ts | 8 +- ionic/util/util.scss | 18 +- scripts/snapshot/snapshot.config.js | 4 +- 60 files changed, 2528 insertions(+), 1727 deletions(-) rename ionic/components/{text-input/text-input.ts => input/native-input.ts} (51%) create mode 100644 ionic/components/radio/radio-button.ts create mode 100644 ionic/components/radio/radio-group.ts create mode 100644 ionic/components/radio/radio-group.ts~3d61141c0391dbefcc6565ad94ab918ba2fff603 create mode 100644 ionic/components/radio/radio-group.ts~HEAD delete mode 100644 ionic/components/radio/radio.ts diff --git a/ionic/animations/scroll-to.ts b/ionic/animations/scroll-to.ts index c80516356d..39ee81fae1 100644 --- a/ionic/animations/scroll-to.ts +++ b/ionic/animations/scroll-to.ts @@ -43,6 +43,8 @@ export class ScrollTo { let xDistance = Math.abs(x - fromX); let yDistance = Math.abs(y - fromY); + console.debug(`scrollTo start, y: ${y}, fromY: ${fromY}, yDistance: ${yDistance}, duration: ${duration}, tolerance: ${tolerance}`); + if (yDistance <= tolerance && xDistance <= tolerance) { // prevent scrolling if already close to there self._el = null; @@ -50,16 +52,7 @@ export class ScrollTo { } return new Promise((resolve, reject) => { - let start; - - // start scroll loop - self.isPlaying = true; - - // chill out for a frame first - raf(() => { - start = Date.now(); - raf(step); - }); + let startTime: number; // scroll loop function step() { @@ -67,19 +60,22 @@ export class ScrollTo { return resolve(); } - let time = Math.min(1, ((Date.now() - start) / duration)); + let time = Math.min(1, ((Date.now() - startTime) / duration)); // where .5 would be 50% of time on a linear scale easedT gives a // fraction based on the easing method let easedT = easeOutCubic(time); if (fromY != y) { - self._el.scrollTop = Math.round((easedT * (y - fromY)) + fromY); + self._el.scrollTop = (easedT * (y - fromY)) + fromY; } + if (fromX != x) { self._el.scrollLeft = Math.round((easedT * (x - fromX)) + fromX); } + console.debug(`scrollTo step, easedT: ${easedT}, scrollTop: ${self._el.scrollTop}`); + if (time < 1 && self.isPlaying) { raf(step); @@ -91,10 +87,20 @@ export class ScrollTo { } else { // done self._el = null; + console.debug(`scrollTo done`); resolve(); } } + // start scroll loop + self.isPlaying = true; + + // chill out for a frame first + raf(() => { + startTime = Date.now(); + raf(step); + }); + }); } diff --git a/ionic/components.ts b/ionic/components.ts index a7d1d36437..acfe79c892 100644 --- a/ionic/components.ts +++ b/ionic/components.ts @@ -28,7 +28,8 @@ export * from './components/navbar/navbar' export * from './components/option/option' export * from './components/overlay/overlay' export * from './components/slides/slides' -export * from './components/radio/radio' +export * from './components/radio/radio-button' +export * from './components/radio/radio-group' export * from './components/scroll/scroll' export * from './components/scroll/pull-to-refresh' export * from './components/searchbar/searchbar' @@ -37,6 +38,5 @@ export * from './components/select/select' export * from './components/tabs/tabs' export * from './components/tabs/tab' export * from './components/tap-click/tap-click' -export * from './components/text-input/text-input' export * from './components/toggle/toggle' export * from './components/toolbar/toolbar' diff --git a/ionic/components/card/card.ios.scss b/ionic/components/card/card.ios.scss index 736a7c02c5..68209993f5 100644 --- a/ionic/components/card/card.ios.scss +++ b/ionic/components/card/card.ios.scss @@ -48,7 +48,7 @@ ion-card { padding-right: 0; } - ion-item-content { + ion-label { padding: 0; } diff --git a/ionic/components/card/card.md.scss b/ionic/components/card/card.md.scss index 4aa2b8d3b4..35df38e784 100644 --- a/ionic/components/card/card.md.scss +++ b/ionic/components/card/card.md.scss @@ -47,7 +47,7 @@ ion-card { ion-list { margin-bottom: 0; - ion-item-content { + ion-label { padding: 0; } diff --git a/ionic/components/checkbox/checkbox.ios.scss b/ionic/components/checkbox/checkbox.ios.scss index 9bef5cc5e6..cc5c52a66b 100644 --- a/ionic/components/checkbox/checkbox.ios.scss +++ b/ionic/components/checkbox/checkbox.ios.scss @@ -1,9 +1,8 @@ @import "../../globals.ios"; -// iOS Checkbox Structure +// iOS Checkbox // -------------------------------------------------- - $checkbox-ios-background-color-off: $list-ios-background-color !default; $checkbox-ios-background-color-on: map-get($colors-ios, primary) !default; @@ -21,52 +20,13 @@ $checkbox-ios-icon-checkmark-color: $background-ios-color !default; $checkbox-ios-media-margin: $item-ios-padding-media-top $item-ios-padding-right $item-ios-padding-media-bottom 2px !default; $checkbox-ios-disabled-opacity: 0.5 !default; -$checkbox-ios-disabled-text-color: $subdued-text-ios-color !default; -ion-checkbox { - cursor: pointer; - @include user-select-none(); - - &[aria-checked=true] .checkbox-icon { - background-color: $checkbox-ios-background-color-on; - border-color: $checkbox-ios-icon-border-color-on; - } - - &[aria-checked=true] .checkbox-icon:after { - position: absolute; - border-width: $checkbox-ios-icon-checkmark-width; - border-style: $checkbox-ios-icon-checkmark-style; - border-color: $checkbox-ios-icon-checkmark-color; - top: 3px; - left: 7px; - width: 4px; - height: 9px; - border-left: none; - border-top: none; - content: ''; - transform: rotate(45deg); - } - - &.item.activated { - background-color: $checkbox-ios-background-color-off; - } - -} - -ion-checkbox[aria-disabled=true] { - opacity: $checkbox-ios-disabled-opacity; - color: $checkbox-ios-disabled-text-color; - pointer-events: none; -} - -.checkbox-media { - margin: $checkbox-ios-media-margin; -} +// iOS Checkbox Outer Circle: Unchecked +// ----------------------------------------- .checkbox-icon { position: relative; - display: block; width: $checkbox-ios-icon-size; height: $checkbox-ios-icon-size; border-radius: $checkbox-ios-icon-border-radius; @@ -77,16 +37,62 @@ ion-checkbox[aria-disabled=true] { } +// iOS Checkbox Outer Circle: Checked +// ----------------------------------------- + +.checkbox-checked { + background-color: $checkbox-ios-background-color-on; + border-color: $checkbox-ios-icon-border-color-on; +} + + +// iOS Checkbox Inner Checkmark: Checked +// ----------------------------------------- + +.checkbox-checked .checkbox-inner { + position: absolute; + border-width: $checkbox-ios-icon-checkmark-width; + border-style: $checkbox-ios-icon-checkmark-style; + border-color: $checkbox-ios-icon-checkmark-color; + top: 4px; + left: 7px; + width: 4px; + height: 9px; + border-left: none; + border-top: none; + transform: rotate(45deg); +} + + +// iOS Checkbox: Disabled +// ----------------------------------------- + +.checkbox-disabled, +.item-checkbox-disabled ion-label { + opacity: $checkbox-ios-disabled-opacity; + pointer-events: none; +} + + +// iOS Checkbox Within An Item +// ----------------------------------------- + +.item ion-checkbox { + display: block; + margin: $checkbox-ios-media-margin; +} + + // iOS Checkbox Color Mixin // -------------------------------------------------- @mixin checkbox-theme-ios($color-name, $bg-on) { - ion-checkbox[#{$color-name}][aria-checked=true] .checkbox-icon { + ion-checkbox[#{$color-name}] .checkbox-checked { background-color: $bg-on; border-color: $bg-on; - &:after { + .checkbox-inner { border-color: inverse($bg-on); } } diff --git a/ionic/components/checkbox/checkbox.md.scss b/ionic/components/checkbox/checkbox.md.scss index 8394396f84..fd752c0082 100644 --- a/ionic/components/checkbox/checkbox.md.scss +++ b/ionic/components/checkbox/checkbox.md.scss @@ -10,7 +10,6 @@ $checkbox-md-padding: $item-md-padding-top ($item-md-padding- $checkbox-md-margin: 0 !default; $checkbox-md-media-margin: $item-md-padding-media-top 36px $item-md-padding-media-bottom 4px !default; $checkbox-md-disabled-opacity: 0.5 !default; -$checkbox-md-disabled-text-color: $subdued-text-md-color !default; $checkbox-md-icon-background-color-off: $list-md-background-color !default; $checkbox-md-icon-background-color-on: map-get($colors-md, primary) !default; @@ -27,54 +26,8 @@ $checkbox-md-icon-border-color-off: darken($list-md-border-color, 40%) !def $checkbox-md-icon-border-color-on: map-get($colors-md, primary) !default; -ion-checkbox { - cursor: pointer; - @include user-select-none(); - - &.item .item-inner { - border: none; - padding-right: 0; - } - - ion-item-content { - border-bottom-width: $checkbox-md-border-bottom-width; - border-bottom-style: $checkbox-md-border-bottom-style; - border-bottom-color: $checkbox-md-border-bottom-color; - margin: $checkbox-md-margin; - padding: $checkbox-md-padding; - } - - &[aria-checked=true] .checkbox-icon { - background-color: $checkbox-md-icon-background-color-on; - border-color: $checkbox-md-icon-border-color-on; - } - - &[aria-checked=true] .checkbox-icon:after { - position: absolute; - border-width: $checkbox-md-icon-checkmark-width; - border-style: $checkbox-md-icon-checkmark-style; - border-color: $checkbox-md-icon-checkmark-color; - top: 0; - left: 3px; - width: 4px; - height: 8px; - border-left: none; - border-top: none; - content: ''; - transform: rotate(45deg); - } - -} - -ion-checkbox[aria-disabled=true] { - opacity: $checkbox-md-disabled-opacity; - color: $checkbox-md-disabled-text-color; - pointer-events: none; -} - -.checkbox-media { - margin: $checkbox-md-media-margin; -} +// Material Design Checkbox Outer Square: Unchecked +// ----------------------------------------- .checkbox-icon { position: relative; @@ -88,16 +41,61 @@ ion-checkbox[aria-disabled=true] { } +// Material Design Checkbox Outer Square: Checked +// ----------------------------------------- + +.checkbox-checked { + background-color: $checkbox-md-icon-background-color-on; + border-color: $checkbox-md-icon-border-color-on; +} + + +// Material Design Checkbox Inner Checkmark: Checked +// ----------------------------------------- + +.checkbox-checked .checkbox-inner { + position: absolute; + border-width: $checkbox-md-icon-checkmark-width; + border-style: $checkbox-md-icon-checkmark-style; + border-color: $checkbox-md-icon-checkmark-color; + top: 0; + left: 4px; + width: 5px; + height: 10px; + border-left: none; + border-top: none; + transform: rotate(45deg); +} + + +// Material Design Checkbox: Disabled +// ----------------------------------------- + +.checkbox-disabled, +.item-checkbox-disabled ion-label { + opacity: $checkbox-md-disabled-opacity; + pointer-events: none; +} + + +// Material Design Checkbox within an item +// ----------------------------------------- + +.item ion-checkbox { + margin: $checkbox-md-media-margin; +} + + // Material Design Checkbox Color Mixin // -------------------------------------------------- @mixin checkbox-theme-md($color-name, $bg-on) { - ion-checkbox[#{$color-name}][aria-checked=true] .checkbox-icon { + ion-checkbox[#{$color-name}] .checkbox-checked { background-color: $bg-on; border-color: $bg-on; - &:after { + .checkbox-inner { border-color: inverse($bg-on); } } diff --git a/ionic/components/checkbox/checkbox.ts b/ionic/components/checkbox/checkbox.ts index 71c0cb1ac1..6c3e88d35b 100644 --- a/ionic/components/checkbox/checkbox.ts +++ b/ionic/components/checkbox/checkbox.ts @@ -1,10 +1,13 @@ -import {Component, Directive, Optional, ElementRef, Input, Renderer, HostListener} from 'angular2/core'; +import {Component, Optional, Input, HostListener} from 'angular2/core'; import {NgControl} from 'angular2/common'; import {Form} from '../../util/form'; +import {Item} from '../item/item'; /** - * The checkbox is no different than the HTML checkbox input, except it's styled differently. + * The checkbox is no different than the HTML checkbox input, except + * it's styled accordingly to the the platform and design mode, such + * as iOS or Material Design. * * See the [Angular 2 Docs](https://angular.io/docs/js/latest/api/core/Form-interface.html) for more info on forms and input. * @@ -14,44 +17,58 @@ import {Form} from '../../util/form'; * * @usage * ```html - * - * HTML5 - * + * + * + * + * + * Pepperoni + * + * + * + * + * Sausage + * + * + * + * + * Mushrooms + * + * + * + * * ``` * @demo /docs/v2/demos/checkbox/ * @see {@link /docs/v2/components#checkbox Checkbox Component Docs} */ @Component({ selector: 'ion-checkbox', - host: { - 'role': 'checkbox', - 'class': 'item', - 'tappable': '', - 'tabindex': '0', - '[attr.aria-disabled]': 'disabled' - }, template: - '
' + - '
' + - '
' + - '
' + - '' + - '' + - '' + - '
' + '
' + + '
' + + '
' + + '', + host: { + '[class.checkbox-disabled]': '_disabled' + } }) export class Checkbox { - private _checked: boolean; + private _checked: any = false; + private _disabled: any = false; private _labelId: string; + id: string; + @Input() value: string = ''; - @Input() disabled: boolean = false; - @Input() id: string; constructor( private _form: Form, - private _elementRef: ElementRef, - private _renderer: Renderer, + @Optional() private _item: Item, @Optional() ngControl: NgControl ) { _form.register(this); @@ -59,19 +76,11 @@ export class Checkbox { if (ngControl) { ngControl.valueAccessor = this; } - } - /** - * @private - */ - ngOnInit() { - if (!this.id) { - this.id = 'chk-' + this._form.nextId(); - this._renderer.setElementAttribute(this._elementRef.nativeElement, 'id', this.id); + if (_item) { + this.id = 'chk-' + _item.registerInput('checkbox'); + this._labelId = 'lbl-' + _item.id; } - - this._labelId = 'lbl-' + this.id; - this._renderer.setElementAttribute(this._elementRef.nativeElement, 'aria-labelledby', this._labelId); } /** @@ -82,22 +91,35 @@ export class Checkbox { this.checked = !this.checked; } - @Input() - get checked(): boolean { - return !!this._checked; + get checked() { + return this._checked; } + @Input() set checked(val) { - this._checked = !!val; - this._renderer.setElementAttribute(this._elementRef.nativeElement, 'aria-checked', this._checked.toString()); - this.onChange(this._checked); + if (!this._disabled) { + this._checked = (val === true || val === 'true'); + this.onChange(this._checked); + this._item && this._item.setCssClass('item-checkbox-checked', this._checked); + } + } + + get disabled() { + return this._disabled; + } + + @Input() + set disabled(val) { + this._disabled = (val === true || val === 'true'); + this._item && this._item.setCssClass('item-checkbox-disabled', this._disabled); } /** * @private */ @HostListener('click', ['$event']) - _click(ev) { + private _click(ev) { + console.debug('checkbox, checked', this.value); ev.preventDefault(); ev.stopPropagation(); this.toggle(); diff --git a/ionic/components/checkbox/test/basic/e2e.ts b/ionic/components/checkbox/test/basic/e2e.ts index 713c06fa10..5dcfccb886 100644 --- a/ionic/components/checkbox/test/basic/e2e.ts +++ b/ionic/components/checkbox/test/basic/e2e.ts @@ -1,6 +1,6 @@ it('should check apple, enable/check grape, submit form', function() { - element(by.css('[ngControl=appleCtrl] .checkbox-media')).click(); + element(by.css('[ngControl=appleCtrl] button')).click(); element(by.css('.e2eGrapeDisabled')).click(); element(by.css('.e2eGrapeChecked')).click(); element(by.css('.e2eSubmit')).click(); diff --git a/ionic/components/checkbox/test/basic/main.html b/ionic/components/checkbox/test/basic/main.html index a2a16cb0d8..4badb5ec30 100644 --- a/ionic/components/checkbox/test/basic/main.html +++ b/ionic/components/checkbox/test/basic/main.html @@ -8,29 +8,35 @@ - - Apple, value=apple, init checked - + + Apple, value=apple, init checked + + - - Banana, init no checked/value attributes - + + Banana, init no checked/value attributes + + - - Cherry, value=cherry, init disabled - + + Cherry, value=cherry, init disabled + + - - Grape, value=grape, init checked, disabled - + + Grape, value=grape, init checked, disabled + + - - secondary color - + + secondary color + + - - light color - + + light color + + diff --git a/ionic/components/input/input.ios.scss b/ionic/components/input/input.ios.scss index a5a24a20ee..c95d93bb55 100644 --- a/ionic/components/input/input.ios.scss +++ b/ionic/components/input/input.ios.scss @@ -12,17 +12,16 @@ $text-input-ios-input-clear-icon-svg: "
' + - '' + - '' + - '' + - '
', - directives: [NgIf, forwardRef(() => InputScrollAssist), TextInput, Button] + +/** + * @private + */ +@Directive({ + selector: '[next-input]' }) -export class ItemInput { - private _assist: boolean; - private input: TextInput; - private label: Label; - private scrollMove: EventListener; - private startCoord: {x: number, y: number}; - private deregScroll: () => void; +class NextInput { - keyboardHeight: number; - value: string = ''; - type: string = null; - lastTouch: number = 0; - displayType: string; + @Output() focused: EventEmitter = new EventEmitter(); - @Input() clearInput: any; + @HostListener('focus') + receivedFocus() { + this.focused.emit(true); + } + +} + +export class TextInputBase { + protected _coord; + protected _deregScroll; + protected _keyboardHeight; + protected _scrollMove: EventListener; + protected _type: string = 'text'; + protected _useAssist: boolean = true; + protected _value = ''; + protected _isTouch: boolean; + + inputControl: NgControl; + + @Input() clearInput; + @Input() placeholder: string = ''; + @ViewChild(NativeInput) protected _native: NativeInput; constructor( config: Config, - private _form: Form, - private _renderer: Renderer, - private _elementRef: ElementRef, - private _app: IonicApp, - private _platform: Platform, - @Optional() @Host() private _scrollView: Content, - @Optional() private _nav: NavController, - @Attribute('floating-label') isFloating: string, - @Attribute('stacked-label') isStacked: string, - @Attribute('fixed-label') isFixed: string, - @Attribute('inset') isInset: string + protected _form: Form, + protected _item: Item, + protected _app: IonicApp, + protected _platform: Platform, + protected _elementRef: ElementRef, + protected _scrollView: Content, + protected _nav: NavController, + ngControl: NgControl ) { + this._useAssist = true;// config.get('scrollAssist'); + this._keyboardHeight = config.get('keyboardHeight'); + + if (ngControl) { + ngControl.valueAccessor = this; + } + _form.register(this); - - //TODO make more gud with pending @Attributes API - this.displayType = (isFloating === '' ? 'floating' : (isStacked === '' ? 'stacked' : (isFixed === '' ? 'fixed' : (isInset === '' ? 'inset' : null)))); - - this._assist = config.get('scrollAssist'); - this.keyboardHeight = config.get('keyboardHeight'); } - /** - * @private - */ - @ContentChild(TextInput) - set _setInput(textInput: TextInput) { - if (textInput) { - textInput.addClass('item-input'); - if (this.displayType) { - textInput.addClass(this.displayType + '-input'); - } - this.input = textInput; - this.type = textInput.type; - - this.hasValue(this.input.value); - textInput.valueChange.subscribe(inputValue => { - this.hasValue(inputValue); - }); - - this.focusChange(this.hasFocus()); - textInput.focusChange.subscribe(textInputHasFocus => { - this.focusChange(textInputHasFocus); - }); - } - } - - /** - * @private - */ - @ContentChild(Label) - set _setLabel(label: Label) { - if (label && this.displayType) { - label.addClass(this.displayType + '-label'); - } - this.label = label; - } - - /** - * @private - */ - @ContentChildren(Button) - set _buttons(buttons) { - buttons.toArray().forEach(button => { - if (!button.isItem) { - button.addClass('item-button'); - } - }); - } - - /** - * @private - */ - @ContentChildren(Icon) - set _icons(icons) { - icons.toArray().forEach(icon => { - icon.addClass('item-icon'); - }); - } - - /** - * @private - * On Initialization check for attributes - */ ngOnInit() { + if (this._item) { + this._item.setCssClass('item-input', true); + this._item.registerInput(this._type); + } + let clearInput = this.clearInput; if (typeof clearInput === 'string') { this.clearInput = (clearInput === '' || clearInput === 'true'); } } - /** - * @private - */ - ngAfterViewInit() { + ngAfterContentInit() { let self = this; - if (self.input && self.label) { - // if there is an input and a label - // then give the label an ID - // and tell the input the ID of who it's labelled by - self.input.labelledBy(self.label.id); - } - self.scrollMove = function(ev: UIEvent) { + self._scrollMove = function(ev: UIEvent) { + // scroll move event listener this instance can reuse if (!(self._nav && self._nav.isTransitioning())) { - self.deregMove(); + self.deregScrollMove(); if (self.hasFocus()) { - self.input.hideFocus(true); + self._native.hideFocus(true); + self._scrollView.onScrollEnd(function() { - self.input.hideFocus(false); + self._native.hideFocus(false); if (self.hasFocus()) { - self.regMove(); + // if it still has focus then keep listening + self.regScrollMove(); } }); } } }; + + this.setItemControlCss(); } - /** - * @private - */ - clearTextInput() { - console.log("Should clear input"); - //console.log(this.textInput.value); + ngAfterContentChecked() { + this.setItemControlCss(); } - /** - * @private - */ - pointerStart(ev) { - if (this._assist && this._app.isEnabled()) { - // remember where the touchstart/mousedown started - this.startCoord = pointerCoord(ev); + private setItemControlCss() { + let item = this._item; + let nativeControl = this._native && this._native.ngControl; + + if (item && nativeControl) { + item.setCssClass('ng-untouched', nativeControl.untouched); + item.setCssClass('ng-touched', nativeControl.touched); + item.setCssClass('ng-pristine', nativeControl.pristine); + item.setCssClass('ng-dirty', nativeControl.dirty); + item.setCssClass('ng-valid', nativeControl.valid); + item.setCssClass('ng-invalid', !nativeControl.valid); + } + } + + ngOnDestroy() { + this._form.deregister(this); + } + + @Input() + get value() { + return this._value; + } + + set value(val) { + this._value = val; + } + + @Input() + get type() { + return this._type; + } + + set type(val) { + this._type = 'text'; + + if (val) { + val = val.toLowerCase(); + + if (/password|email|number|search|tel|url|date|datetime|datetime-local|month/.test(val)) { + this._type = val; + } } } /** * @private */ - pointerEnd(ev) { - if (!this._app.isEnabled()) { + @ViewChild(NativeInput) + private set _nativeInput(nativeInput: NativeInput) { + this._native = nativeInput; + + if (this._item && this._item.labelId !== null) { + nativeInput.labelledBy(this._item.labelId); + } + + nativeInput.valueChange.subscribe(inputValue => { + this.onChange(inputValue); + }); + + this.focusChange(this.hasFocus()); + nativeInput.focusChange.subscribe(textInputHasFocus => { + this.focusChange(textInputHasFocus); + if (!textInputHasFocus) { + this.onTouched(textInputHasFocus); + } + }); + } + + /** + * @private + */ + @ViewChild(NextInput) + private set _nextInput(nextInput: NextInput) { + nextInput.focused.subscribe(() => { + this._form.tabFocus(this); + }); + } + + /** + * @private + * Angular2 Forms API method called by the model (Control) on change to update + * the checked value. + * https://github.com/angular/angular/blob/master/modules/angular2/src/forms/directives/shared.ts#L34 + */ + writeValue(value) { + this._value = value; + } + + /** + * @private + */ + onChange(val) { + this.checkHasValue(val); + } + + /** + * @private + */ + onTouched(val) {} + + /** + * @private + */ + hasFocus(): boolean { + // check if an input has focus or not + return this._native.hasFocus(); + } + + /** + * @private + */ + checkHasValue(inputValue) { + if (this._item) { + this._item.setCssClass('input-has-value', !!(inputValue && inputValue !== '')); + } + } + + /** + * @private + */ + focusChange(inputHasFocus: boolean) { + if (this._item) { + this._item.setCssClass('input-has-focus', inputHasFocus); + } + if (!inputHasFocus) { + this.deregScrollMove(); + } + } + + private pointerStart(ev) { + // input cover touchstart + console.debug('scroll assist pointerStart', ev.type); + + if (ev.type === 'touchstart') { + this._isTouch = true; + } + + if ((this._isTouch || (!this._isTouch && ev.type === 'mousedown')) && this._app.isEnabled()) { + // remember where the touchstart/mousedown started + this._coord = pointerCoord(ev); + } + } + + private pointerEnd(ev) { + // input cover touchend/mouseup + console.debug('scroll assist pointerEnd', ev.type); + + if ((this._isTouch && ev.type === 'mouseup') || !this._app.isEnabled()) { + // the app is actively doing something right now + // don't try to scroll in the input ev.preventDefault(); ev.stopPropagation(); - } else if (this._assist && ev.type === 'touchend') { + } else if (this._coord) { // get where the touchend/mouseup ended let endCoord = pointerCoord(ev); // focus this input if the pointer hasn't moved XX pixels // and the input doesn't already have focus - if (!hasPointerMoved(8, this.startCoord, endCoord) && !this.hasFocus()) { + if (!hasPointerMoved(8, this._coord, endCoord) && !this.hasFocus()) { ev.preventDefault(); ev.stopPropagation(); + // begin the input focus process + console.debug('initFocus', ev.type); this.initFocus(); - - // temporarily prevent mouseup's from focusing - this.lastTouch = Date.now(); } - } else if (this.lastTouch + 999 < Date.now()) { - ev.preventDefault(); - ev.stopPropagation(); - - this.setFocus(); - this.regMove(); } + + this._coord = null; } /** @@ -267,21 +334,24 @@ export class ItemInput { */ initFocus() { // begin the process of setting focus to the inner input element - let scrollView = this._scrollView; - if (scrollView && this._assist) { + if (scrollView) { // this input is inside of a scroll view // find out if text input should be manually scrolled into view let ele = this._elementRef.nativeElement; + let itemEle = closest(ele, 'ion-item'); + if (itemEle) { + ele = itemEle; + } - let scrollData = ItemInput.getScrollData(ele.offsetTop, ele.offsetHeight, scrollView.getContentDimensions(), this.keyboardHeight, this._platform.height()); + let scrollData = TextInput.getScrollData(ele.offsetTop, ele.offsetHeight, scrollView.getContentDimensions(), this._keyboardHeight, this._platform.height()); if (scrollData.scrollAmount > -3 && scrollData.scrollAmount < 3) { - // the text input is in a safe position that doesn't require - // it to be scrolled into view, just set focus now + // the text input is in a safe position that doesn't + // require it to be scrolled into view, just set focus now this.setFocus(); - this.regMove(); + this.regScrollMove(); return; } @@ -297,51 +367,77 @@ export class ItemInput { // temporarily move the focus to the focus holder so the browser // doesn't freak out while it's trying to get the input in place // at this point the native text input still does not have focus - this.input.relocate(true, scrollData.inputSafeY); + this._native.relocate(true, scrollData.inputSafeY); // scroll the input into place scrollView.scrollTo(0, scrollData.scrollTo, scrollDuration).then(() => { // the scroll view is in the correct position now // give the native text input focus - this.input.relocate(false); + this._native.relocate(false, 0); + + this.setFocus(); // all good, allow clicks again this._app.setEnabled(true); this._nav && this._nav.setTransitioning(false); - this.regMove(); + this.regScrollMove(); }); } else { // not inside of a scroll view, just focus it this.setFocus(); - this.regMove(); + this.regScrollMove(); } + } + /** + * @private + */ + clearTextInput() { + console.log("Should clear input"); + //console.log(this.textInput.value); } /** * @private */ - setFocus() { - if (this.input) { - this._form.setAsFocused(this); + private setFocus() { + // immediately set focus + this._form.setAsFocused(this); - // set focus on the actual input element - this.input.setFocus(); + // set focus on the actual input element + this._native.setFocus(); - // ensure the body hasn't scrolled down - document.body.scrollTop = 0; - } + // ensure the body hasn't scrolled down + document.body.scrollTop = 0; } /** * @private + * Angular2 Forms API method called by the view (NgControl) to register the + * onChange event handler that updates the model (Control). + * https://github.com/angular/angular/blob/master/modules/angular2/src/forms/directives/shared.ts#L27 + * @param {Function} fn the onChange event handler. */ - regMove() { - if (this._assist && this._scrollView) { + registerOnChange(fn) { this.onChange = fn; } + + /** + * @private + * Angular2 Forms API method called by the the view (NgControl) to register + * the onTouched event handler that marks model (Control) as touched. + * @param {Function} fn onTouched event handler. + */ + registerOnTouched(fn) { this.onTouched = fn; } + + /** + * @private + */ + private regScrollMove() { + // register scroll move listener + if (this._useAssist && this._scrollView) { setTimeout(() => { - this.deregMove(); - this.deregScroll = this._scrollView.addScrollEventListener(this.scrollMove); + this.deregScrollMove(); + this._deregScroll = this._scrollView.addScrollEventListener(this._scrollMove); }, 80); } } @@ -349,49 +445,13 @@ export class ItemInput { /** * @private */ - deregMove() { - this.deregScroll && this.deregScroll(); + private deregScrollMove() { + // deregister the scroll move listener + this._deregScroll && this._deregScroll(); } - /** - * @private - */ - focusChange(inputHasFocus) { - this._renderer.setElementClass(this._elementRef.nativeElement, 'input-focused', inputHasFocus); - if (!inputHasFocus) { - this.deregMove(); - } - } - - /** - * @private - */ - hasFocus() { - return !!this.input && this.input.hasFocus(); - } - - /** - * @private - */ - hasValue(inputValue) { - let inputHasValue = !!(inputValue && inputValue !== ''); - this._renderer.setElementClass(this._elementRef.nativeElement, 'input-has-value', inputHasValue); - } - - /** - * @private - * This function is used to add the Angular css classes associated with inputs in forms - */ - hasClass(className) { - this.input && this.input.hasClass(className); - } - - /** - * @private - */ - ngOnDestroy() { - this.deregMove(); - this._form.deregister(this); + focusNext() { + this._form.tabFocus(this); } /** @@ -510,31 +570,79 @@ export class ItemInput { return scrollData; } - } -/** - * @private - */ -@Directive({ - selector: '[scroll-assist]' +@Component({ + selector: 'ion-input', + template: + '' + + '' + + '' + + '
', + directives: [ + NgIf, + forwardRef(() => NextInput), + NativeInput, + Button + ] }) -class InputScrollAssist { +export class TextInput extends TextInputBase { + constructor( + config: Config, + form: Form, + item: Item, + app: IonicApp, + platform: Platform, + elementRef: ElementRef, + @Optional() scrollView: Content, + @Optional() nav: NavController, + @Optional() ngControl: NgControl + ) { + super(config, form, item, app, platform, elementRef, scrollView, nav, ngControl); + } +} - constructor(private _form: Form, private _input: ItemInput) {} - @HostListener('focus') - receivedFocus() { - this._form.focusNext(this._input); +@Component({ + selector: 'ion-textarea', + template: + '' + + '' + + '
', + directives: [ + NgIf, + forwardRef(() => NextInput), + NativeInput + ] +}) +export class TextArea extends TextInputBase { + constructor( + config: Config, + form: Form, + item: Item, + app: IonicApp, + platform: Platform, + elementRef: ElementRef, + @Optional() scrollView: Content, + @Optional() nav: NavController, + @Optional() ngControl: NgControl + ) { + super(config, form, item, app, platform, elementRef, scrollView, nav, ngControl); } + ngOnInit() { + super.ngOnInit(); + if (this._item) { + this._item.setCssClass('item-textarea', true); + } + } } -const SCROLL_ASSIST_SPEED = 0.4; +const SCROLL_ASSIST_SPEED = 0.3; function getScrollAssistDuration(distanceToScroll) { //return 3000; distanceToScroll = Math.abs(distanceToScroll); let duration = distanceToScroll / SCROLL_ASSIST_SPEED; - return Math.min(400, Math.max(100, duration)); + return Math.min(400, Math.max(150, duration)); } diff --git a/ionic/components/text-input/text-input.ts b/ionic/components/input/native-input.ts similarity index 51% rename from ionic/components/text-input/text-input.ts rename to ionic/components/input/native-input.ts index 899eafe5c2..16bed35236 100644 --- a/ionic/components/text-input/text-input.ts +++ b/ionic/components/input/native-input.ts @@ -1,51 +1,32 @@ import {Directive, Attribute, ElementRef, Renderer, Input, Output, EventEmitter, HostListener} from 'angular2/core'; +import {NgControl} from 'angular2/common'; -import {CSS, hasFocus} from '../../util/dom'; +import {CSS, hasFocus, raf} from '../../util/dom'; /** * @private */ @Directive({ - selector: 'textarea,input[type=text],input[type=password],input[type=number],input[type=search],input[type=email],input[type=url],input[type=tel],input[type=date],input[type=datetime],input[type=datetime-local],input[type=week],input[type=time]', - host: { - 'class': 'text-input' - } + selector: '.text-input' }) -export class TextInput { +export class NativeInput { private _relocated: boolean; - type: string; - - @Input() ngModel; - @Input() value: string; @Output() focusChange: EventEmitter = new EventEmitter(); @Output() valueChange: EventEmitter = new EventEmitter(); constructor( - @Attribute('type') type: string, private _elementRef: ElementRef, - private _renderer: Renderer - ) { - this.type = type || 'text'; - } + private _renderer: Renderer, + public ngControl: NgControl + ) {} /** * @private */ - ngOnInit() { - if (this.ngModel) { - this.value = this.ngModel; - } else { - this.value = this._elementRef.nativeElement.value; - } - } - - /** - * @private - */ - @HostListener('keyup', ['$event']) - private _keyup(ev) { + @HostListener('input', ['$event']) + private _change(ev) { this.valueChange.emit(ev.target.value); } @@ -80,26 +61,29 @@ export class TextInput { /** * @private */ - relocate(shouldRelocate: boolean, inputRelativeY?) { + relocate(shouldRelocate: boolean, inputRelativeY: number) { if (this._relocated !== shouldRelocate) { let focusedInputEle = this.element(); if (shouldRelocate) { let clonedInputEle = cloneInput(focusedInputEle, 'cloned-input'); - focusedInputEle.classList.add('hide-focused-input'); - focusedInputEle.style[CSS.transform] = `translate3d(-9999px,${inputRelativeY}px,0)`; focusedInputEle.parentNode.insertBefore(clonedInputEle, focusedInputEle); + focusedInputEle.style[CSS.transform] = `translate3d(-9999px,${inputRelativeY}px,0)`; + focusedInputEle.style.opacity = '0'; this.setFocus(); + raf(() => { + focusedInputEle.style.display = 'none'; + }); + } else { - focusedInputEle.classList.remove('hide-focused-input'); focusedInputEle.style[CSS.transform] = ''; - let clonedInputEle = focusedInputEle.parentElement.querySelector('.cloned-input'); - if (clonedInputEle) { - clonedInputEle.parentNode.removeChild(clonedInputEle); - } + focusedInputEle.style.display = ''; + focusedInputEle.style.opacity = ''; + + removeClone(focusedInputEle, 'cloned-input'); } this._relocated = shouldRelocate; @@ -115,17 +99,13 @@ export class TextInput { if (shouldHideFocus) { let clonedInputEle = cloneInput(focusedInputEle, 'cloned-hidden'); - focusedInputEle.classList.add('hide-focused-input'); - focusedInputEle.style[CSS.transform] = 'translate3d(-9999px,0,0)'; + focusedInputEle.style.display = 'none'; focusedInputEle.parentNode.insertBefore(clonedInputEle, focusedInputEle); } else { - focusedInputEle.classList.remove('hide-focused-input'); - focusedInputEle.style[CSS.transform] = ''; - let clonedInputEle = focusedInputEle.parentElement.querySelector('.cloned-hidden'); - if (clonedInputEle) { - clonedInputEle.parentNode.removeChild(clonedInputEle); - } + focusedInputEle.style.display = ''; + + removeClone(focusedInputEle, 'cloned-hidden'); } } @@ -133,17 +113,6 @@ export class TextInput { return hasFocus(this.element()); } - /** - * @private - */ - addClass(className: string) { - this._renderer.setElementClass(this._elementRef.nativeElement, className, true); - } - - hasClass(className): boolean { - return this._elementRef.nativeElement.classList.contains(className); - } - /** * @private */ @@ -153,12 +122,19 @@ export class TextInput { } -function cloneInput(srcInput, addCssClass: string) { - let clonedInputEle = srcInput.cloneNode(true); +function cloneInput(focusedInputEle, addCssClass) { + let clonedInputEle = focusedInputEle.cloneNode(true); clonedInputEle.classList.add(addCssClass); - clonedInputEle.classList.remove('hide-focused-input'); clonedInputEle.setAttribute('aria-hidden', true); clonedInputEle.removeAttribute('aria-labelledby'); clonedInputEle.tabIndex = -1; + clonedInputEle.style.width = (focusedInputEle.offsetWidth + 10) + 'px'; return clonedInputEle; } + +function removeClone(focusedInputEle, queryCssClass) { + let clonedInputEle = focusedInputEle.parentElement.querySelector('.' + queryCssClass); + if (clonedInputEle) { + clonedInputEle.parentNode.removeChild(clonedInputEle); + } +} diff --git a/ionic/components/input/test/fixed-inline-labels/index.ts b/ionic/components/input/test/fixed-inline-labels/index.ts index 43aed36502..64c0c5fba8 100644 --- a/ionic/components/input/test/fixed-inline-labels/index.ts +++ b/ionic/components/input/test/fixed-inline-labels/index.ts @@ -4,4 +4,10 @@ import {App} from 'ionic/ionic'; @App({ templateUrl: 'main.html' }) -class E2EApp {} +class E2EApp { + url; + + constructor() { + this.url = 'hello'; + } +} diff --git a/ionic/components/input/test/fixed-inline-labels/main.html b/ionic/components/input/test/fixed-inline-labels/main.html index 2171e4e359..a30e7824dc 100644 --- a/ionic/components/input/test/fixed-inline-labels/main.html +++ b/ionic/components/input/test/fixed-inline-labels/main.html @@ -1,79 +1,82 @@ -Fixed Inline Label Text Input - + + Fixed Inline Label Text Input + - - To - - + + To + + - - CC - - + + Comments + + - - From - + + CC + + + + + From + - + - - Comments - - - - + - Website - - + Website + + - + - Email - - + Email + + - + - Phone - - + Phone + + - + - - + + - - Score - + + Score + - + - - First Name - - + + First Name + + - - Last Name - - + + Last Name + + - + - Message - - + Message + + +

{{url}}

+
diff --git a/ionic/components/input/test/floating-labels/main.html b/ionic/components/input/test/floating-labels/main.html index 93fe74fe49..edb1fc36de 100644 --- a/ionic/components/input/test/floating-labels/main.html +++ b/ionic/components/input/test/floating-labels/main.html @@ -6,57 +6,57 @@ - - Floating Label 1 - - + + Floating Label 1 + + Value: {{ myValues.value1 }} - - Floating Label 2 - - + + Floating Label 2 + + - - Floating Label 3 - - + + Floating Label 3 + + - - Floating Label 4 - - + + Floating Label 4 + + Value: {{ myValues.value2 }} - - Floating Label 5 - - + + Floating Label 5 + + - - Floating Label 6 - - + + Floating Label 6 + + - - Floating Label 7 - - + + Floating Label 7 + + - - Floating Label 8 - - + + Floating Label 8 + + - - Floating Label 9 - - + + Floating Label 9 + + - - Floating Label 10 - - + + Floating Label 10 + + diff --git a/ionic/components/input/test/form-inputs/main.html b/ionic/components/input/test/form-inputs/main.html index e045e73407..c010237b79 100644 --- a/ionic/components/input/test/form-inputs/main.html +++ b/ionic/components/input/test/form-inputs/main.html @@ -7,25 +7,25 @@
- - Email - - + + Email + + - - Username - - + + Username + + - - Password - - + + Password + + - - Comments - - + + Comments + Comment value +
@@ -43,14 +43,14 @@ - - Username - - - - Password - - + + Username + + + + Password + +
@@ -64,25 +64,25 @@ - + Email - - + + - + Username - - + + - + Password - - + + - + Comments - - + Comment value + diff --git a/ionic/components/input/test/inline-labels/main.html b/ionic/components/input/test/inline-labels/main.html index 4079e5bbf3..4023fac880 100644 --- a/ionic/components/input/test/inline-labels/main.html +++ b/ionic/components/input/test/inline-labels/main.html @@ -8,91 +8,91 @@ - + To: - - + + - + CC: - - + + - + From: - + - + - + Comments: - - + + - + Website: - - + + - + Email: - - + + - + Feedback: - - + + - + More Info: - + - + - + Score: - + - + - + First Name: - - + + - + Last Name: - - + + - + Message: - - + + - + Beginning: - - + + - + Time: - - + + - + No List - - - + + + diff --git a/ionic/components/input/test/input-focus/main.html b/ionic/components/input/test/input-focus/main.html index cc22868d22..18725bb961 100644 --- a/ionic/components/input/test/input-focus/main.html +++ b/ionic/components/input/test/input-focus/main.html @@ -15,90 +15,92 @@ - + Text 1: - - + + Item with button right - + Text 2: - - + + - + Text 3: - + - + - + Comments: - - + Comment value + - + Website: - - + + - + Email: - - + + - + Feedback: - - + + - - Toggle - + + Toggle + + - + More Info: - + - + - - Checkbox - + + Checkbox + + - + Score: - + - + - + First Name: - - + + - + Last Name: - - + + - + Message: - - + + Item @@ -116,13 +118,15 @@ Radios - - Radio 1 - + + Radio 1 + + - - Radio 2 - + + Radio 2 + + diff --git a/ionic/components/input/test/inset-inputs/main.html b/ionic/components/input/test/inset-inputs/main.html index 49e8deb153..5519aa65b8 100644 --- a/ionic/components/input/test/inset-inputs/main.html +++ b/ionic/components/input/test/inset-inputs/main.html @@ -9,61 +9,61 @@ - + - - + + - + - - + + - + - - + + - + - - + + - - + + - + - + - + - + - + - - + + - - + + - + diff --git a/ionic/components/input/test/placeholder-labels/main.html b/ionic/components/input/test/placeholder-labels/main.html index f2b63d61bc..4cd04dd75d 100644 --- a/ionic/components/input/test/placeholder-labels/main.html +++ b/ionic/components/input/test/placeholder-labels/main.html @@ -1,58 +1,60 @@ -Placeholder Label Text Input + + Placeholder Label Text Input + - - - + + + - - - + + + - + - - + + - + - - + + - - - + + + - - - + + ion-textarea value + - - - + + + - - - + + + - + - - + + - + - - + + diff --git a/ionic/components/input/test/stacked-labels/main.html b/ionic/components/input/test/stacked-labels/main.html index 7a53d8acf7..29de0ba767 100644 --- a/ionic/components/input/test/stacked-labels/main.html +++ b/ionic/components/input/test/stacked-labels/main.html @@ -1,60 +1,62 @@ -Stacked Label Text Input + + Stacked Label Text Input + - - Label 1 - - + + Label 1 + + - - Label 2 - - + + Label 2 + + - - Label 3 - - + + Label 3 + + - - Label 4 - - + + Label 4 + + - - Label 5 - - + + Label 5 + + - - Label 6 - - + + Label 6 + + - - Label 7 - - + + Label 7 + + - - Label 8 - - + + Label 8 + + - - Label 9 - - + + Label 9 + + - - Label 10 - - + + Label 10 + + diff --git a/ionic/components/item/item-media.scss b/ionic/components/item/item-media.scss index c8752c82b3..0fd7391c19 100644 --- a/ionic/components/item/item-media.scss +++ b/ionic/components/item/item-media.scss @@ -41,3 +41,13 @@ ion-thumbnail { display: block; } } + +.item-cover { + cursor: pointer; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: transparent; +} \ No newline at end of file diff --git a/ionic/components/item/item-sliding.ts b/ionic/components/item/item-sliding.ts index 63a6aac1a4..60b4f305a5 100644 --- a/ionic/components/item/item-sliding.ts +++ b/ionic/components/item/item-sliding.ts @@ -39,9 +39,9 @@ export class ItemSliding { elementRef.nativeElement.$ionSlide = ++slideIds; } -/** - * @private - */ + /** + * @private + */ close() { this._list.closeSlidingItems(); } diff --git a/ionic/components/item/item.ios.scss b/ionic/components/item/item.ios.scss index 0e5018a77c..ceec190ab7 100644 --- a/ionic/components/item/item.ios.scss +++ b/ionic/components/item/item.ios.scss @@ -23,6 +23,8 @@ $item-ios-sliding-content-bg: $list-ios-background-color !default; // -------------------------------------------------- .item { + position: relative; + padding-left: $item-ios-padding-left; font-size: $item-ios-body-text-font-size; border-radius: 0; @@ -83,10 +85,6 @@ $item-ios-sliding-content-bg: $list-ios-background-color !default; border-bottom: 1px solid $list-ios-border-color; } -ion-item-content { - margin: $item-ios-padding-top ($item-ios-padding-right / 2) $item-ios-padding-bottom 0; -} - &.hairlines .item-inner { border-bottom-width: 0.55px; } diff --git a/ionic/components/item/item.md.scss b/ionic/components/item/item.md.scss index d005354eaa..3393e8642c 100644 --- a/ionic/components/item/item.md.scss +++ b/ionic/components/item/item.md.scss @@ -76,10 +76,6 @@ $item-md-sliding-content-bg: $list-md-background-color !default; border-bottom: 1px solid $list-md-border-color; } -ion-item-content { - margin: $item-md-padding-top ($item-md-padding-right / 2) $item-md-padding-bottom 0; -} - // Material Design Item Media // -------------------------------------------------- @@ -107,7 +103,7 @@ ion-icon[item-right] { padding: 0 1px; } -[text-wrap] ion-item-content { +[text-wrap] ion-label { font-size: $item-md-body-text-font-size; line-height: $item-md-body-text-line-height; } diff --git a/ionic/components/item/item.scss b/ionic/components/item/item.scss index 6c7380a9f1..0ea7f6823e 100644 --- a/ionic/components/item/item.scss +++ b/ionic/components/item/item.scss @@ -60,7 +60,7 @@ ion-item-divider { } } -ion-item-content { +ion-label { margin: 0; flex: 1; white-space: nowrap; @@ -68,7 +68,7 @@ ion-item-content { text-overflow: ellipsis; } -[text-wrap] ion-item-content { +[text-wrap] ion-label { white-space: normal; } @@ -77,9 +77,5 @@ ion-input.item { align-items: flex-start; } -ion-item-content + ion-item-content[cnt] { - display: none; -} - @import "item-media"; @import "item-sliding"; diff --git a/ionic/components/item/item.ts b/ionic/components/item/item.ts index 70762502c8..c8ed66d545 100644 --- a/ionic/components/item/item.ts +++ b/ionic/components/item/item.ts @@ -1,7 +1,10 @@ -import {Component, ContentChildren} from 'angular2/core'; +import {Component, ContentChildren, forwardRef, ViewChild, ContentChild, Renderer, ElementRef} from 'angular2/core'; +import {NgIf} from 'angular2/common'; import {Button} from '../button/button'; +import {Form} from '../../util/form'; import {Icon} from '../icon/icon'; +import {Label} from '../label/label'; /** @@ -18,9 +21,6 @@ import {Icon} from '../icon/icon'; * To hide this icon, add the `detail-none` attribute to the item (eg: `', + host: { + '[class.radio-disabled]': '_disabled' + } +}) +export class RadioButton { + private _checked: any = false; + private _disabled: any = false; + private _labelId: string; + + id: string; + + @Input() value: string = ''; + @Output() select: EventEmitter = new EventEmitter(); + + constructor( + private _form: Form, + @Optional() private _item: Item, + @Optional() private _group: RadioGroup + ) { + _form.register(this); + + if (_item) { + this.id = 'rb-' + _item.registerInput('radio'); + this._labelId = 'lbl-' + _item.id; + } + + if (_group) { + _group.register(this); + } + } + + toggle() { + this.checked = !this._checked; + } + + @Input() + get checked() { + return this._checked; + } + + set checked(val) { +<<<<<<< HEAD + if (!this._disabled) { + this._checked = (val === true || val === 'true'); + this.select.emit(this); + this._item && this._item.setCssClass('item-radio-checked', this._checked); + } +======= + this._checked = (val === true || val === 'true'); + this.select.emit(this); +>>>>>>> 581c666... refactor(radio): place ion-radio insde of ion-item + } + + @Input() + get disabled() { + return this._disabled; + } + + set disabled(val) { + this._disabled = (val === true || val === 'true'); +<<<<<<< HEAD + this._item && this._item.setCssClass('item-radio-disabled', this._disabled); +======= + this._item && this._item.setCssClass('radio-disabled', val); +>>>>>>> 581c666... refactor(radio): place ion-radio insde of ion-item + } + + /** + * @private + */ + setChecked(val: boolean) { + this._checked = val; +<<<<<<< HEAD + this._item && this._item.setCssClass('item-radio-checked', val); +======= + this._item && this._item.setCssClass('radio-checked', val); +>>>>>>> 581c666... refactor(radio): place ion-radio insde of ion-item + } + + /** + * @private + */ + @HostListener('click', ['$event']) + private _click(ev) { +<<<<<<< HEAD + console.debug('radio, select', this.value); +======= + console.debug('RadioButton, select', this.value); +>>>>>>> 581c666... refactor(radio): place ion-radio insde of ion-item + ev.preventDefault(); + ev.stopPropagation(); + this.toggle(); + } + + /** + * @private + */ + ngOnDestroy() { + this._form.deregister(this); + } +} diff --git a/ionic/components/radio/radio-group.ts b/ionic/components/radio/radio-group.ts new file mode 100644 index 0000000000..b739ae2577 --- /dev/null +++ b/ionic/components/radio/radio-group.ts @@ -0,0 +1,167 @@ +import {Directive, ElementRef, Renderer, Optional, Input, Output, ContentChild, EventEmitter} from 'angular2/core'; +import {NgControl} from 'angular2/common'; + +import {RadioButton} from './radio-button'; +import {ListHeader} from '../list/list'; +import {isDefined} from '../../util/util'; + + +/** + * A radio group is a group of radio components, and its value comes + * fr0m the selected radio button's value. Selecting a radio button + * in the group unselects all others in the group. + * + * See the [Angular 2 Docs](https://angular.io/docs/js/latest/api/forms/) for more info on forms and input. + * + * @usage + * ```html + * + * + * + * Auto Manufacturers + * + * + * + * Cord + * + * + * + * + * Duesenberg + * + * + * + * + * Hudson + * + * + * + * + * Packard + * + * + * + * + * Studebaker + * + * + * + * + * ``` + * @demo /docs/v2/demos/radio/ + * @see {@link /docs/v2/components#radio Radio Component Docs} +*/ +@Directive({ + selector: '[radio-group]', + host: { + '[attr.aria-activedescendant]': 'activeId', + 'role': 'radiogroup' + } +}) +export class RadioGroup { + private _buttons: Array = []; + + id; + value; + + @Output() change: EventEmitter = new EventEmitter(); + + constructor( + @Optional() ngControl: NgControl, + private _renderer: Renderer, + private _elementRef: ElementRef + ) { + this.id = ++radioGroupIds; + + if (ngControl) { + ngControl.valueAccessor = this; + } + } + + /** + * @private + * Angular2 Forms API method called by the model (Control) on change to update + * the checked value. + * https://github.com/angular/angular/blob/master/modules/angular2/src/forms/directives/shared.ts#L34 + */ + writeValue(value) { + this.value = isDefined(value) ? value : ''; + this._buttons.forEach(button => { + let isChecked = (button.value === this.value); + button.setChecked(isChecked); + if (isChecked) { + this._renderer.setElementAttribute(this._elementRef.nativeElement, 'aria-activedescendant', button.id); + } + }); + } + + register(button: RadioButton) { + this._buttons.push(button); + + button.select.subscribe(() => { + this.writeValue(button.value); + this.onChange(button.value); + this.change.emit(this); + }); + } + + ngAfterContentInit() { + this._buttons.forEach(button => { + + if (isDefined(this.value)) { + let isChecked = (button.value === this.value) || button.checked; + button.setChecked(isChecked); + if (isChecked) { + this.writeValue(button.value); + //this.onChange(button.value); + this._renderer.setElementAttribute(this._elementRef.nativeElement, 'aria-activedescendant', button.id); + } + } + + }); + } + + @ContentChild(ListHeader) + private set _header(header) { + if (header) { + if (!header.id) { + header.id = 'rg-hdr-' + this.id; + } + this._renderer.setElementAttribute(this._elementRef.nativeElement, 'aria-describedby', header.id); + } + } + + /** + * @private + */ + onChange(val) { + // TODO: figure the whys and the becauses + } + + /** + * @private + */ + onTouched(val) { + // TODO: figure the whys and the becauses + } + + /** + * @private + * Angular2 Forms API method called by the view (NgControl) to register the + * onChange event handler that updates the model (Control). + * https://github.com/angular/angular/blob/master/modules/angular2/src/forms/directives/shared.ts#L27 + * @param {Function} fn the onChange event handler. + */ + registerOnChange(fn) { this.onChange = fn; } + + /** + * @private + * Angular2 Forms API method called by the the view (NgControl) to register + * the onTouched event handler that marks the model (Control) as touched. + * @param {Function} fn onTouched event handler. + */ + registerOnTouched(fn) { this.onTouched = fn; } + +} + +let radioGroupIds = -1; diff --git a/ionic/components/radio/radio-group.ts~3d61141c0391dbefcc6565ad94ab918ba2fff603 b/ionic/components/radio/radio-group.ts~3d61141c0391dbefcc6565ad94ab918ba2fff603 new file mode 100644 index 0000000000..b739ae2577 --- /dev/null +++ b/ionic/components/radio/radio-group.ts~3d61141c0391dbefcc6565ad94ab918ba2fff603 @@ -0,0 +1,167 @@ +import {Directive, ElementRef, Renderer, Optional, Input, Output, ContentChild, EventEmitter} from 'angular2/core'; +import {NgControl} from 'angular2/common'; + +import {RadioButton} from './radio-button'; +import {ListHeader} from '../list/list'; +import {isDefined} from '../../util/util'; + + +/** + * A radio group is a group of radio components, and its value comes + * fr0m the selected radio button's value. Selecting a radio button + * in the group unselects all others in the group. + * + * See the [Angular 2 Docs](https://angular.io/docs/js/latest/api/forms/) for more info on forms and input. + * + * @usage + * ```html + * + * + * + * Auto Manufacturers + * + * + * + * Cord + * + * + * + * + * Duesenberg + * + * + * + * + * Hudson + * + * + * + * + * Packard + * + * + * + * + * Studebaker + * + * + * + * + * ``` + * @demo /docs/v2/demos/radio/ + * @see {@link /docs/v2/components#radio Radio Component Docs} +*/ +@Directive({ + selector: '[radio-group]', + host: { + '[attr.aria-activedescendant]': 'activeId', + 'role': 'radiogroup' + } +}) +export class RadioGroup { + private _buttons: Array = []; + + id; + value; + + @Output() change: EventEmitter = new EventEmitter(); + + constructor( + @Optional() ngControl: NgControl, + private _renderer: Renderer, + private _elementRef: ElementRef + ) { + this.id = ++radioGroupIds; + + if (ngControl) { + ngControl.valueAccessor = this; + } + } + + /** + * @private + * Angular2 Forms API method called by the model (Control) on change to update + * the checked value. + * https://github.com/angular/angular/blob/master/modules/angular2/src/forms/directives/shared.ts#L34 + */ + writeValue(value) { + this.value = isDefined(value) ? value : ''; + this._buttons.forEach(button => { + let isChecked = (button.value === this.value); + button.setChecked(isChecked); + if (isChecked) { + this._renderer.setElementAttribute(this._elementRef.nativeElement, 'aria-activedescendant', button.id); + } + }); + } + + register(button: RadioButton) { + this._buttons.push(button); + + button.select.subscribe(() => { + this.writeValue(button.value); + this.onChange(button.value); + this.change.emit(this); + }); + } + + ngAfterContentInit() { + this._buttons.forEach(button => { + + if (isDefined(this.value)) { + let isChecked = (button.value === this.value) || button.checked; + button.setChecked(isChecked); + if (isChecked) { + this.writeValue(button.value); + //this.onChange(button.value); + this._renderer.setElementAttribute(this._elementRef.nativeElement, 'aria-activedescendant', button.id); + } + } + + }); + } + + @ContentChild(ListHeader) + private set _header(header) { + if (header) { + if (!header.id) { + header.id = 'rg-hdr-' + this.id; + } + this._renderer.setElementAttribute(this._elementRef.nativeElement, 'aria-describedby', header.id); + } + } + + /** + * @private + */ + onChange(val) { + // TODO: figure the whys and the becauses + } + + /** + * @private + */ + onTouched(val) { + // TODO: figure the whys and the becauses + } + + /** + * @private + * Angular2 Forms API method called by the view (NgControl) to register the + * onChange event handler that updates the model (Control). + * https://github.com/angular/angular/blob/master/modules/angular2/src/forms/directives/shared.ts#L27 + * @param {Function} fn the onChange event handler. + */ + registerOnChange(fn) { this.onChange = fn; } + + /** + * @private + * Angular2 Forms API method called by the the view (NgControl) to register + * the onTouched event handler that marks the model (Control) as touched. + * @param {Function} fn onTouched event handler. + */ + registerOnTouched(fn) { this.onTouched = fn; } + +} + +let radioGroupIds = -1; diff --git a/ionic/components/radio/radio-group.ts~HEAD b/ionic/components/radio/radio-group.ts~HEAD new file mode 100644 index 0000000000..b739ae2577 --- /dev/null +++ b/ionic/components/radio/radio-group.ts~HEAD @@ -0,0 +1,167 @@ +import {Directive, ElementRef, Renderer, Optional, Input, Output, ContentChild, EventEmitter} from 'angular2/core'; +import {NgControl} from 'angular2/common'; + +import {RadioButton} from './radio-button'; +import {ListHeader} from '../list/list'; +import {isDefined} from '../../util/util'; + + +/** + * A radio group is a group of radio components, and its value comes + * fr0m the selected radio button's value. Selecting a radio button + * in the group unselects all others in the group. + * + * See the [Angular 2 Docs](https://angular.io/docs/js/latest/api/forms/) for more info on forms and input. + * + * @usage + * ```html + * + * + * + * Auto Manufacturers + * + * + * + * Cord + * + * + * + * + * Duesenberg + * + * + * + * + * Hudson + * + * + * + * + * Packard + * + * + * + * + * Studebaker + * + * + * + * + * ``` + * @demo /docs/v2/demos/radio/ + * @see {@link /docs/v2/components#radio Radio Component Docs} +*/ +@Directive({ + selector: '[radio-group]', + host: { + '[attr.aria-activedescendant]': 'activeId', + 'role': 'radiogroup' + } +}) +export class RadioGroup { + private _buttons: Array = []; + + id; + value; + + @Output() change: EventEmitter = new EventEmitter(); + + constructor( + @Optional() ngControl: NgControl, + private _renderer: Renderer, + private _elementRef: ElementRef + ) { + this.id = ++radioGroupIds; + + if (ngControl) { + ngControl.valueAccessor = this; + } + } + + /** + * @private + * Angular2 Forms API method called by the model (Control) on change to update + * the checked value. + * https://github.com/angular/angular/blob/master/modules/angular2/src/forms/directives/shared.ts#L34 + */ + writeValue(value) { + this.value = isDefined(value) ? value : ''; + this._buttons.forEach(button => { + let isChecked = (button.value === this.value); + button.setChecked(isChecked); + if (isChecked) { + this._renderer.setElementAttribute(this._elementRef.nativeElement, 'aria-activedescendant', button.id); + } + }); + } + + register(button: RadioButton) { + this._buttons.push(button); + + button.select.subscribe(() => { + this.writeValue(button.value); + this.onChange(button.value); + this.change.emit(this); + }); + } + + ngAfterContentInit() { + this._buttons.forEach(button => { + + if (isDefined(this.value)) { + let isChecked = (button.value === this.value) || button.checked; + button.setChecked(isChecked); + if (isChecked) { + this.writeValue(button.value); + //this.onChange(button.value); + this._renderer.setElementAttribute(this._elementRef.nativeElement, 'aria-activedescendant', button.id); + } + } + + }); + } + + @ContentChild(ListHeader) + private set _header(header) { + if (header) { + if (!header.id) { + header.id = 'rg-hdr-' + this.id; + } + this._renderer.setElementAttribute(this._elementRef.nativeElement, 'aria-describedby', header.id); + } + } + + /** + * @private + */ + onChange(val) { + // TODO: figure the whys and the becauses + } + + /** + * @private + */ + onTouched(val) { + // TODO: figure the whys and the becauses + } + + /** + * @private + * Angular2 Forms API method called by the view (NgControl) to register the + * onChange event handler that updates the model (Control). + * https://github.com/angular/angular/blob/master/modules/angular2/src/forms/directives/shared.ts#L27 + * @param {Function} fn the onChange event handler. + */ + registerOnChange(fn) { this.onChange = fn; } + + /** + * @private + * Angular2 Forms API method called by the the view (NgControl) to register + * the onTouched event handler that marks the model (Control) as touched. + * @param {Function} fn onTouched event handler. + */ + registerOnTouched(fn) { this.onTouched = fn; } + +} + +let radioGroupIds = -1; diff --git a/ionic/components/radio/radio.ios.scss b/ionic/components/radio/radio.ios.scss index f33240863a..0629c97994 100644 --- a/ionic/components/radio/radio.ios.scss +++ b/ionic/components/radio/radio.ios.scss @@ -10,49 +10,11 @@ $radio-ios-icon-height: 21px !default; $radio-ios-icon-border-width: 2px !default; $radio-ios-icon-border-style: solid !default; -$radio-ios-disabled-text-color: $subdued-text-ios-color !default; $radio-ios-disabled-opacity: 0.5 !default; -ion-radio { - cursor: pointer; - @include user-select-none(); - - &[aria-checked=true] { - color: $radio-ios-color-on; - } - - &[aria-checked=true] .radio-icon:after { - position: absolute; - border-width: $radio-ios-icon-border-width; - border-style: $radio-ios-icon-border-style; - border-color: $radio-ios-color-on; - top: 3px; - left: 7px; - width: 4px; - height: 10px; - border-left: none; - border-top: none; - content: ''; - transform: rotate(45deg); - } - - &.item.activated { - background-color: $list-ios-background-color; - } - -} - -ion-radio[aria-disabled=true] { - color: $radio-ios-disabled-text-color; - opacity: $radio-ios-disabled-opacity; - pointer-events: none; -} - -.radio-media { - display: block; - margin: $item-ios-padding-media-top ($item-ios-padding-right / 2) $item-ios-padding-media-bottom ($item-ios-padding-left / 2); -} +// iOS Radio Circle: Unchecked +// ----------------------------------------- .radio-icon { position: relative; @@ -62,27 +24,69 @@ ion-radio[aria-disabled=true] { } +// iOS Radio Checkmark: Checked +// ----------------------------------------- + +.radio-checked .radio-inner { + position: absolute; + border-width: $radio-ios-icon-border-width; + border-style: $radio-ios-icon-border-style; + border-color: $radio-ios-color-on; + top: 3px; + left: 7px; + width: 4px; + height: 10px; + border-left: none; + border-top: none; + transform: rotate(45deg); +} + + +// iOS Radio: Disabled +// ----------------------------------------- + +.radio-disabled, +.item-radio-disabled ion-label { + opacity: $radio-ios-disabled-opacity; + pointer-events: none; +} + + +// iOS Radio Within An Item +// ----------------------------------------- + +.item ion-radio { + display: block; + margin: $item-ios-padding-media-top ($item-ios-padding-right / 2) $item-ios-padding-media-bottom ($item-ios-padding-left / 2); +} + + +// iOS Radio Item Label: Checked +// ----------------------------------------- + +.item-radio-checked ion-label { + color: $radio-ios-color-on; +} + + // iOS Radio Color Mixin // -------------------------------------------------- @mixin radio-theme-ios($color-name, $color-value) { - ion-radio[#{$color-name}] { + ion-radio[#{$color-name}] .radio-checked { + color: $color-value; - &[aria-checked=true] .radio-icon:after { + .radio-inner { border-color: $color-value; } - &[aria-checked=true] { - color: $color-value; - } - } } -// Generate iOS Radio Auxiliary Colors +// Generate iOS Radio Colors // -------------------------------------------------- @each $color-name, $color-value in $colors-ios { diff --git a/ionic/components/radio/radio.md.scss b/ionic/components/radio/radio.md.scss index fb9f22ff86..2b2c876e87 100644 --- a/ionic/components/radio/radio.md.scss +++ b/ionic/components/radio/radio.md.scss @@ -15,33 +15,11 @@ $radio-md-icon-border-radius: 50% !default; $radio-md-transition-duration: 280ms !default; $radio-md-transition-easing: cubic-bezier(.4, 0, .2, 1) !default; -$radio-md-disabled-text-color: $subdued-text-md-color !default; $radio-md-disabled-opacity: 0.5 !default; -ion-radio { - cursor: pointer; - @include user-select-none(); - - &[aria-checked=true] { - color: $radio-md-color-on; - } - - &[aria-checked=true] .radio-icon { - border-color: $radio-md-color-on; - - &:after { - transform: scale3d(1, 1, 1); - } - } - -} - -ion-radio[aria-disabled=true] { - color: $radio-md-disabled-text-color; - opacity: $radio-md-disabled-opacity; - pointer-events: none; -} +// Material Design Radio Outer Circle: Unchecked +// ----------------------------------------- .radio-icon { position: relative; @@ -55,44 +33,78 @@ ion-radio[aria-disabled=true] { border-style: $radio-md-icon-border-style; border-color: $radio-md-color-off; border-radius: $radio-md-icon-border-radius; - - &:after { - position: absolute; - top: $radio-md-icon-border-width; - left: $radio-md-icon-border-width; - width: $radio-md-icon-width / 2; - height: $radio-md-icon-height / 2; - background-color: $radio-md-color-on; - border-radius: 50%; - content: ''; - transition: transform $radio-md-transition-duration $radio-md-transition-easing; - transform: scale3d(0, 0, 0); - } } -.radio-media { + +// Material Design Radio Inner Circle: Unchecked +// ----------------------------------------- + +.radio-inner { + position: absolute; + top: $radio-md-icon-border-width; + left: $radio-md-icon-border-width; + width: $radio-md-icon-width / 2; + height: $radio-md-icon-height / 2; + background-color: $radio-md-color-on; + border-radius: 50%; + transition: transform $radio-md-transition-duration $radio-md-transition-easing; + transform: scale3d(0, 0, 0); +} + + +// Material Design Radio Outer Circle: Checked +// ----------------------------------------- + +.radio-checked { + border-color: $radio-md-color-on; +} + + +// Material Design Radio Inner Circle: Checked +// ----------------------------------------- + +.radio-checked .radio-inner { + transform: scale3d(1, 1, 1); +} + + +// Material Design Radio: Disabled +// ----------------------------------------- + +.radio-disabled, +.item-radio-disabled ion-label { + opacity: $radio-md-disabled-opacity; + pointer-events: none; +} + + +// Material Design Radio Within An Item +// ----------------------------------------- + +.item ion-radio { display: block; margin: $item-md-padding-media-top ($item-md-padding-right / 2) $item-md-padding-media-bottom ($item-md-padding-left / 2); } +// Material Design Radio Item Label: Checked +// ----------------------------------------- + +.item-radio-checked ion-label { + color: $radio-md-color-on; +} + + // Material Design Radio Color Mixin // -------------------------------------------------- @mixin radio-theme-md($color-name, $color-value) { - ion-radio[#{$color-name}] { + ion-radio[#{$color-name}] .radio-checked { + border-color: $color-value; - &[aria-checked=true] { - color: $color-value; - } - - &[aria-checked=true] .radio-icon { - border-color: $color-value; - - &:after { - background-color: $color-value; - } + .radio-icon { + background-color: $color-value; } } diff --git a/ionic/components/radio/radio.ts b/ionic/components/radio/radio.ts deleted file mode 100644 index 24023bf53c..0000000000 --- a/ionic/components/radio/radio.ts +++ /dev/null @@ -1,261 +0,0 @@ -import {Component, Directive, ElementRef, Renderer, Optional, Input, Output, HostListener, ContentChildren, ContentChild, EventEmitter} from 'angular2/core'; -import {NgControl} from 'angular2/common'; - -import {ListHeader} from '../list/list'; -import {Form} from '../../util/form'; -import {isDefined} from '../../util/util'; - - -/** - * @description - * A radio button with a unique value. Note that all `` components - * must be wrapped within a ``, and there must be at - * least two `` components within the radio group. - * - * See the [Angular 2 Docs](https://angular.io/docs/js/latest/api/forms/) for more info on forms and input. - * - * @usage - * ```html - * - * Radio Label - * - * ``` - * @demo /docs/v2/demos/radio/ - * @see {@link /docs/v2/components#radio Radio Component Docs} - */ -@Component({ - selector: 'ion-radio', - host: { - 'role': 'radio', - 'class': 'item', - 'tappable': '', - 'tabindex': '0', - '[attr.aria-disabled]': 'disabled' - }, - template: - '
' + - '' + - '' + - '' + - '
' + - '
' + - '
' + - '
' -}) -export class RadioButton { - labelId: any; - - @Input() checked: any = false; - @Input() disabled: boolean = false; - @Input() id: string; - @Input() value: string = ''; - - @Output() select: EventEmitter = new EventEmitter(); - - constructor( - private _form: Form, - private _renderer: Renderer, - private _elementRef: ElementRef - ) { - _form.register(this); - } - - /** - * @private - */ - ngOnInit() { - if (!this.id) { - this.id = 'rb-' + this._form.nextId(); - this._renderer.setElementAttribute(this._elementRef.nativeElement, 'id', this.id); - } - this.labelId = 'lbl-' + this.id; - this._renderer.setElementAttribute(this._elementRef.nativeElement, 'aria-labelledby', this.labelId); - - let checked = this.checked; - if (typeof checked === 'string') { - this.checked = (checked === '' || checked === 'true'); - } - this.isChecked = this.checked; - this._renderer.setElementAttribute(this._elementRef.nativeElement, 'checked', null); - } - - /** - * @private - */ - @HostListener('click') - private _click() { - console.debug('RadioButton, select', this.value); - this.select.emit(this); - } - - public set isChecked(isChecked) { - this._renderer.setElementAttribute(this._elementRef.nativeElement, 'aria-checked', isChecked); - } - - /** - * @private - */ - ngOnDestroy() { - this._form.deregister(this); - } -} - - -/** - * A radio group is a group of radio components. - * - * Selecting a radio button in the group unselects all others in the group. - * - * New radios can be registered dynamically. - * - * See the [Angular 2 Docs](https://angular.io/docs/js/latest/api/forms/) for more info on forms and input. - * - * @usage - * ```html - * - * - * - * Auto Manufacturers - * - * - * - * Cord - * - * - * - * Duesenberg - * - * - * - * Hudson - * - * - * - * Packard - * - * - * - * Studebaker - * - * - * - * Tucker - * - * - * - * ``` - * @demo /docs/v2/demos/radio/ - * @see {@link /docs/v2/components#radio Radio Component Docs} -*/ -@Directive({ - selector: '[radio-group]', - host: { - '[attr.aria-activedescendant]': 'activeId', - 'role': 'radiogroup' - } -}) -export class RadioGroup { - id; - value; - - @Output() change: EventEmitter = new EventEmitter(); - @ContentChildren(RadioButton) private _buttons; - @ContentChild(ListHeader) private _header; - - constructor( - @Optional() ngControl: NgControl, - private _renderer: Renderer, - private _elementRef: ElementRef - ) { - this.id = ++radioGroupIds; - - if (ngControl) { - ngControl.valueAccessor = this; - } - } - - /** - * @private - * Angular2 Forms API method called by the model (Control) on change to update - * the checked value. - * https://github.com/angular/angular/blob/master/modules/angular2/src/forms/directives/shared.ts#L34 - */ - writeValue(value) { - this.value = isDefined(value) ? value : ''; - if (this._buttons) { - let buttons = this._buttons.toArray(); - for (let button of buttons) { - let isChecked = (button.value === this.value); - button.isChecked = isChecked; - if (isChecked) { - this._renderer.setElementAttribute(this._elementRef.nativeElement, 'aria-activedescendant', button.id); - } - } - } - } - - /** - * @private - */ - onChange(val) { - // TODO: figure the whys and the becauses - } - - /** - * @private - */ - onTouched(val) { - // TODO: figure the whys and the becauses - } - - /** - * @private - * Angular2 Forms API method called by the view (NgControl) to register the - * onChange event handler that updates the model (Control). - * https://github.com/angular/angular/blob/master/modules/angular2/src/forms/directives/shared.ts#L27 - * @param {Function} fn the onChange event handler. - */ - registerOnChange(fn) { this.onChange = fn; } - - /** - * @private - * Angular2 Forms API method called by the the view (NgControl) to register - * the onTouched event handler that marks the model (Control) as touched. - * @param {Function} fn onTouched event handler. - */ - registerOnTouched(fn) { this.onTouched = fn; } - - /** - * @private - */ - ngAfterContentInit() { - let header = this._header; - if (header) { - if (!header.id) { - header.id = 'rg-hdr-' + this.id; - } - this._renderer.setElementAttribute(this._elementRef.nativeElement, 'aria-describedby', header.id); - } - - this._buttons.toArray().forEach(button => { - button.select.subscribe(() => { - this.writeValue(button.value); - this.onChange(button.value); - this.change.emit(this); - }); - - if (isDefined(this.value)) { - let isChecked = (button.value === this.value) || button.checked; - button.isChecked = isChecked; - if (isChecked) { - this.writeValue(button.value); - //this.onChange(button.value); - this._renderer.setElementAttribute(this._elementRef.nativeElement, 'aria-activedescendant', button.id); - } - } - }); - } - -} - -let radioGroupIds = -1; diff --git a/ionic/components/radio/test/basic/e2e.ts b/ionic/components/radio/test/basic/e2e.ts index 03b88753bd..dab19b790f 100644 --- a/ionic/components/radio/test/basic/e2e.ts +++ b/ionic/components/radio/test/basic/e2e.ts @@ -1,4 +1,4 @@ it('should check Cherry', function() { - element(by.css('.e2eCherry')).click(); + element(by.css('.e2eCherry button')).click(); }); diff --git a/ionic/components/radio/test/basic/main.html b/ionic/components/radio/test/basic/main.html index 66f7b69ea4..4654eb8661 100644 --- a/ionic/components/radio/test/basic/main.html +++ b/ionic/components/radio/test/basic/main.html @@ -12,21 +12,25 @@ Fruits - - Apple - + + Apple + + - - Banana - + + Banana + + - - Cherry (secondary color) - + + Cherry (secondary color) + + - - Disabled - + + Disabled + +
@@ -48,7 +52,10 @@ Currencies - {{currency}} + + {{currency}} + +
@@ -57,8 +64,14 @@
- Friends - Enemies + + Friends + + + + Enemies + +
@@ -66,9 +79,18 @@
- Dogs - Cats - Turtles + + Dogs + + + + Cats + + + + Turtles + +
diff --git a/ionic/components/select/select.ios.scss b/ionic/components/select/select.ios.scss index 5949a0fa6c..e77bdd8b26 100644 --- a/ionic/components/select/select.ios.scss +++ b/ionic/components/select/select.ios.scss @@ -3,3 +3,13 @@ // iOS Select // -------------------------------------------------- + +$select-ios-padding-top: $item-ios-padding-top !default; +$select-ios-padding-right: ($item-ios-padding-right / 2) !default; +$select-ios-padding-bottom: $item-ios-padding-bottom !default; +$select-ios-padding-left: $item-ios-padding-left !default; + + +ion-select { + padding: $select-ios-padding-top $select-ios-padding-right $select-ios-padding-bottom $select-ios-padding-left; +} \ No newline at end of file diff --git a/ionic/components/select/select.md.scss b/ionic/components/select/select.md.scss index 4f7daf746d..ce790331cd 100644 --- a/ionic/components/select/select.md.scss +++ b/ionic/components/select/select.md.scss @@ -3,3 +3,13 @@ // Material Design Select // -------------------------------------------------- + +$select-md-padding-top: $item-md-padding-top !default; +$select-md-padding-right: ($item-md-padding-right / 2) !default; +$select-md-padding-bottom: $item-md-padding-bottom !default; +$select-md-padding-left: $item-md-padding-left !default; + + +ion-select { + padding: $select-md-padding-top $select-md-padding-right $select-md-padding-bottom $select-md-padding-left; +} \ No newline at end of file diff --git a/ionic/components/select/select.scss b/ionic/components/select/select.scss index 6a79b0a6fd..f478e5eb54 100644 --- a/ionic/components/select/select.scss +++ b/ionic/components/select/select.scss @@ -3,33 +3,41 @@ // Select // -------------------------------------------------- -.select-icon { - position: relative; - min-width: 16px; +ion-select { + display: flex; + overflow: hidden; } -.select-icon:after { +.select-text { + flex: 1; + min-width: 16px; + max-width: 120px; + font-size: inherit; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.select-icon { + position: relative; + width: 16px; + height: 16px; +} + +.select-icon .select-icon-inner { position: absolute; top: 50%; - left: 50%; - margin-top: -3px; + left: 5px; + margin-top: -2px; width: 0; height: 0; border-top: 5px solid; border-right: 5px solid transparent; border-left: 5px solid transparent; color: #999; - content: ""; pointer-events: none; } -.select-text-value { - max-width: 120px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -ion-select ion-label { - margin: 0; -} +.item-multiple-inputs ion-select { + position: relative; +} \ No newline at end of file diff --git a/ionic/components/select/select.ts b/ionic/components/select/select.ts index e43fba9005..cd2743de86 100644 --- a/ionic/components/select/select.ts +++ b/ionic/components/select/select.ts @@ -1,9 +1,9 @@ -import {Component, Directive, Optional, ElementRef, Input, Renderer, HostListener, ContentChild, ContentChildren, QueryList} from 'angular2/core'; +import {Component, Optional, ElementRef, Renderer, Input, HostListener, ContentChildren, QueryList} from 'angular2/core'; import {NgControl} from 'angular2/common'; import {Alert} from '../alert/alert'; import {Form} from '../../util/form'; -import {Label} from '../label/label'; +import {Item} from '../item/item'; import {merge} from '../../util/util'; import {NavController} from '../nav/nav-controller'; import {Option} from '../option/option'; @@ -19,9 +19,9 @@ import {Option} from '../option/option'; * * Under-the-hood the `ion-select` actually uses the * {@link ../../alert/Alert Alert API} to open up the overlay of options - * which the user is presented with. Select takes one child `ion-label` - * component, and numerous child `ion-option` components. Each `ion-option` - * should be given a `value` attribute. + * which the user is presented with. Select can take numerous child + * `ion-option` components. If `ion-option` is not given a `value` attribute + * then it will use its text as the value. * * ### Single Value: Radio Buttons * @@ -31,11 +31,13 @@ import {Option} from '../option/option'; * receives the value of the selected option's value. * * ```html - * + * * Gender - * Female - * Male - * + * + * Female + * Male + * + * * ``` * * ### Multiple Value: Checkboxes @@ -44,18 +46,21 @@ import {Option} from '../option/option'; * to select multiple options. When multiple options can be selected, the alert * overlay presents users with a checkbox styled list of options. The * `ion-select multiple="true"` component's value receives an array of all the - * selected option values. + * selected option values. In the example below, because each option is not given + * a `value`, then it'll use its text as the value instead. * * ```html - * + * * Toppings - * Bacon - * Black Olives - * Extra Cheese - * Mushrooms - * Pepperoni - * Sausage - * + * + * Bacon + * Black Olives + * Extra Cheese + * Mushrooms + * Pepperoni + * Sausage + * + * * ``` * * ### Alert Buttons @@ -91,25 +96,25 @@ import {Option} from '../option/option'; */ @Component({ selector: 'ion-select', - host: { - 'class': 'item', - 'tappable': '', - 'tabindex': '0', - '[attr.aria-disabled]': 'disabled' - }, template: - '' + - '
' + - '' + - '' + - '' + - '
{{selectedText}}
' + - '
' + - '
' + '
{{_selectedText}}
' + + '
' + + '
' + + '
' + + '', + host: { + '[class.select-disabled]': '_disabled' + } }) export class Select { - private selectedText: string = ''; - private labelId: string; + private _selectedText: string = ''; + private _disabled: any = false; + private _labelId: string; + + id: string; @Input() cancelText: string = 'Cancel'; @Input() okText: string = 'OK'; @@ -117,43 +122,34 @@ export class Select { @Input() alertOptions: any = {}; @Input() checked: any = false; @Input() disabled: boolean = false; - @Input() id: string = ''; @Input() multiple: string = ''; - @ContentChild(Label) label: Label; @ContentChildren(Option) options: QueryList
- + Car Options - Backup Camera - Headted Seats - Keyless Entry - Navigation - Parking Assist - Sun Roof - + + Backup Camera + Headted Seats + Keyless Entry + Navigation + Parking Assist + Sun Roof + +
' + - '' + - '' + - '' + - '
' + - '
' + - '
' + - `
` + '
' + + '
' + + '
' + + '', + host: { + '[class.toggle-disabled]': '_disabled' + } }) export class Toggle { - private _checked: boolean; + private _checked: any = false; + private _disabled: any = false; + private _labelId: string; + private _activated: boolean = false; private _mode: string; private _startX; private _touched: number = 0; - private addMoveListener; - private removeMoveListener; - public isActivated: boolean; - public labelId: string; + id: string; @Input() value: string = ''; - @Input() disabled: boolean = false; - @Input() id: string; constructor( private _form: Form, private _elementRef: ElementRef, private _renderer: Renderer, config: Config, - @Optional() ngControl: NgControl + @Optional() ngControl: NgControl, + @Optional() private _item: Item ) { // deprecated warning if (_elementRef.nativeElement.tagName == 'ION-SWITCH') { @@ -103,56 +103,14 @@ export class Toggle { ngControl.valueAccessor = this; } - let self = this; - function pointerMove(ev) { - let currentX = pointerCoord(ev).x; - - if (self.checked) { - if (currentX + 15 < self._startX) { - self.toggle(); - self._startX = currentX; - } - } else if (currentX - 15 > self._startX) { - self.toggle(); - self._startX = currentX; - } + if (_item) { + this.id = 'tgl-' + _item.registerInput('toggle'); + this._labelId = 'lbl-' + _item.id; } - - function pointerOut(ev) { - if (ev.currentTarget === ev.target) { - self.pointerUp(ev); - } - } - - let toggleEle = _elementRef.nativeElement.querySelector('.toggle-media'); - - this.addMoveListener = function() { - toggleEle.addEventListener('touchmove', pointerMove); - toggleEle.addEventListener('mousemove', pointerMove); - _elementRef.nativeElement.addEventListener('mouseout', pointerOut); - }; - - this.removeMoveListener = function() { - toggleEle.removeEventListener('touchmove', pointerMove); - toggleEle.removeEventListener('mousemove', pointerMove); - _elementRef.nativeElement.removeEventListener('mouseout', pointerOut); - }; } /** * @private - */ - ngOnInit() { - if (!this.id) { - this.id = 'tgl-' + this._form.nextId(); - this._renderer.setElementAttribute(this._elementRef.nativeElement, 'id', this.id); - } - - this.labelId = 'lbl-' + this.id; - this._renderer.setElementAttribute(this._elementRef.nativeElement, 'aria-labelledby', this.labelId); - } - - /** * Toggle the checked state of this toggle. */ toggle() { @@ -160,14 +118,26 @@ export class Toggle { } @Input() - get checked(): boolean { - return !!this._checked; + get checked() { + return this._checked; } - set checked(val: boolean) { - this._checked = !!val; - this._renderer.setElementAttribute(this._elementRef.nativeElement, 'aria-checked', this._checked.toString()); - this.onChange(this._checked); + set checked(val) { + if (!this._disabled) { + this._checked = (val === true || val === 'true'); + this.onChange(this._checked); + this._item && this._item.setCssClass('item-toggle-checked', this._checked); + } + } + + @Input() + get disabled() { + return this._disabled; + } + + set disabled(val) { + this._disabled = (val === true || val === 'true'); + this._item && this._item.setCssClass('item-toggle-disabled', this._disabled); } /** @@ -178,34 +148,59 @@ export class Toggle { this._touched = Date.now(); } - if (this.isDisabled(ev)) return; + if (this.isDisabled(ev)) { + return; + } this._startX = pointerCoord(ev).x; - this.removeMoveListener(); - this.addMoveListener(); + this._activated = true; + } - this.isActivated = true; + /** + * @private + */ + private pointerMove(ev) { + if (this._startX) { + let currentX = pointerCoord(ev).x; + console.debug('toggle move', ev.type, currentX); + + if (this._checked) { + if (currentX + 15 < this._startX) { + this.toggle(); + this._startX = currentX; + } + + } else if (currentX - 15 > this._startX) { + this.toggle(); + this._startX = currentX; + } + } } /** * @private */ private pointerUp(ev) { - if (this.isDisabled(ev)) return; + if (this._startX) { - let endX = pointerCoord(ev).x; + if (this.isDisabled(ev)) { + return; + } - if (this.checked) { - if (this._startX + 4 > endX) { + let endX = pointerCoord(ev).x; + + if (this.checked) { + if (this._startX + 4 > endX) { + this.toggle(); + } + } else if (this._startX - 4 < endX) { this.toggle(); } - } else if (this._startX - 4 < endX) { - this.toggle(); - } - this.removeMoveListener(); - this.isActivated = false; + this._activated = false; + this._startX = null; + } } /** @@ -243,22 +238,15 @@ export class Toggle { * @private */ ngOnDestroy() { - this.removeMoveListener(); - this.addMoveListener = this.removeMoveListener = null; this._form.deregister(this); } /** * @private */ - isDisabled(ev) { - return (this._touched + 999 > Date.now() && /mouse/.test(ev.type)) || (this._mode == 'ios' && ev.target.tagName == 'ION-TOGGLE'); + private isDisabled(ev) { + return (this._touched + 999 > Date.now() && (ev.type.indexOf('mouse') > -1)) + || (this._mode == 'ios' && ev.target.tagName == 'ION-TOGGLE'); } - /** - * @private - */ - initFocus() { - - } } diff --git a/ionic/config/directives.ts b/ionic/config/directives.ts index d0367d1aaf..9fc4c72274 100644 --- a/ionic/config/directives.ts +++ b/ionic/config/directives.ts @@ -15,7 +15,6 @@ import {Slides, Slide, SlideLazy} from '../components/slides/slides'; import {Tabs} from '../components/tabs/tabs'; import {Tab} from '../components/tabs/tab'; import {List, ListHeader} from '../components/list/list'; -import {ItemInput} from '../components/input/input'; import {Item} from '../components/item/item'; import {ItemSliding} from '../components/item/item-sliding'; import {Toolbar, ToolbarTitle, ToolbarItem} from '../components/toolbar/toolbar'; @@ -24,10 +23,11 @@ import {Checkbox} from '../components/checkbox/checkbox'; import {Select} from '../components/select/select'; import {Option} from '../components/option/option'; import {Toggle} from '../components/toggle/toggle'; -import {TextInput} from '../components/text-input/text-input'; +import {TextInput, TextArea} from '../components/input/input'; import {Label} from '../components/label/label'; import {Segment, SegmentButton} from '../components/segment/segment'; -import {RadioGroup, RadioButton} from '../components/radio/radio'; +import {RadioButton} from '../components/radio/radio-button'; +import {RadioGroup} from '../components/radio/radio-group'; import {Searchbar, SearchbarInput} from '../components/searchbar/searchbar'; import {Nav} from '../components/nav/nav'; import {NavPush, NavPop} from '../components/nav/nav-push'; @@ -91,7 +91,6 @@ import {ShowWhen, HideWhen} from '../components/show-hide-when/show-hide-when'; * - Select * - Option * - Toggle - * - ItemInput * - TextInput * - Label * @@ -159,7 +158,7 @@ export const IONIC_DIRECTIVES = [ Select, Option, Toggle, - ItemInput, + TextArea, TextInput, Label, diff --git a/ionic/util/form.ts b/ionic/util/form.ts index 2d527d5522..b3df421c81 100644 --- a/ionic/util/form.ts +++ b/ionic/util/form.ts @@ -15,7 +15,7 @@ import {Injectable} from 'angular2/core'; @Injectable() export class Form { private _blur: HTMLElement; - private _focused: any = null; + private _focused = null; private _ids: number = -1; private _inputs: Array = []; @@ -65,13 +65,12 @@ export class Form { /** * Focuses the next input element, if it exists. */ - focusNext(currentInput) { - console.debug('focusNext'); - + tabFocus(currentInput) { let index = this._inputs.indexOf(currentInput); if (index > -1 && (index + 1) < this._inputs.length) { let nextInput = this._inputs[index + 1]; if (nextInput !== this._focused) { + console.debug('tabFocus, next'); return nextInput.initFocus(); } } @@ -80,6 +79,7 @@ export class Form { if (index > 0) { let previousInput = this._inputs[index - 1]; if (previousInput) { + console.debug('tabFocus, previous'); previousInput.initFocus(); } } diff --git a/ionic/util/util.scss b/ionic/util/util.scss index dba4404a59..0ba1764fc3 100755 --- a/ionic/util/util.scss +++ b/ionic/util/util.scss @@ -79,7 +79,7 @@ $focus-outline-box-shadow: 0px 0px 8px 0px $focus-outline-border-color !defa outline: thin solid $focus-outline-border-color; } - ion-input.input-focused, + ion-input.input-has-focus, button[ion-item]:focus, a[ion-item]:focus { border-color: $focus-outline-border-color; @@ -110,22 +110,6 @@ focus-ctrl { } } -.hide-focused-input { - flex: 0 0 8px !important; - margin: 0 !important; - transform: translate3d(-9999px,0,0); - pointer-events: none; -} - -[floating-label] .hide-focused-input, -[stacked-label] .hide-focused-input { - margin-top: -8px !important; -} - -.cloned-input { - pointer-events: none; -} - // Backdrop // -------------------------------------------------- diff --git a/scripts/snapshot/snapshot.config.js b/scripts/snapshot/snapshot.config.js index 3a546721a0..22e81b8977 100644 --- a/scripts/snapshot/snapshot.config.js +++ b/scripts/snapshot/snapshot.config.js @@ -8,8 +8,8 @@ exports.config = { domain: 'ionic-snapshot-go.appspot.com', //domain: 'localhost:8080', - specs: 'dist/e2e/**/*e2e.js', - //specs: 'dist/e2e/action-sheet/**/*e2e.js', + //specs: 'dist/e2e/**/*e2e.js', + specs: 'dist/e2e/checkbox/**/*e2e.js', sleepBetweenSpecs: 350,