Skip to content

Commit d1260b2

Browse files
committed
feat(toggle): add helper and error text properties and design
1 parent ac4ea32 commit d1260b2

File tree

6 files changed

+160
-27
lines changed

6 files changed

+160
-27
lines changed

core/api.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1940,6 +1940,8 @@ ion-toggle,prop,checked,boolean,false,false,false
19401940
ion-toggle,prop,color,"danger" | "dark" | "light" | "medium" | "primary" | "secondary" | "success" | "tertiary" | "warning" | string & Record<never, never> | undefined,undefined,false,true
19411941
ion-toggle,prop,disabled,boolean,false,false,false
19421942
ion-toggle,prop,enableOnOffLabels,boolean | undefined,config.get('toggleOnOffLabels'),false,false
1943+
ion-toggle,prop,errorText,string | undefined,undefined,false,false
1944+
ion-toggle,prop,helperText,string | undefined,undefined,false,false
19431945
ion-toggle,prop,justify,"end" | "space-between" | "start" | undefined,undefined,false,false
19441946
ion-toggle,prop,labelPlacement,"end" | "fixed" | "stacked" | "start",'start',false,false
19451947
ion-toggle,prop,mode,"ios" | "md",undefined,false,false
@@ -1972,8 +1974,11 @@ ion-toggle,css-prop,--track-background,ios
19721974
ion-toggle,css-prop,--track-background,md
19731975
ion-toggle,css-prop,--track-background-checked,ios
19741976
ion-toggle,css-prop,--track-background-checked,md
1977+
ion-toggle,part,error-text
19751978
ion-toggle,part,handle
1979+
ion-toggle,part,helper-text
19761980
ion-toggle,part,label
1981+
ion-toggle,part,supporting-text
19771982
ion-toggle,part,track
19781983

19791984
ion-toolbar,shadow

core/src/components.d.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3264,6 +3264,14 @@ export namespace Components {
32643264
* Enables the on/off accessibility switch labels within the toggle.
32653265
*/
32663266
"enableOnOffLabels": boolean | undefined;
3267+
/**
3268+
* Text that is placed under the toggle and displayed when an error is detected.
3269+
*/
3270+
"errorText"?: string;
3271+
/**
3272+
* Text that is placed under the toggle and displayed when no error is detected.
3273+
*/
3274+
"helperText"?: string;
32673275
/**
32683276
* How to pack the label and toggle within a line. `"start"`: The label and toggle will appear on the left in LTR and on the right in RTL. `"end"`: The label and toggle will appear on the right in LTR and on the left in RTL. `"space-between"`: The label and toggle will appear on opposite ends of the line with space between the two elements. Setting this property will change the toggle `display` to `block`.
32693277
*/
@@ -8127,6 +8135,14 @@ declare namespace LocalJSX {
81278135
* Enables the on/off accessibility switch labels within the toggle.
81288136
*/
81298137
"enableOnOffLabels"?: boolean | undefined;
8138+
/**
8139+
* Text that is placed under the toggle and displayed when an error is detected.
8140+
*/
8141+
"errorText"?: string;
8142+
/**
8143+
* Text that is placed under the toggle and displayed when no error is detected.
8144+
*/
8145+
"helperText"?: string;
81308146
/**
81318147
* How to pack the label and toggle within a line. `"start"`: The label and toggle will appear on the left in LTR and on the right in RTL. `"end"`: The label and toggle will appear on the right in LTR and on the left in RTL. `"space-between"`: The label and toggle will appear on opposite ends of the line with space between the two elements. Setting this property will change the toggle `display` to `block`.
81328148
*/

core/src/components/toggle/toggle.scss

Lines changed: 75 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -134,47 +134,53 @@ input {
134134
align-items: center;
135135
}
136136

137-
// Toggle Justify
138-
// --------------------------------------------------
137+
// Toggle Bottom Content
138+
// ----------------------------------------------------------------
139+
140+
.toggle-bottom {
141+
@include padding(5px, null, null, null);
142+
143+
display: flex;
139144

140-
:host(.toggle-justify-space-between) .toggle-wrapper {
141145
justify-content: space-between;
142-
}
143146

144-
:host(.toggle-justify-start) .toggle-wrapper {
145-
justify-content: start;
147+
font-size: dynamic-font(12px);
148+
149+
white-space: normal;
146150
}
147151

148-
:host(.toggle-justify-end) .toggle-wrapper {
149-
justify-content: end;
152+
:host(.toggle-label-placement-stacked) .toggle-bottom {
153+
font-size: dynamic-font(16px);
150154
}
151155

152-
// Toggle Align
153-
// --------------------------------------------------
156+
// Toggle Hint Text
157+
// ----------------------------------------------------------------
154158

159+
/**
160+
* Error text should only be shown when .ion-invalid is
161+
* present on the checkbox. Otherwise the helper text should
162+
* be shown.
163+
*/
164+
.toggle-bottom .error-text {
165+
display: none;
155166

156-
:host(.toggle-alignment-start) .toggle-wrapper {
157-
align-items: start;
167+
color: ion-color(danger, base);
158168
}
159169

160-
:host(.toggle-alignment-center) .toggle-wrapper {
161-
align-items: center;
162-
}
170+
.toggle-bottom .helper-text {
171+
display: block;
163172

164-
// Justify Content & Align Items
165-
// ---------------------------------------------
173+
color: $text-color-step-300;
174+
}
166175

167-
// The toggle should be displayed as block when either justify
168-
// or alignment is set; otherwise, these properties will have no
169-
// visible effect.
170-
:host(.toggle-justify-space-between),
171-
:host(.toggle-justify-start),
172-
:host(.toggle-justify-end),
173-
:host(.toggle-alignment-start),
174-
:host(.toggle-alignment-center) {
176+
:host(.ion-touched.ion-invalid) .toggle-bottom .error-text {
175177
display: block;
176178
}
177179

180+
:host(.ion-touched.ion-invalid) .toggle-bottom .helper-text {
181+
display: none;
182+
}
183+
178184
// Toggle Label Placement - Start
179185
// ----------------------------------------------------------------
180186

@@ -204,6 +210,8 @@ input {
204210
*/
205211
:host(.toggle-label-placement-end) .toggle-wrapper {
206212
flex-direction: row-reverse;
213+
214+
justify-content: start;
207215
}
208216

209217
/**
@@ -247,6 +255,8 @@ input {
247255
*/
248256
:host(.toggle-label-placement-stacked) .toggle-wrapper {
249257
flex-direction: column;
258+
259+
text-align: center;
250260
}
251261

252262
:host(.toggle-label-placement-stacked) .label-text-wrapper {
@@ -274,6 +284,46 @@ input {
274284
@include transform-origin(center, top);
275285
}
276286

287+
// Toggle Justify
288+
// --------------------------------------------------
289+
290+
:host(.toggle-justify-space-between) .toggle-wrapper {
291+
justify-content: space-between;
292+
}
293+
294+
:host(.toggle-justify-start) .toggle-wrapper {
295+
justify-content: start;
296+
}
297+
298+
:host(.toggle-justify-end) .toggle-wrapper {
299+
justify-content: end;
300+
}
301+
302+
// Toggle Align
303+
// --------------------------------------------------
304+
305+
:host(.toggle-alignment-start) .toggle-wrapper {
306+
align-items: start;
307+
}
308+
309+
:host(.toggle-alignment-center) .toggle-wrapper {
310+
align-items: center;
311+
}
312+
313+
// Justify Content & Align Items
314+
// ---------------------------------------------
315+
316+
// The toggle should be displayed as block when either justify
317+
// or alignment is set; otherwise, these properties will have no
318+
// visible effect.
319+
:host(.toggle-justify-space-between),
320+
:host(.toggle-justify-start),
321+
:host(.toggle-justify-end),
322+
:host(.toggle-alignment-start),
323+
:host(.toggle-alignment-center) {
324+
display: block;
325+
}
326+
277327
// Toggle Background Track: Unchecked
278328
// --------------------------------------------------
279329

core/src/components/toggle/toggle.tsx

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ import type { ToggleChangeEventDetail } from './toggle-interface';
2121
* @part track - The background track of the toggle.
2222
* @part handle - The toggle handle, or knob, used to change the checked state.
2323
* @part label - The label text describing the toggle.
24+
* @part supporting-text - Supporting text displayed beneath the toggle label.
25+
* @part helper-text - Supporting text displayed beneath the toggle label when the toggle is valid.
26+
* @part error-text - Supporting text displayed beneath the toggle label when the toggle is invalid and touched.
2427
*/
2528
@Component({
2629
tag: 'ion-toggle',
@@ -32,6 +35,8 @@ import type { ToggleChangeEventDetail } from './toggle-interface';
3235
})
3336
export class Toggle implements ComponentInterface {
3437
private inputId = `ion-tg-${toggleIds++}`;
38+
private helperTextId = `${this.inputId}-helper-text`;
39+
private errorTextId = `${this.inputId}-error-text`;
3540
private gesture?: Gesture;
3641
private focusEl?: HTMLElement;
3742
private lastDrag = 0;
@@ -65,6 +70,16 @@ export class Toggle implements ComponentInterface {
6570
*/
6671
@Prop() disabled = false;
6772

73+
/**
74+
* Text that is placed under the toggle and displayed when an error is detected.
75+
*/
76+
@Prop() errorText?: string;
77+
78+
/**
79+
* Text that is placed under the toggle and displayed when no error is detected.
80+
*/
81+
@Prop() helperText?: string;
82+
6883
/**
6984
* The value of the toggle does not mean if it's checked or not, use the `checked`
7085
* property for that.
@@ -289,6 +304,48 @@ export class Toggle implements ComponentInterface {
289304
return this.el.textContent !== '';
290305
}
291306

307+
private getHintTextID(): string | undefined {
308+
const { el, helperText, errorText, helperTextId, errorTextId } = this;
309+
310+
if (el.classList.contains('ion-touched') && el.classList.contains('ion-invalid') && errorText) {
311+
return errorTextId;
312+
}
313+
314+
if (helperText) {
315+
return helperTextId;
316+
}
317+
318+
return undefined;
319+
}
320+
321+
/**
322+
* Responsible for rendering helper text and error text.
323+
* This element should only be rendered if hint text is set.
324+
*/
325+
private renderHintText() {
326+
const { helperText, errorText, helperTextId, errorTextId } = this;
327+
328+
/**
329+
* undefined and empty string values should
330+
* be treated as not having helper/error text.
331+
*/
332+
const hasHintText = !!helperText || !!errorText;
333+
if (!hasHintText) {
334+
return;
335+
}
336+
337+
return (
338+
<div class="toggle-bottom">
339+
<div id={helperTextId} class="helper-text" part="supporting-text helper-text">
340+
{helperText}
341+
</div>
342+
<div id={errorTextId} class="error-text" part="supporting-text error-text">
343+
{errorText}
344+
</div>
345+
</div>
346+
);
347+
}
348+
292349
render() {
293350
const { activated, color, checked, disabled, el, justify, labelPlacement, inputId, name, alignment } = this;
294351

@@ -327,6 +384,8 @@ export class Toggle implements ComponentInterface {
327384
onFocus={() => this.onFocus()}
328385
onBlur={() => this.onBlur()}
329386
ref={(focusEl) => (this.focusEl = focusEl)}
387+
aria-describedby={this.getHintTextID()}
388+
aria-invalid={this.getHintTextID() === this.errorTextId}
330389
{...this.inheritedAttributes}
331390
/>
332391
<div
@@ -337,6 +396,7 @@ export class Toggle implements ComponentInterface {
337396
part="label"
338397
>
339398
<slot></slot>
399+
{this.renderHintText()}
340400
</div>
341401
<div class="native-wrapper">{this.renderToggleControl()}</div>
342402
</label>

packages/angular/src/directives/proxies.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2473,14 +2473,14 @@ Shorthand for ionToastDidDismiss.
24732473

24742474

24752475
@ProxyCmp({
2476-
inputs: ['alignment', 'checked', 'color', 'disabled', 'enableOnOffLabels', 'justify', 'labelPlacement', 'mode', 'name', 'value']
2476+
inputs: ['alignment', 'checked', 'color', 'disabled', 'enableOnOffLabels', 'errorText', 'helperText', 'justify', 'labelPlacement', 'mode', 'name', 'value']
24772477
})
24782478
@Component({
24792479
selector: 'ion-toggle',
24802480
changeDetection: ChangeDetectionStrategy.OnPush,
24812481
template: '<ng-content></ng-content>',
24822482
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
2483-
inputs: ['alignment', 'checked', 'color', 'disabled', 'enableOnOffLabels', 'justify', 'labelPlacement', 'mode', 'name', 'value'],
2483+
inputs: ['alignment', 'checked', 'color', 'disabled', 'enableOnOffLabels', 'errorText', 'helperText', 'justify', 'labelPlacement', 'mode', 'name', 'value'],
24842484
})
24852485
export class IonToggle {
24862486
protected el: HTMLElement;

packages/vue/src/proxies.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -895,6 +895,8 @@ export const IonToggle = /*@__PURE__*/ defineContainer<JSX.IonToggle, JSX.IonTog
895895
'name',
896896
'checked',
897897
'disabled',
898+
'errorText',
899+
'helperText',
898900
'value',
899901
'enableOnOffLabels',
900902
'labelPlacement',

0 commit comments

Comments
 (0)