refactor(radio): native input type=radio

This commit is contained in:
Adam Bradley
2017-11-21 08:00:50 -06:00
parent a6d0a11705
commit 048a0cc42a
14 changed files with 629 additions and 547 deletions

View File

@ -8,7 +8,7 @@ import 'ionicons';
import {
ActionSheetButton,
} from './components/action-sheet/action-sheet.js';
} from './components/action-sheet/action-sheet';
import {
AnimationBuilder,
PickerOptions,
@ -18,7 +18,7 @@ import {
import {
AlertButton,
AlertInput,
} from './components/alert/alert.js';
} from './components/alert/alert';
import {
ElementRef,
Side,
@ -26,20 +26,20 @@ import {
import {
GestureCallback,
GestureDetail,
} from './components/gesture/gesture.js';
} from './components/gesture/gesture';
import {
PickerButton,
PickerColumn as PickerColumn2,
} from './components/picker/picker.js';
} from './components/picker/picker';
import {
Event,
} from '@stencil/core';
import {
ScrollCallback,
} from './components/scroll/scroll.js';
} from './components/scroll/scroll';
import {
SelectPopoverOption,
} from './components/select/select-popover.js';
} from './components/select/select-popover';
import {
ActionSheetController as IonActionSheetController
@ -2159,7 +2159,7 @@ declare global {
import {
RadioGroup as IonRadioGroup
} from './components/radio/radio-group';
} from './components/radio-group/radio-group';
declare global {
interface HTMLIonRadioGroupElement extends IonRadioGroup, HTMLElement {
@ -2184,6 +2184,7 @@ declare global {
allowEmptySelection?: boolean,
disabled?: boolean,
name?: string,
value?: string
}
}
@ -2217,8 +2218,9 @@ declare global {
color?: string,
mode?: 'ios' | 'md',
checked?: boolean,
name?: string,
disabled?: boolean,
checked?: boolean,
value?: string
}
}

View File

@ -1,4 +1,4 @@
import { BlurEvent, BooleanInput, BooleanInputChangeEvent, FocusEvent, StyleEvent } from '../../utils/input-interfaces';
import { BlurEvent, CheckboxInput, CheckedInputChangeEvent, FocusEvent, StyleEvent } from '../../utils/input-interfaces';
import { Component, CssClassMap, Event, EventEmitter, Listen, Prop, PropDidChange } from '@stencil/core';
/**
@ -76,7 +76,7 @@ import { Component, CssClassMap, Event, EventEmitter, Listen, Prop, PropDidChang
theme: 'checkbox'
}
})
export class Checkbox implements BooleanInput {
export class Checkbox implements CheckboxInput {
private checkboxId: string;
private labelId: string;
private styleTmr: any;
@ -84,7 +84,7 @@ export class Checkbox implements BooleanInput {
/**
* @output {Event} Emitted when the checked property has changed.
*/
@Event() ionChange: EventEmitter<BooleanInputChangeEvent>;
@Event() ionChange: EventEmitter<CheckedInputChangeEvent>;
/**
* @output {Event} Emitted when the toggle has focus.
@ -123,7 +123,7 @@ export class Checkbox implements BooleanInput {
/*
* @input {boolean} If true, the user cannot interact with the checkbox. Default false.
*/
@Prop({ mutable: true }) disabled: boolean = false;
@Prop() disabled: boolean = false;
/**
* @input {string} the value of the checkbox.
@ -136,13 +136,16 @@ export class Checkbox implements BooleanInput {
}
@PropDidChange('checked')
protected checkedChanged(val: boolean) {
this.ionChange.emit({ checked: val });
checkedChanged(isChecked: boolean) {
this.ionChange.emit({
checked: isChecked,
value: this.value
});
this.emitStyle();
}
@PropDidChange('disabled')
protected disabledChanged() {
disabledChanged() {
this.emitStyle();
}
@ -157,7 +160,6 @@ export class Checkbox implements BooleanInput {
});
}
@Listen('keydown.space')
onSpace(ev: KeyboardEvent) {
this.toggle();

View File

@ -1,9 +1,8 @@
import { Component, Element, Listen, Method, Prop } from '@stencil/core';
import { Component, Element, Listen, Prop, State } from '@stencil/core';
import { createThemedClasses } from '../../utils/theme';
import { CssClassMap } from '../../index';
@Component({
tag: 'ion-item',
styleUrls: {
@ -12,15 +11,12 @@ import { CssClassMap } from '../../index';
}
})
export class Item {
private ids: number = -1;
private itemId: string;
private inputs: any = [];
private itemStyles: { [key: string]: CssClassMap } = Object.create(null);
private label: any;
private itemStyles: { [key: string]: CssClassMap } = {};
@Element() private el: HTMLElement;
@State() hasStyleChange: boolean;
/**
* @input {string} The color to use from your Sass `$colors` map.
* Default options are: `"primary"`, `"secondary"`, `"danger"`, `"light"`, and `"dark"`.
@ -60,27 +56,9 @@ export class Item {
this.itemStyles[tagName] = updatedStyles;
// returning true tells the renderer to queue an update
return hasChildStyleChange;
}
// TODO? this loads after radio group
// @Listen('ionRadioDidLoad')
// protected radioDidLoad(ev: RadioEvent) {
// const radio = ev.detail.radio;
// // register the input inside of the item
// // reset to the item's id instead of the radiogroup id
// radio.id = 'rb-' + this.registerInput('radio');
// radio.labelId = 'lbl-' + this.itemId;
// }
@Method()
getLabelText(): string {
return this.label ? this.label.getText() : '';
}
componentWillLoad() {
this.itemId = (++itemId).toString();
if (hasChildStyleChange) {
this.hasStyleChange = true;
}
}
componentDidLoad() {
@ -89,34 +67,6 @@ export class Item {
for (var i = 0; i < buttons.length; i++) {
buttons[i].itemButton = true;
}
this.label = this.el.querySelector('ion-label');
// if (label) {
// this.label = label;
// this.labelId = label.id = ('lbl-' + this.itemId);
// if (label.type) {
// this.setElementClass('item-label-' + label.type, true);
// }
// this.viewLabel = false;
// }
// if (this._viewLabel && this.inputs.length) {
// let labelText = this.getLabelText().trim();
// this._viewLabel = (labelText.length > 0);
// }
// if (this.inputs.length > 1) {
// this.setElementClass('item-multiple-inputs', true);
// }
}
/**
* @hidden
*/
registerInput(type: string) {
this.inputs.push(type);
return this.itemId + '-' + (++this.ids);
}
render() {
@ -132,6 +82,8 @@ export class Item {
'item-block': true
};
this.hasStyleChange = false;
// TODO add support for button items
const TagType = this.href ? 'a' : 'div';
@ -149,58 +101,4 @@ export class Item {
);
}
// constructor() {
// this._setName(elementRef);
// this._hasReorder = !!reorder;
// this.itemId = form.nextId().toString();
// // auto add "tappable" attribute to ion-item components that have a click listener
// if (!(<any>renderer).orgListen) {
// (<any>renderer).orgListen = renderer.listen;
// renderer.listen = function(renderElement: HTMLElement, name: string, callback: Function): Function {
// if (name === 'click' && renderElement.setAttribute) {
// renderElement.setAttribute('tappable', '');
// }
// return (<any>renderer).orgListen(renderElement, name, callback);
// };
// }
// }
// /**
// * @hidden
// */
// @ContentChild(Label)
// set contentLabel(label: Label) {
// if (label) {
// this._label = label;
// this.labelId = label.id = ('lbl-' + this.itemId);
// if (label.type) {
// this.setElementClass('item-label-' + label.type, true);
// }
// this._viewLabel = false;
// }
// }
// /**
// * @hidden
// */
// @ViewChild(Label)
// set viewLabel(label: Label) {
// if (!this._label) {
// this._label = label;
// }
// }
// /**
// * @hidden
// */
// @ContentChildren(Icon)
// set _icons(icons: QueryList<Icon>) {
// icons.forEach(icon => {
// icon.setElementClass('item-icon', true);
// });
// }
}
var itemId = -1;

View File

@ -0,0 +1,150 @@
import { Component, ComponentDidLoad, Element, Event, EventEmitter, Listen, Prop, PropDidChange, State } from '@stencil/core';
import { HTMLIonRadioElementEvent } from '../radio/radio';
import { RadioGroupInput, TextInputChangeEvent } from '../../utils/input-interfaces';
@Component({
tag: 'ion-radio-group'
})
export class RadioGroup implements ComponentDidLoad, RadioGroupInput {
radios: HTMLIonRadioElement[] = [];
@Element() el: HTMLElement;
@State() labelId: string;
/*
* @input {boolean} If true, the radios can be deselected. Default false.
*/
@Prop() allowEmptySelection = false;
/*
* @input {boolean} If true, the user cannot interact with the radio group. Default false.
*/
@Prop({ mutable: true }) disabled = false;
/**
*/
@Prop({ mutable: true }) name: string;
/**
* @input {string} the value of the radio group.
*/
@Prop({ mutable: true }) value: string;
@PropDidChange('value')
valueChanged() {
// this radio group's value just changed
// double check the button with this value is checked
if (this.value === undefined) {
// set to undefined
// ensure all that are checked become unchecked
this.radios.filter(r => r.checked).forEach(radio => {
radio.checked = false;
});
} else {
let hasChecked = false;
this.radios.forEach(radio => {
if (radio.value === this.value) {
if (!radio.checked && !hasChecked) {
// correct value for this radio
// but this radio isn't checked yet
// and we haven't found a checked yet
// so CHECK IT!
radio.checked = true;
} else if (hasChecked && radio.checked) {
// somehow we've got multiple radios
// with the same value, but only one can be checked
radio.checked = false;
}
// remember we've got a checked radio button now
hasChecked = true;
} else if (radio.checked) {
// this radio doesn't have the correct value
// and it's also checked, so let's uncheck it
radio.checked = false;
}
});
}
// emit the new value
this.ionChange.emit({ value: this.value });
}
/**
* @output {Event} Emitted when the value has changed.
*/
@Event() ionChange: EventEmitter<TextInputChangeEvent>;
@Listen('ionRadioDidLoad')
onRadioDidLoad(ev: HTMLIonRadioElementEvent) {
const radio = ev.target;
this.radios.push(radio);
radio.name = this.name;
if (radio.checked && !this.value) {
// set the initial value from the check radio's value
this.value = radio.value;
}
}
@Listen('ionRadioDidUnload')
onRadioDidUnload(ev: HTMLIonRadioElementEvent) {
const index = this.radios.indexOf(ev.target);
if (index > -1) {
this.radios.splice(index, 1);
}
}
@Listen('ionSelect')
onRadioSelect(ev: HTMLIonRadioElementEvent) {
// ionSelect only come from the checked radio button
this.radios.forEach(radio => {
if (radio === ev.target) {
this.value = radio.value;
} else {
radio.checked = false;
}
});
}
componentWillLoad() {
this.name = this.name || 'ion-rg-' + (radioGroupIds++);
}
componentDidLoad() {
// Get the list header if it exists and set the id
// this is used to set aria-labelledby
let header = this.el.querySelector('ion-list-header');
if (!header) {
header = this.el.querySelector('ion-item-divider');
}
if (header) {
const label = header.querySelector('ion-label');
if (label) {
this.labelId = label.id = this.name + '-lbl';
}
}
}
hostData() {
const hostAttrs: any = {
'role': 'radiogroup'
};
if (this.labelId) {
hostAttrs['aria-labelledby'] = this.labelId;
}
return hostAttrs;
}
render() {
return <slot></slot>;
}
}
let radioGroupIds = 0;

View File

@ -0,0 +1,46 @@
# ion-radio-group
A radio group is a group of [radio buttons](../radio). It allows
a user to select at most one radio button from a set. Checking one radio
button that belongs to a radio group unchecks any previous checked
radio button within the same group.
```html
<ion-list>
<ion-radio-group>
<ion-list-header>
Auto Manufacturers
</ion-list-header>
<ion-item>
<ion-label>Cord</ion-label>
<ion-radio value="cord"></ion-radio>
</ion-item>
<ion-item>
<ion-label>Duesenberg</ion-label>
<ion-radio value="duesenberg"></ion-radio>
</ion-item>
<ion-item>
<ion-label>Hudson</ion-label>
<ion-radio value="hudson"></ion-radio>
</ion-item>
<ion-item>
<ion-label>Packard</ion-label>
<ion-radio value="packard"></ion-radio>
</ion-item>
<ion-item>
<ion-label>Studebaker</ion-label>
<ion-radio value="studebaker"></ion-radio>
</ion-item>
</ion-radio-group>
</ion-list>
```

View File

@ -0,0 +1,105 @@
<!DOCTYPE html>
<html dir="ltr">
<head>
<meta charset="UTF-8">
<title>Radio Group - Form</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
<script src="/dist/ionic.js"></script>
</head>
<body>
<ion-app>
<ion-page>
<ion-header>
<ion-toolbar>
<ion-title>Radio Group - Form</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="outer-content">
<form>
<ion-list>
<ion-radio-group name="tannen" id="group">
<ion-list-header>
<ion-label>Luckiest Man On Earth</ion-label>
</ion-list-header>
<ion-item>
<ion-label>Biff <span id="biff"></span></ion-label>
<ion-radio value="biff" slot="start"></ion-radio>
</ion-item>
<ion-item>
<ion-label>Griff <span id="griff"></span></ion-label>
<ion-radio value="griff" slot="start"></ion-radio>
</ion-item>
<ion-item>
<ion-label>Buford <span id="buford"></span></ion-label>
<ion-radio value="buford" slot="start"></ion-radio>
</ion-item>
<ion-item>
<ion-label>George</ion-label>
<ion-radio value="george" disabled slot="start"></ion-radio>
</ion-item>
<ion-item>
<ion-button type="submit">Submit</ion-button>
</ion-item>
</ion-radio-group>
</ion-list>
</form>
<p style="margin: 20px">
Value: <span id="value"></span>
</p>
<p style="margin: 20px">
Changes: <span id="changes">0</span>
</p>
</ion-content>
</ion-page>
<script>
var changes = 0;
document.getElementById('group').addEventListener('ionChange', function(ev) {
document.getElementById('value').textContent = ev.detail.value;
changes++;
document.getElementById('changes').textContent = changes;
});
var biff = 0;
document.querySelector('[value="biff"]').addEventListener('ionSelect', function(ev) {
biff++;
document.getElementById('biff').textContent = biff;
});
var griff = 0;
document.querySelector('[value="griff"]').addEventListener('ionSelect', function(ev) {
griff++;
document.getElementById('griff').textContent = griff;
});
var buford = 0;
document.querySelector('[value="buford"]').addEventListener('ionSelect', function(ev) {
buford++;
document.getElementById('buford').textContent = buford;
});
</script>
</ion-app>
</body>
</html>

View File

@ -1,183 +0,0 @@
import { Component, Element, Event, EventEmitter, Listen, Prop, PropDidChange, State} from '@stencil/core';
import { isCheckedProperty } from '../../utils/helpers';
import { Radio, RadioEvent } from './radio';
/**
* @name RadioGroup
* @description
* A radio group is a group of [radio buttons](../RadioButton). It allows
* a user to select at most one radio button from a set. Checking one radio
* button that belongs to a radio group unchecks any previous checked
* radio button within the same group.
*
* See the [Angular Forms Docs](https://angular.io/docs/ts/latest/guide/forms.html)
* for more information on forms and inputs.
*
* @usage
* ```html
* <ion-list radio-group [(ngModel)]="autoManufacturers">
*
* <ion-list-header>
* Auto Manufacturers
* </ion-list-header>
*
* <ion-item>
* <ion-label>Cord</ion-label>
* <ion-radio value="cord"></ion-radio>
* </ion-item>
*
* <ion-item>
* <ion-label>Duesenberg</ion-label>
* <ion-radio value="duesenberg"></ion-radio>
* </ion-item>
*
* <ion-item>
* <ion-label>Hudson</ion-label>
* <ion-radio value="hudson"></ion-radio>
* </ion-item>
*
* <ion-item>
* <ion-label>Packard</ion-label>
* <ion-radio value="packard"></ion-radio>
* </ion-item>
*
* <ion-item>
* <ion-label>Studebaker</ion-label>
* <ion-radio value="studebaker"></ion-radio>
* </ion-item>
*
* </ion-list>
* ```
*
* @demo /docs/demos/src/radio/
* @see {@link /docs/components#radio Radio Component Docs}
* @see {@link ../RadioButton RadioButton API Docs}
*/
@Component({
tag: 'ion-radio-group'
})
export class RadioGroup {
radios: Radio[] = [];
radioGroupId: number;
ids = 0;
@Element() private el: HTMLElement;
@State() activeId: string;
@State() headerId: string;
/**
* @output {Event} Emitted when the value has changed.
*/
@Event() ionChange: EventEmitter;
/*
* @input {boolean} If true, the radios can be deselected. Default false.
*/
@Prop() allowEmptySelection: boolean = false;
/*
* @input {boolean} If true, the user cannot interact with the radio group. Default false.
*/
@Prop({ mutable: true }) disabled: boolean = false;
/**
* @input {string} the value of the radio group.
*/
@Prop({ mutable: true }) value: string;
@PropDidChange('value')
protected valueChanged() {
this.update();
this.ionChange.emit(this);
}
@Listen('ionRadioDidLoad')
protected radioDidLoad(ev: RadioEvent) {
const radio = ev.detail.radio;
this.radios.push(radio);
radio.radioId = 'rb-' + this.radioGroupId + '-' + (++this.ids);
// if the value is not defined then use its unique id
radio.value = !radio.value ? radio.radioId : radio.value;
if (radio.checked && !this.value) {
this.value = radio.value;
}
}
@Listen('ionRadioCheckedDidChange')
protected radioCheckedDidChange(ev: RadioEvent) {
const radio = ev.detail.radio;
// TODO shouldn't be able to set radio checked to false
// if allowEmptySelection is false
if (radio.checked && this.value !== radio.value) {
this.value = radio.checked ? radio.value : '';
}
}
@Listen('ionRadioDidToggle')
protected radioDidToggle(ev: RadioEvent) {
const radio = ev.detail.radio;
// If the group does not allow empty selection then checked
// should be true, otherwise leave it as is
radio.checked = this.allowEmptySelection ? radio.checked : true;
this.value = radio.checked ? radio.value : '';
}
componentWillLoad() {
this.radioGroupId = ++radioGroupIds;
// Get the list header if it exists and set the id
const header = this.el.querySelector('ion-list-header');
if (header) {
if (!header.id) {
header.id = 'rg-hdr-' + this.radioGroupId;
}
this.headerId = header.id;
}
}
/**
* @hidden
*/
update() {
// loop through each of the radios
let hasChecked = false;
this.radios.forEach((radio: Radio) => {
// Check the radio if the value is the same as the group value
radio.checked = isCheckedProperty(this.value, radio.value) && !hasChecked;
if (radio.checked) {
// if this button is checked, then set it as
// the radiogroup's active descendant
this.activeId = radio.radioId;
hasChecked = true;
}
});
}
hostData() {
return {
'role': 'radiogroup',
'aria-activedescendant': this.activeId,
'aria-describedby': this.headerId
};
}
render() {
return <slot></slot>;
}
}
let radioGroupIds = -1;

View File

@ -9,7 +9,7 @@ ion-radio {
display: inline-block;
}
.radio-cover {
ion-radio input {
@include position(0, null, null, 0);
position: absolute;
@ -19,4 +19,28 @@ ion-radio {
background: transparent;
cursor: pointer;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
}
.radio-outline {
display: none;
}
.radio-key .radio-outline {
position: absolute;
top: 5px;
left: 10px;
display: block;
width: 36px;
height: 36px;
background: #86A8DF;
opacity: .3;
border-radius: 50%;
}

View File

@ -1,43 +1,8 @@
import { Component, CssClassMap, Event, EventEmitter, Listen, Prop, PropDidChange, State } from '@stencil/core';
import { BlurEvent, CheckedInputChangeEvent, FocusEvent, RadioButtonInput, StyleEvent } from '../../utils/input-interfaces';
import { Component, ComponentDidLoad, ComponentDidUnload, ComponentWillLoad, CssClassMap, Event, EventEmitter, Listen, Prop, PropDidChange, State } from '@stencil/core';
import { createThemedClasses } from '../../utils/theme';
/**
* @description
* A radio button is a button that can be either checked or unchecked. A user can tap
* the button to check or uncheck it. It can also be checked from the template using
* the `checked` property.
*
* Use an element with a `radio-group` attribute to group a set of radio buttons. When
* radio buttons are inside a [radio group](../RadioGroup), exactly one radio button
* in the group can be checked at any time. If a radio button is not placed in a group,
* they will all have the ability to be checked at the same time.
*
* See the [Angular Forms Docs](https://angular.io/docs/ts/latest/guide/forms.html) for
* more information on forms and input.
*
* @usage
* ```html
* <ion-list radio-group [(ngModel)]="relationship">
* <ion-item>
* <ion-label>Friends</ion-label>
* <ion-radio value="friends" checked></ion-radio>
* </ion-item>
* <ion-item>
* <ion-label>Family</ion-label>
* <ion-radio value="family"></ion-radio>
* </ion-item>
* <ion-item>
* <ion-label>Enemies</ion-label>
* <ion-radio value="enemies" [disabled]="isDisabled"></ion-radio>
* </ion-item>
* </ion-list>
* ```
* @demo /docs/demos/src/radio/
* @see {@link /docs/components#radio Radio Component Docs}
* @see {@link ../RadioGroup RadioGroup API Docs}
*/
@Component({
tag: 'ion-radio',
styleUrls: {
@ -48,13 +13,14 @@ import { createThemedClasses } from '../../utils/theme';
theme: 'radio'
}
})
export class Radio {
labelId: string;
export class Radio implements RadioButtonInput, ComponentDidLoad, ComponentDidUnload, ComponentWillLoad {
didLoad: boolean;
inputId: string;
nativeInput: HTMLInputElement;
styleTmr: any;
@State() radioId: string;
@State() activated: boolean;
@State() keyFocus: boolean;
/**
* @output {RadioEvent} Emitted when the radio loads.
@ -66,25 +32,25 @@ export class Radio {
*/
@Event() ionRadioDidUnload: EventEmitter;
/**
* @output {RadioEvent} Emitted when the radio is toggled.
*/
@Event() ionRadioDidToggle: EventEmitter;
/**
* @output {RadioEvent} Emitted when the radio checked property is changed.
*/
@Event() ionRadioCheckedDidChange: EventEmitter;
/**
* @output {Event} Emitted when the styles change.
*/
@Event() ionStyle: EventEmitter;
@Event() ionStyle: EventEmitter<StyleEvent>;
/**
* @output {Event} Emitted when the radio is selected.
* @output {Event} Emitted when the radio button is selected.
*/
@Event() ionSelect: EventEmitter;
@Event() ionSelect: EventEmitter<CheckedInputChangeEvent>;
/**
* @output {Event} Emitted when the radio button has focus.
*/
@Event() ionFocus: EventEmitter<FocusEvent>;
/**
* @output {Event} Emitted when the radio button loses focus.
*/
@Event() ionBlur: EventEmitter<BlurEvent>;
/**
* @input {string} The color to use from your Sass `$colors` map.
@ -101,26 +67,46 @@ export class Radio {
@Prop() mode: 'ios' | 'md';
/**
* @input {boolean} If true, the radio is selected. Defaults to `false`.
*/
@Prop({ mutable: true }) checked: boolean = false;
@Prop() name: string;
/*
* @input {boolean} If true, the user cannot interact with the radio. Default false.
*/
@Prop({ mutable: true }) disabled: boolean = false;
@Prop() disabled = false;
/**
* @input {boolean} If true, the radio is selected. Defaults to `false`.
*/
@Prop({ mutable: true }) checked = false;
/**
* @input {string} the value of the radio.
*/
@Prop({ mutable: true }) value: string;
componentWillLoad() {
this.inputId = 'ion-rb-' + (radioButtonIds++);
if (this.value === undefined) {
this.value = this.inputId;
}
this.emitStyle();
}
componentDidLoad() {
this.ionRadioDidLoad.emit({ radio: this });
this.nativeInput.checked = this.checked;
this.didLoad = true;
const parentItem = this.nativeInput.closest('ion-item');
if (parentItem) {
const itemLabel = parentItem.querySelector('ion-label');
if (itemLabel) {
itemLabel.id = this.inputId + '-lbl';
this.nativeInput.setAttribute('aria-labelledby', itemLabel.id);
}
}
}
componentDidUnload() {
@ -128,53 +114,79 @@ export class Radio {
}
@PropDidChange('color')
protected colorChanged() {
colorChanged() {
this.emitStyle();
}
@PropDidChange('checked')
protected checkedChanged(val: boolean) {
this.ionRadioCheckedDidChange.emit({ radio: this });
this.ionSelect.emit({ checked: val });
checkedChanged(isChecked: boolean) {
if (this.nativeInput.checked !== isChecked) {
// keep the checked value and native input `nync
this.nativeInput.checked = isChecked;
}
if (this.didLoad && isChecked) {
// only emit ionSelect when checked is true
this.ionSelect.emit({
checked: isChecked,
value: this.value
});
}
this.emitStyle();
}
@PropDidChange('disabled')
protected disabledChanged() {
disabledChanged(val: boolean) {
this.nativeInput.disabled = val;
this.emitStyle();
}
private emitStyle() {
emitStyle() {
clearTimeout(this.styleTmr);
this.styleTmr = setTimeout(() => {
this.ionStyle.emit({
...createThemedClasses(this.mode, this.color, 'radio'),
'radio-checked': this.checked,
'radio-disabled': this.disabled,
'radio-disabled': this.disabled
});
});
}
@Listen('keydown.space')
onSpace(ev: KeyboardEvent) {
this.toggle();
ev.stopPropagation();
ev.preventDefault();
onChange() {
this.onClick();
}
toggle() {
this.checked = !this.checked;
this.ionRadioDidToggle.emit({ radio: this });
onKeyUp() {
this.keyFocus = true;
}
onFocus() {
this.ionFocus.emit();
}
onBlur() {
this.keyFocus = false;
this.ionBlur.emit();
}
@Listen('click')
onClick() {
if (!this.checked && !this.disabled) {
this.checked = true;
this.nativeInput.focus();
}
}
hostData() {
return {
class: {
const hostAttrs: any = {
'class': {
'radio-checked': this.checked,
'radio-disabled': this.disabled
'radio-disabled': this.disabled,
'radio-key': this.keyFocus
}
};
return hostAttrs;
}
render() {
@ -182,27 +194,29 @@ export class Radio {
'radio-icon': true,
'radio-checked': this.checked
};
return [
<div class={radioClasses}>
<div class='radio-inner'></div>
<div class='radio-inner'/>
</div>,
<button
class='radio-cover'
onClick={() => this.toggle()}
id={this.radioId}
aria-checked={this.checked ? 'true' : false}
aria-disabled={this.disabled ? 'true' : false}
aria-labelledby={this.labelId}
role='radio'
tabIndex={0}
/>
<div class='radio-outline'/>,
<input
type='radio'
onChange={this.onChange.bind(this)}
onFocus={this.onFocus.bind(this)}
onBlur={this.onBlur.bind(this)}
onKeyUp={this.onKeyUp.bind(this)}
id={this.inputId}
name={this.name}
value={this.value}
disabled={this.disabled}
ref={r => this.nativeInput = (r as any)}/>
];
}
}
export interface RadioEvent extends Event {
detail: {
radio: Radio;
};
export interface HTMLIonRadioElementEvent extends CustomEvent {
target: HTMLIonRadioElement;
}
let radioButtonIds = 0;

View File

@ -0,0 +1,40 @@
# ion-radio
A radio button is a button that can be either checked or unchecked. A user can tap
the button to check or uncheck it. It can also be checked programmatically by changing
the `checked` property or `checked` attribute.
Use an `ion-radio-group` component to group a set of radio buttons. When
radio buttons are inside a [radio group](../radio-group), only one radio button
in the group can be checked at any time. If a radio button is not placed in a group,
then they will all have the ability to be checked at the same time.
```html
<ion-list>
<ion-radio-group>
<ion-list-header>
Name
</ion-list-header>
<ion-item>
<ion-label>Biff</ion-label>
<ion-radio value="biff" checked></ion-radio>
</ion-item>
<ion-item>
<ion-label>Griff</ion-label>
<ion-radio value="griff"></ion-radio>
</ion-item>
<ion-item>
<ion-label>Buford</ion-label>
<ion-radio value="buford"></ion-radio>
</ion-item>
</ion-radio-group>
</ion-list>
```

View File

@ -1,4 +1,4 @@
import { BlurEvent, BooleanInput, BooleanInputChangeEvent, FocusEvent, StyleEvent } from '../../utils/input-interfaces';
import { BlurEvent, CheckboxInput, CheckedInputChangeEvent, FocusEvent, StyleEvent } from '../../utils/input-interfaces';
import { Component, Event, EventEmitter, Listen, Method, Prop, PropDidChange, State } from '@stencil/core';
import { GestureDetail } from '../../index';
import { hapticSelection } from '../../utils/haptic';
@ -14,7 +14,7 @@ import { hapticSelection } from '../../utils/haptic';
theme: 'toggle'
}
})
export class Toggle implements BooleanInput {
export class Toggle implements CheckboxInput {
private toggleId: string;
private labelId: string;
private styleTmr: any;
@ -27,7 +27,7 @@ export class Toggle implements BooleanInput {
/**
* @output {Event} Emitted when the value property has changed.
*/
@Event() ionChange: EventEmitter<BooleanInputChangeEvent>;
@Event() ionChange: EventEmitter<CheckedInputChangeEvent>;
/**
* @output {Event} Emitted when the styles change.
@ -64,8 +64,11 @@ export class Toggle implements BooleanInput {
@Prop({ mutable: true }) checked: boolean = false;
@PropDidChange('checked')
protected checkedChanged(val: boolean) {
this.ionChange.emit({ checked: val });
checkedChanged(isChecked: boolean) {
this.ionChange.emit({
checked: isChecked,
value: this.value
});
this.emitStyle();
}
@ -75,7 +78,7 @@ export class Toggle implements BooleanInput {
@Prop({ mutable: true }) disabled: boolean = false;
@PropDidChange('disabled')
protected disabledChanged() {
disabledChanged() {
this.emitStyle();
}

View File

@ -117,8 +117,8 @@ export {
iOSLeaveAnimation as PopoverIOSLeaveAnimation
} from './components/popover/popover';
export { PopoverController } from './components/popover-controller/popover-controller';
export { RadioGroup } from './components/radio/radio-group';
export { Radio, RadioEvent } from './components/radio/radio';
export { RadioGroup } from './components/radio-group/radio-group';
export { Radio, HTMLIonRadioElementEvent } from './components/radio/radio';
export { RangeKnob } from './components/range/range-knob';
export { Range, RangeEvent } from './components/range/range';
export { ReorderGroup } from './components/reorder/reorder-group';

View File

@ -3,9 +3,6 @@ import { EventEmitter } from '@stencil/core';
export interface BaseInput {
// Properties
// ------------------------
/**
* Indicates that the user cannot interact with the control.
*/
@ -24,9 +21,19 @@ export interface BaseInput {
*/
value: string;
}
// Events
// ------------------------
export interface CheckboxInput extends BaseInput {
/**
* Returns / Sets the current state of the element when type is checkbox or radio.
*/
checked: boolean;
/**
* The change event is fired when the value of has changed.
*/
ionChange: EventEmitter<CheckedInputChangeEvent>;
/**
* Removes focus from input; keystrokes will subsequently go nowhere.
@ -45,27 +52,50 @@ export interface BaseInput {
* its wrapping ion-item a different style.
*/
ionStyle: EventEmitter<StyleEvent>;
}
export interface BooleanInput extends BaseInput {
export interface RadioButtonInput extends BaseInput {
/**
* Returns / Sets the current state of the element when type is checkbox or radio.
*/
checked: boolean;
/**
* The change event is fired when the value of has changed.
* This is actually more similar to the native "input" event
* https://developer.mozilla.org/en-US/docs/Web/Events/input
* A single radio button fires an ionSelect event, whereas
* a radio group fires an ionChange event. It would be more common
* to attach listeners to the radio group, not individual radio buttons.
*/
ionChange: EventEmitter<BooleanInputChangeEvent>;
ionSelect: EventEmitter<CheckedInputChangeEvent>;
/**
* Removes focus from input; keystrokes will subsequently go nowhere.
*/
ionBlur: EventEmitter<BlurEvent>;
/**
* Focus on the input element; keystrokes will subsequently go to this element.
*/
ionFocus: EventEmitter<FocusEvent>;
/**
* Emitted when the styles change. This is useful for parent
* components to know how to style themselves depending on the
* child input. For example, a disabled ion-toggle may give
* its wrapping ion-item a different style.
*/
ionStyle: EventEmitter<StyleEvent>;
}
export interface BooleanInputChangeEvent {
export interface CheckedInputChangeEvent {
checked: boolean;
value: string;
}
export interface TextInputChangeEvent {
value: string;
}
@ -74,7 +104,6 @@ export interface SelectOptionInput extends BaseInput {
* Indicates whether the option is currently selected.
*/
selected: boolean;
}
@ -95,18 +124,92 @@ export interface SelectInput extends BaseInput {
* whether multiple items can be selected.
*/
multiple: boolean;
/**
* The change event is fired when the value of has changed.
* This is actually more similar to the native "input" event
* https://developer.mozilla.org/en-US/docs/Web/Events/input
*/
ionChange: EventEmitter<CheckedInputChangeEvent>;
/**
* Removes focus from input; keystrokes will subsequently go nowhere.
*/
ionBlur: EventEmitter<BlurEvent>;
/**
* Focus on the input element; keystrokes will subsequently go to this element.
*/
ionFocus: EventEmitter<FocusEvent>;
/**
* Emitted when the styles change. This is useful for parent
* components to know how to style themselves depending on the
* child input. For example, a disabled ion-toggle may give
* its wrapping ion-item a different style.
*/
ionStyle: EventEmitter<StyleEvent>;
}
export interface RadioGroupInput extends BaseInput {
ionChange: EventEmitter<TextInputChangeEvent>;
}
export interface TextInput extends BaseInput {
/**
*
* The change event is fired when the value of has changed.
* This is actually more similar to the native "input" event
* https://developer.mozilla.org/en-US/docs/Web/Events/input
*/
ionChange: EventEmitter<CheckedInputChangeEvent>;
/**
* Removes focus from input; keystrokes will subsequently go nowhere.
*/
ionBlur: EventEmitter<BlurEvent>;
/**
* Focus on the input element; keystrokes will subsequently go to this element.
*/
ionFocus: EventEmitter<FocusEvent>;
/**
* Emitted when the styles change. This is useful for parent
* components to know how to style themselves depending on the
* child input. For example, a disabled ion-toggle may give
* its wrapping ion-item a different style.
*/
ionStyle: EventEmitter<StyleEvent>;
}
export interface TextMultiLineInput extends TextInput {
/**
* The change event is fired when the value of has changed.
* This is actually more similar to the native "input" event
* https://developer.mozilla.org/en-US/docs/Web/Events/input
*/
ionChange: EventEmitter<CheckedInputChangeEvent>;
/**
* Removes focus from input; keystrokes will subsequently go nowhere.
*/
ionBlur: EventEmitter<BlurEvent>;
/**
* Focus on the input element; keystrokes will subsequently go to this element.
*/
ionFocus: EventEmitter<FocusEvent>;
/**
* Emitted when the styles change. This is useful for parent
* components to know how to style themselves depending on the
* child input. For example, a disabled ion-toggle may give
* its wrapping ion-item a different style.
*/
ionStyle: EventEmitter<StyleEvent>;
}

View File

@ -1,122 +0,0 @@
import { EventEmitter } from '@stencil/core';
export interface BaseInput {
// Properties
// ------------------------
/**
* Indicates that the user cannot interact with the control.
*/
disabled: boolean;
/**
* Returns / Sets the element's readonly attribute, indicating that
* the user cannot modify the value of the control. HTML5. This is
* ignored if the value of the type attribute is hidden, range, color,
* checkbox, radio, file, or a button type.
*/
readOnly?: boolean;
/**
* Reflects the value of the form control.
*/
value: string;
// Events
// ------------------------
/**
* Removes focus from input; keystrokes will subsequently go nowhere.
*/
ionBlur: EventEmitter<BlurEvent>;
/**
* Focus on the input element; keystrokes will subsequently go to this element.
*/
ionFocus: EventEmitter<FocusEvent>;
/**
* Emitted when the styles change. This is useful for parent
* components to know how to style themselves depending on the
* child input. For example, a disabled ion-toggle may give
* its wrapping ion-item a different style.
*/
ionStyle: EventEmitter<StyleEvent>;
}
export interface BooleanInput extends BaseInput {
/**
* Returns / Sets the current state of the element when type is checkbox or radio.
*/
checked: boolean;
/**
* The change event is fired when the value of has changed.
* This is actually more similar to the native "input" event
* https://developer.mozilla.org/en-US/docs/Web/Events/input
*/
ionChange: EventEmitter<BooleanInputChangeEvent>;
}
export interface BooleanInputChangeEvent {
checked: boolean;
}
export interface SelectOptionInput extends BaseInput {
/**
* Indicates whether the option is currently selected.
*/
selected: boolean;
}
export interface SelectInput extends BaseInput {
/**
* Indicates whether the option is currently selected.
*/
selected: boolean;
/**
* A long reflecting the index of the first selected <option> element.
* The value -1 indicates no element is selected.
*/
selectedIndex: number;
/**
* Reflecting the multiple HTML attribute, which indicates
* whether multiple items can be selected.
*/
multiple: boolean;
}
export interface TextInput extends BaseInput {
/**
*
*/
}
export interface TextMultiLineInput extends TextInput {
}
export interface StyleEvent {
[styleName: string]: boolean;
}
export interface FocusEvent {}
export interface BlurEvent {}