mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-11-09 08:09:32 +08:00
feat(picker): picker column is easier to select with assistive technology (#29371)
Issue number: resolves #25221
This commit is contained in:
@ -1687,6 +1687,7 @@ export class Datetime implements ComponentInterface {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ion-picker-column
|
<ion-picker-column
|
||||||
|
aria-label="Select a date"
|
||||||
class="date-column"
|
class="date-column"
|
||||||
color={this.color}
|
color={this.color}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
@ -1806,6 +1807,7 @@ export class Datetime implements ComponentInterface {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ion-picker-column
|
<ion-picker-column
|
||||||
|
aria-label="Select a day"
|
||||||
class="day-column"
|
class="day-column"
|
||||||
color={this.color}
|
color={this.color}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
@ -1849,6 +1851,7 @@ export class Datetime implements ComponentInterface {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ion-picker-column
|
<ion-picker-column
|
||||||
|
aria-label="Select a month"
|
||||||
class="month-column"
|
class="month-column"
|
||||||
color={this.color}
|
color={this.color}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
@ -1891,6 +1894,7 @@ export class Datetime implements ComponentInterface {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ion-picker-column
|
<ion-picker-column
|
||||||
|
aria-label="Select a year"
|
||||||
class="year-column"
|
class="year-column"
|
||||||
color={this.color}
|
color={this.color}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
@ -1964,6 +1968,7 @@ export class Datetime implements ComponentInterface {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ion-picker-column
|
<ion-picker-column
|
||||||
|
aria-label="Select an hour"
|
||||||
color={this.color}
|
color={this.color}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
value={activePart.hour}
|
value={activePart.hour}
|
||||||
@ -2003,6 +2008,7 @@ export class Datetime implements ComponentInterface {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ion-picker-column
|
<ion-picker-column
|
||||||
|
aria-label="Select a minute"
|
||||||
color={this.color}
|
color={this.color}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
value={activePart.minute}
|
value={activePart.minute}
|
||||||
@ -2045,6 +2051,7 @@ export class Datetime implements ComponentInterface {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ion-picker-column
|
<ion-picker-column
|
||||||
|
aria-label="Select a day period"
|
||||||
style={isDayPeriodRTL ? { order: '-1' } : {}}
|
style={isDayPeriodRTL ? { order: '-1' } : {}}
|
||||||
color={this.color}
|
color={this.color}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
|||||||
@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
:host {
|
:host {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
@ -19,6 +20,23 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders an invisible element on top of the column that receives focus
|
||||||
|
* events. This allows screen readers to navigate the column.
|
||||||
|
*/
|
||||||
|
.assistive-focusable {
|
||||||
|
@include position(0, 0, 0, 0);
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
z-index: 1;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide the focus ring since screen readers will show their own
|
||||||
|
.assistive-focusable:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
.picker-opts {
|
.picker-opts {
|
||||||
/**
|
/**
|
||||||
* This padding must be set here and not on the
|
* This padding must be set here and not on the
|
||||||
|
|||||||
@ -31,6 +31,15 @@ export class PickerColumn implements ComponentInterface {
|
|||||||
private isColumnVisible = false;
|
private isColumnVisible = false;
|
||||||
private parentEl?: HTMLIonPickerElement | null;
|
private parentEl?: HTMLIonPickerElement | null;
|
||||||
private canExitInputMode = true;
|
private canExitInputMode = true;
|
||||||
|
private assistiveFocusable?: HTMLElement;
|
||||||
|
private updateValueTextOnScroll = false;
|
||||||
|
|
||||||
|
@State() ariaLabel: string | null = null;
|
||||||
|
|
||||||
|
@Watch('aria-label')
|
||||||
|
ariaLabelChanged(newValue: string) {
|
||||||
|
this.ariaLabel = newValue;
|
||||||
|
}
|
||||||
|
|
||||||
@State() isActive = false;
|
@State() isActive = false;
|
||||||
|
|
||||||
@ -206,6 +215,10 @@ export class PickerColumn implements ComponentInterface {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
this.ariaLabel = this.el.getAttribute('aria-label') ?? 'Select a value';
|
||||||
|
}
|
||||||
|
|
||||||
private centerPickerItemInView = (target: HTMLElement, smooth = true, canExitInputMode = true) => {
|
private centerPickerItemInView = (target: HTMLElement, smooth = true, canExitInputMode = true) => {
|
||||||
const { isColumnVisible, scrollEl } = this;
|
const { isColumnVisible, scrollEl } = this;
|
||||||
|
|
||||||
@ -222,6 +235,7 @@ export class PickerColumn implements ComponentInterface {
|
|||||||
* of these can cause a scroll to occur.
|
* of these can cause a scroll to occur.
|
||||||
*/
|
*/
|
||||||
this.canExitInputMode = canExitInputMode;
|
this.canExitInputMode = canExitInputMode;
|
||||||
|
this.updateValueTextOnScroll = false;
|
||||||
scrollEl.scroll({
|
scrollEl.scroll({
|
||||||
top,
|
top,
|
||||||
left: 0,
|
left: 0,
|
||||||
@ -396,8 +410,24 @@ export class PickerColumn implements ComponentInterface {
|
|||||||
activeEl = newActiveElement;
|
activeEl = newActiveElement;
|
||||||
this.setPickerItemActiveState(newActiveElement, true);
|
this.setPickerItemActiveState(newActiveElement, true);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the aria-valuetext even though the value prop has not been updated yet.
|
||||||
|
* This enables some screen readers to announce the value as the users drag
|
||||||
|
* as opposed to when their release their pointer from the screen.
|
||||||
|
*
|
||||||
|
* When the value is programmatically updated, we will smoothly scroll
|
||||||
|
* to the new option. However, we do not want to update aria-valuetext mid-scroll
|
||||||
|
* as that can cause the old value to be briefly set before being set to the
|
||||||
|
* correct option. This will cause some screen readers to announce the old value
|
||||||
|
* again before announcing the new value. The correct valuetext will be set on render.
|
||||||
|
*/
|
||||||
|
if (this.updateValueTextOnScroll) {
|
||||||
|
this.assistiveFocusable?.setAttribute('aria-valuetext', this.getOptionValueText(newActiveElement));
|
||||||
|
}
|
||||||
|
|
||||||
timeout = setTimeout(() => {
|
timeout = setTimeout(() => {
|
||||||
this.isScrolling = false;
|
this.isScrolling = false;
|
||||||
|
this.updateValueTextOnScroll = true;
|
||||||
enableHaptics && hapticSelectionEnd();
|
enableHaptics && hapticSelectionEnd();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -481,6 +511,159 @@ export class PickerColumn implements ComponentInterface {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the next enabled option after the active option.
|
||||||
|
* @param stride - How many options to "jump" over in order to select the next option.
|
||||||
|
* This can be used to implement PageUp/PageDown behaviors where pressing these keys
|
||||||
|
* scrolls the picker by more than 1 option. For example, a stride of 5 means select
|
||||||
|
* the enabled option 5 options after the active one. Note that the actual option selected
|
||||||
|
* may be past the stride if the option at the stride is disabled.
|
||||||
|
*/
|
||||||
|
private findNextOption = (stride = 1) => {
|
||||||
|
const { activeItem } = this;
|
||||||
|
if (!activeItem) return null;
|
||||||
|
|
||||||
|
let prevNode = activeItem;
|
||||||
|
let node = activeItem.nextElementSibling as HTMLIonPickerColumnOptionElement | null;
|
||||||
|
while (node != null) {
|
||||||
|
if (stride > 0) {
|
||||||
|
stride--;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.tagName === 'ION-PICKER-COLUMN-OPTION' && !node.disabled && stride === 0) {
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
prevNode = node;
|
||||||
|
|
||||||
|
// Use nextElementSibling instead of nextSibling to avoid text/comment nodes
|
||||||
|
node = node.nextElementSibling as HTMLIonPickerColumnOptionElement | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return prevNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the next enabled option after the active option.
|
||||||
|
* @param stride - How many options to "jump" over in order to select the next option.
|
||||||
|
* This can be used to implement PageUp/PageDown behaviors where pressing these keys
|
||||||
|
* scrolls the picker by more than 1 option. For example, a stride of 5 means select
|
||||||
|
* the enabled option 5 options before the active one. Note that the actual option selected
|
||||||
|
* may be past the stride if the option at the stride is disabled.
|
||||||
|
*/
|
||||||
|
private findPreviousOption = (stride: number = 1) => {
|
||||||
|
const { activeItem } = this;
|
||||||
|
if (!activeItem) return null;
|
||||||
|
|
||||||
|
let nextNode = activeItem;
|
||||||
|
let node = activeItem.previousElementSibling as HTMLIonPickerColumnOptionElement | null;
|
||||||
|
while (node != null) {
|
||||||
|
if (stride > 0) {
|
||||||
|
stride--;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.tagName === 'ION-PICKER-COLUMN-OPTION' && !node.disabled && stride === 0) {
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
nextNode = node;
|
||||||
|
|
||||||
|
// Use previousElementSibling instead of previousSibling to avoid text/comment nodes
|
||||||
|
node = node.previousElementSibling as HTMLIonPickerColumnOptionElement | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return nextNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
private onKeyDown = (ev: KeyboardEvent) => {
|
||||||
|
/**
|
||||||
|
* The below operations should be inverted when running on a mobile device.
|
||||||
|
* For example, swiping up will dispatch an "ArrowUp" event. On desktop,
|
||||||
|
* this should cause the previous option to be selected. On mobile, swiping
|
||||||
|
* up causes a view to scroll down. As a result, swiping up on mobile should
|
||||||
|
* cause the next option to be selected. The Home/End operations remain
|
||||||
|
* unchanged because those always represent the first/last options, respectively.
|
||||||
|
*/
|
||||||
|
const mobile = isPlatform('mobile');
|
||||||
|
let newOption: HTMLIonPickerColumnOptionElement | null = null;
|
||||||
|
switch (ev.key) {
|
||||||
|
case 'ArrowDown':
|
||||||
|
newOption = mobile ? this.findPreviousOption() : this.findNextOption();
|
||||||
|
break;
|
||||||
|
case 'ArrowUp':
|
||||||
|
newOption = mobile ? this.findNextOption() : this.findPreviousOption();
|
||||||
|
break;
|
||||||
|
case 'PageUp':
|
||||||
|
newOption = mobile ? this.findNextOption(5) : this.findPreviousOption(5);
|
||||||
|
break;
|
||||||
|
case 'PageDown':
|
||||||
|
newOption = mobile ? this.findPreviousOption(5) : this.findNextOption(5);
|
||||||
|
break;
|
||||||
|
case 'Home':
|
||||||
|
/**
|
||||||
|
* There is no guarantee that the first child will be an ion-picker-column-option,
|
||||||
|
* so we do not use firstElementChild.
|
||||||
|
*/
|
||||||
|
newOption = this.el.querySelector<HTMLIonPickerColumnOptionElement>('ion-picker-column-option:first-of-type');
|
||||||
|
break;
|
||||||
|
case 'End':
|
||||||
|
/**
|
||||||
|
* There is no guarantee that the last child will be an ion-picker-column-option,
|
||||||
|
* so we do not use lastElementChild.
|
||||||
|
*/
|
||||||
|
newOption = this.el.querySelector<HTMLIonPickerColumnOptionElement>('ion-picker-column-option:last-of-type');
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newOption !== null) {
|
||||||
|
this.value = newOption.value;
|
||||||
|
|
||||||
|
// This stops any default browser behavior such as scrolling
|
||||||
|
ev.preventDefault();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility to generate the correct text for aria-valuetext.
|
||||||
|
*/
|
||||||
|
private getOptionValueText = (el?: HTMLIonPickerColumnOptionElement) => {
|
||||||
|
return el ? el.getAttribute('aria-label') ?? el.innerText : '';
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render an element that overlays the column. This element is for assistive
|
||||||
|
* tech to allow users to navigate the column up/down. This element should receive
|
||||||
|
* focus as it listens for synthesized keyboard events as required by the
|
||||||
|
* slider role: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/slider_role
|
||||||
|
*/
|
||||||
|
private renderAssistiveFocusable = () => {
|
||||||
|
const { activeItem } = this;
|
||||||
|
const valueText = this.getOptionValueText(activeItem);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When using the picker, the valuetext provides important context that valuenow
|
||||||
|
* does not. Additionally, using non-zero valuemin/valuemax values can cause
|
||||||
|
* WebKit to incorrectly announce numeric valuetext values (such as a year
|
||||||
|
* like "2024") as percentages: https://bugs.webkit.org/show_bug.cgi?id=273126
|
||||||
|
*/
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={(el) => (this.assistiveFocusable = el)}
|
||||||
|
class="assistive-focusable"
|
||||||
|
role="slider"
|
||||||
|
tabindex={this.disabled ? undefined : 0}
|
||||||
|
aria-label={this.ariaLabel}
|
||||||
|
aria-valuemin={0}
|
||||||
|
aria-valuemax={0}
|
||||||
|
aria-valuenow={0}
|
||||||
|
aria-valuetext={valueText}
|
||||||
|
aria-orientation="vertical"
|
||||||
|
onKeyDown={(ev) => this.onKeyDown(ev)}
|
||||||
|
></div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { color, disabled, isActive, numericInput } = this;
|
const { color, disabled, isActive, numericInput } = this;
|
||||||
const mode = getIonMode(this);
|
const mode = getIonMode(this);
|
||||||
@ -494,10 +677,11 @@ export class PickerColumn implements ComponentInterface {
|
|||||||
['picker-column-disabled']: disabled,
|
['picker-column-disabled']: disabled,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
|
{this.renderAssistiveFocusable()}
|
||||||
<slot name="prefix"></slot>
|
<slot name="prefix"></slot>
|
||||||
<div
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
class="picker-opts"
|
class="picker-opts"
|
||||||
tabindex={disabled ? undefined : 0}
|
|
||||||
ref={(el) => {
|
ref={(el) => {
|
||||||
this.scrollEl = el;
|
this.scrollEl = el;
|
||||||
}}
|
}}
|
||||||
|
|||||||
114
core/src/components/picker-column/test/picker-column.spec.tsx
Normal file
114
core/src/components/picker-column/test/picker-column.spec.tsx
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
import { h } from '@stencil/core';
|
||||||
|
import { newSpecPage } from '@stencil/core/testing';
|
||||||
|
|
||||||
|
import { PickerColumn } from '../picker-column';
|
||||||
|
import { PickerColumnOption } from '../../picker-column-option/picker-column-option';
|
||||||
|
|
||||||
|
describe('picker-column: assistive element', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
const mockIntersectionObserver = jest.fn();
|
||||||
|
mockIntersectionObserver.mockReturnValue({
|
||||||
|
observe: () => null,
|
||||||
|
unobserve: () => null,
|
||||||
|
disconnect: () => null,
|
||||||
|
});
|
||||||
|
global.IntersectionObserver = mockIntersectionObserver;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have a default label', async () => {
|
||||||
|
const page = await newSpecPage({
|
||||||
|
components: [PickerColumn],
|
||||||
|
template: () => <ion-picker-column></ion-picker-column>,
|
||||||
|
});
|
||||||
|
|
||||||
|
const pickerCol = page.body.querySelector('ion-picker-column')!;
|
||||||
|
const assistiveFocusable = pickerCol.shadowRoot!.querySelector('.assistive-focusable')!;
|
||||||
|
|
||||||
|
expect(assistiveFocusable.getAttribute('aria-label')).not.toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have a custom label', async () => {
|
||||||
|
const page = await newSpecPage({
|
||||||
|
components: [PickerColumn],
|
||||||
|
template: () => <ion-picker-column aria-label="my label"></ion-picker-column>,
|
||||||
|
});
|
||||||
|
|
||||||
|
const pickerCol = page.body.querySelector('ion-picker-column')!;
|
||||||
|
const assistiveFocusable = pickerCol.shadowRoot!.querySelector('.assistive-focusable')!;
|
||||||
|
|
||||||
|
expect(assistiveFocusable.getAttribute('aria-label')).toBe('my label');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update a custom label', async () => {
|
||||||
|
const page = await newSpecPage({
|
||||||
|
components: [PickerColumn],
|
||||||
|
template: () => <ion-picker-column></ion-picker-column>,
|
||||||
|
});
|
||||||
|
|
||||||
|
const pickerCol = page.body.querySelector('ion-picker-column')!;
|
||||||
|
const assistiveFocusable = pickerCol.shadowRoot!.querySelector('.assistive-focusable')!;
|
||||||
|
|
||||||
|
pickerCol.setAttribute('aria-label', 'my label');
|
||||||
|
await page.waitForChanges();
|
||||||
|
|
||||||
|
expect(assistiveFocusable.getAttribute('aria-label')).toBe('my label');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should receive keyboard focus when enabled', async () => {
|
||||||
|
const page = await newSpecPage({
|
||||||
|
components: [PickerColumn],
|
||||||
|
template: () => <ion-picker-column></ion-picker-column>,
|
||||||
|
});
|
||||||
|
|
||||||
|
const pickerCol = page.body.querySelector('ion-picker-column')!;
|
||||||
|
const assistiveFocusable = pickerCol.shadowRoot!.querySelector<HTMLElement>('.assistive-focusable')!;
|
||||||
|
|
||||||
|
expect(assistiveFocusable.tabIndex).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not receive keyboard focus when disabled', async () => {
|
||||||
|
const page = await newSpecPage({
|
||||||
|
components: [PickerColumn],
|
||||||
|
template: () => <ion-picker-column disabled={true}></ion-picker-column>,
|
||||||
|
});
|
||||||
|
|
||||||
|
const pickerCol = page.body.querySelector('ion-picker-column')!;
|
||||||
|
const assistiveFocusable = pickerCol.shadowRoot!.querySelector<HTMLElement>('.assistive-focusable')!;
|
||||||
|
|
||||||
|
expect(assistiveFocusable.tabIndex).toBe(-1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use option aria-label as assistive element aria-valuetext', async () => {
|
||||||
|
const page = await newSpecPage({
|
||||||
|
components: [PickerColumn, PickerColumnOption],
|
||||||
|
template: () => (
|
||||||
|
<ion-picker-column value={1}>
|
||||||
|
<ion-picker-column-option value={1} aria-label="My Label">
|
||||||
|
My Text
|
||||||
|
</ion-picker-column-option>
|
||||||
|
</ion-picker-column>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
const pickerCol = page.body.querySelector('ion-picker-column')!;
|
||||||
|
const assistiveFocusable = pickerCol.shadowRoot!.querySelector('.assistive-focusable')!;
|
||||||
|
|
||||||
|
expect(assistiveFocusable.getAttribute('aria-valuetext')).toBe('My Label');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use option text as assistive element aria-valuetext when no label provided', async () => {
|
||||||
|
const page = await newSpecPage({
|
||||||
|
components: [PickerColumn, PickerColumnOption],
|
||||||
|
template: () => (
|
||||||
|
<ion-picker-column value={1}>
|
||||||
|
<ion-picker-column-option value={1}>My Text</ion-picker-column-option>
|
||||||
|
</ion-picker-column>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
const pickerCol = page.body.querySelector('ion-picker-column')!;
|
||||||
|
const assistiveFocusable = pickerCol.shadowRoot!.querySelector('.assistive-focusable')!;
|
||||||
|
|
||||||
|
expect(assistiveFocusable.getAttribute('aria-valuetext')).toBe('My Text');
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -16,6 +16,15 @@
|
|||||||
<h1>Picker - a11y</h1>
|
<h1>Picker - a11y</h1>
|
||||||
|
|
||||||
<ion-picker>
|
<ion-picker>
|
||||||
|
<ion-picker-column aria-label="Select a month" color="tertiary" value="3">
|
||||||
|
<ion-picker-column-option value="1" color="tertiary">First</ion-picker-column-option>
|
||||||
|
<ion-picker-column-option value="2" color="tertiary">Second</ion-picker-column-option>
|
||||||
|
<ion-picker-column-option value="3" color="tertiary">Third</ion-picker-column-option>
|
||||||
|
<ion-picker-column-option value="4" color="tertiary">Fourth</ion-picker-column-option>
|
||||||
|
<ion-picker-column-option value="5" color="tertiary" disabled="true">Fifth</ion-picker-column-option>
|
||||||
|
<ion-picker-column-option value="6" color="tertiary">Sixth</ion-picker-column-option>
|
||||||
|
<ion-picker-column-option value="7" color="tertiary">Seventh</ion-picker-column-option>
|
||||||
|
</ion-picker-column>
|
||||||
<ion-picker-column color="tertiary" value="3">
|
<ion-picker-column color="tertiary" value="3">
|
||||||
<ion-picker-column-option value="1" color="tertiary">First</ion-picker-column-option>
|
<ion-picker-column-option value="1" color="tertiary">First</ion-picker-column-option>
|
||||||
<ion-picker-column-option value="2" color="tertiary">Second</ion-picker-column-option>
|
<ion-picker-column-option value="2" color="tertiary">Second</ion-picker-column-option>
|
||||||
|
|||||||
Reference in New Issue
Block a user