Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(picker-column-option): add the new component #28591

Merged
merged 9 commits into from
Nov 30, 2023
4 changes: 4 additions & 0 deletions core/api.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
29 changes: 29 additions & 0 deletions core/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1986,6 +1986,16 @@ export namespace Components {
*/
"value"?: string | number;
}
interface IonPickerColumnOption {
/**
* If `true`, the user cannot interact with the select option.
*/
"disabled": boolean;
/**
* The text value of the option.
*/
"value"?: any | null;
}
interface IonPickerLegacy {
/**
* If `true`, the picker will animate.
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -6616,6 +6633,16 @@ declare namespace LocalJSX {
*/
"value"?: string | number;
}
interface IonPickerColumnOption {
/**
* If `true`, the user cannot interact with the select option.
*/
"disabled"?: boolean;
/**
* The text value of the option.
*/
"value"?: any | null;
}
interface IonPickerLegacy {
/**
* If `true`, the picker will animate.
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -8183,6 +8211,7 @@ declare module "@stencil/core" {
"ion-note": LocalJSX.IonNote & JSXBase.HTMLAttributes<HTMLIonNoteElement>;
"ion-picker": LocalJSX.IonPicker & JSXBase.HTMLAttributes<HTMLIonPickerElement>;
"ion-picker-column": LocalJSX.IonPickerColumn & JSXBase.HTMLAttributes<HTMLIonPickerColumnElement>;
"ion-picker-column-option": LocalJSX.IonPickerColumnOption & JSXBase.HTMLAttributes<HTMLIonPickerColumnOptionElement>;
"ion-picker-legacy": LocalJSX.IonPickerLegacy & JSXBase.HTMLAttributes<HTMLIonPickerLegacyElement>;
"ion-picker-legacy-column": LocalJSX.IonPickerLegacyColumn & JSXBase.HTMLAttributes<HTMLIonPickerLegacyColumnElement>;
"ion-popover": LocalJSX.IonPopover & JSXBase.HTMLAttributes<HTMLIonPopoverElement>;
Expand Down
55 changes: 55 additions & 0 deletions core/src/components/picker-column-option/picker-column-option.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import type { ComponentInterface } from '@stencil/core';
import { Component, Element, Host, Prop, h } from '@stencil/core';
import type { Attributes } from '@utils/helpers';
import { inheritAriaAttributes } from '@utils/helpers';

import { getIonMode } from '../../global/ionic-global';

@Component({
tag: 'ion-picker-column-option',
shadow: true,
})
export class PickerColumnOption implements ComponentInterface {
private optionId = `ion-picker-opt-${pickerOptionIds++}`;

private inheritedAttributes: Attributes = {};

@Element() el!: HTMLElement;

/**
* If `true`, the user cannot interact with the select option.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* If `true`, the user cannot interact with the select option.
* If `true`, the user cannot interact with the picker column option.

*/
@Prop() disabled = false;

/**
* The text value of the option.
*/
@Prop() value?: any | null;

componentWillLoad() {
this.inheritedAttributes = inheritAriaAttributes(this.el);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As per the ticket, we should be taking advantage of Stencil's watch decorator for attributes: https://stenciljs.com/docs/reactive-data#watching-native-html-attributes

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To add to this, you will still need to get the initial state of the attribute you are trying to reflect. inheritAttributes would be the better utility for this here, since you only are reflecting the aria-label (where as inheritAriaAttributes will parse all aria attributes on the element node). I would assign this to a @State variable, so a mutation to this variable in the @Watch function will trigger a re-render to reflect the aria label to the inner button.

The desired behavior is if the developer changes the aria-label after the initial render of the component, that it will still update in the shadow dom template.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you both for the deeper explanations. I now have a better idea on how this stuff works.

}

render() {
const { value, disabled, inheritedAttributes } = this;
const ariaLabel = inheritedAttributes['aria-label'] || null;

return (
<Host id={this.optionId} class={getIonMode(this)}>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't seem to be used anywhere. Can we remove it? (Or document why it's needed)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added them for future usage. I'll remove them since this is a stubbed version.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If they are for future usage I'm fine keeping them, I was just hoping we could document why it was needed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking that:

  • id would be necessary for children components that act like an option in order to submit the right value
  • getIonMode() would be necessary for styling based on mode

But since I'm not 100% sure if my reasoning is correct and that it's currently not being used, then I removed it.

<button
tabindex="-1"
aria-label={ariaLabel}
class={{
'picker-opt': true,
'picker-opt-disabled': !!disabled,
averyjohnston marked this conversation as resolved.
Show resolved Hide resolved
}}
disabled={disabled}
>
<slot>{value}</slot>
</button>
</Host>
);
}
}

let pickerOptionIds = 0;
19 changes: 19 additions & 0 deletions core/src/components/picker-column-option/test/a11y/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8" />
<title>Picker Column Option - Basic</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0" />
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet" />
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet" />
<script src="../../../../../scripts/testing/scripts.js"></script>
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
</head>

<body>
<main>
<ion-picker-column-option> my option </ion-picker-column-option>
<ion-picker-column-option aria-label="the best one"> other option </ion-picker-column-option>
</main>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -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([]);
});
});
});
55 changes: 55 additions & 0 deletions core/src/components/picker-column-option/test/basic/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8" />
<title>Picker Column Option - Basic</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0" />
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet" />
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet" />
<script src="../../../../../scripts/testing/scripts.js"></script>
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
<style>
.grid {
display: grid;
grid-template-columns: repeat(3, minmax(250px, 1fr));
grid-row-gap: 20px;
grid-column-gap: 20px;
}

h2 {
font-size: 12px;
font-weight: normal;

color: #6f7378;

margin-top: 10px;
margin-left: 5px;
}

@media screen and (max-width: 800px) {
.grid {
grid-template-columns: 1fr;
padding: 0;
}
}
</style>
</head>

<body>
<ion-app>
<ion-header translucent="true">
<ion-toolbar>
<ion-title>Picker Column Option - Basic</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<div class="grid">
<div class="grid-item">
<h2>Default</h2>
<ion-picker-column-option> my option </ion-picker-column-option>
</div>
</div>
</ion-content>
</ion-app>
</body>
</html>
1 change: 1 addition & 0 deletions packages/angular/src/directives/proxies-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export const DIRECTIVES = [
d.IonNote,
d.IonPicker,
d.IonPickerColumn,
d.IonPickerColumnOption,
d.IonPickerLegacy,
d.IonProgressBar,
d.IonRadio,
Expand Down
22 changes: 22 additions & 0 deletions packages/angular/src/directives/proxies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: '<ng-content></ng-content>',
// 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']
Expand Down
25 changes: 25 additions & 0 deletions packages/angular/standalone/src/directives/proxies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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: '<ng-content></ng-content>',
// 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'],
Expand Down
2 changes: 2 additions & 0 deletions packages/react/src/components/proxies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -113,6 +114,7 @@ export const IonNavLink = /*@__PURE__*/createReactComponent<JSX.IonNavLink, HTML
export const IonNote = /*@__PURE__*/createReactComponent<JSX.IonNote, HTMLIonNoteElement>('ion-note', undefined, undefined, defineIonNote);
export const IonPicker = /*@__PURE__*/createReactComponent<JSX.IonPicker, HTMLIonPickerElement>('ion-picker', undefined, undefined, defineIonPicker);
export const IonPickerColumn = /*@__PURE__*/createReactComponent<JSX.IonPickerColumn, HTMLIonPickerColumnElement>('ion-picker-column', undefined, undefined, defineIonPickerColumn);
export const IonPickerColumnOption = /*@__PURE__*/createReactComponent<JSX.IonPickerColumnOption, HTMLIonPickerColumnOptionElement>('ion-picker-column-option', undefined, undefined, defineIonPickerColumnOption);
export const IonProgressBar = /*@__PURE__*/createReactComponent<JSX.IonProgressBar, HTMLIonProgressBarElement>('ion-progress-bar', undefined, undefined, defineIonProgressBar);
export const IonRadio = /*@__PURE__*/createReactComponent<JSX.IonRadio, HTMLIonRadioElement>('ion-radio', undefined, undefined, defineIonRadio);
export const IonRadioGroup = /*@__PURE__*/createReactComponent<JSX.IonRadioGroup, HTMLIonRadioGroupElement>('ion-radio-group', undefined, undefined, defineIonRadioGroup);
Expand Down
7 changes: 7 additions & 0 deletions packages/vue/src/proxies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -586,6 +587,12 @@ export const IonPickerColumn = /*@__PURE__*/ defineContainer<JSX.IonPickerColumn
]);


export const IonPickerColumnOption = /*@__PURE__*/ defineContainer<JSX.IonPickerColumnOption>('ion-picker-column-option', defineIonPickerColumnOption, [
'disabled',
'value'
]);


export const IonProgressBar = /*@__PURE__*/ defineContainer<JSX.IonProgressBar>('ion-progress-bar', defineIonProgressBar, [
'type',
'reversed',
Expand Down
Loading