iOS switch

This commit is contained in:
Adam Bradley
2015-07-28 14:16:45 -05:00
parent b24a98731f
commit 51516b8b64
16 changed files with 259 additions and 159 deletions

View File

@ -5,7 +5,6 @@
.checkbox { .checkbox {
position: relative; position: relative;
display: block;
cursor: pointer; cursor: pointer;
@include user-select-none(); @include user-select-none();
} }
@ -21,6 +20,10 @@
@include appearance(none); @include appearance(none);
} }
.checkbox .input-label {
max-width: 100%;
}
.checkbox[aria-disabled=true] { .checkbox[aria-disabled=true] {
opacity: 0.5; opacity: 0.5;
color: gray; color: gray;

View File

@ -18,7 +18,7 @@ import {Icon} from '../icon/icon';
@IonicComponent({ @IonicComponent({
selector: 'ion-checkbox', selector: 'ion-checkbox',
host: { host: {
'[class.item]': 'item', 'class': 'item',
'[attr.aria-checked]': 'input.checked' '[attr.aria-checked]': 'input.checked'
} }
}) })
@ -46,8 +46,6 @@ export class Checkbox extends IonInputItem {
this.cd = cd; this.cd = cd;
cd.valueAccessor = this; cd.valueAccessor = this;
this.item = true;
} }
onInit() { onInit() {

View File

@ -2,14 +2,12 @@
// Label // Label
// -------------------------------------------------- // --------------------------------------------------
$input-label-color: #444 !default; $input-label-color: #888 !default;
.input-label { .input-label {
display: block; display: block;
max-width: 200px; max-width: 200px;
//width: 30%;
//min-width: 100px;
color: $input-label-color; color: $input-label-color;
font-size: inherit; font-size: inherit;
white-space: nowrap; white-space: nowrap;

View File

@ -5,6 +5,7 @@ import * as dom from '../../util/dom';
import {Input} from './text-input'; import {Input} from './text-input';
import {Checkbox} from '../checkbox/checkbox'; import {Checkbox} from '../checkbox/checkbox';
import {RadioButton} from '../radio/radio'; import {RadioButton} from '../radio/radio';
import {Switch} from '../switch/switch';
@Directive({ @Directive({
@ -23,9 +24,10 @@ export class Label {
@Optional() @Parent() textContainer: Input, @Optional() @Parent() textContainer: Input,
@Optional() @Parent() checkboxContainer: Checkbox, @Optional() @Parent() checkboxContainer: Checkbox,
@Optional() @Parent() radioContainer: RadioButton, @Optional() @Parent() radioContainer: RadioButton,
@Optional() @Parent() switchContainer: Switch,
config: IonicConfig config: IonicConfig
) { ) {
this.container = textContainer || checkboxContainer || radioContainer; this.container = textContainer || checkboxContainer || radioContainer || switchContainer;
if (this.container) { if (this.container) {
this.container.registerLabel(this); this.container.registerLabel(this);

View File

@ -19,6 +19,9 @@
<ion-input> <ion-input>
<label class="fixed-inline-label">From</label> <label class="fixed-inline-label">From</label>
<input value="Text 3" type="text"> <input value="Text 3" type="text">
<button primary clear>
<icon name="ion-power"></icon>
</button>
</ion-input> </ion-input>
<ion-input> <ion-input>
@ -27,16 +30,19 @@
</ion-input> </ion-input>
<ion-input> <ion-input>
<icon name="ion-earth"></icon>
<label class="fixed-inline-label">Website</label> <label class="fixed-inline-label">Website</label>
<input value="http://ionic.io/" type="url"> <input value="http://ionic.io/" type="url">
</ion-input> </ion-input>
<ion-input> <ion-input>
<icon name="ion-email"></icon>
<label class="fixed-inline-label">Email</label> <label class="fixed-inline-label">Email</label>
<input value="email6@email.com" type="email"> <input value="email6@email.com" type="email">
</ion-input> </ion-input>
<ion-input> <ion-input>
<icon name="ion-earth"></icon>
<label class="fixed-inline-label">Feedback</label> <label class="fixed-inline-label">Feedback</label>
<textarea placeholder="Placeholder Text"></textarea> <textarea placeholder="Placeholder Text"></textarea>
</ion-input> </ion-input>
@ -49,6 +55,7 @@
<ion-input> <ion-input>
<label class="fixed-inline-label">Score</label> <label class="fixed-inline-label">Score</label>
<input value="10" type="number"> <input value="10" type="number">
<button primary outline>Update</button>
</ion-input> </ion-input>
<ion-input> <ion-input>

View File

@ -19,6 +19,9 @@
<ion-input> <ion-input>
<label>From:</label> <label>From:</label>
<input value="Text 3" type="text"> <input value="Text 3" type="text">
<button primary clear>
<icon name="ion-power"></icon>
</button>
</ion-input> </ion-input>
<ion-input> <ion-input>
@ -27,16 +30,19 @@
</ion-input> </ion-input>
<ion-input> <ion-input>
<icon name="ion-earth"></icon>
<label>Website:</label> <label>Website:</label>
<input value="http://ionic.io/" type="url"> <input value="http://ionic.io/" type="url">
</ion-input> </ion-input>
<ion-input> <ion-input>
<icon name="ion-email"></icon>
<label>Email:</label> <label>Email:</label>
<input value="email6@email.com" type="email"> <input value="email6@email.com" type="email">
</ion-input> </ion-input>
<ion-input> <ion-input>
<icon name="ion-edit"></icon>
<label>Feedback:</label> <label>Feedback:</label>
<textarea placeholder="Placeholder Text"></textarea> <textarea placeholder="Placeholder Text"></textarea>
</ion-input> </ion-input>
@ -44,11 +50,13 @@
<ion-input> <ion-input>
<label>More Info:</label> <label>More Info:</label>
<input placeholder="Placeholder Text" type="text"> <input placeholder="Placeholder Text" type="text">
<icon name="ion-flag"></icon>
</ion-input> </ion-input>
<ion-input> <ion-input>
<label>Score:</label> <label>Score:</label>
<input value="10" type="number"> <input value="10" type="number">
<button primary outline>Update</button>
</ion-input> </ion-input>
<ion-input> <ion-input>

View File

@ -83,6 +83,10 @@ $item-ios-note-color: #999 !default;
font-size: 1.3rem; font-size: 1.3rem;
} }
.item-input .input + button {
margin-top: $item-ios-padding-media-top;
}
.badge { .badge {
margin-right: $item-ios-padding-right; margin-right: $item-ios-padding-right;
} }

View File

@ -73,6 +73,8 @@ button.item.item {
.item-content + .item-media, .item-content + .item-media,
.item-content + .item-content, .item-content + .item-content,
icon + .input, icon + .input,
.input + icon,
icon + .input-label,
.input-label + .input { .input-label + .input {
margin-left: 0; margin-left: 0;
} }

View File

@ -71,7 +71,7 @@ export class RadioGroup extends Ion {
@IonicComponent({ @IonicComponent({
selector: 'ion-radio', selector: 'ion-radio',
host: { host: {
'[class.item]': 'item', 'class': 'item',
'[attr.aria-checked]': 'input.checked', '[attr.aria-checked]': 'input.checked',
} }
}) })
@ -91,7 +91,6 @@ export class RadioButton extends IonInputItem {
config: IonicConfig config: IonicConfig
) { ) {
super(elementRef, config); super(elementRef, config);
this.item = true;
this.group = group; this.group = group;
} }

View File

@ -2,37 +2,86 @@
// iOS Switch // iOS Switch
// -------------------------------------------------- // --------------------------------------------------
$switch-ios-width: 52px !default; $switch-ios-width: 51px !default;
$switch-ios-height: 32px !default; $switch-ios-height: 31px !default;
$switch-ios-slider-off-background: #fff !default; $switch-ios-border-width: 2px !default;
$switch-ios-slider-on-background: #4cd964 !default; $switch-ios-border-radius: 30px !default;
$switch-ios-toggle-on-background: #fff !default;
$switch-ios-off-bg-color: #fff !default;
$switch-ios-off-border-color: #e6e6e6 !default;
$switch-ios-on-bg-color: get-color(primary, base) !default;
$switch-ios-on-border-color: $switch-ios-on-bg-color !default;
$switch-ios-handle-width: $switch-ios-height - ($switch-ios-border-width * 2) !default;
$switch-ios-handle-height: $switch-ios-handle-width !default;
$switch-ios-handle-radius: $switch-ios-handle-width !default;
$switch-ios-handle-dragging-bg-color: darken(#fff, 5%) !default;
$switch-ios-handle-box-shadow: 0 3px 12px rgba(0, 0, 0, 0.16), 0 3px 1px rgba(0, 0, 0, 0.1), 0px 0px 1px rgba(0, 0, 0, 0.15) !default;
$switch-ios-handle-off-bg-color: #fff !default;
$switch-ios-handle-on-bg-color: #fff !default;
$switch-ios-hit-area-expansion: 5px !default;
.switch[mode="ios"] { .switch[mode="ios"] {
.switch-toggle { .media-switch {
margin-top: 5px;
margin-bottom: 5px;
}
.switch-track {
/*@include transition-timing-function(ease-in-out);
@include transition-duration($switch-ios-transition-duration);
@include transition-property((background-color, border));*/
position: relative;
width: $switch-ios-width; width: $switch-ios-width;
height: $switch-ios-height; height: $switch-ios-height;
border-radius: $switch-ios-height / 2; border: solid $switch-ios-border-width $switch-ios-off-border-color;
border-radius: $switch-ios-border-radius;
background: #e5e5e5; background-color: $switch-ios-off-bg-color;
content: ' ';
cursor: pointer;
pointer-events: none;
} }
.switch-toggle:before { .switch-handle {
background: $switch-ios-slider-off-background; //@include transition($switch-ios-transition-duration cubic-bezier(0, 1.1, 1, 1.1));
//@include transition-property((background-color, transform));
position: absolute;
width: $switch-ios-handle-width;
height: $switch-ios-handle-height;
border-radius: $switch-ios-handle-radius;
background-color: $switch-ios-handle-off-bg-color;
top: 0;
left: 0;
box-shadow: $switch-ios-handle-box-shadow;
&:before {
position: absolute;
top: -4px;
left: ( ($switch-ios-handle-width / 2) * -1) - 8;
padding: ($switch-ios-handle-height / 2) + 5 ($switch-ios-handle-width + 7);
content: '';
}
} }
.switch-toggle:after { &[aria-checked=true] .switch-track {
box-shadow: 0 2px 5px rgba(0,0,0,.4); background-color: $switch-ios-on-bg-color;
border-color: $switch-ios-on-border-color;
} }
&[aria-checked=true] .switch-toggle { &[aria-checked=true] .switch-handle {
background: $switch-ios-slider-on-background; background-color: $switch-ios-handle-on-bg-color;
transform: translate3d($switch-ios-width - $switch-ios-handle-width - ($switch-ios-border-width * 2), 0, 0);
} }
&[aria-checked=true] .switch-toggle:before { .input-label {
transform: scale(0); color: inherit;
} }
} }

View File

@ -2,73 +2,29 @@
// Switch // Switch
// -------------------------------------------------- // --------------------------------------------------
$switch-padding: 0 15px !default;
$switch-width: 52px !default;
$switch-height: 32px !default;
$switch-border-width: 2px !default;
$switch-slider-off-background: #ccc !default;
$switch-slider-on-background: #387ef5 !default;
$switch-toggle-on-background: #fff !default;
.switch {
.switch .item-media {
padding: $switch-padding;
}
.switch-toggle {
position: relative; position: relative;
cursor: pointer;
width: $switch-width; @include user-select-none();
height: $switch-height;
border-radius: $switch-height / 2;
background: $switch-slider-off-background;
} }
.switch-toggle:before { .switch input {
position: absolute; position: absolute;
left: $switch-border-width; width: 0;
top: $switch-border-width; height: 0;
margin: 0;
width: $switch-width - ($switch-border-width * 2); padding: 0;
height: $switch-height - ($switch-border-width * 2); opacity: 0;
border-radius: $switch-height / 2; border: none;
@include appearance(none);
transition-duration: 300ms;
content: ' ';
} }
.switch[aria-checked=true] .switch-toggle { .switch .input-label {
background: $switch-slider-on-background; max-width: 100%;
}
.switch .switch-toggle:after {
position: absolute;
left: $switch-border-width;
top: $switch-border-width;
width: $switch-height - ($switch-border-width * 2);
height: $switch-height - ($switch-border-width * 2);
border-radius: $switch-height - ($switch-border-width * 2);
background: $switch-toggle-on-background;
transition-duration: 300ms;
content: ' ';
}
.switch[aria-checked=true] .switch-toggle:after {
transform: translate3d(20px,0,0);
} }
.switch[aria-disabled=true] { .switch[aria-disabled=true] {
pointer-events: none;
opacity: 0.5; opacity: 0.5;
color: gray; color: gray;
} }
.switch .item-media,
.switch .item-content {
pointer-events: none;
}

View File

@ -1,71 +1,127 @@
import {View, ElementRef} from 'angular2/angular2'; import {
import {ControlGroup, ControlDirective} from 'angular2/forms'; View,
Directive,
ElementRef,
Renderer,
Optional,
Parent,
NgControl
} from 'angular2/angular2';
import {Ion} from '../ion'; import {Ion} from '../ion';
import {IonInputItem} from '../form/input';
import {IonicConfig} from '../../config/config'; import {IonicConfig} from '../../config/config';
import {IonicComponent} from '../../config/annotations'; import {IonicComponent, IonicView} from '../../config/annotations';
import {Icon} from '../icon/icon';
@IonicComponent({ @IonicComponent({
selector: 'ion-switch', selector: 'ion-switch',
properties: [
'checked'
],
host: { host: {
'(click)': 'switchClicked($event)', 'class': 'item',
'class': 'item' //'[attr.aria-checked]': 'input.checked'
} }
}) })
@View({ @IonicView({
template: ` template:
<div class="item-content"> '<div class="item-content">' +
<div class="item-title"> '<content></content>' +
<content></content> '</div>' +
</div> '<div class="item-media media-switch">' +
<div class="item-media media-switch"> '<div class="switch-track">' +
<div class="switch-toggle"></div> '<div class="switch-handle"></div>' +
</div> '</div>' +
</div>` '</div>'
}) })
export class Switch extends Ion { export class Switch extends IonInputItem {
constructor( constructor(
@Optional() cd: NgControl,
renderer: Renderer,
elementRef: ElementRef, elementRef: ElementRef,
ionicConfig: IonicConfig config: IonicConfig
//cd: ControlDirective
) { ) {
super(elementRef, ionicConfig) super(elementRef, config);
this.onChange = (_) => {};
this.onTouched = (_) => {};
this.renderer = renderer;
this.elementRef = elementRef;
this.cd = cd;
// this.config = Switch.config.invoke(this) if(cd) cd.valueAccessor = this;
// this.controlDirective = cd;
// cd.valueAccessor = this;
// TODO: These rely on the commented-out PropertySetter's above
//setAriaRole('checkbox')
//setInvalid('false')
//setDisabled('false')
//this.setCheckedProperty = setAriaChecked
} }
/** onInit() {
* Much like ngModel, this is called from our valueAccessor for the attached super.onInit();
* ControlDirective to update the value internally. console.log("switch onInit")
*/
writeValue(value) {
// Convert it to a boolean
this.checked = !!value;
} }
set checked(checked) { onAllChangesDone() {
this._checked = checked return
//this.setCheckedProperty(checked) console.log("switch onAllChangesDone")
this.controlDirective._control().updateValue(this._checked); if (this._checked !== void 0 && this.input.checked != this._checked) {
if (this.input.checked !== void 0) {
console.warn("switch checked is set in view template and Control declaration.\n" +
"Value: " + !!this._checked + " from Control takes precedence");
}
this.input.checked = !!this._checked;
}
if (this._value !== void 0 && this.input.value != this._value) {
if (this.input.value !== void 0) {
console.warn("switch value is set in view template and Control declaration.\n" +
"Value: " + this._value + " from Control takes precedence");
}
this.input.value = this._value;
}
if (this.input.value === void 0) {
this.input.value = "on";
}
if (this.input.checked === void 0) {
this.input.checked = false;
}
//TODO check validity
this.cd.control._value = {"checked": !!this.input.checked, "value": this.input.value};
//TODO only want to call this once, we want to set input.checked directly on subsequent
// writeValue's
this.onAllChangesDone = () => {};
// this.onChange({"checked": this.input.checked, "value": this.input.value});
} }
get checked() { //from clicking the label or selecting with keyboard
return this._checked //view -> model (Control)
toggle() {
this.input.checked = this._checked = !this.input.checked;
this.onChange({"checked": this.input.checked, "value": this.input.value});
} }
switchClicked(ev) { // Called by the model (Control) to update the view
this.checked = !this.checked; writeValue(modelValue) {
let type = typeof modelValue;
switch (type) {
case "boolean":
// don't set input.value here, do it in onAllChangesDone
// because they might have set it in the view
this._checked = modelValue; break;
case "object":
if (modelValue.checked !== void 0) this._checked = !!modelValue.checked;
if (modelValue.value !== void 0) this._value = modelValue.value.toString();
break;
default:
// don't set input.checked here, do it in onAllChangesDone
// because they might have set it in the view
this._value = modelValue.toString();
}
//TODO we want to set input.checked directly after the first time
console.log("writeValue, " + this.input.id + " checked: " + this._checked);
console.log("writeValue " + this.input.id + " value: " + this._value);
// this.cd.control._value = {"checked": this.input.checked, "value": this.input.value};
} }
// Used by the view to update the model (Control)
// Up to us to call it in update()
registerOnChange(fn) { this.onChange = fn; }
registerOnTouched(fn) { this.onTouched = fn; }
} }

View File

@ -1,24 +1,32 @@
import {FormBuilder, Validators} from 'angular2/forms';
import {App} from 'ionic/ionic'; import {App} from 'ionic/ionic';
import {
Control,
ControlGroup,
NgForm,
formDirectives,
Validators,
NgControl,
ControlValueAccessor,
NgControlName,
NgFormModel,
FormBuilder
} from 'angular2/forms';
@App({ @App({
templateUrl: 'main.html' templateUrl: 'main.html'
}) })
class IonicApp { class IonicApp {
constructor() { constructor() {
this.fruitsForm = new ControlGroup({
var fb = new FormBuilder(); "appleCtrl": new Control({"checked": false, "value": "apple"}),
this.form = fb.group({ "bananaCtrl": new Control(true),
enableFun: ['', Validators.required], "cherryCtrl": new Control({"checked": false, "value": 12}),
enableIceCream: [false, Validators.required], "grapeCtrl": new Control("grape")
enablePizza: [true, Validators.required]
}); });
} }
doSubmit(ev) { doSubmit(ev) {
console.log('Submitting form', this.form.value); console.log('Submitting form', this.fruitsForm.value);
ev.preventDefault(); event.preventDefault();
} }
} }

View File

@ -1,25 +1,35 @@
<ion-toolbar><ion-title>Switches</ion-title></ion-toolbar>
<ion-content> <ion-content>
<form (^submit)="doSubmit($event)" [control-group]="form"> <form (^submit)="doSubmit($event)">
<ion-list>
<ion-switch aria-checked="true">
<label id="appleLabel">Apple</label>
<input checked="true" type="checkbox">
</ion-switch>
<ion-switch>
<label>Banana</label>
<input value="test" type="checkbox">
</ion-switch>
<ion-switch aria-checked="true">
<label>Cherry</label>
<input type="checkbox">
</ion-switch>
<ion-switch>
<label>Grape</label>
<input value="test" checked="checked" type="checkbox">
</ion-switch>
<ion-list> <ion-list>
<div class="list-header">Some Switches</div>
<ion-switch control="enableFun">
Enable Fun?
</ion-switch>
<ion-switch control="enableIceCream">
Enable Ice Cream?
</ion-switch>
<ion-switch control="enablePizza">
Enable Pizza?
</ion-switch>
</ion-list>
Is fun enabled? <b>{{form.controls.enableFun.value}}</b>
<br>
Is ice cream enabled? <b>{{form.controls.enableIceCream.value}}</b>
<br>
Is pizza enabled? <b>{{form.controls.enablePizza.value}}</b>
</form> </form>
</ion-content> </ion-content>

View File

@ -65,7 +65,7 @@ export const IonicDirectives = [
forwardRef(() => Checkbox), forwardRef(() => Checkbox),
forwardRef(() => RadioGroup), forwardRef(() => RadioGroup),
forwardRef(() => RadioButton), forwardRef(() => RadioButton),
//Switch forwardRef(() => Switch),
//SearchBar, //SearchBar,
// Input // Input