mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2026-03-13 10:22:08 +08:00
Compare commits
36 Commits
shane-patc
...
ROU-11112
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6d5592e63c | ||
|
|
f94bcadc49 | ||
|
|
2dd8a18bb4 | ||
|
|
e296707c94 | ||
|
|
0a502a196f | ||
|
|
e8971df5fe | ||
|
|
8743a4f3be | ||
|
|
7f45146ce3 | ||
|
|
8abf12676a | ||
|
|
621333d927 | ||
|
|
6cf454f7c4 | ||
|
|
66210e60a8 | ||
|
|
b31b97648c | ||
|
|
856a5a051a | ||
|
|
dfae50cba4 | ||
|
|
80d4fa63ec | ||
|
|
72494dbe31 | ||
|
|
d407de5d6c | ||
|
|
82eb743eab | ||
|
|
397c72aadd | ||
|
|
b423ae7c8c | ||
|
|
8ecb96e737 | ||
|
|
d1bc696f9f | ||
|
|
ac4ea3232b | ||
|
|
4af07f3c02 | ||
|
|
1cf303f384 | ||
|
|
e31e5fdca5 | ||
|
|
1d5cd4b6b3 | ||
|
|
76fa00d021 | ||
|
|
3f46aef949 | ||
|
|
934dd0f303 | ||
|
|
3bc976b8c6 | ||
|
|
3f24a126bf | ||
|
|
ed3788271b | ||
|
|
00961c6e4b | ||
|
|
efd3e0fd2b |
@@ -403,6 +403,7 @@ ion-checkbox,prop,justify,"end" | "space-between" | "start" | undefined,undefine
|
||||
ion-checkbox,prop,labelPlacement,"end" | "fixed" | "stacked" | "start",'start',false,false
|
||||
ion-checkbox,prop,mode,"ios" | "md",undefined,false,false
|
||||
ion-checkbox,prop,name,string,this.inputId,false,false
|
||||
ion-checkbox,prop,required,boolean,false,false,false
|
||||
ion-checkbox,prop,value,any,'on',false,false
|
||||
ion-checkbox,event,ionBlur,void,true
|
||||
ion-checkbox,event,ionChange,CheckboxChangeEventDetail<any>,true
|
||||
@@ -1346,6 +1347,7 @@ ion-radio-group,none
|
||||
ion-radio-group,prop,allowEmptySelection,boolean,false,false,false
|
||||
ion-radio-group,prop,compareWith,((currentValue: any, compareValue: any) => boolean) | null | string | undefined,undefined,false,false
|
||||
ion-radio-group,prop,name,string,this.inputId,false,false
|
||||
ion-radio-group,prop,required,boolean,false,false,false
|
||||
ion-radio-group,prop,value,any,undefined,false,false
|
||||
ion-radio-group,event,ionChange,RadioGroupChangeEventDetail<any>,true
|
||||
|
||||
@@ -1631,6 +1633,7 @@ ion-select,prop,multiple,boolean,false,false,false
|
||||
ion-select,prop,name,string,this.inputId,false,false
|
||||
ion-select,prop,okText,string,'OK',false,false
|
||||
ion-select,prop,placeholder,string | undefined,undefined,false,false
|
||||
ion-select,prop,required,boolean,false,false,false
|
||||
ion-select,prop,selectedText,null | string | undefined,undefined,false,false
|
||||
ion-select,prop,shape,"round" | undefined,undefined,false,false
|
||||
ion-select,prop,toggleIcon,string | undefined,undefined,false,false
|
||||
@@ -1944,6 +1947,7 @@ ion-toggle,prop,justify,"end" | "space-between" | "start" | undefined,undefined,
|
||||
ion-toggle,prop,labelPlacement,"end" | "fixed" | "stacked" | "start",'start',false,false
|
||||
ion-toggle,prop,mode,"ios" | "md",undefined,false,false
|
||||
ion-toggle,prop,name,string,this.inputId,false,false
|
||||
ion-toggle,prop,required,boolean,false,false,false
|
||||
ion-toggle,prop,value,null | string | undefined,'on',false,false
|
||||
ion-toggle,event,ionBlur,void,true
|
||||
ion-toggle,event,ionChange,ToggleChangeEventDetail<any>,true
|
||||
@@ -2000,4 +2004,7 @@ ion-toolbar,css-prop,--padding-end,md
|
||||
ion-toolbar,css-prop,--padding-start,ios
|
||||
ion-toolbar,css-prop,--padding-start,md
|
||||
ion-toolbar,css-prop,--padding-top,ios
|
||||
ion-toolbar,css-prop,--padding-top,md
|
||||
ion-toolbar,css-prop,--padding-top,md
|
||||
ion-toolbar,part,background
|
||||
ion-toolbar,part,container
|
||||
ion-toolbar,part,content
|
||||
32
core/src/components.d.ts
vendored
32
core/src/components.d.ts
vendored
@@ -643,6 +643,10 @@ export namespace Components {
|
||||
* The name of the control, which is submitted with the form data.
|
||||
*/
|
||||
"name": string;
|
||||
/**
|
||||
* If true, screen readers will announce it as a required field. This property works only for accessibility purposes, it will not prevent the form from submitting if the value is invalid.
|
||||
*/
|
||||
"required": boolean;
|
||||
"setFocus": () => Promise<void>;
|
||||
/**
|
||||
* The value of the checkbox does not mean if it's checked or not, use the `checked` property for that. The value of a checkbox is analogous to the value of an `<input type="checkbox">`, it's only used when the checkbox participates in a native `<form>`.
|
||||
@@ -2303,6 +2307,10 @@ export namespace Components {
|
||||
* The name of the control, which is submitted with the form data.
|
||||
*/
|
||||
"name": string;
|
||||
/**
|
||||
* If `true`, the user must fill in a value before submitting a form.
|
||||
*/
|
||||
"required": boolean;
|
||||
"setFocus": () => Promise<void>;
|
||||
/**
|
||||
* the value of the radio group.
|
||||
@@ -2808,6 +2816,10 @@ export namespace Components {
|
||||
* The text to display when the select is empty.
|
||||
*/
|
||||
"placeholder"?: string;
|
||||
/**
|
||||
* If true, screen readers will announce it as a required field. This property works only for accessibility purposes, it will not prevent the form from submitting if the value is invalid.
|
||||
*/
|
||||
"required": boolean;
|
||||
/**
|
||||
* The text to display instead of the selected option's value.
|
||||
*/
|
||||
@@ -3280,6 +3292,10 @@ export namespace Components {
|
||||
* The name of the control, which is submitted with the form data.
|
||||
*/
|
||||
"name": string;
|
||||
/**
|
||||
* If true, screen readers will announce it as a required field. This property works only for accessibility purposes, it will not prevent the form from submitting if the value is invalid.
|
||||
*/
|
||||
"required": boolean;
|
||||
/**
|
||||
* The value of the toggle does not mean if it's checked or not, use the `checked` property for that. The value of a toggle is analogous to the value of a `<input type="checkbox">`, it's only used when the toggle participates in a native `<form>`.
|
||||
*/
|
||||
@@ -5435,6 +5451,10 @@ declare namespace LocalJSX {
|
||||
* Emitted when the checkbox has focus.
|
||||
*/
|
||||
"onIonFocus"?: (event: IonCheckboxCustomEvent<void>) => void;
|
||||
/**
|
||||
* If true, screen readers will announce it as a required field. This property works only for accessibility purposes, it will not prevent the form from submitting if the value is invalid.
|
||||
*/
|
||||
"required"?: boolean;
|
||||
/**
|
||||
* The value of the checkbox does not mean if it's checked or not, use the `checked` property for that. The value of a checkbox is analogous to the value of an `<input type="checkbox">`, it's only used when the checkbox participates in a native `<form>`.
|
||||
*/
|
||||
@@ -7067,6 +7087,10 @@ declare namespace LocalJSX {
|
||||
* Emitted when the `value` property has changed. This is used to ensure that `ion-radio` can respond to any value property changes from the group.
|
||||
*/
|
||||
"onIonValueChange"?: (event: IonRadioGroupCustomEvent<RadioGroupChangeEventDetail>) => void;
|
||||
/**
|
||||
* If `true`, the user must fill in a value before submitting a form.
|
||||
*/
|
||||
"required"?: boolean;
|
||||
/**
|
||||
* the value of the radio group.
|
||||
*/
|
||||
@@ -7640,6 +7664,10 @@ declare namespace LocalJSX {
|
||||
* The text to display when the select is empty.
|
||||
*/
|
||||
"placeholder"?: string;
|
||||
/**
|
||||
* If true, screen readers will announce it as a required field. This property works only for accessibility purposes, it will not prevent the form from submitting if the value is invalid.
|
||||
*/
|
||||
"required"?: boolean;
|
||||
/**
|
||||
* The text to display instead of the selected option's value.
|
||||
*/
|
||||
@@ -8155,6 +8183,10 @@ declare namespace LocalJSX {
|
||||
* Emitted when the toggle has focus.
|
||||
*/
|
||||
"onIonFocus"?: (event: IonToggleCustomEvent<void>) => void;
|
||||
/**
|
||||
* If true, screen readers will announce it as a required field. This property works only for accessibility purposes, it will not prevent the form from submitting if the value is invalid.
|
||||
*/
|
||||
"required"?: boolean;
|
||||
/**
|
||||
* The value of the toggle does not mean if it's checked or not, use the `checked` property for that. The value of a toggle is analogous to the value of a `<input type="checkbox">`, it's only used when the toggle participates in a native `<form>`.
|
||||
*/
|
||||
|
||||
@@ -98,6 +98,13 @@ export class Checkbox implements ComponentInterface {
|
||||
*/
|
||||
@Prop() alignment?: 'start' | 'center';
|
||||
|
||||
/**
|
||||
* If true, screen readers will announce it as a required field. This property
|
||||
* works only for accessibility purposes, it will not prevent the form from
|
||||
* submitting if the value is invalid.
|
||||
*/
|
||||
@Prop() required = false;
|
||||
|
||||
/**
|
||||
* Emitted when the checked property has changed as a result of a user action such as a click.
|
||||
*
|
||||
@@ -182,6 +189,7 @@ export class Checkbox implements ComponentInterface {
|
||||
name,
|
||||
value,
|
||||
alignment,
|
||||
required,
|
||||
} = this;
|
||||
const mode = getIonMode(this);
|
||||
const path = getSVGPath(mode, indeterminate);
|
||||
@@ -218,6 +226,7 @@ export class Checkbox implements ComponentInterface {
|
||||
onFocus={() => this.onFocus()}
|
||||
onBlur={() => this.onBlur()}
|
||||
ref={(focusEl) => (this.focusEl = focusEl)}
|
||||
required={required}
|
||||
{...inheritedAttributes}
|
||||
/>
|
||||
<div
|
||||
|
||||
@@ -54,3 +54,33 @@ describe('ion-checkbox: indeterminate', () => {
|
||||
expect(checkbox.getAttribute('aria-checked')).toBe('mixed');
|
||||
});
|
||||
});
|
||||
|
||||
describe('ion-checkbox: required', () => {
|
||||
it('should have a required attribute in inner input when true', async () => {
|
||||
const page = await newSpecPage({
|
||||
components: [Checkbox],
|
||||
html: `
|
||||
<ion-checkbox required="true">Checkbox</ion-checkbox>
|
||||
`,
|
||||
});
|
||||
|
||||
const checkbox = page.body.querySelector('ion-checkbox')!;
|
||||
const nativeInput = checkbox.shadowRoot?.querySelector('input[type=checkbox]')!;
|
||||
|
||||
expect(nativeInput.hasAttribute('required')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should not have a required attribute in inner input when false', async () => {
|
||||
const page = await newSpecPage({
|
||||
components: [Checkbox],
|
||||
html: `
|
||||
<ion-checkbox required="false">Checkbox</ion-checkbox>
|
||||
`,
|
||||
});
|
||||
|
||||
const checkbox = page.body.querySelector('ion-checkbox')!;
|
||||
const nativeInput = checkbox.shadowRoot?.querySelector('input[type=checkbox]')!;
|
||||
|
||||
expect(nativeInput.hasAttribute('required')).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -34,6 +34,11 @@ export class RadioGroup implements ComponentInterface {
|
||||
*/
|
||||
@Prop() name: string = this.inputId;
|
||||
|
||||
/**
|
||||
* If `true`, the user must fill in a value before submitting a form.
|
||||
*/
|
||||
@Prop() required = false;
|
||||
|
||||
/**
|
||||
* the value of the radio group.
|
||||
*/
|
||||
@@ -225,12 +230,12 @@ export class RadioGroup implements ComponentInterface {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { label, labelId, el, name, value } = this;
|
||||
const { label, labelId, el, name, value, required } = this;
|
||||
const mode = getIonMode(this);
|
||||
|
||||
renderHiddenInput(true, el, name, value, false);
|
||||
|
||||
return <Host role="radiogroup" aria-labelledby={label ? labelId : null} onClick={this.onClick} class={mode}></Host>;
|
||||
return <Host role="radiogroup" aria-labelledby={label ? labelId : null} onClick={this.onClick} class={mode} aria-required={`${required}`}></Host>;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { ComponentInterface } from '@stencil/core';
|
||||
import { Component, Element, Host, Prop, Method, State, Watch, forceUpdate, h } from '@stencil/core';
|
||||
import type { ButtonInterface } from '@utils/element-interface';
|
||||
import type { Attributes } from '@utils/helpers';
|
||||
import { addEventListener, removeEventListener, inheritAttributes, getNextSiblingOfType } from '@utils/helpers';
|
||||
import { addEventListener, removeEventListener, inheritAttributes } from '@utils/helpers';
|
||||
import { hostContext } from '@utils/theme';
|
||||
|
||||
import { getIonMode } from '../../global/ionic-global';
|
||||
@@ -65,41 +65,7 @@ export class SegmentButton implements ComponentInterface, ButtonInterface {
|
||||
this.updateState();
|
||||
}
|
||||
|
||||
private waitForSegmentContent(ionSegment: HTMLIonSegmentElement | null, contentId: string): Promise<HTMLElement> {
|
||||
return new Promise((resolve, reject) => {
|
||||
let timeoutId: NodeJS.Timeout | undefined = undefined;
|
||||
let animationFrameId: number;
|
||||
|
||||
const check = () => {
|
||||
if (!ionSegment) {
|
||||
reject(new Error(`Segment not found when looking for Segment Content`));
|
||||
return;
|
||||
}
|
||||
|
||||
const segmentView = getNextSiblingOfType<HTMLIonSegmentViewElement>(ionSegment); // Skip the text nodes
|
||||
const segmentContent = segmentView?.querySelector(
|
||||
`ion-segment-content[id="${contentId}"]`
|
||||
) as HTMLIonSegmentContentElement | null;
|
||||
if (segmentContent && timeoutId) {
|
||||
clearTimeout(timeoutId); // Clear the timeout if the segmentContent is found
|
||||
cancelAnimationFrame(animationFrameId);
|
||||
resolve(segmentContent);
|
||||
} else {
|
||||
animationFrameId = requestAnimationFrame(check); // Keep checking on the next animation frame
|
||||
}
|
||||
};
|
||||
|
||||
check();
|
||||
|
||||
// Set a timeout to reject the promise
|
||||
timeoutId = setTimeout(() => {
|
||||
cancelAnimationFrame(animationFrameId);
|
||||
reject(new Error(`Unable to find Segment Content with id="${contentId} within 1000 ms`));
|
||||
}, 1000);
|
||||
});
|
||||
}
|
||||
|
||||
async connectedCallback() {
|
||||
connectedCallback() {
|
||||
const segmentEl = (this.segmentEl = this.el.closest('ion-segment'));
|
||||
if (segmentEl) {
|
||||
this.updateState();
|
||||
@@ -110,13 +76,12 @@ export class SegmentButton implements ComponentInterface, ButtonInterface {
|
||||
// Return if there is no contentId defined
|
||||
if (!this.contentId) return;
|
||||
|
||||
let segmentContent;
|
||||
try {
|
||||
// Attempt to find the Segment Content by its contentId
|
||||
segmentContent = await this.waitForSegmentContent(segmentEl, this.contentId);
|
||||
} catch (error) {
|
||||
// If no associated Segment Content exists, log an error and return
|
||||
console.error('Segment Button: ', (error as Error).message);
|
||||
// Attempt to find the Segment Content by its contentId
|
||||
const segmentContent = document.getElementById(this.contentId) as HTMLIonSegmentContentElement | null;
|
||||
|
||||
// If no associated Segment Content exists, log an error and return
|
||||
if (!segmentContent) {
|
||||
console.error(`Segment Button: Unable to find Segment Content with id="${this.contentId}".`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -123,8 +123,6 @@
|
||||
<button class="expand" onClick="changeSegmentContent()">Change Segment Content</button>
|
||||
|
||||
<button class="expand" onClick="clearSegmentValue()">Clear Segment Value</button>
|
||||
|
||||
<button class="expand" onClick="addSegmentButtonAndContent()">Add New Segment Button & Content</button>
|
||||
</ion-content>
|
||||
|
||||
<ion-footer>
|
||||
@@ -160,34 +158,6 @@
|
||||
segment.value = undefined;
|
||||
});
|
||||
}
|
||||
|
||||
async function addSegmentButtonAndContent() {
|
||||
const segment = document.querySelector('ion-segment');
|
||||
const segmentView = document.querySelector('ion-segment-view');
|
||||
|
||||
const newButton = document.createElement('ion-segment-button');
|
||||
const newId = `new-${Date.now()}`;
|
||||
newButton.setAttribute('content-id', newId);
|
||||
newButton.setAttribute('value', newId);
|
||||
newButton.innerHTML = '<ion-label>New Button</ion-label>';
|
||||
|
||||
segment.appendChild(newButton);
|
||||
|
||||
setTimeout(() => {
|
||||
// Timeout to test waitForSegmentContent() in segment-button
|
||||
const newContent = document.createElement('ion-segment-content');
|
||||
newContent.setAttribute('id', newId);
|
||||
newContent.innerHTML = 'New Content';
|
||||
|
||||
segmentView.appendChild(newContent);
|
||||
|
||||
// Necessary timeout to ensure the value is set after the content is added.
|
||||
// Otherwise, the transition is unsuccessful and the content is not shown.
|
||||
setTimeout(() => {
|
||||
segment.setAttribute('value', newId);
|
||||
}, 200);
|
||||
}, 200);
|
||||
}
|
||||
</script>
|
||||
</ion-app>
|
||||
</body>
|
||||
|
||||
@@ -196,6 +196,13 @@ export class Select implements ComponentInterface {
|
||||
*/
|
||||
@Prop({ mutable: true }) value?: any | null;
|
||||
|
||||
/**
|
||||
* If true, screen readers will announce it as a required field. This property
|
||||
* works only for accessibility purposes, it will not prevent the form from
|
||||
* submitting if the value is invalid.
|
||||
*/
|
||||
@Prop() required = false;
|
||||
|
||||
/**
|
||||
* Emitted when the value has changed.
|
||||
*
|
||||
@@ -974,7 +981,7 @@ export class Select implements ComponentInterface {
|
||||
}
|
||||
|
||||
private renderListbox() {
|
||||
const { disabled, inputId, isExpanded } = this;
|
||||
const { disabled, inputId, isExpanded, required } = this;
|
||||
|
||||
return (
|
||||
<button
|
||||
@@ -983,6 +990,7 @@ export class Select implements ComponentInterface {
|
||||
aria-label={this.ariaLabel}
|
||||
aria-haspopup="dialog"
|
||||
aria-expanded={`${isExpanded}`}
|
||||
aria-required={`${required}`}
|
||||
onFocus={this.onFocus}
|
||||
onBlur={this.onBlur}
|
||||
ref={(focusEl) => (this.focusEl = focusEl)}
|
||||
|
||||
@@ -125,3 +125,35 @@ describe('select: slot interactivity', () => {
|
||||
expect(divSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('ion-select: required', () => {
|
||||
it('should have a aria-required attribute as true in inner button', async () => {
|
||||
const page = await newSpecPage({
|
||||
components: [Select],
|
||||
html: `
|
||||
<ion-select required="true"></ion-select>
|
||||
`,
|
||||
});
|
||||
|
||||
const select = page.body.querySelector('ion-select')!;
|
||||
|
||||
const nativeButton = select.shadowRoot!.querySelector('button')!;
|
||||
|
||||
expect(nativeButton.getAttribute('aria-required')).toBe('true');
|
||||
});
|
||||
|
||||
it('should not have a aria-required attribute as false in inner button', async () => {
|
||||
const page = await newSpecPage({
|
||||
components: [Select],
|
||||
html: `
|
||||
<ion-select required="false"></ion-select>
|
||||
`,
|
||||
});
|
||||
|
||||
const select = page.body.querySelector('ion-select')!;
|
||||
|
||||
const nativeButton = select.shadowRoot!.querySelector('button')!;
|
||||
|
||||
expect(nativeButton.getAttribute('aria-required')).toBe('false');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -75,3 +75,33 @@ describe('ion-toggle: disabled', () => {
|
||||
expect(toggle.checked).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ion-toggle: required', () => {
|
||||
it('should have a required attribute in inner input when true', async () => {
|
||||
const page = await newSpecPage({
|
||||
components: [Toggle],
|
||||
html: `
|
||||
<ion-toggle required="true">Toggle</ion-toggle>
|
||||
`,
|
||||
});
|
||||
|
||||
const toggle = page.body.querySelector('ion-toggle')!;
|
||||
const nativeInput = toggle.shadowRoot?.querySelector('input[role=switch]')!;
|
||||
|
||||
expect(nativeInput.hasAttribute('required')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should not have a required attribute in inner input when false', async () => {
|
||||
const page = await newSpecPage({
|
||||
components: [Toggle],
|
||||
html: `
|
||||
<ion-toggle required="false">Toggle</ion-toggle>
|
||||
`,
|
||||
});
|
||||
|
||||
const toggle = page.body.querySelector('ion-toggle')!;
|
||||
const nativeInput = toggle.shadowRoot?.querySelector('input[role=switch]')!;
|
||||
|
||||
expect(nativeInput.hasAttribute('required')).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -108,6 +108,13 @@ export class Toggle implements ComponentInterface {
|
||||
*/
|
||||
@Prop() alignment?: 'start' | 'center';
|
||||
|
||||
/**
|
||||
* If true, screen readers will announce it as a required field. This property
|
||||
* works only for accessibility purposes, it will not prevent the form from
|
||||
* submitting if the value is invalid.
|
||||
*/
|
||||
@Prop() required = false;
|
||||
|
||||
/**
|
||||
* Emitted when the user switches the toggle on or off.
|
||||
*
|
||||
@@ -290,7 +297,8 @@ export class Toggle implements ComponentInterface {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { activated, color, checked, disabled, el, justify, labelPlacement, inputId, name, alignment } = this;
|
||||
const { activated, color, checked, disabled, el, justify, labelPlacement, inputId, name, alignment, required } =
|
||||
this;
|
||||
|
||||
const mode = getIonMode(this);
|
||||
const value = this.getValue();
|
||||
@@ -327,6 +335,7 @@ export class Toggle implements ComponentInterface {
|
||||
onFocus={() => this.onFocus()}
|
||||
onBlur={() => this.onBlur()}
|
||||
ref={(focusEl) => (this.focusEl = focusEl)}
|
||||
required={required}
|
||||
{...this.inheritedAttributes}
|
||||
/>
|
||||
<div
|
||||
|
||||
@@ -13,6 +13,10 @@ import type { Color, CssClassMap, StyleEventDetail } from '../../interface';
|
||||
* @slot secondary - Content is placed to the left of the toolbar text in `ios` mode, and directly to the right in `md` mode.
|
||||
* @slot primary - Content is placed to the right of the toolbar text in `ios` mode, and to the far right in `md` mode.
|
||||
* @slot end - Content is placed to the right of the toolbar text in LTR, and to the left in RTL.
|
||||
*
|
||||
* @part background - The background of the toolbar, covering the entire area behind the toolbar content.
|
||||
* @part container - The container that wraps all toolbar content, including the default slot and named slot content.
|
||||
* @part content - The container for the default slot, wrapping content provided without a named slot.
|
||||
*/
|
||||
@Component({
|
||||
tag: 'ion-toolbar',
|
||||
@@ -97,11 +101,11 @@ export class Toolbar implements ComponentInterface {
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<div class="toolbar-background"></div>
|
||||
<div class="toolbar-container">
|
||||
<div class="toolbar-background" part="background"></div>
|
||||
<div class="toolbar-container" part="container">
|
||||
<slot name="start"></slot>
|
||||
<slot name="secondary"></slot>
|
||||
<div class="toolbar-content">
|
||||
<div class="toolbar-content" part="content">
|
||||
<slot></slot>
|
||||
</div>
|
||||
<slot name="primary"></slot>
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { SpinnerTypes } from '../components/spinner/spinner-configs';
|
||||
import type { TabButtonLayout } from '../components/tab-bar/tab-bar-interface';
|
||||
import type { AnimationBuilder, Mode } from '../interface';
|
||||
|
||||
import type { LogLevel } from './logging';
|
||||
import type { PlatformConfig } from './platform';
|
||||
|
||||
export interface IonicConfig {
|
||||
@@ -220,6 +221,15 @@ export interface IonicConfig {
|
||||
*/
|
||||
experimentalCloseWatcher?: boolean;
|
||||
|
||||
/**
|
||||
* Configures the logging level for Ionic Framework:
|
||||
*
|
||||
* - `'OFF'`: No errors or warnings are logged.
|
||||
* - `'ERROR'`: Logs only errors.
|
||||
* - `'WARN'`: Logs errors and warnings.
|
||||
*/
|
||||
logLevel?: LogLevel;
|
||||
|
||||
// PRIVATE configs
|
||||
keyboardHeight?: number;
|
||||
inputShims?: boolean;
|
||||
|
||||
@@ -413,14 +413,3 @@ export const shallowEqualStringMap = (
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export const getNextSiblingOfType = <T extends Element>(element: Element): T | null => {
|
||||
let sibling = element.nextSibling;
|
||||
while (sibling) {
|
||||
if (sibling.nodeType === Node.ELEMENT_NODE && (sibling as T) !== null) {
|
||||
return sibling as T;
|
||||
}
|
||||
sibling = sibling.nextSibling;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
import { config } from '@global/config';
|
||||
|
||||
export const enum LogLevel {
|
||||
OFF = 'OFF',
|
||||
ERROR = 'ERROR',
|
||||
WARN = 'WARN',
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a warning to the console with an Ionic prefix
|
||||
* to indicate the library that is warning the developer.
|
||||
@@ -5,18 +13,24 @@
|
||||
* @param message - The string message to be logged to the console.
|
||||
*/
|
||||
export const printIonWarning = (message: string, ...params: any[]) => {
|
||||
return console.warn(`[Ionic Warning]: ${message}`, ...params);
|
||||
const logLevel = config.get('logLevel', LogLevel.WARN);
|
||||
if ([LogLevel.WARN].includes(logLevel)) {
|
||||
return console.warn(`[Ionic Warning]: ${message}`, ...params);
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
/**
|
||||
* Logs an error to the console with an Ionic prefix
|
||||
* to indicate the library that is warning the developer.
|
||||
*
|
||||
* @param message - The string message to be logged to the console.
|
||||
* @param params - Additional arguments to supply to the console.error.
|
||||
*/
|
||||
export const printIonError = (message: string, ...params: any) => {
|
||||
return console.error(`[Ionic Error]: ${message}`, ...params);
|
||||
export const printIonError = (message: string, ...params: any[]) => {
|
||||
const logLevel = config.get('logLevel', LogLevel.ERROR);
|
||||
if ([LogLevel.ERROR, LogLevel.WARN].includes(logLevel)) {
|
||||
return console.error(`[Ionic Error]: ${message}`, ...params);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
114
core/src/utils/logging/test/logging.spec.ts
Normal file
114
core/src/utils/logging/test/logging.spec.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import { config } from '@global/config';
|
||||
import { LogLevel } from '../index';
|
||||
|
||||
import { printIonError, printIonWarning } from '../index';
|
||||
|
||||
describe('Logging', () => {
|
||||
describe('#printIonWarning', () => {
|
||||
let consoleWarnSpy: jest.SpyInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
consoleWarnSpy = jest.spyOn(console, 'warn');
|
||||
// Suppress console.warn output from polluting the test output
|
||||
consoleWarnSpy.mockImplementation(() => {});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
consoleWarnSpy.mockRestore();
|
||||
});
|
||||
|
||||
describe('when the logLevel configuration is not set', () => {
|
||||
it('logs a warning to the console', () => {
|
||||
config.set('logLevel', undefined);
|
||||
|
||||
printIonWarning('This is a warning message');
|
||||
|
||||
expect(consoleWarnSpy).toHaveBeenCalledWith('[Ionic Warning]: This is a warning message');
|
||||
});
|
||||
});
|
||||
|
||||
describe("when the logLevel configuration is set to 'WARN'", () => {
|
||||
it('logs a warning to the console', () => {
|
||||
config.set('logLevel', LogLevel.WARN);
|
||||
|
||||
printIonWarning('This is a warning message');
|
||||
|
||||
expect(consoleWarnSpy).toHaveBeenCalledWith('[Ionic Warning]: This is a warning message');
|
||||
});
|
||||
});
|
||||
|
||||
describe("when the logLevel configuration is set to 'ERROR'", () => {
|
||||
it('does not log a warning to the console', () => {
|
||||
config.set('logLevel', LogLevel.ERROR);
|
||||
|
||||
printIonWarning('This is a warning message');
|
||||
|
||||
expect(consoleWarnSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("when the logLevel configuration is set to 'OFF'", () => {
|
||||
it('does not log a warning to the console', () => {
|
||||
config.set('logLevel', LogLevel.OFF);
|
||||
|
||||
printIonWarning('This is a warning message');
|
||||
|
||||
expect(consoleWarnSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#printIonError', () => {
|
||||
let consoleErrorSpy: jest.SpyInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
consoleErrorSpy = jest.spyOn(console, 'error');
|
||||
// Suppress console.error output from polluting the test output
|
||||
consoleErrorSpy.mockImplementation(() => {});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
consoleErrorSpy.mockRestore();
|
||||
});
|
||||
|
||||
describe('when the logLevel configuration is not set', () => {
|
||||
it('logs an error to the console', () => {
|
||||
config.set('logLevel', undefined);
|
||||
|
||||
printIonError('This is an error message');
|
||||
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith('[Ionic Error]: This is an error message');
|
||||
});
|
||||
});
|
||||
|
||||
describe("when the logLevel configuration is set to 'ERROR'", () => {
|
||||
it('logs an error to the console', () => {
|
||||
config.set('logLevel', LogLevel.ERROR);
|
||||
|
||||
printIonError('This is an error message');
|
||||
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith('[Ionic Error]: This is an error message');
|
||||
});
|
||||
});
|
||||
|
||||
describe("when the logLevel configuration is set to 'WARN'", () => {
|
||||
it('logs an error to the console', () => {
|
||||
config.set('logLevel', LogLevel.WARN);
|
||||
|
||||
printIonError('This is an error message');
|
||||
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith('[Ionic Error]: This is an error message');
|
||||
});
|
||||
});
|
||||
|
||||
describe("when the logLevel configuration is set to 'OFF'", () => {
|
||||
it('does not log an error to the console', () => {
|
||||
config.set('logLevel', LogLevel.OFF);
|
||||
|
||||
printIonError('This is an error message');
|
||||
|
||||
expect(consoleErrorSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -99,8 +99,7 @@ const isCordova = (win: any): boolean => !!(win['cordova'] || win['phonegap'] ||
|
||||
|
||||
const isCapacitorNative = (win: any): boolean => {
|
||||
const capacitor = win['Capacitor'];
|
||||
// TODO(ROU-11693): Remove when we no longer support Capacitor 2, which does not have isNativePlatform
|
||||
return !!(capacitor?.isNative || (capacitor?.isNativePlatform && !!capacitor.isNativePlatform()));
|
||||
return !!capacitor?.isNative;
|
||||
};
|
||||
|
||||
const isElectron = (win: Window): boolean => testUserAgent(win, /electron/i);
|
||||
|
||||
@@ -29,7 +29,7 @@ export const PlatformConfiguration = {
|
||||
},
|
||||
Capacitor: {
|
||||
Capacitor: {
|
||||
isNativePlatform: () => true,
|
||||
isNative: true,
|
||||
},
|
||||
},
|
||||
PWA: {
|
||||
|
||||
@@ -507,14 +507,14 @@ export declare interface IonCardTitle extends Components.IonCardTitle {}
|
||||
|
||||
|
||||
@ProxyCmp({
|
||||
inputs: ['alignment', 'checked', 'color', 'disabled', 'indeterminate', 'justify', 'labelPlacement', 'mode', 'name', 'value']
|
||||
inputs: ['alignment', 'checked', 'color', 'disabled', 'indeterminate', 'justify', 'labelPlacement', 'mode', 'name', 'required', 'value']
|
||||
})
|
||||
@Component({
|
||||
selector: 'ion-checkbox',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: '<ng-content></ng-content>',
|
||||
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
|
||||
inputs: ['alignment', 'checked', 'color', 'disabled', 'indeterminate', 'justify', 'labelPlacement', 'mode', 'name', 'value'],
|
||||
inputs: ['alignment', 'checked', 'color', 'disabled', 'indeterminate', 'justify', 'labelPlacement', 'mode', 'name', 'required', 'value'],
|
||||
})
|
||||
export class IonCheckbox {
|
||||
protected el: HTMLIonCheckboxElement;
|
||||
@@ -1639,14 +1639,14 @@ export declare interface IonRadio extends Components.IonRadio {
|
||||
|
||||
|
||||
@ProxyCmp({
|
||||
inputs: ['allowEmptySelection', 'compareWith', 'name', 'value']
|
||||
inputs: ['allowEmptySelection', 'compareWith', 'name', 'required', 'value']
|
||||
})
|
||||
@Component({
|
||||
selector: 'ion-radio-group',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: '<ng-content></ng-content>',
|
||||
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
|
||||
inputs: ['allowEmptySelection', 'compareWith', 'name', 'value'],
|
||||
inputs: ['allowEmptySelection', 'compareWith', 'name', 'required', 'value'],
|
||||
})
|
||||
export class IonRadioGroup {
|
||||
protected el: HTMLIonRadioGroupElement;
|
||||
@@ -2060,7 +2060,7 @@ export declare interface IonSegmentView extends Components.IonSegmentView {
|
||||
|
||||
|
||||
@ProxyCmp({
|
||||
inputs: ['cancelText', 'color', 'compareWith', 'disabled', 'expandedIcon', 'fill', 'interface', 'interfaceOptions', 'justify', 'label', 'labelPlacement', 'mode', 'multiple', 'name', 'okText', 'placeholder', 'selectedText', 'shape', 'toggleIcon', 'value'],
|
||||
inputs: ['cancelText', 'color', 'compareWith', 'disabled', 'expandedIcon', 'fill', 'interface', 'interfaceOptions', 'justify', 'label', 'labelPlacement', 'mode', 'multiple', 'name', 'okText', 'placeholder', 'required', 'selectedText', 'shape', 'toggleIcon', 'value'],
|
||||
methods: ['open']
|
||||
})
|
||||
@Component({
|
||||
@@ -2068,7 +2068,7 @@ export declare interface IonSegmentView extends Components.IonSegmentView {
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: '<ng-content></ng-content>',
|
||||
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
|
||||
inputs: ['cancelText', 'color', 'compareWith', 'disabled', 'expandedIcon', 'fill', 'interface', 'interfaceOptions', 'justify', 'label', 'labelPlacement', 'mode', 'multiple', 'name', 'okText', 'placeholder', 'selectedText', 'shape', 'toggleIcon', 'value'],
|
||||
inputs: ['cancelText', 'color', 'compareWith', 'disabled', 'expandedIcon', 'fill', 'interface', 'interfaceOptions', 'justify', 'label', 'labelPlacement', 'mode', 'multiple', 'name', 'okText', 'placeholder', 'required', 'selectedText', 'shape', 'toggleIcon', 'value'],
|
||||
})
|
||||
export class IonSelect {
|
||||
protected el: HTMLIonSelectElement;
|
||||
@@ -2473,14 +2473,14 @@ Shorthand for ionToastDidDismiss.
|
||||
|
||||
|
||||
@ProxyCmp({
|
||||
inputs: ['alignment', 'checked', 'color', 'disabled', 'enableOnOffLabels', 'justify', 'labelPlacement', 'mode', 'name', 'value']
|
||||
inputs: ['alignment', 'checked', 'color', 'disabled', 'enableOnOffLabels', 'justify', 'labelPlacement', 'mode', 'name', 'required', 'value']
|
||||
})
|
||||
@Component({
|
||||
selector: 'ion-toggle',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: '<ng-content></ng-content>',
|
||||
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
|
||||
inputs: ['alignment', 'checked', 'color', 'disabled', 'enableOnOffLabels', 'justify', 'labelPlacement', 'mode', 'name', 'value'],
|
||||
inputs: ['alignment', 'checked', 'color', 'disabled', 'enableOnOffLabels', 'justify', 'labelPlacement', 'mode', 'name', 'required', 'value'],
|
||||
})
|
||||
export class IonToggle {
|
||||
protected el: HTMLIonToggleElement;
|
||||
|
||||
@@ -235,6 +235,7 @@ export const IonCheckbox = /*@__PURE__*/ defineContainer<JSX.IonCheckbox, JSX.Io
|
||||
'labelPlacement',
|
||||
'justify',
|
||||
'alignment',
|
||||
'required',
|
||||
'ionChange',
|
||||
'ionFocus',
|
||||
'ionBlur'
|
||||
@@ -694,6 +695,7 @@ export const IonRadioGroup = /*@__PURE__*/ defineContainer<JSX.IonRadioGroup, JS
|
||||
'allowEmptySelection',
|
||||
'compareWith',
|
||||
'name',
|
||||
'required',
|
||||
'value',
|
||||
'ionChange',
|
||||
'ionValueChange'
|
||||
@@ -883,6 +885,7 @@ export const IonSelect = /*@__PURE__*/ defineContainer<JSX.IonSelect, JSX.IonSel
|
||||
'expandedIcon',
|
||||
'shape',
|
||||
'value',
|
||||
'required',
|
||||
'ionChange',
|
||||
'ionCancel',
|
||||
'ionDismiss',
|
||||
@@ -1016,6 +1019,7 @@ export const IonToggle = /*@__PURE__*/ defineContainer<JSX.IonToggle, JSX.IonTog
|
||||
'labelPlacement',
|
||||
'justify',
|
||||
'alignment',
|
||||
'required',
|
||||
'ionChange',
|
||||
'ionFocus',
|
||||
'ionBlur'
|
||||
|
||||
Reference in New Issue
Block a user