Skip to content

Commit 01130e1

Browse files
mapsandappsIonitronliamdebeasi
authored
fix(datetime): allow disabling datetime with prefer-wheel (#28511)
Issue number: Internal --------- <!-- Please do not submit updates to dependencies unless it fixes an issue. --> <!-- Please try to limit your pull request to one type (bugfix, feature, etc). Submit multiple pull requests if needed. --> ## What is the current behavior? <!-- Please describe the current behavior that you are modifying. --> It is possible to navigate the columns of a disabled Datetime with `prefer-wheel` via the keyboard. https://github.com/ionic-team/ionic-framework/assets/14926794/9c9dafc4-4b77-45a6-a276-70201c5c3ea5 ## What is the new behavior? <!-- Please describe the behavior or changes that are being added by this PR. --> - Picker Column Internal has a disabled state that disables the full column - When a Datetime is disabled with `prefer-wheel`, the columns in the Datetime will be disabled - It is no longer possible to navigate the wheels in a disabled Datetime via the keyboard Comparison of native & Ionic components: ![Screenshot 2023-11-10 at 10 58 25 AM](https://github.com/ionic-team/ionic-framework/assets/14926794/e2bec1b3-30f8-4f64-8658-27b971884b7a) ## Does this introduce a breaking change? - [ ] Yes - [x] No <!-- If this introduces a breaking change, please describe the impact and migration path for existing applications below. --> --------- Co-authored-by: ionitron <[email protected]> Co-authored-by: Liam DeBeasi <[email protected]>
1 parent b833f0e commit 01130e1

14 files changed

+152
-31
lines changed

core/src/components.d.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2043,6 +2043,10 @@ export namespace Components {
20432043
* The color to use from your application's color palette. Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`. For more information on colors, see [theming](/docs/theming/basics).
20442044
*/
20452045
"color"?: Color;
2046+
/**
2047+
* If `true`, the user cannot interact with the picker.
2048+
*/
2049+
"disabled": boolean;
20462050
/**
20472051
* A list of options to be displayed in the picker
20482052
*/
@@ -6683,6 +6687,10 @@ declare namespace LocalJSX {
66836687
* The color to use from your application's color palette. Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`. For more information on colors, see [theming](/docs/theming/basics).
66846688
*/
66856689
"color"?: Color;
6690+
/**
6691+
* If `true`, the user cannot interact with the picker.
6692+
*/
6693+
"disabled"?: boolean;
66866694
/**
66876695
* A list of options to be displayed in the picker
66886696
*/

core/src/components/datetime/datetime.tsx

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1538,7 +1538,7 @@ export class Datetime implements ComponentInterface {
15381538
}
15391539

15401540
private renderCombinedDatePickerColumn() {
1541-
const { defaultParts, workingParts, locale, minParts, maxParts, todayParts, isDateEnabled } = this;
1541+
const { defaultParts, disabled, workingParts, locale, minParts, maxParts, todayParts, isDateEnabled } = this;
15421542

15431543
const activePart = this.getActivePartsWithFallback();
15441544

@@ -1617,6 +1617,7 @@ export class Datetime implements ComponentInterface {
16171617
<ion-picker-column-internal
16181618
class="date-column"
16191619
color={this.color}
1620+
disabled={disabled}
16201621
items={items}
16211622
value={todayString}
16221623
onIonChange={(ev: CustomEvent) => {
@@ -1728,14 +1729,15 @@ export class Datetime implements ComponentInterface {
17281729
return [];
17291730
}
17301731

1731-
const { workingParts } = this;
1732+
const { disabled, workingParts } = this;
17321733

17331734
const activePart = this.getActivePartsWithFallback();
17341735

17351736
return (
17361737
<ion-picker-column-internal
17371738
class="day-column"
17381739
color={this.color}
1740+
disabled={disabled}
17391741
items={days}
17401742
value={(workingParts.day !== null ? workingParts.day : this.defaultParts.day) ?? undefined}
17411743
onIonChange={(ev: CustomEvent) => {
@@ -1772,14 +1774,15 @@ export class Datetime implements ComponentInterface {
17721774
return [];
17731775
}
17741776

1775-
const { workingParts } = this;
1777+
const { disabled, workingParts } = this;
17761778

17771779
const activePart = this.getActivePartsWithFallback();
17781780

17791781
return (
17801782
<ion-picker-column-internal
17811783
class="month-column"
17821784
color={this.color}
1785+
disabled={disabled}
17831786
items={months}
17841787
value={workingParts.month}
17851788
onIonChange={(ev: CustomEvent) => {
@@ -1815,14 +1818,15 @@ export class Datetime implements ComponentInterface {
18151818
return [];
18161819
}
18171820

1818-
const { workingParts } = this;
1821+
const { disabled, workingParts } = this;
18191822

18201823
const activePart = this.getActivePartsWithFallback();
18211824

18221825
return (
18231826
<ion-picker-column-internal
18241827
class="year-column"
18251828
color={this.color}
1829+
disabled={disabled}
18261830
items={years}
18271831
value={workingParts.year}
18281832
onIonChange={(ev: CustomEvent) => {
@@ -1888,14 +1892,15 @@ export class Datetime implements ComponentInterface {
18881892
}
18891893

18901894
private renderHourPickerColumn(hoursData: PickerColumnItem[]) {
1891-
const { workingParts } = this;
1895+
const { disabled, workingParts } = this;
18921896
if (hoursData.length === 0) return [];
18931897

18941898
const activePart = this.getActivePartsWithFallback();
18951899

18961900
return (
18971901
<ion-picker-column-internal
18981902
color={this.color}
1903+
disabled={disabled}
18991904
value={activePart.hour}
19001905
items={hoursData}
19011906
numericInput
@@ -1916,14 +1921,15 @@ export class Datetime implements ComponentInterface {
19161921
);
19171922
}
19181923
private renderMinutePickerColumn(minutesData: PickerColumnItem[]) {
1919-
const { workingParts } = this;
1924+
const { disabled, workingParts } = this;
19201925
if (minutesData.length === 0) return [];
19211926

19221927
const activePart = this.getActivePartsWithFallback();
19231928

19241929
return (
19251930
<ion-picker-column-internal
19261931
color={this.color}
1932+
disabled={disabled}
19271933
value={activePart.minute}
19281934
items={minutesData}
19291935
numericInput
@@ -1944,7 +1950,7 @@ export class Datetime implements ComponentInterface {
19441950
);
19451951
}
19461952
private renderDayPeriodPickerColumn(dayPeriodData: PickerColumnItem[]) {
1947-
const { workingParts } = this;
1953+
const { disabled, workingParts } = this;
19481954
if (dayPeriodData.length === 0) {
19491955
return [];
19501956
}
@@ -1956,6 +1962,7 @@ export class Datetime implements ComponentInterface {
19561962
<ion-picker-column-internal
19571963
style={isDayPeriodRTL ? { order: '-1' } : {}}
19581964
color={this.color}
1965+
disabled={disabled}
19591966
value={activePart.ampm}
19601967
items={dayPeriodData}
19611968
onIonChange={(ev: CustomEvent) => {
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { h } from '@stencil/core';
2+
import { newSpecPage } from '@stencil/core/testing';
3+
4+
import { Datetime } from '../../../datetime/datetime';
5+
import { PickerColumnInternal } from '../../../picker-column-internal/picker-column-internal';
6+
import { PickerInternal } from '../../../picker-internal/picker-internal';
7+
8+
describe('ion-datetime disabled', () => {
9+
beforeEach(() => {
10+
// IntersectionObserver isn't available in test environment
11+
const mockIntersectionObserver = jest.fn();
12+
mockIntersectionObserver.mockReturnValue({
13+
observe: () => null,
14+
unobserve: () => null,
15+
disconnect: () => null,
16+
});
17+
global.IntersectionObserver = mockIntersectionObserver;
18+
});
19+
20+
it('picker should be disabled in prefer wheel mode', async () => {
21+
const page = await newSpecPage({
22+
components: [Datetime, PickerColumnInternal, PickerInternal],
23+
template: () => (
24+
<ion-datetime id="inline-datetime-wheel" disabled prefer-wheel value="2022-04-21T00:00:00"></ion-datetime>
25+
),
26+
});
27+
28+
await page.waitForChanges();
29+
30+
const datetime = page.body.querySelector('ion-datetime')!;
31+
const columns = datetime.shadowRoot!.querySelectorAll('ion-picker-column-internal');
32+
33+
await expect(columns.length).toEqual(4);
34+
35+
columns.forEach((column) => {
36+
expect(column.disabled).toBe(true);
37+
});
38+
});
39+
});

core/src/components/datetime/test/disabled/index.html

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,11 @@ <h2>Inline</h2>
6666
<h2>Inline - No Default Value</h2>
6767
<ion-datetime id="inline-datetime-no-value" disabled></ion-datetime>
6868
</div>
69+
70+
<div class="grid-item">
71+
<h2>Inline - Prefer Wheel</h2>
72+
<ion-datetime id="inline-datetime-wheel" disabled prefer-wheel value="2022-04-21T00:00:00"></ion-datetime>
73+
</div>
6974
</div>
7075
</ion-content>
7176
<script>

core/src/components/picker-column-internal/picker-column-internal.scss

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,13 +71,20 @@
7171
}
7272

7373
:host .picker-item-empty,
74-
:host .picker-item.picker-item-disabled {
74+
:host .picker-item[disabled] {
75+
cursor: default;
76+
}
77+
78+
:host .picker-item-empty,
79+
:host(:not([disabled])) .picker-item[disabled] {
7580
scroll-snap-align: none;
81+
}
7682

77-
cursor: default;
83+
:host([disabled]) {
84+
overflow-y: hidden;
7885
}
7986

80-
:host .picker-item.picker-item-disabled {
87+
:host .picker-item[disabled] {
8188
opacity: 0.4;
8289
}
8390

core/src/components/picker-column-internal/picker-column-internal.tsx

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,11 @@ export class PickerColumnInternal implements ComponentInterface {
3535

3636
@Element() el!: HTMLIonPickerColumnInternalElement;
3737

38+
/**
39+
* If `true`, the user cannot interact with the picker.
40+
*/
41+
@Prop() disabled = false;
42+
3843
/**
3944
* A list of options to be displayed in the picker
4045
*/
@@ -408,13 +413,15 @@ export class PickerColumnInternal implements ComponentInterface {
408413
};
409414

410415
get activeItem() {
411-
return getElementRoot(this.el).querySelector(
412-
`.picker-item[data-value="${this.value}"]:not([disabled])`
413-
) as HTMLElement | null;
416+
// If the whole picker column is disabled, the current value should appear active
417+
// If the current value item is specifically disabled, it should not appear active
418+
const selector = `.picker-item[data-value="${this.value}"]${this.disabled ? '' : ':not([disabled])'}`;
419+
420+
return getElementRoot(this.el).querySelector(selector) as HTMLElement | null;
414421
}
415422

416423
render() {
417-
const { items, color, isActive, numericInput } = this;
424+
const { items, color, disabled: pickerDisabled, isActive, numericInput } = this;
418425
const mode = getIonMode(this);
419426

420427
/**
@@ -423,10 +430,12 @@ export class PickerColumnInternal implements ComponentInterface {
423430
* the attribute can be moved to datetime.tsx and set on every
424431
* instance of ion-picker-column-internal there instead.
425432
*/
433+
426434
return (
427435
<Host
428436
exportparts={`${PICKER_ITEM_PART}, ${PICKER_ITEM_ACTIVE_PART}`}
429-
tabindex={0}
437+
disabled={pickerDisabled}
438+
tabindex={pickerDisabled ? null : 0}
430439
class={createColorClasses(color, {
431440
[mode]: true,
432441
['picker-column-active']: isActive,
@@ -443,6 +452,8 @@ export class PickerColumnInternal implements ComponentInterface {
443452
&nbsp;
444453
</div>
445454
{items.map((item, index) => {
455+
const isItemDisabled = pickerDisabled || item.disabled || false;
456+
446457
{
447458
/*
448459
Users should be able to tab
@@ -458,14 +469,13 @@ export class PickerColumnInternal implements ComponentInterface {
458469
tabindex="-1"
459470
class={{
460471
'picker-item': true,
461-
'picker-item-disabled': item.disabled || false,
462472
}}
463473
data-value={item.value}
464474
data-index={index}
465475
onClick={(ev: Event) => {
466476
this.centerPickerItemInView(ev.target as HTMLElement, true);
467477
}}
468-
disabled={item.disabled}
478+
disabled={isItemDisabled}
469479
part={PICKER_ITEM_PART}
470480
>
471481
{item.text}

core/src/components/picker-column-internal/test/disabled/index.html

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -45,26 +45,39 @@
4545
<ion-content class="ion-padding">
4646
<div class="grid">
4747
<div class="grid-item">
48-
<h2>Default</h2>
48+
<h2>Even items disabled</h2>
4949
<ion-picker-internal>
50-
<ion-picker-column-internal id="default"></ion-picker-column-internal>
50+
<ion-picker-column-internal id="half-disabled"></ion-picker-column-internal>
51+
</ion-picker-internal>
52+
</div>
53+
<div class="grid-item">
54+
<h2>Column disabled</h2>
55+
<ion-picker-internal>
56+
<ion-picker-column-internal id="column-disabled" value="11" disabled></ion-picker-column-internal>
5157
</ion-picker-internal>
5258
</div>
5359
</div>
5460
</ion-content>
5561
<script>
56-
const defaultPickerColumn = document.getElementById('default');
57-
58-
const items = Array(24)
62+
const halfDisabledPicker = document.getElementById('half-disabled');
63+
const halfDisabledItems = Array(24)
5964
.fill()
6065
.map((_, i) => ({
6166
text: `${i}`,
6267
value: i,
6368
disabled: i % 2 === 0,
6469
}));
70+
halfDisabledPicker.items = halfDisabledItems;
71+
halfDisabledPicker.value = 12;
6572

66-
defaultPickerColumn.items = items;
67-
defaultPickerColumn.value = 12;
73+
const fullDisabledPicker = document.getElementById('column-disabled');
74+
const items = Array(24)
75+
.fill()
76+
.map((_, i) => ({
77+
text: `${i}`,
78+
value: i,
79+
}));
80+
fullDisabledPicker.items = items;
6881
</script>
6982
</ion-app>
7083
</body>

0 commit comments

Comments
 (0)