diff --git a/core/api.txt b/core/api.txt index beefc2ff7c..8a02f2a89b 100644 --- a/core/api.txt +++ b/core/api.txt @@ -918,6 +918,10 @@ ion-picker-column,prop,mode,"ios" | "md",undefined,false,false ion-picker-column,prop,value,number | string | undefined,undefined,false,false ion-picker-column,event,ionChange,PickerColumnItem,true +ion-picker-column-option,shadow +ion-picker-column-option,prop,disabled,boolean,false,false,false +ion-picker-column-option,prop,value,any,undefined,false,false + ion-picker-legacy,scoped ion-picker-legacy,prop,animated,boolean,true,false,false ion-picker-legacy,prop,backdropDismiss,boolean,true,false,false diff --git a/core/src/components.d.ts b/core/src/components.d.ts index 7e3d72661b..54929a2188 100644 --- a/core/src/components.d.ts +++ b/core/src/components.d.ts @@ -1986,6 +1986,16 @@ export namespace Components { */ "value"?: string | number; } + interface IonPickerColumnOption { + /** + * If `true`, the user cannot interact with the picker column option. + */ + "disabled": boolean; + /** + * The text value of the option. + */ + "value"?: any | null; + } interface IonPickerLegacy { /** * If `true`, the picker will animate. @@ -4053,6 +4063,12 @@ declare global { prototype: HTMLIonPickerColumnElement; new (): HTMLIonPickerColumnElement; }; + interface HTMLIonPickerColumnOptionElement extends Components.IonPickerColumnOption, HTMLStencilElement { + } + var HTMLIonPickerColumnOptionElement: { + prototype: HTMLIonPickerColumnOptionElement; + new (): HTMLIonPickerColumnOptionElement; + }; interface HTMLIonPickerLegacyElementEventMap { "ionPickerDidPresent": void; "ionPickerWillPresent": void; @@ -4647,6 +4663,7 @@ declare global { "ion-note": HTMLIonNoteElement; "ion-picker": HTMLIonPickerElement; "ion-picker-column": HTMLIonPickerColumnElement; + "ion-picker-column-option": HTMLIonPickerColumnOptionElement; "ion-picker-legacy": HTMLIonPickerLegacyElement; "ion-picker-legacy-column": HTMLIonPickerLegacyColumnElement; "ion-popover": HTMLIonPopoverElement; @@ -6616,6 +6633,16 @@ declare namespace LocalJSX { */ "value"?: string | number; } + interface IonPickerColumnOption { + /** + * If `true`, the user cannot interact with the picker column option. + */ + "disabled"?: boolean; + /** + * The text value of the option. + */ + "value"?: any | null; + } interface IonPickerLegacy { /** * If `true`, the picker will animate. @@ -8086,6 +8113,7 @@ declare namespace LocalJSX { "ion-note": IonNote; "ion-picker": IonPicker; "ion-picker-column": IonPickerColumn; + "ion-picker-column-option": IonPickerColumnOption; "ion-picker-legacy": IonPickerLegacy; "ion-picker-legacy-column": IonPickerLegacyColumn; "ion-popover": IonPopover; @@ -8183,6 +8211,7 @@ declare module "@stencil/core" { "ion-note": LocalJSX.IonNote & JSXBase.HTMLAttributes; "ion-picker": LocalJSX.IonPicker & JSXBase.HTMLAttributes; "ion-picker-column": LocalJSX.IonPickerColumn & JSXBase.HTMLAttributes; + "ion-picker-column-option": LocalJSX.IonPickerColumnOption & JSXBase.HTMLAttributes; "ion-picker-legacy": LocalJSX.IonPickerLegacy & JSXBase.HTMLAttributes; "ion-picker-legacy-column": LocalJSX.IonPickerLegacyColumn & JSXBase.HTMLAttributes; "ion-popover": LocalJSX.IonPopover & JSXBase.HTMLAttributes; diff --git a/core/src/components/picker-column-option/picker-column-option.tsx b/core/src/components/picker-column-option/picker-column-option.tsx new file mode 100644 index 0000000000..b45adb6e2c --- /dev/null +++ b/core/src/components/picker-column-option/picker-column-option.tsx @@ -0,0 +1,71 @@ +import type { ComponentInterface } from '@stencil/core'; +import { Component, Element, Host, Prop, State, Watch, h } from '@stencil/core'; +import { inheritAttributes } from '@utils/helpers'; + +@Component({ + tag: 'ion-picker-column-option', + shadow: true, +}) +export class PickerColumnOption implements ComponentInterface { + @Element() el!: HTMLElement; + + /** + * The aria-label of the option. + * + * If the value changes, then it will trigger a + * re-render of the picker since it's a @State variable. + * Otherwise, the `aria-label` attribute cannot be updated + * after the component is loaded. + */ + @State() ariaLabel?: string | null = null; + + /** + * If `true`, the user cannot interact with the picker column option. + */ + @Prop() disabled = false; + + /** + * The text value of the option. + */ + @Prop() value?: any | null; + + /** + * The aria-label of the option has changed after the + * first render and needs to be updated within the component. + * + * @param ariaLbl The new aria-label value. + */ + @Watch('aria-label') + onAriaLabelChange(ariaLbl: string) { + this.ariaLabel = ariaLbl; + } + + componentWillLoad() { + const inheritedAttributes = inheritAttributes(this.el, ['aria-label']); + /** + * The initial value of `aria-label` needs to be set for + * the first render. + */ + this.ariaLabel = inheritedAttributes['aria-label'] || null; + } + + render() { + const { value, disabled, ariaLabel } = this; + + return ( + + + + ); + } +} diff --git a/core/src/components/picker-column-option/test/a11y/index.html b/core/src/components/picker-column-option/test/a11y/index.html new file mode 100644 index 0000000000..9c19fd0b73 --- /dev/null +++ b/core/src/components/picker-column-option/test/a11y/index.html @@ -0,0 +1,19 @@ + + + + + Picker Column Option - Basic + + + + + + + + +
+ my option + other option +
+ + diff --git a/core/src/components/picker-column-option/test/a11y/picker-column-option.e2e.ts b/core/src/components/picker-column-option/test/a11y/picker-column-option.e2e.ts new file mode 100644 index 0000000000..aef2df2c7e --- /dev/null +++ b/core/src/components/picker-column-option/test/a11y/picker-column-option.e2e.ts @@ -0,0 +1,18 @@ +import AxeBuilder from '@axe-core/playwright'; +import { expect } from '@playwright/test'; +import { configs, test } from '@utils/test/playwright'; + +/** + * This behavior does not vary across directions + */ +configs({ directions: ['ltr'] }).forEach(({ config, title }) => { + test.describe(title('picker column option: a11y'), () => { + test('should not have accessibility violations', async ({ page }) => { + await page.goto(`/src/components/picker-column-option/test/a11y`, config); + + const results = await new AxeBuilder({ page }).analyze(); + + expect(results.violations).toEqual([]); + }); + }); +}); diff --git a/core/src/components/picker-column-option/test/basic/index.html b/core/src/components/picker-column-option/test/basic/index.html new file mode 100644 index 0000000000..e8d4fdd39d --- /dev/null +++ b/core/src/components/picker-column-option/test/basic/index.html @@ -0,0 +1,55 @@ + + + + + Picker Column Option - Basic + + + + + + + + + + + + + Picker Column Option - Basic + + + +
+
+

Default

+ my option +
+
+
+
+ + diff --git a/packages/angular/src/directives/proxies-list.ts b/packages/angular/src/directives/proxies-list.ts index f5dd11472d..172c4ec49d 100644 --- a/packages/angular/src/directives/proxies-list.ts +++ b/packages/angular/src/directives/proxies-list.ts @@ -53,6 +53,7 @@ export const DIRECTIVES = [ d.IonNote, d.IonPicker, d.IonPickerColumn, + d.IonPickerColumnOption, d.IonPickerLegacy, d.IonProgressBar, d.IonRadio, diff --git a/packages/angular/src/directives/proxies.ts b/packages/angular/src/directives/proxies.ts index 7b8aa75675..c6e7c3bb3b 100644 --- a/packages/angular/src/directives/proxies.ts +++ b/packages/angular/src/directives/proxies.ts @@ -1475,6 +1475,28 @@ export declare interface IonPickerColumn extends Components.IonPickerColumn { } +@ProxyCmp({ + inputs: ['disabled', 'value'] +}) +@Component({ + selector: 'ion-picker-column-option', + changeDetection: ChangeDetectionStrategy.OnPush, + template: '', + // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property + inputs: ['disabled', 'value'], +}) +export class IonPickerColumnOption { + protected el: HTMLElement; + constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) { + c.detach(); + this.el = r.nativeElement; + } +} + + +export declare interface IonPickerColumnOption extends Components.IonPickerColumnOption {} + + @ProxyCmp({ inputs: ['animated', 'backdropDismiss', 'buttons', 'columns', 'cssClass', 'duration', 'enterAnimation', 'htmlAttributes', 'isOpen', 'keyboardClose', 'leaveAnimation', 'mode', 'showBackdrop', 'trigger'], methods: ['present', 'dismiss', 'onDidDismiss', 'onWillDismiss', 'getColumn'] diff --git a/packages/angular/standalone/src/directives/proxies.ts b/packages/angular/standalone/src/directives/proxies.ts index 5153b3fc32..e9a5b52a5b 100644 --- a/packages/angular/standalone/src/directives/proxies.ts +++ b/packages/angular/standalone/src/directives/proxies.ts @@ -53,6 +53,7 @@ import { defineCustomElement as defineIonNavLink } from '@ionic/core/components/ import { defineCustomElement as defineIonNote } from '@ionic/core/components/ion-note.js'; import { defineCustomElement as defineIonPicker } from '@ionic/core/components/ion-picker.js'; import { defineCustomElement as defineIonPickerColumn } from '@ionic/core/components/ion-picker-column.js'; +import { defineCustomElement as defineIonPickerColumnOption } from '@ionic/core/components/ion-picker-column-option.js'; import { defineCustomElement as defineIonPickerLegacy } from '@ionic/core/components/ion-picker-legacy.js'; import { defineCustomElement as defineIonProgressBar } from '@ionic/core/components/ion-progress-bar.js'; import { defineCustomElement as defineIonRadio } from '@ionic/core/components/ion-radio.js'; @@ -1470,6 +1471,30 @@ export declare interface IonPickerColumn extends Components.IonPickerColumn { } +@ProxyCmp({ + defineCustomElementFn: defineIonPickerColumnOption, + inputs: ['disabled', 'value'] +}) +@Component({ + selector: 'ion-picker-column-option', + changeDetection: ChangeDetectionStrategy.OnPush, + template: '', + // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property + inputs: ['disabled', 'value'], + standalone: true +}) +export class IonPickerColumnOption { + protected el: HTMLElement; + constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) { + c.detach(); + this.el = r.nativeElement; + } +} + + +export declare interface IonPickerColumnOption extends Components.IonPickerColumnOption {} + + @ProxyCmp({ defineCustomElementFn: defineIonPickerLegacy, inputs: ['animated', 'backdropDismiss', 'buttons', 'columns', 'cssClass', 'duration', 'enterAnimation', 'htmlAttributes', 'isOpen', 'keyboardClose', 'leaveAnimation', 'mode', 'showBackdrop', 'trigger'], diff --git a/packages/react/src/components/proxies.ts b/packages/react/src/components/proxies.ts index a43851e1be..54e5d4f605 100644 --- a/packages/react/src/components/proxies.ts +++ b/packages/react/src/components/proxies.ts @@ -46,6 +46,7 @@ import { defineCustomElement as defineIonNavLink } from '@ionic/core/components/ import { defineCustomElement as defineIonNote } from '@ionic/core/components/ion-note.js'; import { defineCustomElement as defineIonPicker } from '@ionic/core/components/ion-picker.js'; import { defineCustomElement as defineIonPickerColumn } from '@ionic/core/components/ion-picker-column.js'; +import { defineCustomElement as defineIonPickerColumnOption } from '@ionic/core/components/ion-picker-column-option.js'; import { defineCustomElement as defineIonProgressBar } from '@ionic/core/components/ion-progress-bar.js'; import { defineCustomElement as defineIonRadio } from '@ionic/core/components/ion-radio.js'; import { defineCustomElement as defineIonRadioGroup } from '@ionic/core/components/ion-radio-group.js'; @@ -113,6 +114,7 @@ export const IonNavLink = /*@__PURE__*/createReactComponent('ion-note', undefined, undefined, defineIonNote); export const IonPicker = /*@__PURE__*/createReactComponent('ion-picker', undefined, undefined, defineIonPicker); export const IonPickerColumn = /*@__PURE__*/createReactComponent('ion-picker-column', undefined, undefined, defineIonPickerColumn); +export const IonPickerColumnOption = /*@__PURE__*/createReactComponent('ion-picker-column-option', undefined, undefined, defineIonPickerColumnOption); export const IonProgressBar = /*@__PURE__*/createReactComponent('ion-progress-bar', undefined, undefined, defineIonProgressBar); export const IonRadio = /*@__PURE__*/createReactComponent('ion-radio', undefined, undefined, defineIonRadio); export const IonRadioGroup = /*@__PURE__*/createReactComponent('ion-radio-group', undefined, undefined, defineIonRadioGroup); diff --git a/packages/vue/src/proxies.ts b/packages/vue/src/proxies.ts index 21ba23640c..26518e9bfd 100644 --- a/packages/vue/src/proxies.ts +++ b/packages/vue/src/proxies.ts @@ -52,6 +52,7 @@ import { defineCustomElement as defineIonNavLink } from '@ionic/core/components/ import { defineCustomElement as defineIonNote } from '@ionic/core/components/ion-note.js'; import { defineCustomElement as defineIonPicker } from '@ionic/core/components/ion-picker.js'; import { defineCustomElement as defineIonPickerColumn } from '@ionic/core/components/ion-picker-column.js'; +import { defineCustomElement as defineIonPickerColumnOption } from '@ionic/core/components/ion-picker-column-option.js'; import { defineCustomElement as defineIonProgressBar } from '@ionic/core/components/ion-progress-bar.js'; import { defineCustomElement as defineIonRadio } from '@ionic/core/components/ion-radio.js'; import { defineCustomElement as defineIonRadioGroup } from '@ionic/core/components/ion-radio-group.js'; @@ -586,6 +587,12 @@ export const IonPickerColumn = /*@__PURE__*/ defineContainer('ion-picker-column-option', defineIonPickerColumnOption, [ + 'disabled', + 'value' +]); + + export const IonProgressBar = /*@__PURE__*/ defineContainer('ion-progress-bar', defineIonProgressBar, [ 'type', 'reversed',