-
Notifications
You must be signed in to change notification settings - Fork 13.4k
Expand file tree
/
Copy pathradio.tsx
More file actions
319 lines (277 loc) · 9.53 KB
/
Copy pathradio.tsx
File metadata and controls
319 lines (277 loc) · 9.53 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
import type { ComponentInterface, EventEmitter } from '@stencil/core';
import { Component, Element, Event, Host, Method, Prop, State, Watch, h } from '@stencil/core';
import { isOptionSelected } from '@utils/forms';
import { addEventListener, removeEventListener } from '@utils/helpers';
import { createColorClasses, hostContext } from '@utils/theme';
import { getIonMode } from '../../global/ionic-global';
import type { Color } from '../../interface';
/**
* @virtualProp {"ios" | "md"} mode - The mode determines which platform styles to use.
*
* @slot - The label text to associate with the radio. Use the "labelPlacement" property to control where the label is placed relative to the radio.
*
* @part container - The container for the radio mark.
* @part label - The label text describing the radio.
* @part mark - The checkmark or dot used to indicate the checked state.
* @part supporting-text - Supporting text displayed beneath the radio label.
* @part helper-text - Supporting text displayed beneath the radio label when the radio is valid.
* @part error-text - Supporting text displayed beneath the radio label when the radio is invalid and touched.
*/
@Component({
tag: 'ion-radio',
styleUrls: {
ios: 'radio.ios.scss',
md: 'radio.md.scss',
},
shadow: true,
})
export class Radio implements ComponentInterface {
private inputId = `ion-rb-${radioButtonIds++}`;
private helperTextId = `${this.inputId}-helper-text`;
private errorTextId = `${this.inputId}-error-text`;
private radioGroup: HTMLIonRadioGroupElement | null = null;
@Element() el!: HTMLIonRadioElement;
/**
* If `true`, the radio is selected.
*/
@State() checked = false;
/**
* The tabindex of the radio button.
* @internal
*/
@State() buttonTabindex = -1;
/**
* The color to use from your application's color palette.
* Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`.
* For more information on colors, see [theming](/docs/theming/basics).
*/
@Prop({ reflect: true }) color?: Color;
/**
* The name of the control, which is submitted with the form data.
*/
@Prop() name: string = this.inputId;
/**
* If `true`, the user cannot interact with the radio.
*/
@Prop() disabled = false;
/**
* Text that is placed under the radio label and displayed when an error is detected.
*/
@Prop() errorText?: string;
/**
* Text that is placed under the radio label and displayed when no error is detected.
*/
@Prop() helperText?: string;
/**
* the value of the radio.
*/
@Prop() value?: any | null;
@Watch('value')
valueChanged() {
/**
* The new value of the radio may
* match the radio group's value,
* so we see if it should be checked.
*/
this.updateState();
}
/**
* Where to place the label relative to the radio.
* `"start"`: The label will appear to the left of the radio in LTR and to the right in RTL.
* `"end"`: The label will appear to the right of the radio in LTR and to the left in RTL.
* `"fixed"`: The label has the same behavior as `"start"` except it also has a fixed width. Long text will be truncated with ellipses ("...").
* `"stacked"`: The label will appear above the radio regardless of the direction. The alignment of the label can be controlled with the `alignment` property.
*/
@Prop() labelPlacement: 'start' | 'end' | 'fixed' | 'stacked' = 'start';
/**
* How to pack the label and radio within a line.
* `"start"`: The label and radio will appear on the left in LTR and
* on the right in RTL.
* `"end"`: The label and radio will appear on the right in LTR and
* on the left in RTL.
* `"space-between"`: The label and radio will appear on opposite
* ends of the line with space between the two elements.
* Setting this property will change the radio `display` to `block`.
*/
@Prop() justify?: 'start' | 'end' | 'space-between';
/**
* How to control the alignment of the radio and label on the cross axis.
* `"start"`: The label and control will appear on the left of the cross axis in LTR, and on the right side in RTL.
* `"center"`: The label and control will appear at the center of the cross axis in both LTR and RTL.
* Setting this property will change the radio `display` to `block`.
*/
@Prop() alignment?: 'start' | 'center';
/**
* Emitted when the radio button has focus.
*/
@Event() ionFocus!: EventEmitter<void>;
/**
* Emitted when the radio button loses focus.
*/
@Event() ionBlur!: EventEmitter<void>;
componentDidLoad() {
/**
* The value may be `undefined` if it
* gets set before the radio is
* rendered. This ensures that the radio
* is checked if the value matches. This
* happens most often when Angular is
* rendering the radio.
*/
this.updateState();
}
/** @internal */
@Method()
async setFocus(ev?: globalThis.Event) {
if (ev !== undefined) {
ev.stopPropagation();
ev.preventDefault();
}
this.el.focus();
}
/** @internal */
@Method()
async setButtonTabindex(value: number) {
this.buttonTabindex = value;
}
connectedCallback() {
if (this.value === undefined) {
this.value = this.inputId;
}
const radioGroup = (this.radioGroup = this.el.closest('ion-radio-group'));
if (radioGroup) {
this.updateState();
addEventListener(radioGroup, 'ionValueChange', this.updateState);
}
}
disconnectedCallback() {
const radioGroup = this.radioGroup;
if (radioGroup) {
removeEventListener(radioGroup, 'ionValueChange', this.updateState);
this.radioGroup = null;
}
}
private updateState = () => {
if (this.radioGroup) {
const { compareWith, value: radioGroupValue } = this.radioGroup;
this.checked = isOptionSelected(radioGroupValue, this.value, compareWith);
}
};
private onClick = () => {
const { radioGroup, checked, disabled } = this;
if (disabled) {
return;
}
/**
* The modern control does not use a native input
* inside of the radio host, so we cannot rely on the
* ev.preventDefault() behavior above. If the radio
* is checked and the parent radio group allows for empty
* selection, then we can set the checked state to false.
* Otherwise, the checked state should always be set
* to true because the checked state cannot be toggled.
*/
if (checked && radioGroup?.allowEmptySelection) {
this.checked = false;
} else {
this.checked = true;
}
};
private onFocus = () => {
this.ionFocus.emit();
};
private onBlur = () => {
this.ionBlur.emit();
};
private get hasLabel() {
return this.el.textContent !== '';
}
private renderRadioControl() {
return (
<div class="radio-icon" part="container">
<div class="radio-inner" part="mark" />
<div class="radio-ripple"></div>
</div>
);
}
private getHintTextID(): string | undefined {
const { el, helperText, errorText, helperTextId, errorTextId } = this;
if (el.classList.contains('ion-touched') && el.classList.contains('ion-invalid') && errorText) {
return errorTextId;
}
if (helperText) {
return helperTextId;
}
return undefined;
}
/**
* Responsible for rendering helper text and error text.
* This element should only be rendered if hint text is set.
*/
private renderHintText() {
const { helperText, errorText, helperTextId, errorTextId } = this;
/**
* undefined and empty string values should
* be treated as not having helper/error text.
*/
const hasHintText = !!helperText || !!errorText;
if (!hasHintText) {
return;
}
return (
<div class="radio-bottom">
<div id={helperTextId} class="helper-text" part="supporting-text helper-text">
{helperText}
</div>
<div id={errorTextId} class="error-text" part="supporting-text error-text">
{errorText}
</div>
</div>
);
}
render() {
const { checked, disabled, color, el, justify, labelPlacement, hasLabel, buttonTabindex, alignment } = this;
const mode = getIonMode(this);
const inItem = hostContext('ion-item', el);
return (
<Host
onFocus={this.onFocus}
onBlur={this.onBlur}
onClick={this.onClick}
class={createColorClasses(color, {
[mode]: true,
'in-item': inItem,
'radio-checked': checked,
'radio-disabled': disabled,
[`radio-justify-${justify}`]: justify !== undefined,
[`radio-alignment-${alignment}`]: alignment !== undefined,
[`radio-label-placement-${labelPlacement}`]: true,
// Focus and active styling should not apply when the radio is in an item
'ion-activatable': !inItem,
'ion-focusable': !inItem,
})}
role="radio"
aria-checked={checked ? 'true' : 'false'}
aria-disabled={disabled ? 'true' : null}
aria-describedby={this.getHintTextID()}
aria-invalid={this.getHintTextID() === this.errorTextId}
tabindex={buttonTabindex}
>
<label class="radio-wrapper">
<div
class={{
'label-text-wrapper': true,
'label-text-wrapper-hidden': !hasLabel,
}}
part="label"
>
<slot></slot>
{this.renderHintText()}
</div>
<div class="native-wrapper">{this.renderRadioControl()}</div>
</label>
</Host>
);
}
}
let radioButtonIds = 0;