Compare commits

..

36 Commits

Author SHA1 Message Date
giuliana
6d5592e63c Merge branch 'ROU-11112-toggle' into ROU-11112 2025-01-30 10:43:40 +00:00
giuliana
f94bcadc49 Merge branch 'ROU-11112-checkbox' into ROU-11112 2025-01-30 10:43:00 +00:00
giuliana
2dd8a18bb4 Merge branch 'ROU-11112-select' into ROU-11112 2025-01-30 10:40:59 +00:00
giuliana
e296707c94 chore(vue): update proxies file 2025-01-30 10:14:23 +00:00
giuliana
0a502a196f chore(vue): update proxies file 2025-01-30 10:12:20 +00:00
giuliana
e8971df5fe chore(vue): update proxies file 2025-01-30 09:53:38 +00:00
Giuliana Silva
8743a4f3be Merge branch 'feature-8.5' into ROU-11112-checkbox 2025-01-30 09:34:05 +00:00
Giuliana Silva
7f45146ce3 Merge branch 'feature-8.5' into ROU-11112-select 2025-01-30 09:34:01 +00:00
Giuliana Silva
8abf12676a Merge branch 'feature-8.5' into ROU-11112-toggle 2025-01-30 09:33:58 +00:00
Brandy Smith
621333d927 chore(git): update feature-8.5 from main (#30168) 2025-01-29 14:34:11 -05:00
Brandy Smith
6cf454f7c4 Merge branch 'main' into chore-main-to-8.5 2025-01-29 14:20:00 -05:00
Giuliana Silva
66210e60a8 Merge branch 'feature-8.5' into ROU-11112-checkbox 2025-01-24 18:11:23 +00:00
Giuliana Silva
b31b97648c Merge branch 'feature-8.5' into ROU-11112-select 2025-01-24 18:10:42 +00:00
Giuliana Silva
856a5a051a Merge branch 'feature-8.5' into ROU-11112-toggle 2025-01-24 18:09:00 +00:00
giuliana
dfae50cba4 test(toggle): remove unecessary import 2025-01-24 12:40:22 +00:00
giuliana
80d4fa63ec lint 2025-01-24 12:34:59 +00:00
giuliana
72494dbe31 lint 2025-01-24 12:32:15 +00:00
giuliana
d407de5d6c test(select): add required prop tests 2025-01-24 12:19:00 +00:00
giuliana
82eb743eab test(toggle): add required prop tests 2025-01-24 12:10:20 +00:00
giuliana
397c72aadd test(checkbox): add required prop tests 2025-01-24 12:05:41 +00:00
giuliana
b423ae7c8c feat(checkbox): change required prop description 2025-01-24 11:30:04 +00:00
giuliana
8ecb96e737 feat(toggle): change required prop description 2025-01-24 11:27:42 +00:00
giuliana
d1bc696f9f feat(select): change required prop description 2025-01-24 11:24:22 +00:00
fudom
ac4ea3232b feat(toolbar): add shadow parts for background, container, and content (#30069)
Resolves #30068

---------

Add `part` attributes to toolbar for the `background`, `container` and `content`.
2025-01-23 15:48:58 -05:00
giuliana
4af07f3c02 chore(): add files after build 2025-01-23 15:07:11 +00:00
giuliana
1cf303f384 chore(): add files after build 2025-01-23 15:06:11 +00:00
giuliana
e31e5fdca5 chore(): add files after build 2025-01-23 15:04:42 +00:00
giuliana
1d5cd4b6b3 feat(select): add required prop 2025-01-23 14:57:08 +00:00
giuliana
76fa00d021 feat(toggle): add required prop 2025-01-23 14:56:51 +00:00
giuliana
3f46aef949 feat(checkbox): add required prop 2025-01-23 14:56:33 +00:00
giuliana
934dd0f303 chore(): build project 2025-01-23 11:50:20 +00:00
giuliana
3bc976b8c6 feat(toggle): add required prop 2025-01-23 11:46:53 +00:00
giuliana
3f24a126bf feat(select): add required prop 2025-01-23 11:46:33 +00:00
giuliana
ed3788271b feat(radio-group): add required prop 2025-01-23 11:45:36 +00:00
giuliana
00961c6e4b feat(checkbox): add required prop 2025-01-23 11:44:36 +00:00
Sean Perkins
efd3e0fd2b feat(config): add logLevel option to suppress ionic warnings and errors (#30015)
resolves #29814

---------

- Developers can assign a value to `logLevel` in the Ionic config to control the log level that Ionic Framework will produce logs for.
  - `OFF` will completely disable all warnings and errors from Ionic
  - `WARN` will log warnings and errors
  - `ERROR` will log only errors
- Default behavior is that developers receive both Ionic warnings and errors
- Configuration only applies to usages of `printIonWarning` and `printIonError`

---------

Co-authored-by: Brandy Smith <6577830+brandyscarney@users.noreply.github.com>
2025-01-20 16:15:02 -05:00
20 changed files with 338 additions and 107 deletions

View File

@@ -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

View File

@@ -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>`.
*/

View File

@@ -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

View File

@@ -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();
});
});

View File

@@ -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>;
}
}

View File

@@ -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;
}

View File

@@ -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>

View File

@@ -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)}

View File

@@ -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');
});
});

View File

@@ -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();
});
});

View File

@@ -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

View File

@@ -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>

View File

@@ -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;

View File

@@ -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;
};

View File

@@ -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);
}
};
/**

View 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();
});
});
});
});

View File

@@ -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);

View File

@@ -29,7 +29,7 @@ export const PlatformConfiguration = {
},
Capacitor: {
Capacitor: {
isNativePlatform: () => true,
isNative: true,
},
},
PWA: {

View File

@@ -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;

View File

@@ -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'