Browse Source

0210348-删除项目错位文件

0210348 2 months ago
parent
commit
fc3dcd5810
100 changed files with 14835 additions and 0 deletions
  1. 52 0
      src/node_modules/@ionic/core/dist/collection/components/datetime/test/custom/datetime.e2e.js
  2. 456 0
      src/node_modules/@ionic/core/dist/collection/components/datetime/test/data.spec.js
  3. 39 0
      src/node_modules/@ionic/core/dist/collection/components/datetime/test/datetime.e2e.js
  4. 150 0
      src/node_modules/@ionic/core/dist/collection/components/datetime/test/disable-dates/datetime.e2e.js
  5. 68 0
      src/node_modules/@ionic/core/dist/collection/components/datetime/test/disabled/datetime.e2e.js
  6. 33 0
      src/node_modules/@ionic/core/dist/collection/components/datetime/test/disabled/datetime.spec.js
  7. 121 0
      src/node_modules/@ionic/core/dist/collection/components/datetime/test/display/datetime.e2e.js
  8. 14 0
      src/node_modules/@ionic/core/dist/collection/components/datetime/test/first-day-of-week/datetime.e2e.js
  9. 174 0
      src/node_modules/@ionic/core/dist/collection/components/datetime/test/format.spec.js
  10. 72 0
      src/node_modules/@ionic/core/dist/collection/components/datetime/test/helpers.spec.js
  11. 86 0
      src/node_modules/@ionic/core/dist/collection/components/datetime/test/highlighted-dates/datetime.e2e.js
  12. 37 0
      src/node_modules/@ionic/core/dist/collection/components/datetime/test/hour-cycle/datetime.e2e.js
  13. 129 0
      src/node_modules/@ionic/core/dist/collection/components/datetime/test/locale/datetime.e2e.js
  14. 565 0
      src/node_modules/@ionic/core/dist/collection/components/datetime/test/manipulation.spec.js
  15. 266 0
      src/node_modules/@ionic/core/dist/collection/components/datetime/test/minmax/datetime.e2e.js
  16. 29 0
      src/node_modules/@ionic/core/dist/collection/components/datetime/test/month-year-picker/datetime.e2e.js
  17. 242 0
      src/node_modules/@ionic/core/dist/collection/components/datetime/test/multiple/datetime.e2e.js
  18. 34 0
      src/node_modules/@ionic/core/dist/collection/components/datetime/test/overlay-roles/datetime.e2e.js
  19. 222 0
      src/node_modules/@ionic/core/dist/collection/components/datetime/test/parse.spec.js
  20. 22 0
      src/node_modules/@ionic/core/dist/collection/components/datetime/test/position/datetime.e2e.js
  21. 502 0
      src/node_modules/@ionic/core/dist/collection/components/datetime/test/prefer-wheel/datetime.e2e.js
  22. 27 0
      src/node_modules/@ionic/core/dist/collection/components/datetime/test/prefer-wheel/datetime.spec.js
  23. 169 0
      src/node_modules/@ionic/core/dist/collection/components/datetime/test/presentation/datetime.e2e.js
  24. 113 0
      src/node_modules/@ionic/core/dist/collection/components/datetime/test/readonly/datetime.e2e.js
  25. 50 0
      src/node_modules/@ionic/core/dist/collection/components/datetime/test/set-value/datetime.e2e.js
  26. 114 0
      src/node_modules/@ionic/core/dist/collection/components/datetime/test/state.spec.js
  27. 25 0
      src/node_modules/@ionic/core/dist/collection/components/datetime/test/time-label/datetime.e2e.js
  28. 18 0
      src/node_modules/@ionic/core/dist/collection/components/datetime/test/utils/month-did-change-event.js
  29. 143 0
      src/node_modules/@ionic/core/dist/collection/components/datetime/test/values/datetime.e2e.js
  30. 44 0
      src/node_modules/@ionic/core/dist/collection/components/datetime/utils/comparison.js
  31. 504 0
      src/node_modules/@ionic/core/dist/collection/components/datetime/utils/data.js
  32. 290 0
      src/node_modules/@ionic/core/dist/collection/components/datetime/utils/format.js
  33. 131 0
      src/node_modules/@ionic/core/dist/collection/components/datetime/utils/helpers.js
  34. 466 0
      src/node_modules/@ionic/core/dist/collection/components/datetime/utils/manipulation.js
  35. 197 0
      src/node_modules/@ionic/core/dist/collection/components/datetime/utils/parse.js
  36. 173 0
      src/node_modules/@ionic/core/dist/collection/components/datetime/utils/state.js
  37. 45 0
      src/node_modules/@ionic/core/dist/collection/components/datetime/utils/validate.js
  38. 422 0
      src/node_modules/@ionic/core/dist/collection/components/fab-button/fab-button.ios.css
  39. 392 0
      src/node_modules/@ionic/core/dist/collection/components/fab-button/fab-button.js
  40. 393 0
      src/node_modules/@ionic/core/dist/collection/components/fab-button/fab-button.md.css
  41. 66 0
      src/node_modules/@ionic/core/dist/collection/components/fab-button/test/a11y/fab-button.e2e.js
  42. 202 0
      src/node_modules/@ionic/core/dist/collection/components/fab-list/fab-list.css
  43. 86 0
      src/node_modules/@ionic/core/dist/collection/components/fab-list/fab-list.js
  44. 366 0
      src/node_modules/@ionic/core/dist/collection/components/fab/fab.css
  45. 191 0
      src/node_modules/@ionic/core/dist/collection/components/fab/fab.js
  46. 65 0
      src/node_modules/@ionic/core/dist/collection/components/fab/test/basic/fab.e2e.js
  47. 17 0
      src/node_modules/@ionic/core/dist/collection/components/fab/test/custom-size/fab.e2e.js
  48. 63 0
      src/node_modules/@ionic/core/dist/collection/components/fab/test/safe-area/fab.e2e.js
  49. 14 0
      src/node_modules/@ionic/core/dist/collection/components/fab/test/states/fab.e2e.js
  50. 28 0
      src/node_modules/@ionic/core/dist/collection/components/fab/test/translucent/fab.e2e.js
  51. 146 0
      src/node_modules/@ionic/core/dist/collection/components/footer/footer.ios.css
  52. 151 0
      src/node_modules/@ionic/core/dist/collection/components/footer/footer.js
  53. 129 0
      src/node_modules/@ionic/core/dist/collection/components/footer/footer.md.css
  54. 33 0
      src/node_modules/@ionic/core/dist/collection/components/footer/footer.utils.js
  55. 61 0
      src/node_modules/@ionic/core/dist/collection/components/footer/test/basic/footer.e2e.js
  56. 21 0
      src/node_modules/@ionic/core/dist/collection/components/footer/test/fade/footer.e2e.js
  57. 26 0
      src/node_modules/@ionic/core/dist/collection/components/footer/test/scroll-target/footer.e2e.js
  58. 17 0
      src/node_modules/@ionic/core/dist/collection/components/footer/test/with-tabs/footer.e2e.js
  59. 235 0
      src/node_modules/@ionic/core/dist/collection/components/grid/grid.css
  60. 51 0
      src/node_modules/@ionic/core/dist/collection/components/grid/grid.js
  61. 17 0
      src/node_modules/@ionic/core/dist/collection/components/grid/test/basic/grid.e2e.js
  62. 17 0
      src/node_modules/@ionic/core/dist/collection/components/grid/test/offsets/grid.e2e.js
  63. 17 0
      src/node_modules/@ionic/core/dist/collection/components/grid/test/padding/grid.e2e.js
  64. 17 0
      src/node_modules/@ionic/core/dist/collection/components/grid/test/sizes/grid.e2e.js
  65. 250 0
      src/node_modules/@ionic/core/dist/collection/components/header/header.ios.css
  66. 205 0
      src/node_modules/@ionic/core/dist/collection/components/header/header.js
  67. 133 0
      src/node_modules/@ionic/core/dist/collection/components/header/header.md.css
  68. 180 0
      src/node_modules/@ionic/core/dist/collection/components/header/header.utils.js
  69. 29 0
      src/node_modules/@ionic/core/dist/collection/components/header/test/a11y/header.e2e.js
  70. 147 0
      src/node_modules/@ionic/core/dist/collection/components/header/test/basic/header.e2e.js
  71. 33 0
      src/node_modules/@ionic/core/dist/collection/components/header/test/condense/header.e2e.js
  72. 21 0
      src/node_modules/@ionic/core/dist/collection/components/header/test/fade/header.e2e.js
  73. 26 0
      src/node_modules/@ionic/core/dist/collection/components/header/test/scroll-target/header.e2e.js
  74. 16 0
      src/node_modules/@ionic/core/dist/collection/components/icon/test/basic/icon.e2e.js
  75. 5 0
      src/node_modules/@ionic/core/dist/collection/components/icon/test/dir/heart-broken.svg
  76. 23 0
      src/node_modules/@ionic/core/dist/collection/components/icon/test/dir/icon.e2e.js
  77. 12 0
      src/node_modules/@ionic/core/dist/collection/components/img/img.css
  78. 204 0
      src/node_modules/@ionic/core/dist/collection/components/img/img.js
  79. 73 0
      src/node_modules/@ionic/core/dist/collection/components/img/test/basic/img.e2e.js
  80. 21 0
      src/node_modules/@ionic/core/dist/collection/components/img/test/draggable/img.e2e.js
  81. 156 0
      src/node_modules/@ionic/core/dist/collection/components/infinite-scroll-content/infinite-scroll-content.ios.css
  82. 99 0
      src/node_modules/@ionic/core/dist/collection/components/infinite-scroll-content/infinite-scroll-content.js
  83. 156 0
      src/node_modules/@ionic/core/dist/collection/components/infinite-scroll-content/infinite-scroll-content.md.css
  84. 37 0
      src/node_modules/@ionic/core/dist/collection/components/infinite-scroll-content/test/infinite-scroll-content.spec.js
  85. 1 0
      src/node_modules/@ionic/core/dist/collection/components/infinite-scroll/infinite-scroll-interface.js
  86. 8 0
      src/node_modules/@ionic/core/dist/collection/components/infinite-scroll/infinite-scroll.css
  87. 299 0
      src/node_modules/@ionic/core/dist/collection/components/infinite-scroll/infinite-scroll.js
  88. 19 0
      src/node_modules/@ionic/core/dist/collection/components/infinite-scroll/test/basic/infinite-scroll.e2e.js
  89. 19 0
      src/node_modules/@ionic/core/dist/collection/components/infinite-scroll/test/scroll-target/infinite-scroll.e2e.js
  90. 31 0
      src/node_modules/@ionic/core/dist/collection/components/infinite-scroll/test/small-dom-update/infinite-scroll.e2e.js
  91. 19 0
      src/node_modules/@ionic/core/dist/collection/components/infinite-scroll/test/top/infinite-scroll.e2e.js
  92. 0 0
      src/node_modules/@ionic/core/dist/collection/components/input-password-toggle/input-password-toggle.css
  93. 183 0
      src/node_modules/@ionic/core/dist/collection/components/input-password-toggle/input-password-toggle.js
  94. 21 0
      src/node_modules/@ionic/core/dist/collection/components/input-password-toggle/test/a11y/input-password-toggle.e2e.js
  95. 38 0
      src/node_modules/@ionic/core/dist/collection/components/input-password-toggle/test/basic/input-password-toggle.e2e.js
  96. 76 0
      src/node_modules/@ionic/core/dist/collection/components/input-password-toggle/test/input-password-toggle.spec.js
  97. 1 0
      src/node_modules/@ionic/core/dist/collection/components/input/input-interface.js
  98. 751 0
      src/node_modules/@ionic/core/dist/collection/components/input/input.ios.css
  99. 1202 0
      src/node_modules/@ionic/core/dist/collection/components/input/input.js
  100. 1276 0
      src/node_modules/@ionic/core/dist/collection/components/input/input.md.css

+ 52 - 0
src/node_modules/@ionic/core/dist/collection/components/datetime/test/custom/datetime.e2e.js

@@ -0,0 +1,52 @@
+/*!
+ * (C) Ionic http://ionicframework.com - MIT License
+ */
+import { expect } from "@playwright/test";
+import { configs, test } from "../../../../utils/test/playwright/index";
+configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
+    test.describe(title('datetime: custom'), () => {
+        test.beforeEach(async ({ page }) => {
+            await page.goto(`/src/components/datetime/test/custom`, config);
+        });
+        test('should allow styling wheel style datetimes', async ({ page }) => {
+            const datetime = page.locator('#custom-wheel');
+            await expect(datetime).toHaveScreenshot(screenshot(`datetime-custom-wheel`));
+        });
+        test('should allow styling time picker in grid style datetimes', async ({ page }) => {
+            const timeButton = page.locator('#custom-grid .time-body');
+            const popover = page.locator('.popover-viewport');
+            const ionPopoverDidPresent = await page.spyOnEvent('ionPopoverDidPresent');
+            await expect(timeButton).toHaveScreenshot(screenshot(`datetime-custom-time-button`));
+            await timeButton.click();
+            await ionPopoverDidPresent.next();
+            await expect(popover).toHaveScreenshot(screenshot(`datetime-custom-time-picker`));
+            await expect(timeButton).toHaveScreenshot(screenshot(`datetime-custom-time-button-active`));
+        });
+        test('should allow styling calendar days in grid style datetimes', async ({ page }) => {
+            const datetime = page.locator('#custom-calendar-days');
+            await expect(datetime).toHaveScreenshot(screenshot(`datetime-custom-calendar-days`));
+        });
+    });
+});
+/**
+ * This behavior does not differ across
+ * directions.
+ */
+configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
+    test.describe(title('datetime: custom focus'), () => {
+        test('should focus the selected day and then the day after', async ({ page }) => {
+            await page.goto(`/src/components/datetime/test/custom`, config);
+            const datetime = page.locator('#custom-calendar-days');
+            const day = datetime.locator(`.calendar-day[data-day='15'][data-month='6']`);
+            await day.focus();
+            await page.waitForChanges();
+            await expect(day).toBeFocused();
+            await expect(datetime).toHaveScreenshot(screenshot(`datetime-custom-focus-selected-calendar-day`));
+            await page.keyboard.press('ArrowRight');
+            await page.waitForChanges();
+            const nextDay = datetime.locator(`.calendar-day[data-day='16'][data-month='6']`);
+            await expect(nextDay).toBeFocused();
+            await expect(datetime).toHaveScreenshot(screenshot(`datetime-custom-focus-calendar-day`));
+        });
+    });
+});

+ 456 - 0
src/node_modules/@ionic/core/dist/collection/components/datetime/test/data.spec.js

@@ -0,0 +1,456 @@
+/*!
+ * (C) Ionic http://ionicframework.com - MIT License
+ */
+import { generateMonths, getDaysOfWeek, generateTime, getToday, getCombinedDateColumnData, getTimeColumnsData, } from "../utils/data";
+// The minutes are the same across all hour cycles, so we don't check those
+describe('getTimeColumnsData()', () => {
+    it('should generate formatted h12 hours and AM/PM data data', () => {
+        const refParts = { month: 5, year: 2021, day: 1, hour: 4, minute: 30 };
+        const results = getTimeColumnsData('en-US', refParts, 'h12');
+        expect(results.hoursData).toEqual([
+            { text: '12', value: 0 },
+            { text: '1', value: 1 },
+            { text: '2', value: 2 },
+            { text: '3', value: 3 },
+            { text: '4', value: 4 },
+            { text: '5', value: 5 },
+            { text: '6', value: 6 },
+            { text: '7', value: 7 },
+            { text: '8', value: 8 },
+            { text: '9', value: 9 },
+            { text: '10', value: 10 },
+            { text: '11', value: 11 },
+        ]);
+        expect(results.dayPeriodData).toEqual([
+            { text: 'AM', value: 'am' },
+            { text: 'PM', value: 'pm' },
+        ]);
+    });
+    it('should generate formatted h23 hours and AM/PM data data', () => {
+        const refParts = { month: 5, year: 2021, day: 1, hour: 4, minute: 30 };
+        const results = getTimeColumnsData('en-US', refParts, 'h23');
+        expect(results.hoursData).toEqual([
+            { text: '00', value: 0 },
+            { text: '01', value: 1 },
+            { text: '02', value: 2 },
+            { text: '03', value: 3 },
+            { text: '04', value: 4 },
+            { text: '05', value: 5 },
+            { text: '06', value: 6 },
+            { text: '07', value: 7 },
+            { text: '08', value: 8 },
+            { text: '09', value: 9 },
+            { text: '10', value: 10 },
+            { text: '11', value: 11 },
+            { text: '12', value: 12 },
+            { text: '13', value: 13 },
+            { text: '14', value: 14 },
+            { text: '15', value: 15 },
+            { text: '16', value: 16 },
+            { text: '17', value: 17 },
+            { text: '18', value: 18 },
+            { text: '19', value: 19 },
+            { text: '20', value: 20 },
+            { text: '21', value: 21 },
+            { text: '22', value: 22 },
+            { text: '23', value: 23 },
+        ]);
+        expect(results.dayPeriodData).toEqual([]);
+    });
+    it('should generate formatted h11 hours and AM/PM data data', () => {
+        const refParts = { month: 5, year: 2021, day: 1, hour: 4, minute: 30 };
+        const results = getTimeColumnsData('en-US', refParts, 'h11');
+        expect(results.hoursData).toEqual([
+            { text: '0', value: 0 },
+            { text: '1', value: 1 },
+            { text: '2', value: 2 },
+            { text: '3', value: 3 },
+            { text: '4', value: 4 },
+            { text: '5', value: 5 },
+            { text: '6', value: 6 },
+            { text: '7', value: 7 },
+            { text: '8', value: 8 },
+            { text: '9', value: 9 },
+            { text: '10', value: 10 },
+            { text: '11', value: 11 },
+        ]);
+        expect(results.dayPeriodData).toEqual([
+            { text: 'AM', value: 'am' },
+            { text: 'PM', value: 'pm' },
+        ]);
+    });
+    it('should generate formatted h24 hours and AM/PM data data', () => {
+        const refParts = { month: 5, year: 2021, day: 1, hour: 4, minute: 30 };
+        const results = getTimeColumnsData('en-US', refParts, 'h24');
+        expect(results.hoursData).toEqual([
+            { text: '01', value: 1 },
+            { text: '02', value: 2 },
+            { text: '03', value: 3 },
+            { text: '04', value: 4 },
+            { text: '05', value: 5 },
+            { text: '06', value: 6 },
+            { text: '07', value: 7 },
+            { text: '08', value: 8 },
+            { text: '09', value: 9 },
+            { text: '10', value: 10 },
+            { text: '11', value: 11 },
+            { text: '12', value: 12 },
+            { text: '13', value: 13 },
+            { text: '14', value: 14 },
+            { text: '15', value: 15 },
+            { text: '16', value: 16 },
+            { text: '17', value: 17 },
+            { text: '18', value: 18 },
+            { text: '19', value: 19 },
+            { text: '20', value: 20 },
+            { text: '21', value: 21 },
+            { text: '22', value: 22 },
+            { text: '23', value: 23 },
+            { text: '24', value: 0 },
+        ]);
+        expect(results.dayPeriodData).toEqual([]);
+    });
+});
+describe('generateMonths()', () => {
+    it('should generate correct month data', () => {
+        expect(generateMonths({ month: 5, year: 2021, day: 1 })).toEqual([
+            { month: 4, year: 2021, day: 1 },
+            { month: 5, year: 2021, day: 1 },
+            { month: 6, year: 2021, day: 1 },
+        ]);
+    });
+});
+describe('getDaysOfWeek()', () => {
+    it('should return English short names given a locale and mode', () => {
+        expect(getDaysOfWeek('en-US', 'ios')).toEqual(['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']);
+    });
+    it('should return English narrow names given a locale and mode', () => {
+        expect(getDaysOfWeek('en-US', 'md')).toEqual(['S', 'M', 'T', 'W', 'T', 'F', 'S']);
+    });
+    it('should return Spanish short names given a locale and mode', () => {
+        expect(getDaysOfWeek('es-ES', 'ios')).toEqual(['dom', 'lun', 'mar', 'mié', 'jue', 'vie', 'sáb']);
+    });
+    it('should return Spanish narrow names given a locale and mode', () => {
+        expect(getDaysOfWeek('es-ES', 'md')).toEqual(['D', 'L', 'M', 'X', 'J', 'V', 'S']);
+    });
+    it('should return English short names given a locale, mode and startOfWeek', () => {
+        expect(getDaysOfWeek('en-US', 'ios', 1)).toEqual(['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']);
+    });
+});
+describe('generateTime()', () => {
+    it('should not filter and hours/minutes when no bounds set', () => {
+        const today = {
+            day: 19,
+            month: 5,
+            year: 2021,
+            hour: 5,
+            minute: 43,
+        };
+        const { hours, minutes } = generateTime('en-US', today);
+        expect(hours.length).toEqual(12);
+        expect(minutes.length).toEqual(60);
+    });
+    it('should filter according to min', () => {
+        const today = {
+            day: 19,
+            month: 5,
+            year: 2021,
+            hour: 5,
+            minute: 43,
+        };
+        const min = {
+            day: 19,
+            month: 5,
+            year: 2021,
+            hour: 2,
+            minute: 40,
+        };
+        const { hours, minutes } = generateTime('en-US', today, 'h12', min);
+        expect(hours.length).toEqual(10);
+        expect(minutes.length).toEqual(60);
+    });
+    it('should not filter according to min if not on reference day', () => {
+        const today = {
+            day: 20,
+            month: 5,
+            year: 2021,
+            hour: 5,
+            minute: 43,
+        };
+        const min = {
+            day: 19,
+            month: 5,
+            year: 2021,
+            hour: 2,
+            minute: 40,
+        };
+        const { hours, minutes } = generateTime('en-US', today, 'h12', min);
+        expect(hours.length).toEqual(12);
+        expect(minutes.length).toEqual(60);
+    });
+    it('should filter according to max', () => {
+        const today = {
+            day: 19,
+            month: 5,
+            year: 2021,
+            hour: 7,
+            minute: 43,
+        };
+        const max = {
+            day: 19,
+            month: 5,
+            year: 2021,
+            hour: 7,
+            minute: 44,
+        };
+        const { hours, minutes } = generateTime('en-US', today, 'h12', undefined, max);
+        expect(hours.length).toEqual(8);
+        expect(minutes.length).toEqual(45);
+    });
+    it('should not filter according to min if not on reference day', () => {
+        const today = {
+            day: 20,
+            month: 5,
+            year: 2021,
+            hour: 5,
+            minute: 43,
+        };
+        const max = {
+            day: 21,
+            month: 5,
+            year: 2021,
+            hour: 2,
+            minute: 40,
+        };
+        const { hours, minutes } = generateTime('en-US', today, 'h12', undefined, max);
+        expect(hours.length).toEqual(12);
+        expect(minutes.length).toEqual(60);
+    });
+    it('should return no values for a day less than the min', () => {
+        const today = {
+            day: 20,
+            month: 5,
+            year: 2021,
+            hour: 5,
+            minute: 43,
+        };
+        const min = {
+            day: 21,
+            month: 5,
+            year: 2021,
+            hour: 2,
+            minute: 40,
+        };
+        const { hours, minutes } = generateTime('en-US', today, 'h12', min);
+        expect(hours.length).toEqual(0);
+        expect(minutes.length).toEqual(0);
+    });
+    it('should return no values for a day greater than the max', () => {
+        const today = {
+            day: 22,
+            month: 5,
+            year: 2021,
+            hour: 5,
+            minute: 43,
+        };
+        const max = {
+            day: 21,
+            month: 5,
+            year: 2021,
+            hour: 2,
+            minute: 40,
+        };
+        const { hours, minutes } = generateTime('en-US', today, 'h12', undefined, max);
+        expect(hours.length).toEqual(0);
+        expect(minutes.length).toEqual(0);
+    });
+    it('should allow all hours and minutes if not set in min/max', () => {
+        const today = {
+            day: 22,
+            month: 5,
+            year: 2021,
+            hour: 5,
+            minute: 43,
+        };
+        const min = {
+            day: 22,
+            month: 5,
+            year: 2021,
+        };
+        const max = {
+            day: 22,
+            month: 5,
+            year: 2021,
+        };
+        const { hours, minutes } = generateTime('en-US', today, 'h12', min, max);
+        expect(hours.length).toEqual(12);
+        expect(minutes.length).toEqual(60);
+    });
+    it('should allow certain hours and minutes based on minuteValues and hourValues', () => {
+        const today = {
+            day: 22,
+            month: 5,
+            year: 2021,
+            hour: 5,
+            minute: 43,
+        };
+        const { hours, minutes } = generateTime('en-US', today, 'h12', undefined, undefined, [1, 2, 3], [10, 15, 20]);
+        expect(hours).toStrictEqual([1, 2, 3]);
+        expect(minutes).toStrictEqual([10, 15, 20]);
+    });
+    it('should allow both am/pm when min is am and max is pm', () => {
+        // https://github.com/ionic-team/ionic-framework/issues/26216
+        const today = {
+            day: 22,
+            month: 5,
+            year: 2021,
+            hour: 5,
+            minute: 43,
+        };
+        const min = {
+            day: 22,
+            month: 5,
+            year: 2021,
+            hour: 11,
+            minute: 14,
+        };
+        const max = {
+            day: 22,
+            month: 5,
+            year: 2021,
+            hour: 12,
+            minute: 14,
+        };
+        const { am, pm } = generateTime('en-US', today, 'h12', min, max);
+        expect(am).toBe(true);
+        expect(pm).toBe(true);
+    });
+    describe('hourCycle is 23', () => {
+        it('should return hours in 24 hour format', () => {
+            const refValue = {
+                day: undefined,
+                month: undefined,
+                year: undefined,
+                hour: 19,
+                minute: 50,
+            };
+            const minParts = {
+                day: undefined,
+                month: undefined,
+                year: undefined,
+                hour: 19,
+                minute: 50,
+            };
+            const { hours } = generateTime('en-US', refValue, 'h23', minParts);
+            expect(hours).toStrictEqual([19, 20, 21, 22, 23]);
+        });
+        describe('current hour is above min hour range', () => {
+            it('should return minutes above the min minute range', () => {
+                const refValue = {
+                    day: undefined,
+                    month: undefined,
+                    year: undefined,
+                    hour: 20,
+                    minute: 22,
+                };
+                const minParts = {
+                    day: undefined,
+                    month: undefined,
+                    year: undefined,
+                    hour: 19,
+                    minute: 30,
+                };
+                const { hours, minutes } = generateTime('en-US', refValue, 'h23', minParts);
+                expect(hours).toStrictEqual([19, 20, 21, 22, 23]);
+                expect(minutes.length).toEqual(60);
+            });
+        });
+        it('should respect the min & max bounds', () => {
+            const refValue = {
+                day: undefined,
+                month: undefined,
+                year: undefined,
+                hour: 20,
+                minute: 30,
+            };
+            const minParts = {
+                day: undefined,
+                month: undefined,
+                year: undefined,
+                hour: 19,
+                minute: 30,
+            };
+            const maxParts = {
+                day: undefined,
+                month: undefined,
+                year: undefined,
+                hour: 20,
+                minute: 40,
+            };
+            const { hours } = generateTime('en-US', refValue, 'h23', minParts, maxParts);
+            expect(hours).toStrictEqual([19, 20]);
+        });
+        it('should return the filtered minutes when the max bound is set', () => {
+            const refValue = {
+                day: undefined,
+                month: undefined,
+                year: undefined,
+                hour: 13,
+                minute: 0,
+            };
+            const maxParts = {
+                day: undefined,
+                month: undefined,
+                year: undefined,
+                hour: 13,
+                minute: 2,
+            };
+            const { minutes } = generateTime('en-US', refValue, 'h23', undefined, maxParts);
+            expect(minutes).toStrictEqual([0, 1, 2]);
+        });
+        it('should not filter minutes when the current hour is less than the max hour bound', () => {
+            const refValue = {
+                day: undefined,
+                month: undefined,
+                year: undefined,
+                hour: 12,
+                minute: 0,
+            };
+            const maxParts = {
+                day: undefined,
+                month: undefined,
+                year: undefined,
+                hour: 13,
+                minute: 2,
+            };
+            const { minutes } = generateTime('en-US', refValue, 'h23', undefined, maxParts);
+            expect(minutes.length).toEqual(60);
+        });
+    });
+});
+describe('getToday', () => {
+    beforeAll(() => {
+        jest.useFakeTimers();
+        // System time is zero based, 1 = February
+        jest.setSystemTime(new Date(2022, 1, 21, 18, 30));
+    });
+    it('should return today without converting to UTC time', () => {
+        const res = getToday();
+        expect(res).toEqual('2022-02-21T18:30:00.000Z');
+    });
+});
+describe('getCombinedDateColumnData', () => {
+    it('should return correct data with dates across years', () => {
+        const { parts, items } = getCombinedDateColumnData('en-US', { day: 1, month: 1, year: 2021 }, { day: 31, month: 12, year: 2020 }, { day: 2, month: 1, year: 2021 });
+        expect(parts).toEqual([
+            { month: 12, year: 2020, day: 31 },
+            { month: 1, year: 2021, day: 1 },
+            { month: 1, year: 2021, day: 2 },
+        ]);
+        expect(items).toEqual([
+            { text: 'Thu, Dec 31', value: '2020-12-31' },
+            { text: 'Today', value: '2021-1-1' },
+            { text: 'Sat, Jan 2', value: '2021-1-2' },
+        ]);
+    });
+});

+ 39 - 0
src/node_modules/@ionic/core/dist/collection/components/datetime/test/datetime.e2e.js

@@ -0,0 +1,39 @@
+/*!
+ * (C) Ionic http://ionicframework.com - MIT License
+ */
+import { expect } from "@playwright/test";
+import { configs, test } from "../../../utils/test/playwright/index";
+configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
+    test.describe(title('datetime: switching months with different number of days'), () => {
+        test.beforeEach(async ({ page }) => {
+            await page.setContent(`
+        <ion-datetime locale="en-US" presentation="date" value="2022-01-31"></ion-datetime>
+      `, config);
+            await page.locator('.datetime-ready').waitFor();
+        });
+        test('should switch the calendar header when moving to a month with a different number of days', async ({ page, }) => {
+            const monthYearToggle = page.locator('ion-datetime .calendar-month-year');
+            const monthColumnItems = page.locator('ion-datetime .month-column ion-picker-column-option');
+            await expect(monthYearToggle).toContainText('January 2022');
+            await monthYearToggle.click();
+            await page.waitForChanges();
+            // February
+            await monthColumnItems.nth(1).click();
+            await page.waitForChanges();
+            await expect(monthYearToggle).toContainText('February 2022');
+        });
+        test('should adjust the selected day when moving to a month with a different number of days', async ({ page }) => {
+            const monthYearToggle = page.locator('ion-datetime .calendar-month-year');
+            const monthColumnItems = page.locator('ion-datetime .month-column ion-picker-column-option');
+            const datetime = page.locator('ion-datetime');
+            const ionChange = await page.spyOnEvent('ionChange');
+            await monthYearToggle.click();
+            await page.waitForChanges();
+            // February
+            await monthColumnItems.nth(1).click();
+            await ionChange.next();
+            await expect(ionChange).toHaveReceivedEventTimes(1);
+            await expect(datetime).toHaveJSProperty('value', '2022-02-28');
+        });
+    });
+});

+ 150 - 0
src/node_modules/@ionic/core/dist/collection/components/datetime/test/disable-dates/datetime.e2e.js

@@ -0,0 +1,150 @@
+/*!
+ * (C) Ionic http://ionicframework.com - MIT License
+ */
+import { expect } from "@playwright/test";
+import { configs, test } from "../../../../utils/test/playwright/index";
+const DISABLED_CALENDAR_DAY_SELECTOR = '.calendar-day[disabled]:not(.calendar-day-padding)';
+const queryAllDisabledDays = (page, datetimeSelector = 'ion-datetime') => {
+    return page.locator(`${datetimeSelector} ${DISABLED_CALENDAR_DAY_SELECTOR}`);
+};
+const queryAllWorkingMonthDisabledDays = (page, datetimeSelector = 'ion-datetime') => {
+    return page.locator(`${datetimeSelector} .calendar-month:nth-child(2) ${DISABLED_CALENDAR_DAY_SELECTOR}`);
+};
+/**
+ * This is testing component functionality
+ * that does not differ across modes/directions.
+ */
+configs({ directions: ['ltr'], modes: ['ios'] }).forEach(({ title, config }) => {
+    test.describe(title('datetime: disable dates'), () => {
+        test.describe('check return values', () => {
+            test.beforeEach(async ({ page }) => {
+                await page.setContent('<ion-datetime value="2021-10-01"></ion-datetime>', config);
+            });
+            test.describe('when isDateEnabled returns true', () => {
+                test('calendar days should be enabled', async ({ page }) => {
+                    const datetime = page.locator('ion-datetime');
+                    await datetime.evaluate((el) => (el.isDateEnabled = () => true));
+                    await page.waitForChanges();
+                    const disabledDays = queryAllDisabledDays(page);
+                    expect(await disabledDays.count()).toBe(0);
+                });
+            });
+            test.describe('when isDateEnabled returns false', () => {
+                test('calendar days should be disabled', async ({ page }) => {
+                    const datetime = page.locator('ion-datetime');
+                    await datetime.evaluate((el) => (el.isDateEnabled = () => false));
+                    await page.waitForChanges();
+                    const disabledDays = queryAllDisabledDays(page);
+                    expect(await disabledDays.count()).toBe(91);
+                });
+            });
+            test.describe('when isDateEnabled returns undefined', () => {
+                test('calendar days should be disabled', async ({ page }) => {
+                    const datetime = page.locator('ion-datetime');
+                    await datetime.evaluate((el) => {
+                        /**
+                         * isDateEnabled expects a boolean, but we need
+                         * to check what happens when users pass in unexpected
+                         * values which is why we do the ts-ignore.
+                         */
+                        // @ts-ignore
+                        el.isDateEnabled = () => {
+                            undefined;
+                        };
+                    });
+                    await page.waitForChanges();
+                    const disabledDays = queryAllDisabledDays(page);
+                    expect(await disabledDays.count()).toBe(91);
+                });
+            });
+            test.describe('when isDateEnabled returns null', () => {
+                test('calendar days should be disabled', async ({ page }) => {
+                    const datetime = page.locator('ion-datetime');
+                    await datetime.evaluate((el) => {
+                        /**
+                         * isDateEnabled expects a boolean, but we need
+                         * to check what happens when users pass in unexpected
+                         * values which is why we do the ts-ignore.
+                         */
+                        // @ts-ignore
+                        el.isDateEnabled = () => null;
+                    });
+                    await page.waitForChanges();
+                    const disabledDays = queryAllDisabledDays(page);
+                    expect(await disabledDays.count()).toBe(91);
+                });
+            });
+            test.describe('when isDateEnabled throws an exception', () => {
+                test.beforeEach(async ({ page }) => {
+                    const datetime = page.locator('ion-datetime');
+                    await datetime.evaluate((el) => {
+                        el.isDateEnabled = (dateIsoString) => {
+                            const date = new Date(dateIsoString);
+                            if (date.getUTCDate() === 10 && date.getUTCMonth() === 9 && date.getUTCFullYear() === 2021) {
+                                // Throws an exception on October 10, 2021
+                                // Expected behavior: the day should be enabled
+                                throw new Error('Expected exception for e2e test.');
+                            }
+                            return false;
+                        };
+                    });
+                });
+                test('calendar days should be enabled', async ({ page }) => {
+                    await page.waitForChanges();
+                    const enabledDays = page.locator('ion-datetime .calendar-month:nth-child(2) .calendar-day:not([disabled]):not(.calendar-day-padding)');
+                    expect(await enabledDays.count()).toBe(1);
+                });
+            });
+        });
+        test.describe('check example usages', () => {
+            test.beforeEach(async ({ page }) => {
+                await page.goto('/src/components/datetime/test/disable-dates', config);
+                await page.locator('.datetime-ready').first().waitFor();
+            });
+            test('should disable a specific date', async ({ page }) => {
+                const disabledDay = queryAllDisabledDays(page, '#specificDate');
+                await expect(disabledDay).toHaveText('10');
+            });
+            test('should disable specific days of the week', async ({ page }) => {
+                const disabledDays = queryAllWorkingMonthDisabledDays(page, '#weekends');
+                expect(await disabledDays.count()).toEqual(10);
+                await expect(disabledDays).toHaveText(['2', '3', '9', '10', '16', '17', '23', '24', '30', '31']);
+            });
+            test('should disable a range of dates', async ({ page }) => {
+                const disabledDays = queryAllDisabledDays(page, '#dateRange');
+                expect(await disabledDays.count()).toEqual(11);
+                await expect(disabledDays).toHaveText(['10', '11', '12', '13', '14', '15', '16', '17', '18', '19', '20']);
+            });
+            test('should disable a month', async ({ page }) => {
+                const disabledDays = queryAllDisabledDays(page, '#month');
+                expect(await disabledDays.count()).toBe(31);
+            });
+        });
+        test.describe('with a min date range', () => {
+            test('should not enable already disabled dates', async ({ page }) => {
+                await page.setContent(`
+          <ion-datetime min="2021-10-15" value="2021-10-16"></ion-datetime>
+          <script>
+            const datetime = document.querySelector('ion-datetime');
+            datetime.isDateEnabled = () => true;
+          </script>
+        `, config);
+                const disabledDays = queryAllWorkingMonthDisabledDays(page);
+                expect(await disabledDays.count()).toBe(14);
+            });
+        });
+        test.describe('with a max date range', () => {
+            test('should not enable already disabled dates', async ({ page }) => {
+                await page.setContent(`
+          <ion-datetime max="2021-10-15" value="2021-10-16"></ion-datetime>
+          <script>
+            const datetime = document.querySelector('ion-datetime');
+            datetime.isDateEnabled = () => true;
+          </script>
+        `, config);
+                const disabledDays = queryAllWorkingMonthDisabledDays(page);
+                expect(await disabledDays.count()).toBe(16);
+            });
+        });
+    });
+});

+ 68 - 0
src/node_modules/@ionic/core/dist/collection/components/datetime/test/disabled/datetime.e2e.js

@@ -0,0 +1,68 @@
+/*!
+ * (C) Ionic http://ionicframework.com - MIT License
+ */
+import { expect } from "@playwright/test";
+import { configs, test } from "../../../../utils/test/playwright/index";
+/**
+ * This behavior does not differ across
+ * modes/directions.
+ */
+configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config, screenshot }) => {
+    test.describe(title('datetime: disabled'), () => {
+        test('should not have visual regressions', async ({ page }) => {
+            await page.setContent(`
+        <ion-datetime value="2022-02-05T00:00:00" min="2022-01-01T00:00:00" max="2022-02-20T23:59:59" day-values="5,6,10,11,15,16,20" show-default-buttons disabled></ion-datetime>
+    `, config);
+            const datetime = page.locator('ion-datetime');
+            await expect(datetime).toHaveScreenshot(screenshot(`datetime-disabled`));
+        });
+        test('date should be disabled', async ({ page }) => {
+            await page.setContent(`
+        <ion-datetime value="2022-02-28" disabled></ion-datetime>
+    `, config);
+            await page.locator('.datetime-ready').waitFor();
+            const febFirstButton = page.locator(`.calendar-day[data-day='1'][data-month='2']`);
+            await expect(febFirstButton).toBeDisabled();
+        });
+        test('month-year button should be disabled', async ({ page }) => {
+            await page.setContent(`
+        <ion-datetime value="2022-02-28" disabled></ion-datetime>
+    `, config);
+            await page.locator('.datetime-ready').waitFor();
+            const calendarMonthYear = page.locator('ion-datetime .calendar-month-year');
+            await expect(calendarMonthYear.locator('button')).toBeDisabled();
+        });
+        test('next and prev buttons should be disabled', async ({ page }) => {
+            await page.setContent(`
+        <ion-datetime value="2022-02-28" disabled></ion-datetime>
+    `, config);
+            const prevMonthButton = page.locator('ion-datetime .calendar-next-prev ion-button:first-of-type button');
+            const nextMonthButton = page.locator('ion-datetime .calendar-next-prev ion-button:last-of-type button');
+            await expect(prevMonthButton).toBeDisabled();
+            await expect(nextMonthButton).toBeDisabled();
+        });
+        test('clear button should be disabled', async ({ page }) => {
+            await page.setContent(`
+
+        <ion-datetime value="2022-02-22T16:30:00" show-default-buttons="true" show-clear-button="true" disabled></ion-datetime>
+    `, config);
+            await page.locator('.datetime-ready').waitFor();
+            const clearButton = page.locator('#clear-button button');
+            await expect(clearButton).toBeDisabled();
+        });
+        test('should not navigate through months via right arrow key', async ({ page }) => {
+            await page.setContent(`
+        <ion-datetime value="2022-02-28" disabled></ion-datetime>
+    `, config);
+            await page.locator('.datetime-ready').waitFor();
+            const calendarMonthYear = page.locator('ion-datetime .calendar-month-year');
+            const calendarBody = page.locator('.calendar-body');
+            await expect(calendarMonthYear).toHaveText('February 2022');
+            await calendarBody.focus();
+            await page.waitForChanges();
+            await page.keyboard.press('ArrowRight');
+            await page.waitForChanges();
+            await expect(calendarMonthYear).toHaveText('February 2022');
+        });
+    });
+});

+ 33 - 0
src/node_modules/@ionic/core/dist/collection/components/datetime/test/disabled/datetime.spec.js

@@ -0,0 +1,33 @@
+/*!
+ * (C) Ionic http://ionicframework.com - MIT License
+ */
+import { h } from "@stencil/core";
+import { newSpecPage } from "@stencil/core/testing";
+import { Datetime } from "../../../datetime/datetime";
+import { PickerColumn } from "../../../picker-column/picker-column";
+import { Picker } from "../../../picker/picker";
+describe('ion-datetime disabled', () => {
+    beforeEach(() => {
+        // IntersectionObserver isn't available in test environment
+        const mockIntersectionObserver = jest.fn();
+        mockIntersectionObserver.mockReturnValue({
+            observe: () => null,
+            unobserve: () => null,
+            disconnect: () => null,
+        });
+        global.IntersectionObserver = mockIntersectionObserver;
+    });
+    it('picker should be disabled in prefer wheel mode', async () => {
+        const page = await newSpecPage({
+            components: [Datetime, PickerColumn, Picker],
+            template: () => (h("ion-datetime", { id: "inline-datetime-wheel", disabled: true, "prefer-wheel": true, value: "2022-04-21T00:00:00" })),
+        });
+        await page.waitForChanges();
+        const datetime = page.body.querySelector('ion-datetime');
+        const columns = datetime.shadowRoot.querySelectorAll('ion-picker-column');
+        await expect(columns.length).toEqual(4);
+        columns.forEach((column) => {
+            expect(column.disabled).toBe(true);
+        });
+    });
+});

+ 121 - 0
src/node_modules/@ionic/core/dist/collection/components/datetime/test/display/datetime.e2e.js

@@ -0,0 +1,121 @@
+/*!
+ * (C) Ionic http://ionicframework.com - MIT License
+ */
+import { expect } from "@playwright/test";
+import { configs, test } from "../../../../utils/test/playwright/index";
+/**
+ * This behavior does not vary across directions
+ * since it is texting fixed vs fluid widths.
+ */
+configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
+    test.describe(title('datetime: rendering'), () => {
+        test.describe('fixed sizes', () => {
+            test('date-time should not have any visual regressions', async ({ page }) => {
+                await page.setContent(`
+          <ion-datetime value="2022-02-22T16:30:00" presentation="date-time"></ion-datetime>
+        `, config);
+                await page.locator('.datetime-ready').waitFor();
+                const datetime = page.locator('ion-datetime');
+                await expect(datetime).toHaveScreenshot(screenshot(`datetime-display-date-time`));
+            });
+            test('time-date should not have any visual regressions', async ({ page }) => {
+                await page.setContent(`
+          <ion-datetime value="2022-02-22T16:30:00" presentation="time-date"></ion-datetime>
+        `, config);
+                await page.locator('.datetime-ready').waitFor();
+                const datetime = page.locator('ion-datetime');
+                await expect(datetime).toHaveScreenshot(screenshot(`datetime-display-time-date`));
+            });
+            test('time should not have any visual regressions', async ({ page }) => {
+                await page.setContent(`
+          <ion-datetime value="2022-02-22T16:30:00" presentation="time"></ion-datetime>
+        `, config);
+                await page.locator('.datetime-ready').waitFor();
+                const datetime = page.locator('ion-datetime');
+                await expect(datetime).toHaveScreenshot(screenshot(`datetime-display-time`));
+            });
+            test('date should not have any visual regressions', async ({ page }) => {
+                await page.setContent(`
+          <ion-datetime value="2022-02-22T16:30:00" presentation="date"></ion-datetime>
+        `, config);
+                await page.locator('.datetime-ready').waitFor();
+                const datetime = page.locator('ion-datetime');
+                await expect(datetime).toHaveScreenshot(screenshot(`datetime-display-date`));
+            });
+        });
+        test.describe('cover sizes', () => {
+            test.beforeEach(async ({ page }) => {
+                /**
+                 * We need to take a screenshot of the entire page
+                 * here as we want to test that the datetime fills
+                 * the entire screen.
+                 */
+                await page.setViewportSize({ width: 500, height: 500 });
+            });
+            test('date-time should not have any visual regressions', async ({ page }) => {
+                await page.setContent(`
+          <ion-datetime size="cover" value="2022-02-22T16:30:00" presentation="date-time"></ion-datetime>
+        `, config);
+                await page.locator('.datetime-ready').waitFor();
+                const datetime = page.locator('ion-datetime');
+                await expect(datetime).toHaveScreenshot(screenshot(`datetime-display-cover-date-time`));
+            });
+            test('time-date should not have any visual regressions', async ({ page }) => {
+                await page.setContent(`
+          <ion-datetime size="cover" value="2022-02-22T16:30:00" presentation="time-date"></ion-datetime>
+        `, config);
+                await page.locator('.datetime-ready').waitFor();
+                const datetime = page.locator('ion-datetime');
+                await expect(datetime).toHaveScreenshot(screenshot(`datetime-display-cover-time-date`));
+            });
+            test('time should not have any visual regressions', async ({ page }) => {
+                await page.setContent(`
+          <ion-datetime size="cover" value="2022-02-22T16:30:00" presentation="time"></ion-datetime>
+        `, config);
+                await page.locator('.datetime-ready').waitFor();
+                const datetime = page.locator('ion-datetime');
+                await expect(datetime).toHaveScreenshot(screenshot(`datetime-display-cover-time`));
+            });
+            test('date should not have any visual regressions', async ({ page }) => {
+                await page.setContent(`
+          <ion-datetime size="cover" value="2022-02-22T16:30:00" presentation="date"></ion-datetime>
+        `, config);
+                await page.locator('.datetime-ready').waitFor();
+                const datetime = page.locator('ion-datetime');
+                await expect(datetime).toHaveScreenshot(screenshot(`datetime-display-cover-date`));
+            });
+        });
+    });
+});
+/**
+ * This is testing functionality
+ * and does not vary across modes/directions.
+ */
+configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => {
+    test.describe(title('datetime: switch presentations'), () => {
+        test('month selection should work after changing presentation', async ({ page }) => {
+            await page.goto('/src/components/datetime/test/display', config);
+            const ionWorkingPartsDidChange = await page.spyOnEvent('ionWorkingPartsDidChange');
+            await page.locator('.datetime-ready').waitFor();
+            const select = page.locator('select#presentation');
+            await select.selectOption('date-time');
+            await page.waitForChanges();
+            await select.selectOption('time-date');
+            await page.waitForChanges();
+            const nextMonthButton = page.locator('ion-datetime .calendar-next-prev ion-button + ion-button');
+            await nextMonthButton.click();
+            await page.waitForChanges();
+            await ionWorkingPartsDidChange.next();
+            const calendarMonthYear = page.locator('ion-datetime .calendar-month-year');
+            await expect(calendarMonthYear).toHaveText(/March 2022/);
+            // ensure it still works if presentation is changed more than once
+            await select.selectOption('date-time');
+            await page.waitForChanges();
+            const prevMonthButton = page.locator('ion-datetime .calendar-next-prev ion-button:first-child');
+            await prevMonthButton.click();
+            await page.waitForChanges();
+            await ionWorkingPartsDidChange.next();
+            await expect(calendarMonthYear).toHaveText(/February 2022/);
+        });
+    });
+});

+ 14 - 0
src/node_modules/@ionic/core/dist/collection/components/datetime/test/first-day-of-week/datetime.e2e.js

@@ -0,0 +1,14 @@
+/*!
+ * (C) Ionic http://ionicframework.com - MIT License
+ */
+import { expect } from "@playwright/test";
+import { configs, test } from "../../../../utils/test/playwright/index";
+configs().forEach(({ title, screenshot, config }) => {
+    test.describe(title('datetime: first day of the week'), () => {
+        test('should set the first day of the week correctly', async ({ page }) => {
+            await page.goto('/src/components/datetime/test/first-day-of-week', config);
+            const datetime = page.locator('ion-datetime');
+            await expect(datetime).toHaveScreenshot(screenshot(`datetime-day-of-week`));
+        });
+    });
+});

+ 174 - 0
src/node_modules/@ionic/core/dist/collection/components/datetime/test/format.spec.js

@@ -0,0 +1,174 @@
+/*!
+ * (C) Ionic http://ionicframework.com - MIT License
+ */
+import { generateDayAriaLabel, getFormattedHour, addTimePadding, getMonthAndYear, getLocalizedDayPeriod, getLocalizedTime, stripTimeZone, } from "../utils/format";
+describe('generateDayAriaLabel()', () => {
+    it('should return Wednesday, May 12', () => {
+        const reference = { month: 5, day: 12, year: 2021 };
+        expect(generateDayAriaLabel('en-US', false, reference)).toEqual('Wednesday, May 12');
+    });
+    it('should return Today, Wednesday, May 12', () => {
+        const reference = { month: 5, day: 12, year: 2021 };
+        expect(generateDayAriaLabel('en-US', true, reference)).toEqual('Today, Wednesday, May 12');
+    });
+    it('should return Saturday, May 1', () => {
+        const reference = { month: 5, day: 1, year: 2021 };
+        expect(generateDayAriaLabel('en-US', false, reference)).toEqual('Saturday, May 1');
+    });
+    it('should return Monday, May 31', () => {
+        const reference = { month: 5, day: 31, year: 2021 };
+        expect(generateDayAriaLabel('en-US', false, reference)).toEqual('Monday, May 31');
+    });
+    it('should return Saturday, April 1', () => {
+        const reference = { month: 4, day: 1, year: 2006 };
+        expect(generateDayAriaLabel('en-US', false, reference)).toEqual('Saturday, April 1');
+    });
+});
+describe('getFormattedHour()', () => {
+    it('should only add padding if using 24 hour time', () => {
+        expect(getFormattedHour(1, 'h11')).toEqual('1');
+        expect(getFormattedHour(1, 'h12')).toEqual('1');
+        expect(getFormattedHour(1, 'h23')).toEqual('01');
+        expect(getFormattedHour(1, 'h24')).toEqual('01');
+    });
+    it('should return correct hour value for hour cycle', () => {
+        expect(getFormattedHour(0, 'h11')).toEqual('0');
+        expect(getFormattedHour(0, 'h12')).toEqual('12');
+        expect(getFormattedHour(0, 'h23')).toEqual('00');
+        expect(getFormattedHour(0, 'h24')).toEqual('24');
+    });
+});
+describe('addTimePadding()', () => {
+    it('should add correct amount of padding', () => {
+        expect(addTimePadding(0)).toEqual('00');
+        expect(addTimePadding(9)).toEqual('09');
+        expect(addTimePadding(10)).toEqual('10');
+        expect(addTimePadding(100)).toEqual('100');
+    });
+});
+describe('getMonthAndYear()', () => {
+    it('should return May 2021', () => {
+        expect(getMonthAndYear('en-US', { month: 5, day: 11, year: 2021 })).toEqual('May 2021');
+    });
+    it('should return mayo de 2021', () => {
+        expect(getMonthAndYear('es-ES', { month: 5, day: 11, year: 2021 })).toEqual('mayo de 2021');
+    });
+    it('should return April 2006', () => {
+        expect(getMonthAndYear('en-US', { month: 4, day: 1, year: 2006 })).toEqual('April 2006');
+    });
+    it('should return abril de 2006', () => {
+        expect(getMonthAndYear('es-ES', { month: 4, day: 1, year: 2006 })).toEqual('abril de 2006');
+    });
+});
+describe('getLocalizedDayPeriod', () => {
+    it('should return AM when the date is in the morning', () => {
+        expect(getLocalizedDayPeriod('en-US', 'am'));
+    });
+    it('should return PM when the date is in the afternoon', () => {
+        expect(getLocalizedDayPeriod('en-US', 'pm'));
+    });
+});
+describe('getLocalizedTime', () => {
+    it('should localize the time to PM', () => {
+        const datetimeParts = {
+            day: 1,
+            month: 1,
+            year: 2022,
+            hour: 13,
+            minute: 40,
+        };
+        expect(getLocalizedTime('en-US', datetimeParts, 'h12')).toEqual('1:40 PM');
+    });
+    it('should localize the time to AM', () => {
+        const datetimeParts = {
+            day: 1,
+            month: 1,
+            year: 2022,
+            hour: 9,
+            minute: 40,
+        };
+        expect(getLocalizedTime('en-US', datetimeParts, 'h12')).toEqual('9:40 AM');
+    });
+    it('should avoid Chromium bug when using 12 hour time in a 24 hour locale', () => {
+        const datetimeParts = {
+            day: 1,
+            month: 1,
+            year: 2022,
+            hour: 0,
+            minute: 0,
+        };
+        expect(getLocalizedTime('en-GB', datetimeParts, 'h12')).toEqual('12:00 am');
+    });
+    it('should parse time-only values correctly', () => {
+        const datetimeParts = {
+            hour: 22,
+            minute: 40,
+        };
+        expect(getLocalizedTime('en-US', datetimeParts, 'h12')).toEqual('10:40 PM');
+        expect(getLocalizedTime('en-US', datetimeParts, 'h23')).toEqual('22:40');
+    });
+    it('should use formatOptions', () => {
+        const datetimeParts = {
+            day: 1,
+            month: 1,
+            year: 2022,
+            hour: 9,
+            minute: 40,
+        };
+        const formatOptions = {
+            hour: '2-digit',
+            minute: '2-digit',
+            dayPeriod: 'short',
+            day: '2-digit',
+        };
+        // Even though this method is intended to be used for time, the date may be displayed as well when passing formatOptions
+        expect(getLocalizedTime('en-US', datetimeParts, 'h12', formatOptions)).toEqual('01, 09:40 in the morning');
+    });
+    it('should override provided time zone with UTC', () => {
+        const datetimeParts = {
+            day: 1,
+            month: 1,
+            year: 2022,
+            hour: 9,
+            minute: 40,
+        };
+        const formatOptions = {
+            timeZone: 'Australia/Sydney',
+            timeZoneName: 'long',
+            hour: 'numeric',
+            minute: 'numeric',
+        };
+        expect(getLocalizedTime('en-US', datetimeParts, 'h12', formatOptions)).toEqual('9:40 AM');
+    });
+    it('should not include time zone name', () => {
+        const datetimeParts = {
+            day: 1,
+            month: 1,
+            year: 2022,
+            hour: 9,
+            minute: 40,
+        };
+        const formatOptions = {
+            timeZone: 'America/Los_Angeles',
+            timeZoneName: 'long',
+            hour: 'numeric',
+            minute: 'numeric',
+        };
+        expect(getLocalizedTime('en-US', datetimeParts, 'h12', formatOptions)).toEqual('9:40 AM');
+    });
+});
+describe('stripTimeZone', () => {
+    it('should remove the time zone name from the options and set the time zone to UTC', () => {
+        const formatOptions = {
+            timeZone: 'America/Los_Angeles',
+            timeZoneName: 'long',
+            hour: 'numeric',
+            minute: 'numeric',
+        };
+        expect(stripTimeZone(formatOptions)).toEqual({
+            timeZone: 'UTC',
+            hour: 'numeric',
+            minute: 'numeric',
+        });
+    });
+});

+ 72 - 0
src/node_modules/@ionic/core/dist/collection/components/datetime/test/helpers.spec.js

@@ -0,0 +1,72 @@
+/*!
+ * (C) Ionic http://ionicframework.com - MIT License
+ */
+import { isLeapYear, getNumDaysInMonth, is24Hour, isMonthFirstLocale, getHourCycle } from "../utils/helpers";
+describe('daysInMonth()', () => {
+    it('should return correct days in month for month and year', () => {
+        expect(getNumDaysInMonth(1, 2019)).toBe(31);
+        expect(getNumDaysInMonth(2, 2019)).toBe(28);
+        expect(getNumDaysInMonth(3, 2019)).toBe(31);
+        expect(getNumDaysInMonth(4, 2019)).toBe(30);
+        expect(getNumDaysInMonth(5, 2019)).toBe(31);
+        expect(getNumDaysInMonth(6, 2019)).toBe(30);
+        expect(getNumDaysInMonth(7, 2019)).toBe(31);
+        expect(getNumDaysInMonth(8, 2019)).toBe(31);
+        expect(getNumDaysInMonth(9, 2019)).toBe(30);
+        expect(getNumDaysInMonth(10, 2019)).toBe(31);
+        expect(getNumDaysInMonth(11, 2019)).toBe(30);
+        expect(getNumDaysInMonth(12, 2019)).toBe(31);
+        expect(getNumDaysInMonth(2, 2020)).toBe(29);
+        expect(getNumDaysInMonth(2, 2021)).toBe(28);
+        expect(getNumDaysInMonth(2, 1900)).toBe(28);
+        expect(getNumDaysInMonth(2, 1800)).toBe(28);
+        expect(getNumDaysInMonth(2, 2400)).toBe(29);
+    });
+});
+describe('isLeapYear()', () => {
+    it('should return true if year is leapyear', () => {
+        expect(isLeapYear(2096)).toBe(true);
+        expect(isLeapYear(2021)).toBe(false);
+        expect(isLeapYear(2012)).toBe(true);
+        expect(isLeapYear(2000)).toBe(true);
+        expect(isLeapYear(1900)).toBe(false);
+        expect(isLeapYear(1800)).toBe(false);
+    });
+});
+describe('is24Hour()', () => {
+    it('should return true if the locale uses 24 hour time', () => {
+        expect(is24Hour('h11')).toBe(false);
+        expect(is24Hour('h12')).toBe(false);
+        expect(is24Hour('h23')).toBe(true);
+        expect(is24Hour('h24')).toBe(true);
+    });
+});
+describe('getHourCycle()', () => {
+    it('should return the correct hour cycle', () => {
+        expect(getHourCycle('en-US')).toBe('h12');
+        expect(getHourCycle('en-US', 'h23')).toBe('h23');
+        expect(getHourCycle('en-US', 'h12')).toBe('h12');
+        expect(getHourCycle('en-US-u-hc-h23')).toBe('h23');
+        expect(getHourCycle('en-GB')).toBe('h23');
+        expect(getHourCycle('en-GB', 'h23')).toBe('h23');
+        expect(getHourCycle('en-GB', 'h12')).toBe('h12');
+        expect(getHourCycle('en-GB-u-hc-h12')).toBe('h12');
+        expect(getHourCycle('en-GB', 'h11')).toBe('h11');
+        expect(getHourCycle('en-GB-u-hc-h11')).toBe('h11');
+        expect(getHourCycle('en-GB', 'h24')).toBe('h24');
+        expect(getHourCycle('en-GB-u-hc-h24')).toBe('h24');
+    });
+});
+describe('isMonthFirstLocale()', () => {
+    it('should return true if the locale shows months first', () => {
+        expect(isMonthFirstLocale('en-US')).toBe(true);
+        expect(isMonthFirstLocale('en-GB')).toBe(true);
+        expect(isMonthFirstLocale('es-ES')).toBe(true);
+        expect(isMonthFirstLocale('ro-RO')).toBe(true);
+    });
+    it('should return false if the locale shows years first', () => {
+        expect(isMonthFirstLocale('zh-CN')).toBe(false);
+        expect(isMonthFirstLocale('ja-JP')).toBe(false);
+        expect(isMonthFirstLocale('ko-KR')).toBe(false);
+    });
+});

+ 86 - 0
src/node_modules/@ionic/core/dist/collection/components/datetime/test/highlighted-dates/datetime.e2e.js

@@ -0,0 +1,86 @@
+/*!
+ * (C) Ionic http://ionicframework.com - MIT License
+ */
+import { expect } from "@playwright/test";
+import { configs, test } from "../../../../utils/test/playwright/index";
+configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
+    test.describe(title('datetime: highlightedDates'), () => {
+        test.beforeEach(async ({ page }) => {
+            await page.setContent(`
+        <ion-datetime value="2023-01-01" locale="en-US"></ion-datetime>
+      `, config);
+        });
+        test('should render highlights correctly when using an array', async ({ page }) => {
+            const datetime = page.locator('ion-datetime');
+            await datetime.evaluate((el) => {
+                el.highlightedDates = [
+                    {
+                        date: '2023-01-01', // ensure selected date style overrides highlight
+                        textColor: '#800080',
+                        backgroundColor: '#ffc0cb',
+                    },
+                    {
+                        date: '2023-01-02',
+                        textColor: '#b22222',
+                        backgroundColor: '#fa8072',
+                    },
+                    {
+                        date: '2023-01-03',
+                        textColor: '#0000ff',
+                        backgroundColor: '#add8e6',
+                    },
+                ];
+            });
+            await page.waitForChanges();
+            await expect(datetime).toHaveScreenshot(screenshot(`datetime-highlightedDates-array`));
+        });
+        test('should render highlights correctly when using a callback', async ({ page }) => {
+            const datetime = page.locator('ion-datetime');
+            await datetime.evaluate((el) => {
+                el.highlightedDates = (isoString) => {
+                    const date = new Date(isoString);
+                    const utcDay = date.getUTCDate();
+                    // ensure selected date style overrides highlight
+                    if (utcDay === 1) {
+                        return {
+                            textColor: '#b22222',
+                            backgroundColor: '#fa8072',
+                        };
+                    }
+                    if (utcDay % 5 === 0) {
+                        return {
+                            textColor: '#800080',
+                            backgroundColor: '#ffc0cb',
+                        };
+                    }
+                    if (utcDay % 3 === 0) {
+                        return {
+                            textColor: '#0000ff',
+                            backgroundColor: '#add8e6',
+                        };
+                    }
+                    return undefined;
+                };
+            });
+            await page.waitForChanges();
+            await expect(datetime).toHaveScreenshot(screenshot(`datetime-highlightedDates-callback`));
+        });
+        test('should render highlights correctly when only using one color or the other', async ({ page }) => {
+            const datetime = page.locator('ion-datetime');
+            await datetime.evaluate((el) => {
+                el.highlightedDates = [
+                    {
+                        date: '2023-01-02',
+                        backgroundColor: '#fa8072',
+                    },
+                    {
+                        date: '2023-01-03',
+                        textColor: '#0000ff',
+                    },
+                ];
+            });
+            await page.waitForChanges();
+            await expect(datetime).toHaveScreenshot(screenshot(`datetime-highlightedDates-single-color`));
+        });
+    });
+});

+ 37 - 0
src/node_modules/@ionic/core/dist/collection/components/datetime/test/hour-cycle/datetime.e2e.js

@@ -0,0 +1,37 @@
+/*!
+ * (C) Ionic http://ionicframework.com - MIT License
+ */
+import { expect } from "@playwright/test";
+import { configs, test } from "../../../../utils/test/playwright/index";
+configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => {
+    test.describe(title('datetime: hour cycle'), () => {
+        test('should set the h23 hour cycle correctly', async ({ page }) => {
+            await page.setContent(`
+        <ion-datetime hour-cycle="h23" value="2022-01-01T16:30:00"></ion-datetime>
+      `, config);
+            const timeButton = page.locator('ion-datetime .time-body');
+            await expect(timeButton).toHaveText('16:30');
+        });
+        test('should set the h12 hour cycle correctly', async ({ page }) => {
+            await page.setContent(`
+        <ion-datetime hour-cycle="h12" value="2022-01-01T16:30:00"></ion-datetime>
+      `, config);
+            const timeButton = page.locator('ion-datetime .time-body');
+            await expect(timeButton).toHaveText('4:30 PM');
+        });
+        test('should set the h11 hour cycle correctly', async ({ page }) => {
+            await page.setContent(`
+        <ion-datetime hour-cycle="h11" value="2022-01-01T00:30:00"></ion-datetime>
+      `, config);
+            const timeButton = page.locator('ion-datetime .time-body');
+            await expect(timeButton).toHaveText('0:30 AM');
+        });
+        test('should set the h24 hour cycle correctly', async ({ page }) => {
+            await page.setContent(`
+        <ion-datetime hour-cycle="h24" value="2022-01-01T00:30:00"></ion-datetime>
+      `, config);
+            const timeButton = page.locator('ion-datetime .time-body');
+            await expect(timeButton).toHaveText('24:30');
+        });
+    });
+});

+ 129 - 0
src/node_modules/@ionic/core/dist/collection/components/datetime/test/locale/datetime.e2e.js

@@ -0,0 +1,129 @@
+/*!
+ * (C) Ionic http://ionicframework.com - MIT License
+ */
+import { expect } from "@playwright/test";
+import { configs, test } from "../../../../utils/test/playwright/index";
+/**
+ * This is testing text content of the
+ * datetime and not layout, so we skip
+ * direction tests.
+ */
+configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
+    test.describe(title('datetime: locale'), () => {
+        let datetimeFixture;
+        test.beforeEach(async ({ page }) => {
+            datetimeFixture = new DatetimeLocaleFixture(page);
+        });
+        test.describe('en-US', () => {
+            test('should not have visual regressions', async () => {
+                await datetimeFixture.goto(config, 'en-US', 'date');
+                await datetimeFixture.expectLocalizedDatePicker(screenshot);
+            });
+            test('month/year picker should not have visual regressions', async () => {
+                await datetimeFixture.goto(config, 'en-US', 'month-year');
+                await datetimeFixture.expectLocalizedMonthYearPicker(screenshot);
+            });
+            test('time picker should not have visual regressions', async () => {
+                await datetimeFixture.goto(config, 'en-US', 'time');
+                await datetimeFixture.expectLocalizedTimePicker(screenshot);
+            });
+        });
+        test.describe('ja-JP', () => {
+            test('should not have visual regressions', async () => {
+                await datetimeFixture.goto(config, 'ja-JP', 'date');
+                await datetimeFixture.expectLocalizedDatePicker(screenshot);
+            });
+            test('month/year picker should not have visual regressions', async () => {
+                await datetimeFixture.goto(config, 'ja-JP', 'month-year');
+                await datetimeFixture.expectLocalizedMonthYearPicker(screenshot);
+            });
+            test('time picker should not have visual regressions', async () => {
+                await datetimeFixture.goto(config, 'ja-JP', 'time');
+                await datetimeFixture.expectLocalizedTimePicker(screenshot);
+            });
+            test('should correctly localize calendar day buttons without literal', async ({ page }) => {
+                await datetimeFixture.goto(config, 'ja-JP', 'date');
+                const datetimeButtons = page.locator('ion-datetime .calendar-day:not([disabled])');
+                /**
+                 * Note: The Intl.DateTimeFormat typically adds literals
+                 * for certain languages. For Japanese, that could look
+                 * something like "29日". However, we only want the "29"
+                 * to be shown.
+                 */
+                await expect(datetimeButtons.nth(0)).toHaveText('1');
+                await expect(datetimeButtons.nth(1)).toHaveText('2');
+                await expect(datetimeButtons.nth(2)).toHaveText('3');
+            });
+        });
+        test.describe('es-ES', () => {
+            test('should not have visual regressions', async () => {
+                await datetimeFixture.goto(config, 'es-ES', 'date');
+                await datetimeFixture.expectLocalizedDatePicker(screenshot);
+            });
+            test('month/year picker should not have visual regressions', async () => {
+                await datetimeFixture.goto(config, 'es-ES', 'month-year');
+                await datetimeFixture.expectLocalizedMonthYearPicker(screenshot);
+            });
+            test('time picker should not have visual regressions', async () => {
+                await datetimeFixture.goto(config, 'es-ES', 'time');
+                await datetimeFixture.expectLocalizedTimePicker(screenshot);
+            });
+        });
+    });
+});
+configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => {
+    test.describe(title('ar-EG'), () => {
+        test('should correctly localize calendar day buttons', async ({ page }) => {
+            await page.setContent(`
+        <ion-datetime locale="ar-EG" presentation="date" value="2022-01-01"></ion-datetime>
+      `, config);
+            await page.locator('.datetime-ready').waitFor();
+            const datetimeButtons = page.locator('ion-datetime .calendar-day:not([disabled])');
+            await expect(datetimeButtons.nth(0)).toHaveText('١');
+            await expect(datetimeButtons.nth(1)).toHaveText('٢');
+            await expect(datetimeButtons.nth(2)).toHaveText('٣');
+        });
+        test('should correctly localize year column data', async ({ page }) => {
+            await page.setContent(`
+        <ion-datetime prefer-wheel="true" locale="ar-EG" presentation="date" value="2022-01-01" max="2022" min="2022"></ion-datetime>
+      `, config);
+            await page.locator('.datetime-ready').waitFor();
+            const datetimeYear = page.locator('ion-datetime .year-column ion-picker-column-option').nth(0);
+            await expect(datetimeYear).toHaveText('٢٠٢٢');
+        });
+    });
+});
+class DatetimeLocaleFixture {
+    constructor(page) {
+        this.locale = 'en-US';
+        this.page = page;
+    }
+    async goto(config, locale = 'en-US', presentation = 'date') {
+        this.locale = locale;
+        await this.page.setContent(`
+      <ion-datetime
+        show-default-title="true"
+        show-default-buttons="true"
+        locale="${this.locale}"
+        presentation="${presentation}"
+        value="2022-04-19T04:20:00"
+        max="2022"
+      ></ion-datetime>
+    `, config);
+        this.datetime = this.page.locator('ion-datetime');
+        await this.page.locator('.datetime-ready').waitFor();
+    }
+    async expectLocalizedDatePicker(screenshot) {
+        await this.expectLocalizedPicker(screenshot);
+    }
+    async expectLocalizedMonthYearPicker(screenshot) {
+        await this.expectLocalizedPicker(screenshot, 'month-year');
+    }
+    async expectLocalizedTimePicker(screenshot) {
+        await this.expectLocalizedPicker(screenshot, 'time');
+    }
+    async expectLocalizedPicker(screenshot, modifier) {
+        const modifierString = modifier === undefined ? '' : `-${modifier}`;
+        await expect(this.datetime).toHaveScreenshot(screenshot(`datetime-locale-${this.locale}${modifierString}-diff`));
+    }
+}

+ 565 - 0
src/node_modules/@ionic/core/dist/collection/components/datetime/test/manipulation.spec.js

@@ -0,0 +1,565 @@
+/*!
+ * (C) Ionic http://ionicframework.com - MIT License
+ */
+import { getPreviousYear, getNextYear, getPreviousMonth, getNextMonth, getPreviousDay, getNextDay, getPreviousWeek, getNextWeek, getEndOfWeek, getStartOfWeek, convert12HourTo24Hour, getInternalHourValue, calculateHourFromAMPM, subtractDays, addDays, validateParts, getClosestValidDate, } from "../utils/manipulation";
+describe('addDays()', () => {
+    it('should correctly add days', () => {
+        expect(addDays({
+            day: 1,
+            month: 1,
+            year: 2021,
+        }, 31)).toEqual({
+            day: 1,
+            month: 2,
+            year: 2021,
+        });
+        expect(addDays({
+            day: 31,
+            month: 12,
+            year: 2021,
+        }, 1)).toEqual({
+            day: 1,
+            month: 1,
+            year: 2022,
+        });
+    });
+});
+describe('subtractDays()', () => {
+    it('should correctly subtract days', () => {
+        expect(subtractDays({
+            day: 1,
+            month: 1,
+            year: 2021,
+        }, 1)).toEqual({
+            day: 31,
+            month: 12,
+            year: 2020,
+        });
+        expect(subtractDays({
+            day: 1,
+            month: 2,
+            year: 2021,
+        }, 31)).toEqual({
+            day: 1,
+            month: 1,
+            year: 2021,
+        });
+    });
+});
+describe('getInternalHourValue()', () => {
+    it('should correctly get the internal hour value', () => {
+        expect(getInternalHourValue(12, true)).toEqual(12);
+        expect(getInternalHourValue(12, true)).toEqual(12);
+        expect(getInternalHourValue(12, false, 'am')).toEqual(0);
+        expect(getInternalHourValue(12, false, 'pm')).toEqual(12);
+        expect(getInternalHourValue(1, true)).toEqual(1);
+        expect(getInternalHourValue(1, true)).toEqual(1);
+        expect(getInternalHourValue(1, false, 'am')).toEqual(1);
+        expect(getInternalHourValue(1, false, 'pm')).toEqual(13);
+    });
+});
+describe('calculateHourFromAMPM()', () => {
+    it('should correctly convert from AM to PM', () => {
+        expect(calculateHourFromAMPM({ hour: 12, ampm: 'am' }, 'pm')).toEqual(12);
+        expect(calculateHourFromAMPM({ hour: 1, ampm: 'am' }, 'pm')).toEqual(13);
+        expect(calculateHourFromAMPM({ hour: 2, ampm: 'am' }, 'pm')).toEqual(14);
+        expect(calculateHourFromAMPM({ hour: 3, ampm: 'am' }, 'pm')).toEqual(15);
+        expect(calculateHourFromAMPM({ hour: 4, ampm: 'am' }, 'pm')).toEqual(16);
+        expect(calculateHourFromAMPM({ hour: 5, ampm: 'am' }, 'pm')).toEqual(17);
+        expect(calculateHourFromAMPM({ hour: 6, ampm: 'am' }, 'pm')).toEqual(18);
+        expect(calculateHourFromAMPM({ hour: 7, ampm: 'am' }, 'pm')).toEqual(19);
+        expect(calculateHourFromAMPM({ hour: 8, ampm: 'am' }, 'pm')).toEqual(20);
+        expect(calculateHourFromAMPM({ hour: 9, ampm: 'am' }, 'pm')).toEqual(21);
+        expect(calculateHourFromAMPM({ hour: 10, ampm: 'am' }, 'pm')).toEqual(22);
+        expect(calculateHourFromAMPM({ hour: 11, ampm: 'am' }, 'pm')).toEqual(23);
+        expect(calculateHourFromAMPM({ hour: 13, ampm: 'pm' }, 'am')).toEqual(1);
+        expect(calculateHourFromAMPM({ hour: 14, ampm: 'pm' }, 'am')).toEqual(2);
+        expect(calculateHourFromAMPM({ hour: 15, ampm: 'pm' }, 'am')).toEqual(3);
+        expect(calculateHourFromAMPM({ hour: 16, ampm: 'pm' }, 'am')).toEqual(4);
+        expect(calculateHourFromAMPM({ hour: 17, ampm: 'pm' }, 'am')).toEqual(5);
+        expect(calculateHourFromAMPM({ hour: 18, ampm: 'pm' }, 'am')).toEqual(6);
+        expect(calculateHourFromAMPM({ hour: 19, ampm: 'pm' }, 'am')).toEqual(7);
+        expect(calculateHourFromAMPM({ hour: 20, ampm: 'pm' }, 'am')).toEqual(8);
+        expect(calculateHourFromAMPM({ hour: 21, ampm: 'pm' }, 'am')).toEqual(9);
+        expect(calculateHourFromAMPM({ hour: 22, ampm: 'pm' }, 'am')).toEqual(10);
+        expect(calculateHourFromAMPM({ hour: 23, ampm: 'pm' }, 'am')).toEqual(11);
+        expect(calculateHourFromAMPM({ hour: 0, ampm: 'pm' }, 'am')).toEqual(12);
+    });
+});
+describe('convert12HourTo24Hour()', () => {
+    it('should correctly convert 12 hour to 24 hour', () => {
+        expect(convert12HourTo24Hour(12, 'am')).toEqual(0);
+        expect(convert12HourTo24Hour(1, 'am')).toEqual(1);
+        expect(convert12HourTo24Hour(2, 'am')).toEqual(2);
+        expect(convert12HourTo24Hour(3, 'am')).toEqual(3);
+        expect(convert12HourTo24Hour(4, 'am')).toEqual(4);
+        expect(convert12HourTo24Hour(5, 'am')).toEqual(5);
+        expect(convert12HourTo24Hour(6, 'am')).toEqual(6);
+        expect(convert12HourTo24Hour(7, 'am')).toEqual(7);
+        expect(convert12HourTo24Hour(8, 'am')).toEqual(8);
+        expect(convert12HourTo24Hour(9, 'am')).toEqual(9);
+        expect(convert12HourTo24Hour(10, 'am')).toEqual(10);
+        expect(convert12HourTo24Hour(11, 'am')).toEqual(11);
+        expect(convert12HourTo24Hour(12, 'pm')).toEqual(12);
+        expect(convert12HourTo24Hour(1, 'pm')).toEqual(13);
+        expect(convert12HourTo24Hour(2, 'pm')).toEqual(14);
+        expect(convert12HourTo24Hour(3, 'pm')).toEqual(15);
+        expect(convert12HourTo24Hour(4, 'pm')).toEqual(16);
+        expect(convert12HourTo24Hour(5, 'pm')).toEqual(17);
+        expect(convert12HourTo24Hour(6, 'pm')).toEqual(18);
+        expect(convert12HourTo24Hour(7, 'pm')).toEqual(19);
+        expect(convert12HourTo24Hour(8, 'pm')).toEqual(20);
+        expect(convert12HourTo24Hour(9, 'pm')).toEqual(21);
+        expect(convert12HourTo24Hour(10, 'pm')).toEqual(22);
+        expect(convert12HourTo24Hour(11, 'pm')).toEqual(23);
+    });
+});
+describe('getStartOfWeek()', () => {
+    it('should correctly return the start of the week', () => {
+        expect(getStartOfWeek({
+            month: 5,
+            day: 17,
+            year: 2021,
+            dayOfWeek: 1,
+        })).toEqual({
+            month: 5,
+            day: 16,
+            year: 2021,
+        });
+        expect(getStartOfWeek({
+            month: 5,
+            day: 1,
+            year: 2021,
+            dayOfWeek: 6,
+        })).toEqual({
+            month: 4,
+            day: 25,
+            year: 2021,
+        });
+        expect(getStartOfWeek({
+            month: 1,
+            day: 2,
+            year: 2021,
+            dayOfWeek: 6,
+        })).toEqual({
+            month: 12,
+            day: 27,
+            year: 2020,
+        });
+    });
+});
+describe('getEndOfWeek()', () => {
+    it('should correctly return the end of the week', () => {
+        expect(getEndOfWeek({
+            month: 5,
+            day: 17,
+            year: 2021,
+            dayOfWeek: 1,
+        })).toEqual({
+            month: 5,
+            day: 22,
+            year: 2021,
+        });
+        expect(getEndOfWeek({
+            month: 5,
+            day: 31,
+            year: 2021,
+            dayOfWeek: 1,
+        })).toEqual({
+            month: 6,
+            day: 5,
+            year: 2021,
+        });
+        expect(getEndOfWeek({
+            month: 12,
+            day: 29,
+            year: 2021,
+            dayOfWeek: 3,
+        })).toEqual({
+            month: 1,
+            day: 1,
+            year: 2022,
+        });
+    });
+});
+describe('getNextWeek()', () => {
+    it('should correctly return the next week', () => {
+        expect(getNextWeek({
+            month: 5,
+            day: 17,
+            year: 2021,
+        })).toEqual({
+            month: 5,
+            day: 24,
+            year: 2021,
+        });
+        expect(getNextWeek({
+            month: 5,
+            day: 31,
+            year: 2021,
+        })).toEqual({
+            month: 6,
+            day: 7,
+            year: 2021,
+        });
+        expect(getNextWeek({
+            month: 12,
+            day: 29,
+            year: 2021,
+        })).toEqual({
+            month: 1,
+            day: 5,
+            year: 2022,
+        });
+    });
+});
+describe('getPreviousWeek()', () => {
+    it('should correctly return the previous week', () => {
+        expect(getPreviousWeek({
+            month: 5,
+            day: 17,
+            year: 2021,
+        })).toEqual({
+            month: 5,
+            day: 10,
+            year: 2021,
+        });
+        expect(getPreviousWeek({
+            month: 5,
+            day: 1,
+            year: 2021,
+        })).toEqual({
+            month: 4,
+            day: 24,
+            year: 2021,
+        });
+        expect(getPreviousWeek({
+            month: 1,
+            day: 4,
+            year: 2021,
+        })).toEqual({
+            month: 12,
+            day: 28,
+            year: 2020,
+        });
+    });
+});
+describe('getNextDay()', () => {
+    it('should correctly return the next day', () => {
+        expect(getNextDay({
+            month: 5,
+            day: 17,
+            year: 2021,
+        })).toEqual({
+            month: 5,
+            day: 18,
+            year: 2021,
+        });
+        expect(getNextDay({
+            month: 5,
+            day: 31,
+            year: 2021,
+        })).toEqual({
+            month: 6,
+            day: 1,
+            year: 2021,
+        });
+        expect(getNextDay({
+            month: 12,
+            day: 31,
+            year: 2021,
+        })).toEqual({
+            month: 1,
+            day: 1,
+            year: 2022,
+        });
+    });
+});
+describe('getPreviousDay()', () => {
+    it('should correctly return the previous day', () => {
+        expect(getPreviousDay({
+            month: 5,
+            day: 17,
+            year: 2021,
+        })).toEqual({
+            month: 5,
+            day: 16,
+            year: 2021,
+        });
+        expect(getPreviousDay({
+            month: 5,
+            day: 1,
+            year: 2021,
+        })).toEqual({
+            month: 4,
+            day: 30,
+            year: 2021,
+        });
+        expect(getPreviousDay({
+            month: 1,
+            day: 1,
+            year: 2021,
+        })).toEqual({
+            month: 12,
+            day: 31,
+            year: 2020,
+        });
+    });
+});
+describe('getNextMonth()', () => {
+    it('should return correct next month', () => {
+        expect(getNextMonth({ month: 5, year: 2021, day: 1 })).toEqual({
+            month: 6,
+            year: 2021,
+            day: 1,
+        });
+        expect(getNextMonth({ month: 12, year: 2021, day: 30 })).toEqual({
+            month: 1,
+            year: 2022,
+            day: 30,
+        });
+        expect(getNextMonth({ month: 12, year: 1999, day: 30 })).toEqual({
+            month: 1,
+            year: 2000,
+            day: 30,
+        });
+    });
+});
+describe('getPreviousMonth()', () => {
+    it('should return correct previous month', () => {
+        expect(getPreviousMonth({ month: 5, year: 2021, day: 1 })).toEqual({
+            month: 4,
+            year: 2021,
+            day: 1,
+        });
+        expect(getPreviousMonth({ month: 1, year: 2021, day: 30 })).toEqual({
+            month: 12,
+            year: 2020,
+            day: 30,
+        });
+        expect(getPreviousMonth({ month: 1, year: 2000, day: 30 })).toEqual({
+            month: 12,
+            year: 1999,
+            day: 30,
+        });
+    });
+});
+describe('getNextYear()', () => {
+    it('should return correct next year', () => {
+        expect(getNextYear({ month: 5, year: 2021, day: 1 })).toEqual({
+            month: 5,
+            year: 2022,
+            day: 1,
+        });
+        expect(getNextYear({ month: 12, year: 1999, day: 30 })).toEqual({
+            month: 12,
+            year: 2000,
+            day: 30,
+        });
+        // Leap year
+        expect(getNextYear({ month: 2, year: 2024, day: 29 })).toEqual({
+            month: 2,
+            year: 2025,
+            day: 28,
+        });
+    });
+});
+describe('getPreviousYear()', () => {
+    it('should return correct next year', () => {
+        expect(getPreviousYear({ month: 5, year: 2021, day: 1 })).toEqual({
+            month: 5,
+            year: 2020,
+            day: 1,
+        });
+        expect(getPreviousYear({ month: 12, year: 1999, day: 30 })).toEqual({
+            month: 12,
+            year: 1998,
+            day: 30,
+        });
+        // Leap year
+        expect(getPreviousYear({ month: 2, year: 2024, day: 29 })).toEqual({
+            month: 2,
+            year: 2023,
+            day: 28,
+        });
+    });
+});
+describe('validateParts()', () => {
+    it('should move day in bounds', () => {
+        expect(validateParts({ month: 2, day: 31, year: 2022, hour: 8, minute: 0 })).toEqual({
+            month: 2,
+            day: 28,
+            year: 2022,
+            hour: 8,
+            minute: 0,
+        });
+    });
+    it('should move the hour back in bounds according to the min', () => {
+        expect(validateParts({ month: 1, day: 1, year: 2022, hour: 8, minute: 0 }, { month: 1, day: 1, year: 2022, hour: 9, minute: 0 })).toEqual({ month: 1, day: 1, year: 2022, hour: 9, minute: 0 });
+    });
+    it('should move the minute back in bounds according to the min', () => {
+        expect(validateParts({ month: 1, day: 1, year: 2022, hour: 9, minute: 20 }, { month: 1, day: 1, year: 2022, hour: 9, minute: 30 })).toEqual({ month: 1, day: 1, year: 2022, hour: 9, minute: 30 });
+    });
+    it('should move the hour and minute back in bounds according to the min', () => {
+        expect(validateParts({ month: 1, day: 1, year: 2022, hour: 8, minute: 30 }, { month: 1, day: 1, year: 2022, hour: 9, minute: 0 })).toEqual({ month: 1, day: 1, year: 2022, hour: 9, minute: 0 });
+    });
+    it('should move the hour back in bounds according to the max', () => {
+        expect(validateParts({ month: 1, day: 1, year: 2022, hour: 10, minute: 0 }, undefined, {
+            month: 1,
+            day: 1,
+            year: 2022,
+            hour: 9,
+            minute: 0,
+        })).toEqual({ month: 1, day: 1, year: 2022, hour: 9, minute: 0 });
+    });
+    it('should move the minute back in bounds according to the max', () => {
+        expect(validateParts({ month: 1, day: 1, year: 2022, hour: 9, minute: 40 }, undefined, {
+            month: 1,
+            day: 1,
+            year: 2022,
+            hour: 9,
+            minute: 30,
+        })).toEqual({ month: 1, day: 1, year: 2022, hour: 9, minute: 30 });
+    });
+    it('should move the hour and minute back in bounds according to the max', () => {
+        expect(validateParts({ month: 1, day: 1, year: 2022, hour: 10, minute: 20 }, undefined, {
+            month: 1,
+            day: 1,
+            year: 2022,
+            hour: 9,
+            minute: 30,
+        })).toEqual({ month: 1, day: 1, year: 2022, hour: 9, minute: 30 });
+    });
+});
+describe('getClosestValidDate()', () => {
+    it('should match a date with only month/day/year', () => {
+        // October 10, 2023
+        const refParts = { month: 10, day: 10, year: 2023 };
+        // April 10, 2021
+        const minParts = { month: 4, day: 10, year: 2021 };
+        // September 14, 2021
+        const maxParts = { month: 9, day: 14, year: 2021 };
+        // September 4, 2021
+        const expected = { month: 9, day: 4, year: 2021, dayOfWeek: undefined };
+        expect(getClosestValidDate({
+            refParts,
+            monthValues: [2, 3, 7, 9, 10],
+            dayValues: [4, 15, 25],
+            yearValues: [2020, 2021, 2023],
+            maxParts,
+            minParts,
+        })).toEqual(expected);
+    });
+    it('should match a date when the reference date is before the min', () => {
+        // April 2, 2020 3:20 PM
+        const refParts = { month: 4, day: 2, year: 2020, hour: 15, minute: 20 };
+        // September 10, 2021 10:10 AM
+        const minParts = { month: 9, day: 10, year: 2021, hour: 10, minute: 10 };
+        // September 14, 2021 10:11 AM
+        const maxParts = { month: 9, day: 14, year: 2021, hour: 10, minute: 11 };
+        // September 11, 2021 11:15 AM
+        const expected = {
+            year: 2021,
+            day: 11,
+            month: 9,
+            hour: 11,
+            minute: 15,
+            ampm: 'am',
+            dayOfWeek: undefined,
+        };
+        expect(getClosestValidDate({
+            refParts,
+            monthValues: [4, 9, 11],
+            dayValues: [11, 12, 13, 14],
+            yearValues: [2020, 2021, 2023],
+            hourValues: [9, 10, 11],
+            minuteValues: [11, 12, 13, 14, 15],
+            maxParts,
+            minParts,
+        })).toEqual(expected);
+    });
+    it('should match a date when the reference date is before the min', () => {
+        // April 2, 2020 3:20 PM
+        const refParts = { month: 4, day: 2, year: 2020, hour: 15, minute: 20 };
+        // September 10, 2021 10:10 AM
+        const minParts = { month: 9, day: 10, year: 2021, hour: 10, minute: 10 };
+        // September 10, 2021 10:15 AM
+        const maxParts = { month: 9, day: 10, year: 2021, hour: 10, minute: 15 };
+        // September 10, 2021 10:15 AM
+        const expected = {
+            month: 9,
+            day: 10,
+            year: 2021,
+            hour: 10,
+            minute: 15,
+            ampm: 'am',
+            dayOfWeek: undefined,
+        };
+        expect(getClosestValidDate({
+            refParts,
+            monthValues: [4, 9, 11],
+            dayValues: [10, 12, 13, 14],
+            yearValues: [2020, 2021, 2023],
+            hourValues: [9, 10, 11],
+            minuteValues: [11, 12, 13, 14, 15],
+            minParts,
+            maxParts,
+        })).toEqual(expected);
+    });
+    it('should only clamp minutes if within the same day and hour as min/max', () => {
+        // April 2, 2020 9:16 AM
+        const refParts = { month: 4, day: 2, year: 2020, hour: 9, minute: 16 };
+        // September 10, 2021 10:10 AM
+        const minParts = { month: 9, day: 10, year: 2021, hour: 10, minute: 10 };
+        // September 10, 2021 11:15 AM
+        const maxParts = { month: 9, day: 10, year: 2021, hour: 11, minute: 15 };
+        // September 10, 2021 10:16 AM
+        const expected = {
+            month: 9,
+            day: 10,
+            year: 2021,
+            hour: 10,
+            minute: 16,
+            ampm: 'am',
+            dayOfWeek: undefined,
+        };
+        expect(getClosestValidDate({
+            refParts,
+            monthValues: [4, 9, 11],
+            dayValues: [10, 12, 13, 14],
+            yearValues: [2020, 2021, 2023],
+            hourValues: [9, 10, 11],
+            minuteValues: [10, 15, 16],
+            minParts,
+            maxParts,
+        })).toEqual(expected);
+    });
+    it('should return the closest valid date after adjusting the allowed year', () => {
+        // April 2, 2022 9:16 AM
+        const refParts = { month: 4, day: 2, year: 2022, hour: 9, minute: 16 };
+        // September 10, 2021 10:10 AM
+        const minParts = { month: 9, day: 10, year: 2021, hour: 10, minute: 10 };
+        // September 10, 2023 11:15 AM
+        const maxParts = { month: 9, day: 10, year: 2023, hour: 11, minute: 15 };
+        // April 2, 2022 9:16 AM
+        const expected = {
+            month: 4,
+            day: 2,
+            year: 2022,
+            hour: 9,
+            minute: 16,
+            ampm: 'am',
+            dayOfWeek: undefined,
+        };
+        expect(getClosestValidDate({
+            refParts,
+            monthValues: [4, 9, 11],
+            dayValues: [2, 10, 12, 13, 14],
+            yearValues: [2020, 2021, 2022, 2023],
+            hourValues: [9, 10, 11],
+            minuteValues: [10, 15, 16],
+            minParts,
+            maxParts,
+        })).toEqual(expected);
+    });
+});

+ 266 - 0
src/node_modules/@ionic/core/dist/collection/components/datetime/test/minmax/datetime.e2e.js

@@ -0,0 +1,266 @@
+/*!
+ * (C) Ionic http://ionicframework.com - MIT License
+ */
+import { expect } from "@playwright/test";
+import { configs, test } from "../../../../utils/test/playwright/index";
+/**
+ * This behavior is the same across
+ * modes/directions.
+ */
+configs({ directions: ['ltr'], modes: ['ios'] }).forEach(({ title, config }) => {
+    test.describe(title('datetime: minmax'), () => {
+        test('calendar arrow navigation should respect min/max values', async ({ page }) => {
+            test.info().annotations.push({
+                type: 'issue',
+                description: 'https://github.com/ionic-team/ionic-framework/issues/25073',
+            });
+            await page.setContent(`
+        <ion-datetime min="2022-04-22" max="2022-05-21" value="2022-04-22T10:00:00"></ion-datetime>
+
+        <script>
+          const observer = new MutationObserver((mutationRecords) => {
+            if (mutationRecords) {
+              window.dispatchEvent(new CustomEvent('datetimeMonthDidChange'));
+            }
+          });
+
+          const initDatetimeChangeEvent = () => {
+            observer.observe(document.querySelector('ion-datetime').shadowRoot.querySelector('.calendar-body'), {
+              subtree: true,
+              childList: true
+            });
+          }
+        </script>
+    `, config);
+            await page.locator('.datetime-ready').waitFor();
+            const prevButton = page.locator('ion-datetime .calendar-next-prev ion-button:nth-child(1)');
+            const nextButton = page.locator('ion-datetime .calendar-next-prev ion-button:nth-child(2)');
+            await expect(nextButton).toHaveJSProperty('disabled', false);
+            await expect(prevButton).toHaveJSProperty('disabled', true);
+            await page.evaluate('initDatetimeChangeEvent()');
+            const monthDidChangeSpy = await page.spyOnEvent('datetimeMonthDidChange');
+            await nextButton.click();
+            await page.waitForChanges();
+            await monthDidChangeSpy.next();
+            await expect(nextButton).toHaveJSProperty('disabled', true);
+            await expect(prevButton).toHaveJSProperty('disabled', false);
+        });
+        test('datetime: minmax months disabled', async ({ page }) => {
+            await page.goto('/src/components/datetime/test/minmax', config);
+            const calendarMonths = page.locator('ion-datetime#inside .calendar-month');
+            await page.locator('.datetime-ready').first().waitFor();
+            await expect(calendarMonths.nth(0)).not.toHaveClass(/calendar-month-disabled/);
+            await expect(calendarMonths.nth(1)).not.toHaveClass(/calendar-month-disabled/);
+            await expect(calendarMonths.nth(2)).toHaveClass(/calendar-month-disabled/);
+        });
+        test('datetime: minmax navigation disabled', async ({ page }) => {
+            await page.goto('/src/components/datetime/test/minmax', config);
+            await page.locator('.datetime-ready').first().waitFor();
+            const navButtons = page.locator('ion-datetime#outside .calendar-next-prev ion-button');
+            await expect(navButtons.nth(0)).toHaveAttribute('disabled', '');
+            await expect(navButtons.nth(1)).toHaveAttribute('disabled', '');
+        });
+        test('datetime: min including day should not disable month', async ({ page }) => {
+            await page.goto('/src/components/datetime/test/minmax', config);
+            await page.locator('.datetime-ready').first().waitFor();
+            const calendarMonths = page.locator('ion-datetime#min-with-day .calendar-month');
+            await expect(calendarMonths.nth(0)).toHaveClass(/calendar-month-disabled/);
+            await expect(calendarMonths.nth(1)).not.toHaveClass(/calendar-month-disabled/);
+            await expect(calendarMonths.nth(2)).not.toHaveClass(/calendar-month-disabled/);
+        });
+        test.describe('when the datetime does not have a value', () => {
+            test('all time values should be available for selection', async ({ page }) => {
+                /**
+                 * When the datetime does not have an initial value and today falls outside of
+                 * the specified min and max values, all times values should be available for selection.
+                 */
+                await page.setContent(`
+          <ion-datetime min="2022-04-22T04:10:00" max="2022-05-21T21:30:00"></ion-datetime>
+      `, config);
+                await page.locator('.datetime-ready').waitFor();
+                const ionPopoverDidPresent = await page.spyOnEvent('ionPopoverDidPresent');
+                await page.click('.time-body');
+                await ionPopoverDidPresent.next();
+                const hours = page.locator('ion-popover ion-picker-column:nth-child(1) ion-picker-column-option');
+                const minutes = page.locator('ion-popover ion-picker-column:nth-child(2) ion-picker-column-option');
+                expect(await hours.count()).toBe(12);
+                expect(await minutes.count()).toBe(60);
+            });
+        });
+        test.describe('setting value outside bounds should show in-bounds month', () => {
+            const testDisplayedMonth = async (page, content, expectedString = /June 2021/) => {
+                await page.setContent(content, config);
+                await page.locator('.datetime-ready').waitFor();
+                const calendarMonthYear = page.locator('ion-datetime .calendar-month-year');
+                await expect(calendarMonthYear).toHaveText(expectedString);
+            };
+            test('when min and value are defined', async ({ page }) => {
+                await testDisplayedMonth(page, `<ion-datetime min="2021-06-01" value="2021-05-01"></ion-datetime>`);
+            });
+            test('when max and value are defined', async ({ page }) => {
+                await testDisplayedMonth(page, `<ion-datetime max="2021-06-30" value="2021-07-01"></ion-datetime>`);
+            });
+            test('when min, max, and value are defined', async ({ page }) => {
+                await testDisplayedMonth(page, `<ion-datetime min="2021-06-01" max="2021-06-30" value="2021-05-01"></ion-datetime>`);
+            });
+            test('when max is defined', async ({ page }) => {
+                await testDisplayedMonth(page, `<ion-datetime max="2012-06-01"></ion-datetime>`, /June 2012/);
+            });
+        });
+        // TODO(FW-2165)
+        test('should not loop infinitely in webkit', async ({ page, skip }) => {
+            test.info().annotations.push({
+                type: 'issue',
+                description: 'https://github.com/ionic-team/ionic-framework/issues/25752',
+            });
+            skip.browser('chromium');
+            skip.browser('firefox');
+            await page.setContent(`
+        <button id="bind">Bind datetimeMonthDidChange event</button>
+        <ion-datetime min="2022-04-15" value="2022-04-20" presentation="date" locale="en-US"></ion-datetime>
+
+        <script type="module">
+          import { InitMonthDidChangeEvent } from '/src/components/datetime/test/utils/month-did-change-event.js';
+          document.querySelector('#bind').addEventListener('click', function() {
+            InitMonthDidChangeEvent();
+          });
+        </script>
+      `, config);
+            await page.locator('.datetime-ready').waitFor();
+            const datetimeMonthDidChange = await page.spyOnEvent('datetimeMonthDidChange');
+            const eventButton = page.locator('button#bind');
+            await eventButton.click();
+            const buttons = page.locator('ion-datetime .calendar-next-prev ion-button');
+            await buttons.nth(1).click();
+            await page.waitForChanges();
+            await datetimeMonthDidChange.next();
+            /**
+             * This is hacky, but its purpose is to make sure
+             * we are not triggering a WebKit bug. When the fix
+             * for the bug ships in WebKit, this will be removed.
+             */
+            await page.evaluate(() => {
+                return new Promise((resolve) => {
+                    setTimeout(resolve, 500);
+                });
+            });
+            await expect(datetimeMonthDidChange).toHaveReceivedEventTimes(1);
+        });
+        test('should not include 12AM when minimum is greater than 12AM', async ({ page }) => {
+            test.info().annotations.push({
+                type: 'issue',
+                description: 'https://github.com/ionic-team/ionic-framework/issues/25183',
+            });
+            await page.setContent(`
+        <ion-datetime
+          presentation="time"
+          min="2022-04-25T08:30:00"
+          max="2022-04-25T21:30:00"
+          value="2022-04-25T08:30:00"
+        ></ion-datetime>
+      `, config);
+            const hourPickerItems = page.locator('ion-datetime ion-picker-column:first-of-type ion-picker-column-option');
+            await expect(hourPickerItems).toHaveText(['8', '9', '10', '11']);
+        });
+        test('should include 12PM when minimum is greater than 12', async ({ page }) => {
+            test.info().annotations.push({
+                type: 'issue',
+                description: 'https://github.com/ionic-team/ionic-framework/issues/25183',
+            });
+            await page.setContent(`
+        <ion-datetime
+          locale="en-US"
+          presentation="time"
+          min="2022-07-29T08:00:00"
+          value="2022-07-29T12:00:00"
+        ></ion-datetime>
+      `, config);
+            const hourPickerItems = page.locator('ion-datetime ion-picker-column:first-of-type ion-picker-column-option');
+            await expect(hourPickerItems).toHaveText(['12', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11']);
+        });
+        test.describe('minmax value adjustment when out of bounds', () => {
+            test('should reset to min time if out of bounds', async ({ page }) => {
+                await page.setContent(`
+          <ion-datetime
+            min="2022-10-10T08:00"
+            value="2022-10-11T06:00"
+          ></ion-datetime>
+        `, config);
+                await page.locator('.datetime-ready').waitFor();
+                const datetime = page.locator('ion-datetime');
+                const ionChange = await page.spyOnEvent('ionChange');
+                const dayButton = page.locator('ion-datetime .calendar-day[data-day="10"][data-month="10"][data-year="2022"]');
+                await dayButton.click();
+                await ionChange.next();
+                await expect(datetime).toHaveJSProperty('value', '2022-10-10T08:00:00');
+            });
+            test('should reset to max time if out of bounds', async ({ page }) => {
+                await page.setContent(`
+          <ion-datetime
+            max="2022-10-10T08:00"
+            value="2022-10-11T09:00"
+          ></ion-datetime>
+        `, config);
+                await page.locator('.datetime-ready').waitFor();
+                const datetime = page.locator('ion-datetime');
+                const ionChange = await page.spyOnEvent('ionChange');
+                const dayButton = page.locator('ion-datetime .calendar-day[data-day="10"][data-month="10"][data-year="2022"]');
+                await dayButton.click();
+                await ionChange.next();
+                await expect(datetime).toHaveJSProperty('value', '2022-10-10T08:00:00');
+            });
+            test('should adjust to in-bounds when using month picker', async ({ page }) => {
+                await page.setContent(`
+          <ion-datetime
+            min="2022-01-15"
+            value="2022-02-01"
+            presentation="month-year"
+          ></ion-datetime>
+        `, config);
+                const datetime = page.locator('ion-datetime');
+                const monthColumnItems = page.locator('ion-datetime .month-column ion-picker-column-option');
+                const ionChange = await page.spyOnEvent('ionChange');
+                await page.locator('.datetime-ready').waitFor();
+                await monthColumnItems.nth(0).click(); // switch to January
+                await ionChange.next();
+                await expect(datetime).toHaveJSProperty('value', '2022-01-15T00:00:00');
+            });
+        });
+        test.describe('datetime: confirm button', () => {
+            test('should apply max and min constraints even when user confirmation is required', async ({ page }) => {
+                test.info().annotations.push({
+                    type: 'issue',
+                    description: 'https://github.com/ionic-team/ionic-framework/issues/25073',
+                });
+                await page.setContent(`
+          <ion-datetime max="2022-01-10T15:30" show-default-buttons="true"></ion-datetime>
+
+          <script>
+            const mockToday = '2022-01-10T12:22';
+            Date = class extends Date {
+              constructor(...args) {
+                if (args.length === 0) {
+                  super(mockToday)
+                } else {
+                  super(...args);
+                }
+              }
+            }
+          </script>
+        `, config);
+                await page.locator('.datetime-ready').waitFor();
+                // Select Jan 10, 2022
+                const maxDate = page.locator('ion-datetime .calendar-day[data-day="10"][data-month="1"][data-year="2022"]');
+                await maxDate.click();
+                await page.waitForChanges();
+                // Check to see that the hours have been filtered.
+                const ionPopoverDidPresent = await page.spyOnEvent('ionPopoverDidPresent');
+                const timeButton = page.locator('ion-datetime .time-body');
+                await timeButton.click();
+                await ionPopoverDidPresent.next();
+                const hours = page.locator('ion-popover ion-picker-column:nth-child(1) ion-picker-column-option');
+                await expect(await hours.count()).toBe(4);
+            });
+        });
+    });
+});

+ 29 - 0
src/node_modules/@ionic/core/dist/collection/components/datetime/test/month-year-picker/datetime.e2e.js

@@ -0,0 +1,29 @@
+/*!
+ * (C) Ionic http://ionicframework.com - MIT License
+ */
+import { expect } from "@playwright/test";
+import { configs, test } from "../../../../utils/test/playwright/index";
+/**
+ * This is testing element rendering which
+ * does not vary across modes/directions.
+ */
+configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => {
+    test.describe(title('datetime: month-year picker'), () => {
+        test.beforeEach(async ({ page }) => {
+            await page.goto('/src/components/datetime/test/month-year-picker', config);
+            await page.locator('.datetime-ready').first().waitFor();
+        });
+        test('should hide the footer when picker is open', async ({ page }) => {
+            const datetimeFooter = page.locator('#date-time .datetime-footer');
+            await expect(datetimeFooter).toBeVisible();
+            const pickerButton = page.locator('#date-time .calendar-month-year > .calendar-month-year-toggle');
+            await pickerButton.click();
+            await page.waitForChanges();
+            await expect(datetimeFooter).not.toBeVisible();
+        });
+        test('should not hide the footer on month-year presentation', async ({ page }) => {
+            const monthyearFooter = page.locator('#month-year .datetime-footer');
+            await expect(monthyearFooter).toBeVisible();
+        });
+    });
+});

+ 242 - 0
src/node_modules/@ionic/core/dist/collection/components/datetime/test/multiple/datetime.e2e.js

@@ -0,0 +1,242 @@
+/*!
+ * (C) Ionic http://ionicframework.com - MIT License
+ */
+import { expect } from "@playwright/test";
+import { configs, test } from "../../../../utils/test/playwright/index";
+const SINGLE_DATE = '2022-06-01';
+const MULTIPLE_DATES = ['2022-06-01', '2022-06-02', '2022-06-03'];
+const MULTIPLE_DATES_SEPARATE_MONTHS = ['2022-03-01', '2022-04-01', '2022-05-01'];
+class DatetimeMultipleFixture {
+    constructor(page) {
+        this.page = page;
+    }
+    processValue(value = []) {
+        if (!Array.isArray(value)) {
+            return `'${value}'`;
+        }
+        const toString = value.map((v) => `'${v}'`).join(',');
+        return `[${toString}]`;
+    }
+    async goto(config, value, datetimeConfig) {
+        const { showDefaultButtons, showClearButton, showDefaultTitle, multiple, customFormatter } = datetimeConfig !== null && datetimeConfig !== void 0 ? datetimeConfig : {};
+        const formattedValue = this.processValue(value);
+        await this.page.setContent(`
+      <ion-datetime
+        locale="en-US"
+        presentation="date"
+        multiple="${multiple !== null && multiple !== void 0 ? multiple : true}"
+        value="2022-04-19T04:20:00"
+        show-default-title="${showDefaultTitle !== null && showDefaultTitle !== void 0 ? showDefaultTitle : false}"
+        show-default-buttons="${showDefaultButtons !== null && showDefaultButtons !== void 0 ? showDefaultButtons : false}"
+        show-clear-button="${showClearButton !== null && showClearButton !== void 0 ? showClearButton : false}"
+      ></ion-datetime>
+
+      <script>
+        const datetime = document.querySelector('ion-datetime');
+        datetime.value = ${formattedValue};
+
+        if (${customFormatter}) {
+          datetime.titleSelectedDatesFormatter = (selectedDates) => "Selected: " + selectedDates.length;
+        }
+      </script>
+    `, config);
+        this.datetime = this.page.locator('ion-datetime');
+        await this.page.locator('.datetime-ready').waitFor();
+        return this.datetime;
+    }
+    async expectMultipleDatePicker(id, screenshot) {
+        await expect(this.datetime).toHaveScreenshot(screenshot(`datetime-multiple-${id}`));
+    }
+}
+configs().forEach(({ title, screenshot, config }) => {
+    test.describe(title('datetime: multiple date selection (visual regressions)'), () => {
+        let datetimeFixture;
+        test.beforeEach(async ({ page }) => {
+            datetimeFixture = new DatetimeMultipleFixture(page);
+        });
+        test('single default value should not have visual regressions', async () => {
+            await datetimeFixture.goto(config, SINGLE_DATE);
+            await datetimeFixture.expectMultipleDatePicker('singleDefaultValue', screenshot);
+        });
+        test('multiple default values should not have visual regressions', async () => {
+            await datetimeFixture.goto(config, MULTIPLE_DATES);
+            await datetimeFixture.expectMultipleDatePicker('multipleDefaultValues', screenshot);
+        });
+        test('header should not have visual regressions', async () => {
+            await datetimeFixture.goto(config, SINGLE_DATE, { showDefaultTitle: true });
+            await datetimeFixture.expectMultipleDatePicker('withHeader', screenshot);
+        });
+    });
+});
+/**
+ * Multiple date selection functionality
+ * is the same across modes/directions.
+ */
+configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
+    test.describe(title('datetime: multiple date selection (functionality)'), () => {
+        let datetimeFixture;
+        test.beforeEach(async ({ page }) => {
+            datetimeFixture = new DatetimeMultipleFixture(page);
+        });
+        test('clicking unselected days should select them', async ({ page }) => {
+            const datetime = await datetimeFixture.goto(config, SINGLE_DATE);
+            const ionChangeSpy = await page.spyOnEvent('ionChange');
+            const juneButtons = datetime.locator('[data-month="6"][data-day]');
+            await juneButtons.nth(1).click();
+            await ionChangeSpy.next();
+            await expect(datetime).toHaveJSProperty('value', ['2022-06-01', '2022-06-02']);
+            await juneButtons.nth(2).click();
+            await ionChangeSpy.next();
+            await expect(datetime).toHaveJSProperty('value', MULTIPLE_DATES);
+            for (let i = 0; i < 3; i++) {
+                await expect(juneButtons.nth(i)).toHaveClass(/calendar-day-active/);
+            }
+        });
+        test('clicking selected days should unselect them', async ({ page }) => {
+            const datetime = await datetimeFixture.goto(config, MULTIPLE_DATES);
+            const juneButtons = datetime.locator('[data-month="6"][data-day]');
+            const ionChangeSpy = await page.spyOnEvent('ionChange');
+            await juneButtons.nth(0).click();
+            await ionChangeSpy.next();
+            await expect(datetime).toHaveJSProperty('value', ['2022-06-02', '2022-06-03']);
+            await juneButtons.nth(1).click();
+            await ionChangeSpy.next();
+            await expect(datetime).toHaveJSProperty('value', ['2022-06-03']);
+            await juneButtons.nth(2).click();
+            await ionChangeSpy.next();
+            await expect(datetime).toHaveJSProperty('value', undefined);
+            for (let i = 0; i < 3; i++) {
+                await expect(juneButtons.nth(i)).not.toHaveClass(/calendar-day-active/);
+            }
+        });
+        test('change event should emit with array detail', async ({ page }) => {
+            const datetime = await datetimeFixture.goto(config, SINGLE_DATE);
+            const june2Button = datetime.locator('[data-month="6"][data-day="2"]');
+            const ionChangeSpy = await page.spyOnEvent('ionChange');
+            await june2Button.click();
+            expect(ionChangeSpy).toHaveReceivedEventDetail({
+                value: ['2022-06-01', '2022-06-02'],
+            });
+        });
+        test('should scroll to new month when value is updated with multiple dates in the same month', async ({ page }) => {
+            test.info().annotations.push({
+                type: 'issue',
+                description: 'https://github.com/ionic-team/ionic-framework/issues/28602',
+            });
+            const datetime = await datetimeFixture.goto(config, MULTIPLE_DATES_SEPARATE_MONTHS);
+            await datetime.evaluate((el, dates) => {
+                el.value = dates;
+            }, MULTIPLE_DATES);
+            await page.waitForChanges();
+            const monthYear = datetime.locator('.calendar-month-year');
+            await expect(monthYear).toHaveText(/June 2022/);
+        });
+        test('should not scroll to new month when value is updated with dates in different months', async ({ page }) => {
+            const datetime = await datetimeFixture.goto(config, MULTIPLE_DATES);
+            await datetime.evaluate((el, dates) => {
+                el.value = dates;
+            }, MULTIPLE_DATES_SEPARATE_MONTHS);
+            await page.waitForChanges();
+            const monthYear = datetime.locator('.calendar-month-year');
+            await expect(monthYear).toHaveText(/June 2022/);
+        });
+        test('with buttons, should only update value when confirm is called', async ({ page }) => {
+            const datetime = await datetimeFixture.goto(config, SINGLE_DATE, { showDefaultButtons: true });
+            const june2Button = datetime.locator('[data-month="6"][data-day="2"]');
+            await june2Button.click();
+            await page.waitForChanges();
+            await expect(datetime).toHaveJSProperty('value', SINGLE_DATE); // value should not change yet
+            await datetime.evaluate((el) => el.confirm());
+            await expect(datetime).toHaveJSProperty('value', ['2022-06-01', '2022-06-02']);
+        });
+        test('clear button should work with multiple values', async () => {
+            const datetime = await datetimeFixture.goto(config, SINGLE_DATE, {
+                showClearButton: true,
+                showDefaultButtons: true,
+            });
+            const june2Button = datetime.locator('[data-month="6"][data-day="2"]');
+            const doneButton = datetime.locator('#confirm-button');
+            const clearButton = datetime.locator('#clear-button');
+            await june2Button.click();
+            await doneButton.click();
+            await clearButton.click();
+            await expect(datetime).toHaveJSProperty('value', undefined);
+        });
+        test('setting value programmatically should update active days', async () => {
+            const datetime = await datetimeFixture.goto(config, SINGLE_DATE);
+            const juneButtons = datetime.locator('[data-month="6"][data-day]');
+            await datetime.evaluate((el, dates) => {
+                el.value = dates;
+            }, MULTIPLE_DATES);
+            for (let i = 0; i < 3; i++) {
+                await expect(juneButtons.nth(i)).toHaveClass(/calendar-day-active/);
+            }
+            // ensure all days are still highlighted if we click another one after
+            await juneButtons.nth(3).click();
+            for (let i = 0; i < 4; i++) {
+                await expect(juneButtons.nth(i)).toHaveClass(/calendar-day-active/);
+            }
+        });
+        test('clicking day when no default value should set value to only clicked day', async ({ page }) => {
+            const datetime = await datetimeFixture.goto(config);
+            const ionChangeSpy = await page.spyOnEvent('ionChange');
+            // can't use specific data-month b/c no default value -- we don't know what it'll be
+            const firstDayButton = datetime.locator('.calendar-month:nth-child(2) [data-day="1"]');
+            const year = await firstDayButton.getAttribute('data-year');
+            let month = await firstDayButton.getAttribute('data-month');
+            if (month && month.length < 2)
+                month = '0' + month; // pad with zero
+            await firstDayButton.click();
+            await ionChangeSpy.next();
+            await expect(datetime).toHaveJSProperty('value', [`${year}-${month}-01`]);
+        });
+        test('header text should update correctly', async () => {
+            const datetime = await datetimeFixture.goto(config, SINGLE_DATE, { showDefaultTitle: true });
+            const header = datetime.locator('.datetime-selected-date');
+            const juneButtons = datetime.locator('[data-month="6"][data-day]');
+            await expect(header).toHaveText('Wed, Jun 1');
+            await juneButtons.nth(1).click();
+            await expect(header).toHaveText('2 days');
+            await juneButtons.nth(0).click();
+            await expect(header).toHaveText('Thu, Jun 2');
+            await juneButtons.nth(1).click();
+            await expect(header).toHaveText('0 days');
+        });
+        test('header text should update correctly with custom formatter', async () => {
+            const datetime = await datetimeFixture.goto(config, MULTIPLE_DATES, {
+                showDefaultTitle: true,
+                customFormatter: true,
+            });
+            const header = datetime.locator('.datetime-selected-date');
+            const juneButtons = datetime.locator('[data-month="6"][data-day]');
+            await expect(header).toHaveText('Selected: 3');
+            await juneButtons.nth(1).click();
+            await juneButtons.nth(2).click();
+            await expect(header).toHaveText('Wed, Jun 1');
+            await juneButtons.nth(0).click();
+            await expect(header).toHaveText('Selected: 0');
+        });
+        test('header text should render default date when multiple="false"', async ({ page }) => {
+            await page.setContent(`
+        <ion-datetime locale="en-US" show-default-title="true"></ion-datetime>
+
+        <script>
+          const mockToday = '2022-10-10T16:22';
+          Date = class extends Date {
+            constructor(...args) {
+              if (args.length === 0) {
+                super(mockToday)
+              } else {
+                super(...args);
+              }
+            }
+          }
+        </script>
+      `, config);
+            await page.locator(`.datetime-ready`).waitFor();
+            const datetime = page.locator('ion-datetime');
+            const header = datetime.locator('.datetime-selected-date');
+            await expect(header).toHaveText('Mon, Oct 10');
+        });
+    });
+});

+ 34 - 0
src/node_modules/@ionic/core/dist/collection/components/datetime/test/overlay-roles/datetime.e2e.js

@@ -0,0 +1,34 @@
+/*!
+ * (C) Ionic http://ionicframework.com - MIT License
+ */
+import { expect } from "@playwright/test";
+import { configs, test } from "../../../../utils/test/playwright/index";
+configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ config, title }) => {
+    test.describe(title('datetime: overlay roles'), () => {
+        test.beforeEach(async ({ page }) => {
+            await page.setContent(`
+          <ion-modal>
+            <ion-datetime></ion-datetime>
+          </ion-modal>  
+        `, config);
+        });
+        test('should pass role to overlay when calling confirm method', async ({ page }) => {
+            const ionModalDidDismiss = await page.spyOnEvent('ionModalDidDismiss');
+            const modal = page.locator('ion-modal');
+            const datetime = page.locator('ion-datetime');
+            await modal.evaluate((el) => el.present());
+            await datetime.evaluate((el) => el.confirm(true));
+            await ionModalDidDismiss.next();
+            expect(ionModalDidDismiss).toHaveReceivedEventDetail({ data: undefined, role: 'datetime-confirm' });
+        });
+        test('should pass role to overlay when calling cancel method', async ({ page }) => {
+            const ionModalDidDismiss = await page.spyOnEvent('ionModalDidDismiss');
+            const modal = page.locator('ion-modal');
+            const datetime = page.locator('ion-datetime');
+            await modal.evaluate((el) => el.present());
+            await datetime.evaluate((el) => el.cancel(true));
+            await ionModalDidDismiss.next();
+            expect(ionModalDidDismiss).toHaveReceivedEventDetail({ data: undefined, role: 'datetime-cancel' });
+        });
+    });
+});

+ 222 - 0
src/node_modules/@ionic/core/dist/collection/components/datetime/test/parse.spec.js

@@ -0,0 +1,222 @@
+/*!
+ * (C) Ionic http://ionicframework.com - MIT License
+ */
+import { clampDate, getPartsFromCalendarDay, parseAmPm, parseDate, parseMinParts, parseMaxParts } from "../utils/parse";
+describe('getPartsFromCalendarDay()', () => {
+    it('should extract DatetimeParts from a calendar day element', () => {
+        const div = document.createElement('div');
+        div.setAttribute('data-month', '4');
+        div.setAttribute('data-day', '15');
+        div.setAttribute('data-year', '2010');
+        div.setAttribute('data-day-of-week', '5');
+        expect(getPartsFromCalendarDay(div)).toEqual({
+            day: 15,
+            month: 4,
+            year: 2010,
+            dayOfWeek: 5,
+        });
+    });
+});
+describe('parseDate()', () => {
+    it('should return undefined when passed undefined', () => {
+        expect(parseDate(undefined)).toStrictEqual(undefined);
+    });
+    it('should return undefined when passed null', () => {
+        expect(parseDate(null)).toStrictEqual(undefined);
+    });
+    it('should return the correct date object when passed a date', () => {
+        expect(parseDate('2022-12-15T13:47')).toEqual({
+            ampm: 'pm',
+            day: 15,
+            hour: 13,
+            minute: 47,
+            month: 12,
+            year: 2022,
+        });
+    });
+    /**
+     * Note: As Ionic v7 datetime no longer parses time zone information/
+     * See https://github.com/ionic-team/ionic-framework/commit/3fb4caf21ffac12f765c4c80bf1850e05d211c6a
+     */
+    it('should return the correct time zone offset', () => {
+        var _a;
+        // Casting as any since `tzOffset` does not exist on DatetimeParts
+        expect((_a = parseDate('2022-12-15T13:47:30-02:00')) === null || _a === void 0 ? void 0 : _a.tzOffset).toEqual(undefined);
+    });
+    it('should parse an array of dates', () => {
+        expect(parseDate(['2022-12-15T13:47', '2023-03-23T20:19:33.517Z'])).toEqual([
+            {
+                ampm: 'pm',
+                day: 15,
+                hour: 13,
+                minute: 47,
+                month: 12,
+                year: 2022,
+            },
+            {
+                ampm: 'pm',
+                day: 23,
+                hour: 20,
+                minute: 19,
+                month: 3,
+                year: 2023,
+            },
+        ]);
+    });
+});
+describe('clampDate()', () => {
+    const minParts = {
+        year: 2021,
+        month: 6,
+        day: 5,
+    };
+    const maxParts = {
+        year: 2021,
+        month: 8,
+        day: 19,
+    };
+    it('should return the max month when the value is greater than the max', () => {
+        const dateParts = {
+            year: 2022,
+            month: 5,
+            day: 24,
+        };
+        const value = clampDate(dateParts, minParts, maxParts);
+        expect(value).toStrictEqual(maxParts);
+    });
+    it('should return the min month when the value is less than the min', () => {
+        const dateParts = {
+            year: 2020,
+            month: 5,
+            day: 24,
+        };
+        const value = clampDate(dateParts, minParts, maxParts);
+        expect(value).toStrictEqual(minParts);
+    });
+    it('should return the value when the value is greater than the min and less than the max', () => {
+        const dateParts = {
+            year: 2021,
+            month: 7,
+            day: 10,
+        };
+        const value = clampDate(dateParts, minParts, maxParts);
+        expect(value).toStrictEqual(dateParts);
+    });
+});
+describe('parseAmPm()', () => {
+    it('should return pm when the hour is greater than or equal to 12', () => {
+        expect(parseAmPm(12)).toEqual('pm');
+        expect(parseAmPm(13)).toEqual('pm');
+    });
+    it('should return am when the hour is less than 12', () => {
+        expect(parseAmPm(11)).toEqual('am');
+    });
+});
+describe('parseMinParts()', () => {
+    it('should fill in missing information when not provided', () => {
+        const today = {
+            day: 14,
+            month: 3,
+            year: 2022,
+            minute: 4,
+            hour: 2,
+        };
+        expect(parseMinParts('2012', today)).toEqual({
+            month: 1,
+            day: 1,
+            year: 2012,
+            hour: 0,
+            minute: 0,
+        });
+    });
+    it('should default to current year when only given HH:mm', () => {
+        const today = {
+            day: 14,
+            month: 3,
+            year: 2022,
+            minute: 4,
+            hour: 2,
+        };
+        expect(parseMinParts('04:30', today)).toEqual({
+            month: 1,
+            day: 1,
+            year: 2022,
+            hour: 4,
+            minute: 30,
+        });
+    });
+    it('should return undefined when given invalid info', () => {
+        const today = {
+            day: 14,
+            month: 3,
+            year: 2022,
+            minute: 4,
+            hour: 2,
+        };
+        expect(parseMinParts(undefined, today)).toEqual(undefined);
+        expect(parseMinParts(null, today)).toEqual(undefined);
+        expect(parseMinParts('foo', today)).toEqual(undefined);
+    });
+});
+describe('parseMaxParts()', () => {
+    it('should fill in missing information when not provided', () => {
+        const today = {
+            day: 14,
+            month: 3,
+            year: 2022,
+            minute: 4,
+            hour: 2,
+        };
+        expect(parseMaxParts('2012', today)).toEqual({
+            month: 12,
+            day: 31,
+            year: 2012,
+            hour: 23,
+            minute: 59,
+        });
+    });
+    it('should default to current year when only given HH:mm', () => {
+        const today = {
+            day: 14,
+            month: 3,
+            year: 2022,
+            minute: 4,
+            hour: 2,
+        };
+        expect(parseMaxParts('04:30', today)).toEqual({
+            month: 12,
+            day: 31,
+            year: 2022,
+            hour: 4,
+            minute: 30,
+        });
+    });
+    it('should fill in correct day during a leap year', () => {
+        const today = {
+            day: 14,
+            month: 3,
+            year: 2022,
+            minute: 4,
+            hour: 2,
+        };
+        expect(parseMaxParts('2012-02', today)).toEqual({
+            month: 2,
+            day: 29,
+            year: 2012,
+            hour: 23,
+            minute: 59,
+        });
+    });
+    it('should return undefined when given invalid info', () => {
+        const today = {
+            day: 14,
+            month: 3,
+            year: 2022,
+            minute: 4,
+            hour: 2,
+        };
+        expect(parseMaxParts(undefined, today)).toEqual(undefined);
+        expect(parseMaxParts(null, today)).toEqual(undefined);
+        expect(parseMaxParts('foo', today)).toEqual(undefined);
+    });
+});

+ 22 - 0
src/node_modules/@ionic/core/dist/collection/components/datetime/test/position/datetime.e2e.js

@@ -0,0 +1,22 @@
+/*!
+ * (C) Ionic http://ionicframework.com - MIT License
+ */
+import { expect } from "@playwright/test";
+import { configs, test } from "../../../../utils/test/playwright/index";
+configs().forEach(({ title, screenshot, config }) => {
+    test.describe(title('datetime: position'), () => {
+        test('should position the time picker relative to the click target', async ({ page }) => {
+            await page.goto('/src/components/datetime/test/position', config);
+            const ionPopoverDidPresent = await page.spyOnEvent('ionPopoverDidPresent');
+            const openDateTimeBtn = page.locator('#open-datetime');
+            await openDateTimeBtn.click();
+            await ionPopoverDidPresent.next();
+            await page.locator('.datetime-ready').waitFor();
+            await expect(page).toHaveScreenshot(screenshot(`datetime-position-base`));
+            const timepickerBtn = page.locator('ion-datetime .time-body');
+            await timepickerBtn.click();
+            await ionPopoverDidPresent.next();
+            await expect(page).toHaveScreenshot(screenshot(`datetime-position-popover`));
+        });
+    });
+});

+ 502 - 0
src/node_modules/@ionic/core/dist/collection/components/datetime/test/prefer-wheel/datetime.e2e.js

@@ -0,0 +1,502 @@
+/*!
+ * (C) Ionic http://ionicframework.com - MIT License
+ */
+import { expect } from "@playwright/test";
+import { configs, test } from "../../../../utils/test/playwright/index";
+configs().forEach(({ title, screenshot, config }) => {
+    /**
+     * When taking screenshots, be sure to
+     * set the datetime to size="cover". There
+     * are rendering quirks on Linux
+     * if the datetime is too small.
+     */
+    test.describe(title('datetime: wheel rendering'), () => {
+        test.beforeEach(async ({ page }) => {
+            await page.setViewportSize({
+                width: 400,
+                height: 200,
+            });
+        });
+        test('should not have visual regressions for date wheel', async ({ page }) => {
+            await page.setContent(`
+        <ion-datetime size="cover" presentation="date" prefer-wheel="true" value="2019-05-30" max="2022"></ion-datetime>
+      `, config);
+            await page.locator('.datetime-ready').waitFor();
+            await expect(page).toHaveScreenshot(screenshot(`datetime-wheel-date-diff`));
+        });
+        test('should not have visual regressions for date-time wheel', async ({ page }) => {
+            await page.setContent(`
+        <ion-datetime size="cover" presentation="date-time" prefer-wheel="true" value="2019-05-30T16:30:00" max="2022"></ion-datetime>
+      `, config);
+            await page.locator('.datetime-ready').waitFor();
+            await expect(page).toHaveScreenshot(screenshot(`datetime-wheel-date-time-diff`));
+        });
+        test('should not have visual regressions for time-date wheel', async ({ page }) => {
+            await page.setContent(`
+        <ion-datetime size="cover" presentation="time-date" prefer-wheel="true" value="2019-05-30T16:30:00" max="2022"></ion-datetime>
+      `, config);
+            await page.locator('.datetime-ready').waitFor();
+            await expect(page).toHaveScreenshot(screenshot(`datetime-wheel-time-date-diff`));
+        });
+        test('should render a condense header when specified', async ({ page }) => {
+            await page.setContent(`
+        <ion-datetime size="cover" presentation="time-date" prefer-wheel="true" value="2019-05-30T16:30:00" max="2022"><div slot="title">My Custom Title</div></ion-datetime>
+      `, config);
+            await page.locator('.datetime-ready').waitFor();
+            const datetime = page.locator('ion-datetime');
+            await expect(datetime).toHaveScreenshot(screenshot(`datetime-wheel-header-diff`));
+        });
+    });
+});
+/**
+ * This is testing component functionality which
+ * does not vary across modes/directions.
+ */
+configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
+    test.describe(title('datetime: preferWheel functionality'), () => {
+        test.describe('datetime: date wheel', () => {
+            test('should respect the min bounds', async ({ page }) => {
+                await page.setContent(`
+          <ion-datetime presentation="date" prefer-wheel="true" min="2019-05-05" max="2023-10-01" value="2019-05-30"></ion-datetime>
+        `, config);
+                await page.locator('.datetime-ready').waitFor();
+                const dayValues = page.locator('ion-datetime .day-column ion-picker-column-option');
+                expect(await dayValues.count()).toEqual(27);
+            });
+            test('should respect the max bounds', async ({ page }) => {
+                await page.setContent(`
+          <ion-datetime presentation="date" prefer-wheel="true" min="2019-05-05" max="2023-10-01" value="2023-10-01"></ion-datetime>
+        `, config);
+                await page.locator('.datetime-ready').waitFor();
+                const dayValues = page.locator('ion-datetime .day-column ion-picker-column-option');
+                expect(await dayValues.count()).toEqual(1);
+            });
+            test('should respect isDateEnabled preference', async ({ page }) => {
+                await page.setContent(`
+          <ion-datetime presentation="date" prefer-wheel="true" value="2022-01-01"></ion-datetime>
+          <script>
+            const datetime = document.querySelector('ion-datetime');
+            datetime.isDateEnabled = (dateIsoString) => {
+              const date = new Date(dateIsoString);
+              if (date.getUTCDate() % 2 === 0) {
+                return false;
+              }
+              return true;
+            }
+          </script>
+        `, config);
+                await page.locator('.datetime-ready').waitFor();
+                const disabledMonths = page.locator('.month-column ion-picker-column-option.option-disabled');
+                const disabledYears = page.locator('.year-column ion-picker-column-option.option-disabled');
+                const disabledDays = page.locator('.day-column ion-picker-column-option.option-disabled');
+                expect(await disabledMonths.count()).toBe(0);
+                expect(await disabledYears.count()).toBe(0);
+                expect(await disabledDays.count()).toBe(15);
+            });
+            test('should respect month, day, and year preferences', async ({ page }) => {
+                await page.setContent(`
+          <ion-datetime
+            presentation="date"
+            prefer-wheel="true"
+            value="2022-01-01"
+            month-values="4,6"
+            day-values="1,2,3,4,5"
+            year-values="2022,2020,2019"
+          ></ion-datetime>
+        `, config);
+                await page.locator('.datetime-ready').waitFor();
+                const monthValues = page.locator('.month-column ion-picker-column-option');
+                const yearValues = page.locator('.year-column ion-picker-column-option');
+                const dayValues = page.locator('.day-column ion-picker-column-option');
+                expect(await monthValues.count()).toBe(2);
+                expect(await yearValues.count()).toBe(3);
+                expect(await dayValues.count()).toBe(5);
+            });
+            test('selecting month should update value when no value is set', async ({ page }) => {
+                await page.setContent(`
+          <ion-datetime
+            presentation="date"
+            prefer-wheel="true"
+          ></ion-datetime>
+
+          <script>
+            const mockToday = '2022-10-10T16:22';
+            Date = class extends Date {
+              constructor(...args) {
+                if (args.length === 0) {
+                  super(mockToday)
+                } else {
+                  super(...args);
+                }
+              }
+            }
+          </script>
+        `, config);
+                await page.locator('.datetime-ready').waitFor();
+                const ionChange = await page.spyOnEvent('ionChange');
+                const monthValues = page.locator('.month-column ion-picker-column-option');
+                // Change month value
+                await monthValues.nth(0).click();
+                await ionChange.next();
+            });
+            test('selecting day should update value when no value is set', async ({ page }) => {
+                await page.setContent(`
+          <ion-datetime
+            presentation="date"
+            prefer-wheel="true"
+          ></ion-datetime>
+
+          <script>
+            const mockToday = '2022-10-10T16:22';
+            Date = class extends Date {
+              constructor(...args) {
+                if (args.length === 0) {
+                  super(mockToday)
+                } else {
+                  super(...args);
+                }
+              }
+            }
+          </script>
+        `, config);
+                await page.locator('.datetime-ready').waitFor();
+                const ionChange = await page.spyOnEvent('ionChange');
+                const dayValues = page.locator('.day-column ion-picker-column-option');
+                // Change day value
+                await dayValues.nth(0).click();
+                await ionChange.next();
+            });
+            test('selecting year should update value when no value is set', async ({ page }) => {
+                await page.setContent(`
+          <ion-datetime
+            presentation="date"
+            prefer-wheel="true"
+          ></ion-datetime>
+        `, config);
+                await page.locator('.datetime-ready').waitFor();
+                const ionChange = await page.spyOnEvent('ionChange');
+                const yearValues = page.locator('.year-column ion-picker-column-option');
+                /**
+                 * Change year value
+                 * The 0th index is the current
+                 * year, so select something other than that.
+                 */
+                await yearValues.nth(10).click();
+                await ionChange.next();
+            });
+            test('should jump to selected date when programmatically updating value', async ({ page }) => {
+                await page.setContent(`
+            <ion-datetime presentation="date" prefer-wheel="true" min="2019-05-05" max="2023-10-01" value="2019-05-30"></ion-datetime>
+          `, config);
+                await page.locator('.datetime-ready').waitFor();
+                const datetime = page.locator('ion-datetime');
+                await datetime.evaluate((el) => (el.value = '2021-05-25T12:40:00.000Z'));
+                await page.waitForChanges();
+                const selectedMonth = datetime.locator('.month-column ion-picker-column-option.option-active');
+                const selectedDay = datetime.locator('.day-column ion-picker-column-option.option-active');
+                const selectedYear = datetime.locator('.year-column ion-picker-column-option.option-active');
+                await expect(selectedMonth).toHaveText(/May/);
+                await expect(selectedDay).toHaveText(/25/);
+                await expect(selectedYear).toHaveText(/2021/);
+            });
+            test.describe('datetime: date wheel localization', () => {
+                test('should correctly localize the date data', async ({ page }) => {
+                    await page.setContent(`
+            <ion-datetime
+              presentation="date"
+              prefer-wheel="true"
+              locale="ja-JP"
+              min="2022-01-01"
+              max="2022-03-01"
+              day-values="1,2,3"
+              value="2022-01-01"
+            ></ion-datetime>
+          `, config);
+                    await page.locator('.datetime-ready').waitFor();
+                    const monthValues = page.locator('.month-column ion-picker-column-option');
+                    const dayValues = page.locator('.day-column ion-picker-column-option');
+                    await expect(monthValues).toHaveText(['1月', '2月', '3月']);
+                    await expect(dayValues).toHaveText(['1日', '2日', '3日']);
+                });
+                test('should render the columns according to locale - en-US', async ({ page }) => {
+                    await page.setContent(`
+            <ion-datetime
+              presentation="date"
+              prefer-wheel="true"
+              locale="en-US"
+              value="2022-01-01"
+            ></ion-datetime>
+          `, config);
+                    await page.locator('.datetime-ready').waitFor();
+                    const columns = page.locator('ion-picker-column');
+                    await expect(columns.nth(0)).toHaveClass(/month-column/);
+                    await expect(columns.nth(1)).toHaveClass(/day-column/);
+                    await expect(columns.nth(2)).toHaveClass(/year-column/);
+                });
+                test('should render the columns according to locale - en-GB', async ({ page }) => {
+                    await page.setContent(`
+            <ion-datetime
+              presentation="date"
+              prefer-wheel="true"
+              locale="en-GB"
+              value="2022-01-01"
+            ></ion-datetime>
+          `, config);
+                    await page.locator('.datetime-ready').waitFor();
+                    const columns = page.locator('ion-picker-column');
+                    await expect(columns.nth(0)).toHaveClass(/day-column/);
+                    await expect(columns.nth(1)).toHaveClass(/month-column/);
+                    await expect(columns.nth(2)).toHaveClass(/year-column/);
+                });
+            });
+        });
+        test.describe('datetime: date-time wheel', () => {
+            test('should respect the min bounds', async ({ page }) => {
+                await page.setContent(`
+          <ion-datetime presentation="date-time" prefer-wheel="true" min="2019-05-05" value="2019-05-10T16:30:00"></ion-datetime>
+        `, config);
+                await page.locator('.datetime-ready').waitFor();
+                const dayValues = page.locator('ion-datetime .date-column ion-picker-column-option');
+                expect(await dayValues.count()).toEqual(57);
+            });
+            test('should respect the max bounds', async ({ page }) => {
+                await page.setContent(`
+          <ion-datetime presentation="date-time" prefer-wheel="true" max="2023-06-10" value="2023-06-05T16:30:00"></ion-datetime>
+        `, config);
+                await page.locator('.datetime-ready').waitFor();
+                const dayValues = page.locator('ion-datetime .date-column ion-picker-column-option');
+                expect(await dayValues.count()).toEqual(41);
+            });
+            test('should respect isDateEnabled preference', async ({ page }) => {
+                await page.setContent(`
+          <ion-datetime presentation="date-time" prefer-wheel="true" value="2022-02-01T16:30:00"></ion-datetime>
+          <script>
+            const datetime = document.querySelector('ion-datetime');
+            datetime.isDateEnabled = (dateIsoString) => {
+              const date = new Date(dateIsoString);
+              if (date.getUTCDate() % 2 === 0) {
+                return false;
+              }
+              return true;
+            }
+          </script>
+        `, config);
+                await page.locator('.datetime-ready').waitFor();
+                const disabledDates = page.locator('.date-column ion-picker-column-option.option-disabled');
+                expect(await disabledDates.count()).toBe(44);
+            });
+            test('should respect month, day, and year preferences', async ({ page }) => {
+                await page.setContent(`
+          <ion-datetime
+            presentation="date-time"
+            prefer-wheel="true"
+            value="2022-02-01"
+            month-values="2"
+            day-values="1,2,3,4,5"
+            year-values="2022,2020,2019"
+          ></ion-datetime>
+        `, config);
+                await page.locator('.datetime-ready').waitFor();
+                const dateValues = page.locator('.date-column ion-picker-column-option');
+                expect(await dateValues.count()).toBe(5);
+            });
+            test('should correctly localize the date data', async ({ page }) => {
+                await page.setContent(`
+          <ion-datetime
+            presentation="date-time"
+            prefer-wheel="true"
+            locale="ja-JP"
+            month-values="2"
+            day-values="1,2,3"
+            value="2022-02-01"
+          ></ion-datetime>
+        `, config);
+                await page.locator('.datetime-ready').waitFor();
+                const dateValues = page.locator('.date-column ion-picker-column-option');
+                await expect(dateValues).toHaveText(['2月1日(火)', '2月2日(水)', '2月3日(木)']);
+            });
+            test('should respect min and max bounds even across years', async ({ page }) => {
+                await page.setContent(`
+          <ion-datetime
+            presentation="date-time"
+            prefer-wheel="true"
+            value="2022-02-01"
+            min="2021-12-01"
+            max="2023-01-01"
+          ></ion-datetime>
+        `, config);
+                await page.locator('.datetime-ready').waitFor();
+                const dateColumn = page.locator('.date-column');
+                const dateColumnScrollEl = dateColumn.locator('.picker-opts');
+                const dateValues = dateColumn.locator('ion-picker-column-option');
+                expect(await dateValues.count()).toBe(90);
+                /**
+                 * Select 1st item to change the dates rendered
+                 */
+                await expect(dateValues.nth(0)).toHaveJSProperty('value', '2022-1-1');
+                await dateColumnScrollEl.evaluate((el) => (el.scrollTop = 0));
+                await page.waitForChanges();
+                await expect(dateValues.nth(0)).toHaveJSProperty('value', '2021-12-1');
+            });
+            test('should keep sliding window if default window is within min and max constraints', async ({ page }) => {
+                await page.setContent(`
+          <ion-datetime
+            presentation="date-time"
+            prefer-wheel="true"
+            value="2022-06-01"
+            max="2030-01-01"
+            min="2010-01-01"
+          ></ion-datetime>
+        `, config);
+                await page.locator('.datetime-ready').waitFor();
+                const dayValues = page.locator('.date-column ion-picker-column-option');
+                expect(await dayValues.count()).toBe(92);
+            });
+            test('should narrow sliding window if default window is not within min and max constraints', async ({ page }) => {
+                await page.setContent(`
+          <ion-datetime
+            presentation="date-time"
+            prefer-wheel="true"
+            value="2022-06-01"
+            max="2022-05-15"
+            min="2022-05-01"
+          ></ion-datetime>
+        `, config);
+                await page.locator('.datetime-ready').waitFor();
+                const dayValues = page.locator('.date-column ion-picker-column-option');
+                expect(await dayValues.count()).toBe(15);
+            });
+            test('selecting date should update value when no value is set', async ({ page }) => {
+                await page.setContent(`
+          <ion-datetime
+            presentation="date-time"
+            prefer-wheel="true"
+          ></ion-datetime>
+        `, config);
+                await page.locator('.datetime-ready').waitFor();
+                const ionChange = await page.spyOnEvent('ionChange');
+                const dayValues = page.locator('.date-column ion-picker-column-option');
+                // Change day/month value
+                await dayValues.nth(0).click();
+                await ionChange.next();
+            });
+        });
+        test.describe('datetime: time-date wheel', () => {
+            test('should respect the min bounds', async ({ page }) => {
+                await page.setContent(`
+          <ion-datetime presentation="time-date" prefer-wheel="true" min="2019-05-05" value="2019-05-10T16:30:00"></ion-datetime>
+        `, config);
+                await page.locator('.datetime-ready').waitFor();
+                const dayValues = page.locator('ion-datetime .date-column ion-picker-column-option');
+                expect(await dayValues.count()).toEqual(57);
+            });
+            test('should respect the max bounds', async ({ page }) => {
+                await page.setContent(`
+          <ion-datetime presentation="time-date" prefer-wheel="true" max="2023-06-10" value="2023-06-05T16:30:00"></ion-datetime>
+        `, config);
+                await page.locator('.datetime-ready').waitFor();
+                const dayValues = page.locator('ion-datetime .date-column ion-picker-column-option');
+                expect(await dayValues.count()).toEqual(41);
+            });
+            test('should respect isDateEnabled preference', async ({ page }) => {
+                await page.setContent(`
+          <ion-datetime presentation="time-date" prefer-wheel="true" value="2022-02-01T16:30:00"></ion-datetime>
+          <script>
+            const datetime = document.querySelector('ion-datetime');
+            datetime.isDateEnabled = (dateIsoString) => {
+              const date = new Date(dateIsoString);
+              if (date.getUTCDate() % 2 === 0) {
+                return false;
+              }
+              return true;
+            }
+          </script>
+        `, config);
+                await page.locator('.datetime-ready').waitFor();
+                const disabledDates = page.locator('.date-column ion-picker-column-option.option-disabled');
+                expect(await disabledDates.count()).toBe(44);
+            });
+            test('should respect month, day, and year preferences', async ({ page }) => {
+                await page.setContent(`
+          <ion-datetime
+            presentation="time-date"
+            prefer-wheel="true"
+            value="2022-02-01"
+            month-values="2"
+            day-values="1,2,3,4,5"
+            year-values="2022,2020,2019"
+          ></ion-datetime>
+        `, config);
+                await page.locator('.datetime-ready').waitFor();
+                const dateValues = page.locator('.date-column ion-picker-column-option');
+                expect(await dateValues.count()).toBe(5);
+            });
+            test('should correctly localize the date data', async ({ page }) => {
+                await page.setContent(`
+          <ion-datetime
+            presentation="time-date"
+            prefer-wheel="true"
+            locale="ja-JP"
+            month-values="2"
+            day-values="1,2,3"
+            value="2022-02-01"
+          ></ion-datetime>
+        `, config);
+                await page.locator('.datetime-ready').waitFor();
+                const dateValues = page.locator('.date-column ion-picker-column-option');
+                await expect(dateValues).toHaveText(['2月1日(火)', '2月2日(水)', '2月3日(木)']);
+            });
+            test('should respect min and max bounds even across years', async ({ page }) => {
+                await page.setContent(`
+          <ion-datetime
+            presentation="time-date"
+            prefer-wheel="true"
+            value="2022-02-01"
+            min="2021-12-01"
+            max="2023-01-01"
+          ></ion-datetime>
+        `, config);
+                await page.locator('.datetime-ready').waitFor();
+                const dateColumn = page.locator('.date-column');
+                const dateColumnScrollEl = dateColumn.locator('.picker-opts');
+                const dateValues = dateColumn.locator('ion-picker-column-option');
+                expect(await dateValues.count()).toBe(90);
+                /**
+                 * Select 1st item to change the dates rendered
+                 */
+                await expect(dateValues.nth(0)).toHaveJSProperty('value', '2022-1-1');
+                await dateColumnScrollEl.evaluate((el) => (el.scrollTop = 0));
+                await page.waitForChanges();
+                await expect(dateValues.nth(0)).toHaveJSProperty('value', '2021-12-1');
+            });
+            test('should keep sliding window if default window is within min and max constraints', async ({ page }) => {
+                await page.setContent(`
+        <ion-datetime
+          presentation="time-date"
+          prefer-wheel="true"
+          value="2022-06-01"
+          max="2030-01-01"
+          min="2010-01-01"
+        ></ion-datetime>
+      `, config);
+                await page.locator('.datetime-ready').waitFor();
+                const dayValues = page.locator('.date-column ion-picker-column-option');
+                expect(await dayValues.count()).toBe(92);
+            });
+            test('should narrow sliding window if default window is not within min and max constraints', async ({ page }) => {
+                await page.setContent(`
+        <ion-datetime
+          presentation="time-date"
+          prefer-wheel="true"
+          value="2022-06-01"
+          max="2022-05-15"
+          min="2022-05-01"
+        ></ion-datetime>
+      `, config);
+                await page.locator('.datetime-ready').waitFor();
+                const dayValues = page.locator('.date-column ion-picker-column-option');
+                expect(await dayValues.count()).toBe(15);
+            });
+        });
+    });
+});

+ 27 - 0
src/node_modules/@ionic/core/dist/collection/components/datetime/test/prefer-wheel/datetime.spec.js

@@ -0,0 +1,27 @@
+/*!
+ * (C) Ionic http://ionicframework.com - MIT License
+ */
+import { newSpecPage } from "@stencil/core/testing";
+import { Datetime } from "../../datetime";
+describe('datetime: preferWheel', () => {
+    beforeEach(() => {
+        const mockIntersectionObserver = jest.fn();
+        mockIntersectionObserver.mockReturnValue({
+            observe: () => null,
+            unobserve: () => null,
+            disconnect: () => null,
+        });
+        global.IntersectionObserver = mockIntersectionObserver;
+    });
+    it('should select the working day when clicking the confirm button', async () => {
+        const page = await newSpecPage({
+            components: [Datetime],
+            html: '<ion-datetime prefer-wheel="true" max="2021" show-default-buttons="true"></ion-datetime>',
+        });
+        const datetime = page.body.querySelector('ion-datetime');
+        const confirmButton = datetime.shadowRoot.querySelector('#confirm-button');
+        confirmButton.click();
+        await page.waitForChanges();
+        expect(datetime.value).toBe('2021-12-31T23:59:00');
+    });
+});

+ 169 - 0
src/node_modules/@ionic/core/dist/collection/components/datetime/test/presentation/datetime.e2e.js

@@ -0,0 +1,169 @@
+/*!
+ * (C) Ionic http://ionicframework.com - MIT License
+ */
+import { expect } from "@playwright/test";
+import { configs, test } from "../../../../utils/test/playwright/index";
+configs().forEach(({ title, screenshot, config }) => {
+    test.describe(title('datetime: presentation rendering'), () => {
+        let presentationFixture;
+        test.beforeEach(({ page }) => {
+            presentationFixture = new DatetimePresentationFixture(page);
+        });
+        test('should not have visual regressions with date-time presentation', async () => {
+            await presentationFixture.goto('date-time', config);
+            await presentationFixture.screenshot('datetime-presentation-date-time-diff', screenshot);
+        });
+        test('should not have visual regressions with time-date presentation', async () => {
+            await presentationFixture.goto('time-date', config);
+            await presentationFixture.screenshot('datetime-presentation-time-date-diff', screenshot);
+        });
+        test('should not have visual regressions with time presentation', async () => {
+            await presentationFixture.goto('time', config);
+            await presentationFixture.screenshot('datetime-presentation-time-diff', screenshot);
+        });
+        test('should not have visual regressions with date presentation', async () => {
+            await presentationFixture.goto('date', config);
+            await presentationFixture.screenshot('datetime-presentation-date-diff', screenshot);
+        });
+        test('should not have visual regressions with month-year presentation', async () => {
+            await presentationFixture.goto('month-year', config);
+            await presentationFixture.screenshot('datetime-presentation-month-year-diff', screenshot);
+        });
+        test('should not have visual regressions with month presentation', async () => {
+            await presentationFixture.goto('month', config);
+            await presentationFixture.screenshot('datetime-presentation-month-diff', screenshot);
+        });
+        test('should not have visual regressions with year presentation', async () => {
+            await presentationFixture.goto('year', config);
+            await presentationFixture.screenshot('datetime-presentation-year-diff', screenshot);
+        });
+    });
+});
+/**
+ * This is testing component event behavior
+ * which does not vary across modes/directions.
+ */
+configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => {
+    test.describe(title('datetime: presentation'), () => {
+        test('presentation: time: should emit ionChange', async ({ page }) => {
+            await page.goto(`/src/components/datetime/test/presentation`, config);
+            const ionChangeSpy = await page.spyOnEvent('ionChange');
+            await page.locator('select').selectOption('time');
+            await page.waitForChanges();
+            await page.locator('text=AM').click();
+            await ionChangeSpy.next();
+            expect(ionChangeSpy.length).toBe(1);
+        });
+        test('presentation: month-year: should emit ionChange', async ({ page }) => {
+            await page.goto(`/src/components/datetime/test/presentation`, config);
+            const ionChangeSpy = await page.spyOnEvent('ionChange');
+            await page.locator('select').selectOption('month-year');
+            await page.waitForChanges();
+            await page.locator('text=April').click();
+            await ionChangeSpy.next();
+            expect(ionChangeSpy.length).toBe(1);
+        });
+        test('presentation: month: should emit ionChange', async ({ page }) => {
+            await page.goto(`/src/components/datetime/test/presentation`, config);
+            const ionChangeSpy = await page.spyOnEvent('ionChange');
+            await page.locator('select').selectOption('month');
+            await page.waitForChanges();
+            await page.locator('text=April').click();
+            await ionChangeSpy.next();
+            expect(ionChangeSpy.length).toBe(1);
+        });
+        test('presentation: year: should emit ionChange', async ({ page }) => {
+            await page.goto(`/src/components/datetime/test/presentation`, config);
+            const ionChangeSpy = await page.spyOnEvent('ionChange');
+            await page.locator('select').selectOption('year');
+            await page.waitForChanges();
+            await page.locator('text=2021').click();
+            await ionChangeSpy.next();
+            expect(ionChangeSpy.length).toBe(1);
+        });
+        test('switching presentation should close month/year picker', async ({ page }) => {
+            await page.setContent(`
+        <ion-datetime presentation="date"></ion-datetime>
+      `, config);
+            await page.locator('.datetime-ready').waitFor();
+            const datetime = page.locator('ion-datetime');
+            const monthYearButton = page.locator('ion-datetime .calendar-month-year');
+            await monthYearButton.click();
+            await expect(datetime).toHaveClass(/show-month-and-year/);
+            await datetime.evaluate((el) => (el.presentation = 'time'));
+            await page.waitForChanges();
+            await expect(datetime).not.toHaveClass(/show-month-and-year/);
+        });
+    });
+});
+/**
+ * This is testing time rendering behavior
+ * which does not vary across modes/directions.
+ */
+configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => {
+    test.describe(title('datetime: presentation: time'), () => {
+        test('changing value from AM to AM should update the text', async ({ page }) => {
+            const timePickerFixture = new TimePickerFixture(page);
+            await timePickerFixture.goto('04:20:00', config);
+            await timePickerFixture.setValue('11:03:00');
+            await timePickerFixture.expectTime(11, 3, 'am');
+        });
+        test('changing value from AM to PM should update the text', async ({ page }) => {
+            const timePickerFixture = new TimePickerFixture(page);
+            await timePickerFixture.goto('05:30:00', config);
+            await timePickerFixture.setValue('16:40:00');
+            await timePickerFixture.expectTime(16, 40, 'pm');
+        });
+        test('changing the value from PM to AM should update the text', async ({ page }) => {
+            const timePickerFixture = new TimePickerFixture(page);
+            await timePickerFixture.goto('16:40:00', config);
+            await timePickerFixture.setValue('04:20:00');
+            await timePickerFixture.expectTime(4, 20, 'am');
+        });
+        test('changing the value from PM to PM should update the text', async ({ page }) => {
+            const timePickerFixture = new TimePickerFixture(page);
+            await timePickerFixture.goto('16:40:00', config);
+            await timePickerFixture.setValue('19:32:00');
+            await timePickerFixture.expectTime(19, 32, 'pm');
+        });
+    });
+});
+class DatetimePresentationFixture {
+    constructor(page) {
+        this.page = page;
+    }
+    async goto(presentation, config) {
+        await this.page.setContent(`
+      <ion-datetime presentation="${presentation}" value="2010-03-10T13:00:00"></ion-datetime>
+    `, config);
+        await this.page.locator('.datetime-ready').waitFor();
+        this.datetime = this.page.locator('ion-datetime');
+    }
+    async screenshot(name, screenshotFn) {
+        await expect(this.datetime).toHaveScreenshot(screenshotFn(name));
+    }
+}
+class TimePickerFixture {
+    constructor(page) {
+        this.page = page;
+    }
+    async goto(value, config) {
+        await this.page.setContent(`
+      <ion-datetime presentation="time" value="${value}"></ion-datetime>
+    `, config);
+        await this.page.locator('.datetime-ready').waitFor();
+        this.timePicker = this.page.locator('ion-datetime');
+    }
+    async setValue(value) {
+        await this.timePicker.evaluate((el, newValue) => {
+            el.value = newValue;
+        }, value);
+        await this.page.waitForChanges();
+    }
+    async expectTime(hour, minute, ampm) {
+        const pickerColumns = this.timePicker.locator('ion-picker-column');
+        await expect(pickerColumns.nth(0)).toHaveJSProperty('value', hour);
+        await expect(pickerColumns.nth(1)).toHaveJSProperty('value', minute);
+        await expect(pickerColumns.nth(2)).toHaveJSProperty('value', ampm);
+    }
+}

+ 113 - 0
src/node_modules/@ionic/core/dist/collection/components/datetime/test/readonly/datetime.e2e.js

@@ -0,0 +1,113 @@
+/*!
+ * (C) Ionic http://ionicframework.com - MIT License
+ */
+import { expect } from "@playwright/test";
+import { configs, test } from "../../../../utils/test/playwright/index";
+/**
+ * This behavior does not differ across
+ * modes/directions.
+ */
+configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config, screenshot }) => {
+    test.describe(title('datetime: readonly'), () => {
+        test('should not have visual regressions', async ({ page }) => {
+            await page.setContent(`
+        <ion-datetime value="2022-02-05T00:00:00" min="2022-01-01T00:00:00" max="2022-02-20T23:59:59" day-values="5,6,10,11,15,16,20" show-default-buttons readonly></ion-datetime>
+    `, config);
+            const datetime = page.locator('ion-datetime');
+            await expect(datetime).toHaveScreenshot(screenshot(`datetime-readonly`));
+        });
+        test('date should be disabled', async ({ page }) => {
+            await page.setContent(`
+        <ion-datetime value="2022-02-28" readonly></ion-datetime>
+    `, config);
+            await page.locator('.datetime-ready').waitFor();
+            const febFirstButton = page.locator(`.calendar-day[data-day='1'][data-month='2']`);
+            await expect(febFirstButton).toBeDisabled();
+        });
+        test('should navigate months via month-year button', async ({ page }) => {
+            await page.setContent(`
+        <ion-datetime value="2022-02-22T16:30:00" readonly></ion-datetime>
+    `, config);
+            const ionChange = await page.spyOnEvent('ionChange');
+            await page.locator('.datetime-ready').waitFor();
+            const calendarMonthYear = page.locator('ion-datetime .calendar-month-year');
+            await expect(calendarMonthYear).toHaveText('February 2022');
+            await calendarMonthYear.click();
+            await page.waitForChanges();
+            await page.locator('.month-column ion-picker-column-option').nth(2).click();
+            await page.waitForChanges();
+            await expect(calendarMonthYear).toHaveText('March 2022');
+            await expect(ionChange).not.toHaveReceivedEvent();
+        });
+        test('should open picker using keyboard navigation', async ({ page, browserName }) => {
+            await page.setContent(`
+        <ion-datetime value="2022-02-22T16:30:00" readonly></ion-datetime>
+    `, config);
+            const tabKey = browserName === 'webkit' ? 'Alt+Tab' : 'Tab';
+            await page.locator('.datetime-ready').waitFor();
+            const calendarMonthYear = page.locator('ion-datetime .calendar-month-year');
+            const monthYearButton = page.locator('.calendar-month-year-toggle');
+            await expect(calendarMonthYear).toHaveText('February 2022');
+            await page.keyboard.press(tabKey);
+            await expect(monthYearButton).toBeFocused();
+            await page.waitForChanges();
+            await page.keyboard.press('Enter');
+            await page.waitForChanges();
+            const marchPickerItem = page.locator('.month-column ion-picker-column-option').nth(2);
+            await expect(marchPickerItem).toBeVisible();
+        });
+        test('should view next month via next button', async ({ page }) => {
+            await page.setContent(`
+        <ion-datetime value="2022-02-22T16:30:00" readonly></ion-datetime>
+    `, config);
+            const ionChange = await page.spyOnEvent('ionChange');
+            const calendarMonthYear = page.locator('ion-datetime .calendar-month-year');
+            await expect(calendarMonthYear).toHaveText('February 2022');
+            const nextMonthButton = page.locator('ion-datetime .calendar-next-prev ion-button + ion-button');
+            await nextMonthButton.click();
+            await page.waitForChanges();
+            await expect(calendarMonthYear).toHaveText('March 2022');
+            await expect(ionChange).not.toHaveReceivedEvent();
+        });
+        test('should not change value when the month is changed via keyboard navigation', async ({ page, browserName }) => {
+            await page.setContent(`
+        <ion-datetime value="2022-02-22T16:30:00" readonly></ion-datetime>
+    `, config);
+            const tabKey = browserName === 'webkit' ? 'Alt+Tab' : 'Tab';
+            const datetime = page.locator('ion-datetime');
+            const monthYearButton = page.locator('.calendar-month-year-toggle');
+            const prevButton = page.locator('.calendar-next-prev ion-button:nth-child(1)');
+            const nextButton = page.locator('.calendar-next-prev ion-button:nth-child(2)');
+            const calendarMonthYear = page.locator('ion-datetime .calendar-month-year');
+            await page.keyboard.press(tabKey);
+            await expect(monthYearButton).toBeFocused();
+            await page.keyboard.press(tabKey);
+            await expect(prevButton).toBeFocused();
+            await page.keyboard.press(tabKey);
+            await expect(nextButton).toBeFocused();
+            // check value before & after selecting via keyboard
+            const initialValue = await datetime.evaluate((el) => el.value);
+            expect(initialValue).toBe('2022-02-22T16:30:00');
+            await expect(calendarMonthYear).toHaveText('February 2022');
+            await page.keyboard.press(tabKey);
+            await page.waitForChanges();
+            await page.keyboard.press('ArrowLeft');
+            await page.waitForChanges();
+            await expect(calendarMonthYear).toHaveText('January 2022');
+            await page.keyboard.press('Enter');
+            await page.waitForChanges();
+            const newValue = await datetime.evaluate((el) => el.value);
+            // should not have changed
+            expect(newValue).toBe('2022-02-22T16:30:00');
+        });
+        test('clear button should be disabled', async ({ page }) => {
+            await page.setContent(`
+
+        <ion-datetime value="2022-02-22T16:30:00" show-default-buttons="true" show-clear-button="true" readonly></ion-datetime>
+    `, config);
+            await page.locator('.datetime-ready').waitFor();
+            const clearButton = page.locator('#clear-button button');
+            await expect(clearButton).toBeDisabled();
+        });
+    });
+});

+ 50 - 0
src/node_modules/@ionic/core/dist/collection/components/datetime/test/set-value/datetime.e2e.js

@@ -0,0 +1,50 @@
+/*!
+ * (C) Ionic http://ionicframework.com - MIT License
+ */
+import { expect } from "@playwright/test";
+import { configs, test } from "../../../../utils/test/playwright/index";
+configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => {
+    test.describe(title('datetime: set-value'), () => {
+        test('should update the active date when value is initially set', async ({ page }) => {
+            await page.goto('/src/components/datetime/test/set-value', config);
+            await page.locator('.datetime-ready').waitFor();
+            const datetime = page.locator('ion-datetime');
+            await datetime.evaluate((el) => (el.value = '2021-11-25T12:40:00.000Z'));
+            await page.waitForChanges();
+            const activeDate = page.locator('ion-datetime .calendar-day-active');
+            await expect(activeDate).toHaveText('25');
+        });
+        test('should update the active time when value is initially set', async ({ page }) => {
+            await page.goto('/src/components/datetime/test/set-value', config);
+            await page.locator('.datetime-ready').waitFor();
+            const datetime = page.locator('ion-datetime');
+            await datetime.evaluate((el) => (el.value = '2021-11-25T12:40:00.000Z'));
+            await page.waitForChanges();
+            const activeDate = page.locator('ion-datetime .time-body');
+            await expect(activeDate).toHaveText('12:40 PM');
+        });
+        test('should update active item when value is not initially set', async ({ page }) => {
+            await page.setContent(`
+        <ion-datetime presentation="date" locale="en-US"></ion-datetime>
+      `, config);
+            await page.locator('.datetime-ready').waitFor();
+            const datetime = page.locator('ion-datetime');
+            const activeDayButton = page.locator('.calendar-day-active');
+            await datetime.evaluate((el) => (el.value = '2021-10-05'));
+            await page.waitForChanges();
+            // Check that correct day is highlighted
+            await expect(activeDayButton).toHaveAttribute('data-day', '5');
+            await expect(activeDayButton).toHaveAttribute('data-month', '10');
+            await expect(activeDayButton).toHaveAttribute('data-year', '2021');
+        });
+        test('should scroll to new month when value is initially set and then updated', async ({ page }) => {
+            await page.goto('/src/components/datetime/test/set-value', config);
+            await page.locator('.datetime-ready').waitFor();
+            const datetime = page.locator('ion-datetime');
+            await datetime.evaluate((el) => (el.value = '2021-05-25T12:40:00.000Z'));
+            await page.waitForChanges();
+            const calendarHeader = datetime.locator('.calendar-month-year');
+            await expect(calendarHeader).toHaveText(/May 2021/);
+        });
+    });
+});

+ 114 - 0
src/node_modules/@ionic/core/dist/collection/components/datetime/test/state.spec.js

@@ -0,0 +1,114 @@
+/*!
+ * (C) Ionic http://ionicframework.com - MIT License
+ */
+import { getCalendarDayState, isDayDisabled, isNextMonthDisabled, isPrevMonthDisabled } from "../utils/state";
+describe('getCalendarDayState()', () => {
+    it('should return correct state', () => {
+        const refA = { month: 1, day: 1, year: 2019 };
+        const refB = { month: 1, day: 1, year: 2021 };
+        const refC = { month: 1, day: 1, year: 2023 };
+        expect(getCalendarDayState('en-US', refA, refB, refC)).toEqual({
+            isActive: false,
+            isToday: false,
+            disabled: false,
+            ariaSelected: null,
+            ariaLabel: 'Tuesday, January 1',
+            text: '1',
+        });
+        expect(getCalendarDayState('en-US', refA, refA, refC)).toEqual({
+            isActive: true,
+            isToday: false,
+            disabled: false,
+            ariaSelected: 'true',
+            ariaLabel: 'Tuesday, January 1',
+            text: '1',
+        });
+        expect(getCalendarDayState('en-US', refA, refB, refA)).toEqual({
+            isActive: false,
+            isToday: true,
+            disabled: false,
+            ariaSelected: null,
+            ariaLabel: 'Today, Tuesday, January 1',
+            text: '1',
+        });
+        expect(getCalendarDayState('en-US', refA, refA, refA)).toEqual({
+            isActive: true,
+            isToday: true,
+            disabled: false,
+            ariaSelected: 'true',
+            ariaLabel: 'Today, Tuesday, January 1',
+            text: '1',
+        });
+        expect(getCalendarDayState('en-US', refA, refA, refA, undefined, undefined, [1])).toEqual({
+            isActive: true,
+            isToday: true,
+            disabled: false,
+            ariaSelected: 'true',
+            ariaLabel: 'Today, Tuesday, January 1',
+            text: '1',
+        });
+        expect(getCalendarDayState('en-US', refA, refA, refA, undefined, undefined, [2])).toEqual({
+            isActive: true,
+            isToday: true,
+            disabled: true,
+            ariaSelected: 'true',
+            ariaLabel: 'Today, Tuesday, January 1',
+            text: '1',
+        });
+    });
+});
+describe('isDayDisabled()', () => {
+    it('should correctly return whether or not a day is disabled', () => {
+        const refDate = { month: 5, day: 12, year: 2021 };
+        expect(isDayDisabled(refDate, undefined, undefined)).toEqual(false);
+        expect(isDayDisabled(refDate, { month: 5, day: 12, year: 2021 }, undefined)).toEqual(false);
+        expect(isDayDisabled(refDate, { month: 6, day: 12, year: 2021 }, undefined)).toEqual(true);
+        expect(isDayDisabled(refDate, { month: 5, day: 13, year: 2022 }, undefined)).toEqual(true);
+        expect(isDayDisabled(refDate, undefined, { month: 5, day: 12, year: 2021 })).toEqual(false);
+        expect(isDayDisabled(refDate, undefined, { month: 4, day: 12, year: 2021 })).toEqual(true);
+        expect(isDayDisabled(refDate, undefined, { month: 5, day: 11, year: 2021 })).toEqual(true);
+    });
+});
+describe('isPrevMonthDisabled()', () => {
+    it('should return true', () => {
+        // Date month is before min month, in the same year
+        expect(isPrevMonthDisabled({ month: 5, year: 2021, day: null }, { month: 6, year: 2021, day: null })).toEqual(true);
+        // Date month and year is the same as min month and year
+        expect(isPrevMonthDisabled({ month: 1, year: 2021, day: null }, { month: 1, year: 2021, day: null })).toEqual(true);
+        // Date year is the same as min year (month not provided)
+        expect(isPrevMonthDisabled({ month: 1, year: 2021, day: null }, { year: 2021, month: null, day: null })).toEqual(true);
+        // Date year is less than the min year (month not provided)
+        expect(isPrevMonthDisabled({ month: 5, year: 2021, day: null }, { year: 2022, month: null, day: null })).toEqual(true);
+        // Date is above the maximum bounds and the previous month does not does not fall within the
+        // min-max range.
+        expect(isPrevMonthDisabled({ month: 12, year: 2021, day: null }, { month: 9, year: 2021, day: null }, { month: 10, year: 2021, day: null })).toEqual(true);
+        // Date is above the maximum bounds and a year ahead of the max range. The previous month/year
+        // does not fall within the min-max range.
+        expect(isPrevMonthDisabled({ month: 1, year: 2022, day: null }, { month: 9, year: 2021, day: null }, { month: 10, year: 2021, day: null })).toEqual(true);
+    });
+    it('should return false', () => {
+        // No min range provided
+        expect(isPrevMonthDisabled({ month: 12, year: 2021, day: null })).toEqual(false);
+        // Date year is the same as min year,
+        // but can navigate to a previous month without reducing the year.
+        expect(isPrevMonthDisabled({ month: 12, year: 2021, day: null }, { year: 2021, month: null, day: null })).toEqual(false);
+        expect(isPrevMonthDisabled({ month: 2, year: 2021, day: null }, { year: 2021, month: null, day: null })).toEqual(false);
+    });
+});
+describe('isNextMonthDisabled()', () => {
+    it('should return true', () => {
+        // Date month is the same as max month (in the same year)
+        expect(isNextMonthDisabled({ month: 10, year: 2021, day: null }, { month: 10, year: 2021, day: null })).toEqual(true);
+        // Date month is after the max month (in the same year)
+        expect(isNextMonthDisabled({ month: 10, year: 2021, day: null }, { month: 9, year: 2021, day: null })).toEqual(true);
+        // Date year is after the max month and year
+        expect(isNextMonthDisabled({ month: 10, year: 2022, day: null }, { month: 12, year: 2021, day: null })).toEqual(true);
+    });
+    it('should return false', () => {
+        // No max range provided
+        expect(isNextMonthDisabled({ month: 10, year: 2021, day: null })).toBe(false);
+        // Date month is before max month and is the previous month,
+        // so that navigating the next month would re-enter the max range
+        expect(isNextMonthDisabled({ month: 10, year: 2021, day: null }, { month: 11, year: 2021, day: null })).toEqual(false);
+    });
+});

+ 25 - 0
src/node_modules/@ionic/core/dist/collection/components/datetime/test/time-label/datetime.e2e.js

@@ -0,0 +1,25 @@
+/*!
+ * (C) Ionic http://ionicframework.com - MIT License
+ */
+import { expect } from "@playwright/test";
+import { configs, test } from "../../../../utils/test/playwright/index";
+configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => {
+    test.describe(title('datetime: time label'), () => {
+        test('should render default time label', async ({ page }) => {
+            await page.setContent(`
+        <ion-datetime></ion-datetime>
+      `, config);
+            await page.locator('.datetime-ready').waitFor();
+            const timeLabel = page.locator('ion-datetime .time-header');
+            await expect(timeLabel).toHaveText('Time');
+        });
+        test('should not render a custom time label', async ({ page }) => {
+            await page.setContent(`
+        <ion-datetime show-default-time-label="false"></ion-datetime>
+      `, config);
+            await page.locator('.datetime-ready').waitFor();
+            const timeLabel = page.locator('ion-datetime .time-header');
+            await expect(timeLabel).toHaveText('');
+        });
+    });
+});

+ 18 - 0
src/node_modules/@ionic/core/dist/collection/components/datetime/test/utils/month-did-change-event.js

@@ -0,0 +1,18 @@
+/**
+ * Initializes a mutation observer to detect when the calendar month
+ * text is updated as a result of a month change in `ion-datetime`.
+ *
+ * @param {*} datetimeSelector The element selector for the `ion-datetime` component.
+ */
+export function InitMonthDidChangeEvent(datetimeSelector = 'ion-datetime') {
+  const observer = new MutationObserver((mutationRecords) => {
+    if (mutationRecords[0].type === 'characterData') {
+      window.dispatchEvent(new CustomEvent('datetimeMonthDidChange'));
+    }
+  });
+
+  observer.observe(document.querySelector(datetimeSelector).shadowRoot.querySelector('.calendar-month-year'), {
+    characterData: true,
+    subtree: true,
+  });
+}

+ 143 - 0
src/node_modules/@ionic/core/dist/collection/components/datetime/test/values/datetime.e2e.js

@@ -0,0 +1,143 @@
+/*!
+ * (C) Ionic http://ionicframework.com - MIT License
+ */
+import { expect } from "@playwright/test";
+import { configs, test } from "../../../../utils/test/playwright/index";
+configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => {
+    test.describe(title('datetime: values'), () => {
+        test('should render correct days', async ({ page }) => {
+            await page.setContent(`
+        <ion-datetime locale="en-US" presentation="date" day-values="1,2,3"></ion-datetime>
+      `, config);
+            await page.locator('.datetime-ready').waitFor();
+            const items = page.locator('.calendar-day:not([disabled])');
+            /**
+             * Datetime calendar grid renders 3 months by default,
+             * so this ensures that dayValues is applying to all
+             * rendered months, not just the initial month.
+             */
+            await expect(items).toHaveText(['1', '2', '3', '1', '2', '3', '1', '2', '3']);
+        });
+        test('should render correct months', async ({ page }) => {
+            await page.setContent(`
+        <ion-datetime locale="en-US" presentation="month-year" month-values="5,6,10"></ion-datetime>
+      `, config);
+            const items = page.locator('.month-column ion-picker-column-option');
+            await expect(items).toHaveText(['May', 'June', 'October']);
+        });
+        test('should render correct years', async ({ page }) => {
+            await page.setContent(`
+        <ion-datetime locale="en-US" presentation="month-year" year-values="2022,2021,2020"></ion-datetime>
+      `, config);
+            const items = page.locator('.year-column ion-picker-column-option');
+            await expect(items).toHaveText(['2022', '2021', '2020']);
+        });
+        test('should render correct hours', async ({ page }) => {
+            await page.setContent(`
+        <ion-datetime locale="en-US" presentation="time" hour-values="1,2,3"></ion-datetime>
+      `, config);
+            const items = page.locator('ion-picker-column:first-of-type ion-picker-column-option');
+            await expect(items).toHaveText(['1', '2', '3']);
+        });
+        test('should render correct minutes', async ({ page }) => {
+            await page.setContent(`
+        <ion-datetime locale="en-US" presentation="time" minute-values="1,2,3"></ion-datetime>
+      `, config);
+            const items = page.locator('ion-picker-column:nth-of-type(2) ion-picker-column-option');
+            await expect(items).toHaveText(['01', '02', '03']);
+        });
+        test('should adjust default parts for allowed hour and minute values', async ({ page }) => {
+            /**
+             * Mock today's date for testing.
+             * Playwright does not support this natively
+             * so we extend the native Date interface: https://github.com/microsoft/playwright/issues/6347
+             */
+            await page.setContent(`
+        <ion-datetime presentation="time" locale="en-US" hour-values="02" minute-values="0,15,30,45"></ion-datetime>
+
+        <script>
+          const mockToday = '2022-10-10T16:22';
+          Date = class extends Date {
+            constructor(...args) {
+              if (args.length === 0) {
+                super(mockToday)
+              } else {
+                super(...args);
+              }
+            }
+          }
+        </script>
+      `, config);
+            await page.locator('.datetime-ready').waitFor();
+            const minuteColumn = page.locator('ion-picker-column').nth(1);
+            const minuteItems = minuteColumn.locator('ion-picker-column-option');
+            await expect(minuteItems).toHaveText(['00', '15', '30', '45']);
+            await expect(minuteColumn).toHaveJSProperty('value', 15);
+            const hourColumn = page.locator('ion-picker-column').nth(0);
+            const hourItems = hourColumn.locator('ion-picker-column-option');
+            await expect(hourItems).toHaveText(['2']);
+            await expect(hourColumn).toHaveJSProperty('value', 2);
+            /**
+             * Since the allowed hour is 2AM, the time period
+             * should switch from PM to AM.
+             */
+            const ampmColumn = page.locator('ion-picker-column').nth(2);
+            const ampmItems = ampmColumn.locator('ion-picker-column-option');
+            await expect(ampmItems).toHaveText(['AM', 'PM']);
+            await expect(ampmColumn).toHaveJSProperty('value', 'am');
+        });
+        test('should adjust default parts month for allowed month values', async ({ page }) => {
+            /**
+             * Mock today's date for testing.
+             * Playwright does not support this natively
+             * so we extend the native Date interface: https://github.com/microsoft/playwright/issues/6347
+             */
+            await page.setContent(`
+        <ion-datetime prefer-wheel="true" presentation="date" locale="en-US" month-values="01" hour-values="02" minute-values="0,15,30,45"></ion-datetime>
+
+        <script>
+          const mockToday = '2022-10-10T16:22';
+          Date = class extends Date {
+            constructor(...args) {
+              if (args.length === 0) {
+                super(mockToday)
+              } else {
+                super(...args);
+              }
+            }
+          }
+        </script>
+      `, config);
+            await page.locator('.datetime-ready').waitFor();
+            const monthItems = page.locator('.month-column ion-picker-column-option');
+            await expect(monthItems).toHaveText(['January']);
+            await expect(monthItems.nth(0)).toHaveClass(/option-active/);
+        });
+        test('today date highlight should persist even if disallowed from dayValues', async ({ page }) => {
+            /**
+             * Mock today's date for testing.
+             * Playwright does not support this natively
+             * so we extend the native Date interface: https://github.com/microsoft/playwright/issues/6347
+             */
+            await page.setContent(`
+        <ion-datetime day-values="9" presentation="date" locale="en-US"></ion-datetime>
+
+        <script>
+          const mockToday = '2022-10-10T16:22';
+          Date = class extends Date {
+            constructor(...args) {
+              if (args.length === 0) {
+                super(mockToday)
+              } else {
+                super(...args);
+              }
+            }
+          }
+        </script>
+      `, config);
+            await page.locator('.datetime-ready').waitFor();
+            const todayButton = page.locator('.calendar-day[data-day="10"][data-month="10"][data-year="2022"]');
+            await expect(todayButton).toHaveClass(/calendar-day-today/);
+        });
+    });
+});

+ 44 - 0
src/node_modules/@ionic/core/dist/collection/components/datetime/utils/comparison.js

@@ -0,0 +1,44 @@
+/*!
+ * (C) Ionic http://ionicframework.com - MIT License
+ */
+import { printIonWarning } from "../../../utils/logging/index";
+/**
+ * Returns true if the selected day is equal to the reference day
+ */
+export const isSameDay = (baseParts, compareParts) => {
+    return (baseParts.month === compareParts.month && baseParts.day === compareParts.day && baseParts.year === compareParts.year);
+};
+/**
+ * Returns true is the selected day is before the reference day.
+ */
+export const isBefore = (baseParts, compareParts) => {
+    return !!(baseParts.year < compareParts.year ||
+        (baseParts.year === compareParts.year && baseParts.month < compareParts.month) ||
+        (baseParts.year === compareParts.year &&
+            baseParts.month === compareParts.month &&
+            baseParts.day !== null &&
+            baseParts.day < compareParts.day));
+};
+/**
+ * Returns true is the selected day is after the reference day.
+ */
+export const isAfter = (baseParts, compareParts) => {
+    return !!(baseParts.year > compareParts.year ||
+        (baseParts.year === compareParts.year && baseParts.month > compareParts.month) ||
+        (baseParts.year === compareParts.year &&
+            baseParts.month === compareParts.month &&
+            baseParts.day !== null &&
+            baseParts.day > compareParts.day));
+};
+export const warnIfValueOutOfBounds = (value, min, max) => {
+    const valueArray = Array.isArray(value) ? value : [value];
+    for (const val of valueArray) {
+        if ((min !== undefined && isBefore(val, min)) || (max !== undefined && isAfter(val, max))) {
+            printIonWarning('The value provided to ion-datetime is out of bounds.\n\n' +
+                `Min: ${JSON.stringify(min)}\n` +
+                `Max: ${JSON.stringify(max)}\n` +
+                `Value: ${JSON.stringify(value)}`);
+            break;
+        }
+    }
+};

+ 504 - 0
src/node_modules/@ionic/core/dist/collection/components/datetime/utils/data.js

@@ -0,0 +1,504 @@
+/*!
+ * (C) Ionic http://ionicframework.com - MIT License
+ */
+import { isAfter, isBefore, isSameDay } from "./comparison";
+import { getLocalizedDayPeriod, removeDateTzOffset, getFormattedHour, addTimePadding, getTodayLabel, getYear, } from "./format";
+import { getNumDaysInMonth, is24Hour, getHourCycle } from "./helpers";
+import { getNextMonth, getPreviousMonth, getInternalHourValue } from "./manipulation";
+/**
+ * Returns the current date as
+ * an ISO string in the user's
+ * time zone.
+ */
+export const getToday = () => {
+    /**
+     * ion-datetime intentionally does not
+     * parse time zones/do automatic time zone
+     * conversion when accepting user input.
+     * However when we get today's date string,
+     * we want it formatted relative to the user's
+     * time zone.
+     *
+     * When calling toISOString(), the browser
+     * will convert the date to UTC time by either adding
+     * or subtracting the time zone offset.
+     * To work around this, we need to either add
+     * or subtract the time zone offset to the Date
+     * object prior to calling toISOString().
+     * This allows us to get an ISO string
+     * that is in the user's time zone.
+     */
+    return removeDateTzOffset(new Date()).toISOString();
+};
+const minutes = [
+    0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31,
+    32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59,
+];
+// h11 hour system uses 0-11. Midnight starts at 0:00am.
+const hour11 = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
+// h12 hour system uses 0-12. Midnight starts at 12:00am.
+const hour12 = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
+// h23 hour system uses 0-23. Midnight starts at 0:00.
+const hour23 = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23];
+// h24 hour system uses 1-24. Midnight starts at 24:00.
+const hour24 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 0];
+/**
+ * Given a locale and a mode,
+ * return an array with formatted days
+ * of the week. iOS should display days
+ * such as "Mon" or "Tue".
+ * MD should display days such as "M"
+ * or "T".
+ */
+export const getDaysOfWeek = (locale, mode, firstDayOfWeek = 0) => {
+    /**
+     * Nov 1st, 2020 starts on a Sunday.
+     * ion-datetime assumes weeks start on Sunday,
+     * but is configurable via `firstDayOfWeek`.
+     */
+    const weekdayFormat = mode === 'ios' ? 'short' : 'narrow';
+    const intl = new Intl.DateTimeFormat(locale, { weekday: weekdayFormat });
+    const startDate = new Date('11/01/2020');
+    const daysOfWeek = [];
+    /**
+     * For each day of the week,
+     * get the day name.
+     */
+    for (let i = firstDayOfWeek; i < firstDayOfWeek + 7; i++) {
+        const currentDate = new Date(startDate);
+        currentDate.setDate(currentDate.getDate() + i);
+        daysOfWeek.push(intl.format(currentDate));
+    }
+    return daysOfWeek;
+};
+/**
+ * Returns an array containing all of the
+ * days in a month for a given year. Values are
+ * aligned with a week calendar starting on
+ * the firstDayOfWeek value (Sunday by default)
+ * using null values.
+ */
+export const getDaysOfMonth = (month, year, firstDayOfWeek) => {
+    const numDays = getNumDaysInMonth(month, year);
+    const firstOfMonth = new Date(`${month}/1/${year}`).getDay();
+    /**
+     * To get the first day of the month aligned on the correct
+     * day of the week, we need to determine how many "filler" days
+     * to generate. These filler days as empty/disabled buttons
+     * that fill the space of the days of the week before the first
+     * of the month.
+     *
+     * There are two cases here:
+     *
+     * 1. If firstOfMonth = 4, firstDayOfWeek = 0 then the offset
+     * is (4 - (0 + 1)) = 3. Since the offset loop goes from 0 to 3 inclusive,
+     * this will generate 4 filler days (0, 1, 2, 3), and then day of week 4 will have
+     * the first day of the month.
+     *
+     * 2. If firstOfMonth = 2, firstDayOfWeek = 4 then the offset
+     * is (6 - (4 - 2)) = 4. Since the offset loop goes from 0 to 4 inclusive,
+     * this will generate 5 filler days (0, 1, 2, 3, 4), and then day of week 5 will have
+     * the first day of the month.
+     */
+    const offset = firstOfMonth >= firstDayOfWeek ? firstOfMonth - (firstDayOfWeek + 1) : 6 - (firstDayOfWeek - firstOfMonth);
+    let days = [];
+    for (let i = 1; i <= numDays; i++) {
+        days.push({ day: i, dayOfWeek: (offset + i) % 7 });
+    }
+    for (let i = 0; i <= offset; i++) {
+        days = [{ day: null, dayOfWeek: null }, ...days];
+    }
+    return days;
+};
+/**
+ * Returns an array of pre-defined hour
+ * values based on the provided hourCycle.
+ */
+const getHourData = (hourCycle) => {
+    switch (hourCycle) {
+        case 'h11':
+            return hour11;
+        case 'h12':
+            return hour12;
+        case 'h23':
+            return hour23;
+        case 'h24':
+            return hour24;
+        default:
+            throw new Error(`Invalid hour cycle "${hourCycle}"`);
+    }
+};
+/**
+ * Given a local, reference datetime parts and option
+ * max/min bound datetime parts, calculate the acceptable
+ * hour and minute values according to the bounds and locale.
+ */
+export const generateTime = (locale, refParts, hourCycle = 'h12', minParts, maxParts, hourValues, minuteValues) => {
+    const computedHourCycle = getHourCycle(locale, hourCycle);
+    const use24Hour = is24Hour(computedHourCycle);
+    let processedHours = getHourData(computedHourCycle);
+    let processedMinutes = minutes;
+    let isAMAllowed = true;
+    let isPMAllowed = true;
+    if (hourValues) {
+        processedHours = processedHours.filter((hour) => hourValues.includes(hour));
+    }
+    if (minuteValues) {
+        processedMinutes = processedMinutes.filter((minute) => minuteValues.includes(minute));
+    }
+    if (minParts) {
+        /**
+         * If ref day is the same as the
+         * minimum allowed day, filter hour/minute
+         * values according to min hour and minute.
+         */
+        if (isSameDay(refParts, minParts)) {
+            /**
+             * Users may not always set the hour/minute for
+             * min value (i.e. 2021-06-02) so we should allow
+             * all hours/minutes in that case.
+             */
+            if (minParts.hour !== undefined) {
+                processedHours = processedHours.filter((hour) => {
+                    const convertedHour = refParts.ampm === 'pm' ? (hour + 12) % 24 : hour;
+                    return (use24Hour ? hour : convertedHour) >= minParts.hour;
+                });
+                isAMAllowed = minParts.hour < 13;
+            }
+            if (minParts.minute !== undefined) {
+                /**
+                 * The minimum minute range should not be enforced when
+                 * the hour is greater than the min hour.
+                 *
+                 * For example with a minimum range of 09:30, users
+                 * should be able to select 10:00-10:29 and beyond.
+                 */
+                let isPastMinHour = false;
+                if (minParts.hour !== undefined && refParts.hour !== undefined) {
+                    if (refParts.hour > minParts.hour) {
+                        isPastMinHour = true;
+                    }
+                }
+                processedMinutes = processedMinutes.filter((minute) => {
+                    if (isPastMinHour) {
+                        return true;
+                    }
+                    return minute >= minParts.minute;
+                });
+            }
+            /**
+             * If ref day is before minimum
+             * day do not render any hours/minute values
+             */
+        }
+        else if (isBefore(refParts, minParts)) {
+            processedHours = [];
+            processedMinutes = [];
+            isAMAllowed = isPMAllowed = false;
+        }
+    }
+    if (maxParts) {
+        /**
+         * If ref day is the same as the
+         * maximum allowed day, filter hour/minute
+         * values according to max hour and minute.
+         */
+        if (isSameDay(refParts, maxParts)) {
+            /**
+             * Users may not always set the hour/minute for
+             * max value (i.e. 2021-06-02) so we should allow
+             * all hours/minutes in that case.
+             */
+            if (maxParts.hour !== undefined) {
+                processedHours = processedHours.filter((hour) => {
+                    const convertedHour = refParts.ampm === 'pm' ? (hour + 12) % 24 : hour;
+                    return (use24Hour ? hour : convertedHour) <= maxParts.hour;
+                });
+                isPMAllowed = maxParts.hour >= 12;
+            }
+            if (maxParts.minute !== undefined && refParts.hour === maxParts.hour) {
+                // The available minutes should only be filtered when the hour is the same as the max hour.
+                // For example if the max hour is 10:30 and the current hour is 10:00,
+                // users should be able to select 00-30 minutes.
+                // If the current hour is 09:00, users should be able to select 00-60 minutes.
+                processedMinutes = processedMinutes.filter((minute) => minute <= maxParts.minute);
+            }
+            /**
+             * If ref day is after minimum
+             * day do not render any hours/minute values
+             */
+        }
+        else if (isAfter(refParts, maxParts)) {
+            processedHours = [];
+            processedMinutes = [];
+            isAMAllowed = isPMAllowed = false;
+        }
+    }
+    return {
+        hours: processedHours,
+        minutes: processedMinutes,
+        am: isAMAllowed,
+        pm: isPMAllowed,
+    };
+};
+/**
+ * Given DatetimeParts, generate the previous,
+ * current, and and next months.
+ */
+export const generateMonths = (refParts, forcedDate) => {
+    const current = { month: refParts.month, year: refParts.year, day: refParts.day };
+    /**
+     * If we're forcing a month to appear, and it's different from the current month,
+     * ensure it appears by replacing the next or previous month as appropriate.
+     */
+    if (forcedDate !== undefined && (refParts.month !== forcedDate.month || refParts.year !== forcedDate.year)) {
+        const forced = { month: forcedDate.month, year: forcedDate.year, day: forcedDate.day };
+        const forcedMonthIsBefore = isBefore(forced, current);
+        return forcedMonthIsBefore
+            ? [forced, current, getNextMonth(refParts)]
+            : [getPreviousMonth(refParts), current, forced];
+    }
+    return [getPreviousMonth(refParts), current, getNextMonth(refParts)];
+};
+export const getMonthColumnData = (locale, refParts, minParts, maxParts, monthValues, formatOptions = {
+    month: 'long',
+}) => {
+    const { year } = refParts;
+    const months = [];
+    if (monthValues !== undefined) {
+        let processedMonths = monthValues;
+        if ((maxParts === null || maxParts === void 0 ? void 0 : maxParts.month) !== undefined) {
+            processedMonths = processedMonths.filter((month) => month <= maxParts.month);
+        }
+        if ((minParts === null || minParts === void 0 ? void 0 : minParts.month) !== undefined) {
+            processedMonths = processedMonths.filter((month) => month >= minParts.month);
+        }
+        processedMonths.forEach((processedMonth) => {
+            const date = new Date(`${processedMonth}/1/${year} GMT+0000`);
+            const monthString = new Intl.DateTimeFormat(locale, Object.assign(Object.assign({}, formatOptions), { timeZone: 'UTC' })).format(date);
+            months.push({ text: monthString, value: processedMonth });
+        });
+    }
+    else {
+        const maxMonth = maxParts && maxParts.year === year ? maxParts.month : 12;
+        const minMonth = minParts && minParts.year === year ? minParts.month : 1;
+        for (let i = minMonth; i <= maxMonth; i++) {
+            /**
+             *
+             * There is a bug on iOS 14 where
+             * Intl.DateTimeFormat takes into account
+             * the local timezone offset when formatting dates.
+             *
+             * Forcing the timezone to 'UTC' fixes the issue. However,
+             * we should keep this workaround as it is safer. In the event
+             * this breaks in another browser, we will not be impacted
+             * because all dates will be interpreted in UTC.
+             *
+             * Example:
+             * new Intl.DateTimeFormat('en-US', { month: 'long' }).format(new Date('Sat Apr 01 2006 00:00:00 GMT-0400 (EDT)')) // "March"
+             * new Intl.DateTimeFormat('en-US', { month: 'long', timeZone: 'UTC' }).format(new Date('Sat Apr 01 2006 00:00:00 GMT-0400 (EDT)')) // "April"
+             *
+             * In certain timezones, iOS 14 shows the wrong
+             * date for .toUTCString(). To combat this, we
+             * force all of the timezones to GMT+0000 (UTC).
+             *
+             * Example:
+             * Time Zone: Central European Standard Time
+             * new Date('1/1/1992').toUTCString() // "Tue, 31 Dec 1991 23:00:00 GMT"
+             * new Date('1/1/1992 GMT+0000').toUTCString() // "Wed, 01 Jan 1992 00:00:00 GMT"
+             */
+            const date = new Date(`${i}/1/${year} GMT+0000`);
+            const monthString = new Intl.DateTimeFormat(locale, Object.assign(Object.assign({}, formatOptions), { timeZone: 'UTC' })).format(date);
+            months.push({ text: monthString, value: i });
+        }
+    }
+    return months;
+};
+/**
+ * Returns information regarding
+ * selectable dates (i.e 1st, 2nd, 3rd, etc)
+ * within a reference month.
+ * @param locale The locale to format the date with
+ * @param refParts The reference month/year to generate dates for
+ * @param minParts The minimum bound on the date that can be returned
+ * @param maxParts The maximum bound on the date that can be returned
+ * @param dayValues The allowed date values
+ * @returns Date data to be used in ion-picker-column
+ */
+export const getDayColumnData = (locale, refParts, minParts, maxParts, dayValues, formatOptions = {
+    day: 'numeric',
+}) => {
+    const { month, year } = refParts;
+    const days = [];
+    /**
+     * If we have max/min bounds that in the same
+     * month/year as the refParts, we should
+     * use the define day as the max/min day.
+     * Otherwise, fallback to the max/min days in a month.
+     */
+    const numDaysInMonth = getNumDaysInMonth(month, year);
+    const maxDay = (maxParts === null || maxParts === void 0 ? void 0 : maxParts.day) !== null && (maxParts === null || maxParts === void 0 ? void 0 : maxParts.day) !== undefined && maxParts.year === year && maxParts.month === month
+        ? maxParts.day
+        : numDaysInMonth;
+    const minDay = (minParts === null || minParts === void 0 ? void 0 : minParts.day) !== null && (minParts === null || minParts === void 0 ? void 0 : minParts.day) !== undefined && minParts.year === year && minParts.month === month
+        ? minParts.day
+        : 1;
+    if (dayValues !== undefined) {
+        let processedDays = dayValues;
+        processedDays = processedDays.filter((day) => day >= minDay && day <= maxDay);
+        processedDays.forEach((processedDay) => {
+            const date = new Date(`${month}/${processedDay}/${year} GMT+0000`);
+            const dayString = new Intl.DateTimeFormat(locale, Object.assign(Object.assign({}, formatOptions), { timeZone: 'UTC' })).format(date);
+            days.push({ text: dayString, value: processedDay });
+        });
+    }
+    else {
+        for (let i = minDay; i <= maxDay; i++) {
+            const date = new Date(`${month}/${i}/${year} GMT+0000`);
+            const dayString = new Intl.DateTimeFormat(locale, Object.assign(Object.assign({}, formatOptions), { timeZone: 'UTC' })).format(date);
+            days.push({ text: dayString, value: i });
+        }
+    }
+    return days;
+};
+export const getYearColumnData = (locale, refParts, minParts, maxParts, yearValues) => {
+    var _a, _b;
+    let processedYears = [];
+    if (yearValues !== undefined) {
+        processedYears = yearValues;
+        if ((maxParts === null || maxParts === void 0 ? void 0 : maxParts.year) !== undefined) {
+            processedYears = processedYears.filter((year) => year <= maxParts.year);
+        }
+        if ((minParts === null || minParts === void 0 ? void 0 : minParts.year) !== undefined) {
+            processedYears = processedYears.filter((year) => year >= minParts.year);
+        }
+    }
+    else {
+        const { year } = refParts;
+        const maxYear = (_a = maxParts === null || maxParts === void 0 ? void 0 : maxParts.year) !== null && _a !== void 0 ? _a : year;
+        const minYear = (_b = minParts === null || minParts === void 0 ? void 0 : minParts.year) !== null && _b !== void 0 ? _b : year - 100;
+        for (let i = minYear; i <= maxYear; i++) {
+            processedYears.push(i);
+        }
+    }
+    return processedYears.map((year) => ({
+        text: getYear(locale, { year, month: refParts.month, day: refParts.day }),
+        value: year,
+    }));
+};
+/**
+ * Given a starting date and an upper bound,
+ * this functions returns an array of all
+ * month objects in that range.
+ */
+const getAllMonthsInRange = (currentParts, maxParts) => {
+    if (currentParts.month === maxParts.month && currentParts.year === maxParts.year) {
+        return [currentParts];
+    }
+    return [currentParts, ...getAllMonthsInRange(getNextMonth(currentParts), maxParts)];
+};
+/**
+ * Creates and returns picker items
+ * that represent the days in a month.
+ * Example: "Thu, Jun 2"
+ */
+export const getCombinedDateColumnData = (locale, todayParts, minParts, maxParts, dayValues, monthValues) => {
+    let items = [];
+    let parts = [];
+    /**
+     * Get all month objects from the min date
+     * to the max date. Note: Do not use getMonthColumnData
+     * as that function only generates dates within a
+     * single year.
+     */
+    let months = getAllMonthsInRange(minParts, maxParts);
+    /**
+     * Filter out any disallowed month values.
+     */
+    if (monthValues) {
+        months = months.filter(({ month }) => monthValues.includes(month));
+    }
+    /**
+     * Get all of the days in the month.
+     * From there, generate an array where
+     * each item has the month, date, and day
+     * of work as the text.
+     */
+    months.forEach((monthObject) => {
+        const referenceMonth = { month: monthObject.month, day: null, year: monthObject.year };
+        const monthDays = getDayColumnData(locale, referenceMonth, minParts, maxParts, dayValues, {
+            month: 'short',
+            day: 'numeric',
+            weekday: 'short',
+        });
+        const dateParts = [];
+        const dateColumnItems = [];
+        monthDays.forEach((dayObject) => {
+            const isToday = isSameDay(Object.assign(Object.assign({}, referenceMonth), { day: dayObject.value }), todayParts);
+            /**
+             * Today's date should read as "Today" (localized)
+             * not the actual date string
+             */
+            dateColumnItems.push({
+                text: isToday ? getTodayLabel(locale) : dayObject.text,
+                value: `${referenceMonth.year}-${referenceMonth.month}-${dayObject.value}`,
+            });
+            /**
+             * When selecting a date in the wheel picker
+             * we need access to the raw datetime parts data.
+             * The picker column only accepts values of
+             * type string or number, so we need to return
+             * two sets of data: A data set to be passed
+             * to the picker column, and a data set to
+             * be used to reference the raw data when
+             * updating the picker column value.
+             */
+            dateParts.push({
+                month: referenceMonth.month,
+                year: referenceMonth.year,
+                day: dayObject.value,
+            });
+        });
+        parts = [...parts, ...dateParts];
+        items = [...items, ...dateColumnItems];
+    });
+    return {
+        parts,
+        items,
+    };
+};
+export const getTimeColumnsData = (locale, refParts, hourCycle, minParts, maxParts, allowedHourValues, allowedMinuteValues) => {
+    const computedHourCycle = getHourCycle(locale, hourCycle);
+    const use24Hour = is24Hour(computedHourCycle);
+    const { hours, minutes, am, pm } = generateTime(locale, refParts, computedHourCycle, minParts, maxParts, allowedHourValues, allowedMinuteValues);
+    const hoursItems = hours.map((hour) => {
+        return {
+            text: getFormattedHour(hour, computedHourCycle),
+            value: getInternalHourValue(hour, use24Hour, refParts.ampm),
+        };
+    });
+    const minutesItems = minutes.map((minute) => {
+        return {
+            text: addTimePadding(minute),
+            value: minute,
+        };
+    });
+    const dayPeriodItems = [];
+    if (am && !use24Hour) {
+        dayPeriodItems.push({
+            text: getLocalizedDayPeriod(locale, 'am'),
+            value: 'am',
+        });
+    }
+    if (pm && !use24Hour) {
+        dayPeriodItems.push({
+            text: getLocalizedDayPeriod(locale, 'pm'),
+            value: 'pm',
+        });
+    }
+    return {
+        minutesData: minutesItems,
+        hoursData: hoursItems,
+        dayPeriodData: dayPeriodItems,
+    };
+};

+ 290 - 0
src/node_modules/@ionic/core/dist/collection/components/datetime/utils/format.js

@@ -0,0 +1,290 @@
+/*!
+ * (C) Ionic http://ionicframework.com - MIT License
+ */
+import { is24Hour } from "./helpers";
+import { convertDataToISO } from "./manipulation";
+const getFormattedDayPeriod = (dayPeriod) => {
+    if (dayPeriod === undefined) {
+        return '';
+    }
+    return dayPeriod.toUpperCase();
+};
+/**
+ * Including time zone options may lead to the rendered text showing a
+ * different time from what was selected in the Datetime, which could cause
+ * confusion.
+ */
+export const stripTimeZone = (formatOptions) => {
+    return Object.assign(Object.assign({}, formatOptions), {
+        /**
+         * Setting the time zone to UTC ensures that the value shown is always the
+         * same as what was selected and safeguards against older Safari bugs with
+         * Intl.DateTimeFormat.
+         */
+        timeZone: 'UTC',
+        /**
+         * We do not want to display the time zone name
+         */
+        timeZoneName: undefined
+    });
+};
+export const getLocalizedTime = (locale, refParts, hourCycle, formatOptions = { hour: 'numeric', minute: 'numeric' }) => {
+    const timeParts = {
+        hour: refParts.hour,
+        minute: refParts.minute,
+    };
+    if (timeParts.hour === undefined || timeParts.minute === undefined) {
+        return 'Invalid Time';
+    }
+    return new Intl.DateTimeFormat(locale, Object.assign(Object.assign({}, stripTimeZone(formatOptions)), {
+        /**
+         * We use hourCycle here instead of hour12 due to:
+         * https://bugs.chromium.org/p/chromium/issues/detail?id=1347316&q=hour12&can=2
+         */
+        hourCycle
+    })).format(new Date(convertDataToISO(Object.assign({
+        /**
+         * JS uses a simplified ISO 8601 format which allows for
+         * date-only formats and date-time formats, but not
+         * time-only formats: https://tc39.es/ecma262/#sec-date-time-string-format
+         * As a result, developers who only pass a time will get
+         * an "Invalid Date" error. To account for this, we make sure that
+         * year/day/month values are set when passing to new Date().
+         * The Intl.DateTimeFormat call above only uses the hour/minute
+         * values, so passing these date values should have no impact
+         * on the time output.
+         */
+        year: 2023, day: 1, month: 1
+    }, timeParts)) + 'Z'));
+};
+/**
+ * Adds padding to a time value so
+ * that it is always 2 digits.
+ */
+export const addTimePadding = (value) => {
+    const valueToString = value.toString();
+    if (valueToString.length > 1) {
+        return valueToString;
+    }
+    return `0${valueToString}`;
+};
+/**
+ * Formats 24 hour times so that
+ * it always has 2 digits. For
+ * 12 hour times it ensures that
+ * hour 0 is formatted as '12'.
+ */
+export const getFormattedHour = (hour, hourCycle) => {
+    /**
+     * Midnight for h11 starts at 0:00am
+     * Midnight for h12 starts at 12:00am
+     * Midnight for h23 starts at 00:00
+     * Midnight for h24 starts at 24:00
+     */
+    if (hour === 0) {
+        switch (hourCycle) {
+            case 'h11':
+                return '0';
+            case 'h12':
+                return '12';
+            case 'h23':
+                return '00';
+            case 'h24':
+                return '24';
+            default:
+                throw new Error(`Invalid hour cycle "${hourCycle}"`);
+        }
+    }
+    const use24Hour = is24Hour(hourCycle);
+    /**
+     * h23 and h24 use 24 hour times.
+     */
+    if (use24Hour) {
+        return addTimePadding(hour);
+    }
+    return hour.toString();
+};
+/**
+ * Generates an aria-label to be read by screen readers
+ * given a local, a date, and whether or not that date is
+ * today's date.
+ */
+export const generateDayAriaLabel = (locale, today, refParts) => {
+    if (refParts.day === null) {
+        return null;
+    }
+    /**
+     * MM/DD/YYYY will return midnight in the user's timezone.
+     */
+    const date = getNormalizedDate(refParts);
+    const labelString = new Intl.DateTimeFormat(locale, {
+        weekday: 'long',
+        month: 'long',
+        day: 'numeric',
+        timeZone: 'UTC',
+    }).format(date);
+    /**
+     * If date is today, prepend "Today" so screen readers indicate
+     * that the date is today.
+     */
+    return today ? `Today, ${labelString}` : labelString;
+};
+/**
+ * Given a locale and a date object,
+ * return a formatted string that includes
+ * the month name and full year.
+ * Example: May 2021
+ */
+export const getMonthAndYear = (locale, refParts) => {
+    const date = getNormalizedDate(refParts);
+    return new Intl.DateTimeFormat(locale, { month: 'long', year: 'numeric', timeZone: 'UTC' }).format(date);
+};
+/**
+ * Given a locale and a date object,
+ * return a formatted string that includes
+ * the numeric day.
+ * Note: Some languages will add literal characters
+ * to the end. This function removes those literals.
+ * Example: 29
+ */
+export const getDay = (locale, refParts) => {
+    return getLocalizedDateTimeParts(locale, refParts, { day: 'numeric' }).find((obj) => obj.type === 'day').value;
+};
+/**
+ * Given a locale and a date object,
+ * return a formatted string that includes
+ * the numeric year.
+ * Example: 2022
+ */
+export const getYear = (locale, refParts) => {
+    return getLocalizedDateTime(locale, refParts, { year: 'numeric' });
+};
+/**
+ * Given reference parts, return a JS Date object
+ * with a normalized time.
+ */
+export const getNormalizedDate = (refParts) => {
+    var _a, _b, _c;
+    const timeString = refParts.hour !== undefined && refParts.minute !== undefined ? ` ${refParts.hour}:${refParts.minute}` : '';
+    /**
+     * We use / notation here for the date
+     * so we do not need to do extra work and pad values with zeroes.
+     * Values such as YYYY-MM are still valid, so
+     * we add fallback values so we still get
+     * a valid date otherwise we will pass in a string
+     * like "//2023". Some browsers, such as Chrome, will
+     * account for this and still return a valid date. However,
+     * this is not a consistent behavior across all browsers.
+     */
+    return new Date(`${(_a = refParts.month) !== null && _a !== void 0 ? _a : 1}/${(_b = refParts.day) !== null && _b !== void 0 ? _b : 1}/${(_c = refParts.year) !== null && _c !== void 0 ? _c : 2023}${timeString} GMT+0000`);
+};
+/**
+ * Given a locale, DatetimeParts, and options
+ * format the DatetimeParts according to the options
+ * and locale combination. This returns a string. If
+ * you want an array of the individual pieces
+ * that make up the localized date string, use
+ * getLocalizedDateTimeParts.
+ */
+export const getLocalizedDateTime = (locale, refParts, options) => {
+    const date = getNormalizedDate(refParts);
+    return getDateTimeFormat(locale, stripTimeZone(options)).format(date);
+};
+/**
+ * Given a locale, DatetimeParts, and options
+ * format the DatetimeParts according to the options
+ * and locale combination. This returns an array of
+ * each piece of the date.
+ */
+export const getLocalizedDateTimeParts = (locale, refParts, options) => {
+    const date = getNormalizedDate(refParts);
+    return getDateTimeFormat(locale, options).formatToParts(date);
+};
+/**
+ * Wrapper function for Intl.DateTimeFormat.
+ * Allows developers to apply an allowed format to DatetimeParts.
+ * This function also has built in safeguards for older browser bugs
+ * with Intl.DateTimeFormat.
+ */
+const getDateTimeFormat = (locale, options) => {
+    return new Intl.DateTimeFormat(locale, Object.assign(Object.assign({}, options), { timeZone: 'UTC' }));
+};
+/**
+ * Gets a localized version of "Today"
+ * Falls back to "Today" in English for
+ * browsers that do not support RelativeTimeFormat.
+ */
+export const getTodayLabel = (locale) => {
+    if ('RelativeTimeFormat' in Intl) {
+        const label = new Intl.RelativeTimeFormat(locale, { numeric: 'auto' }).format(0, 'day');
+        return label.charAt(0).toUpperCase() + label.slice(1);
+    }
+    else {
+        return 'Today';
+    }
+};
+/**
+ * When calling toISOString(), the browser
+ * will convert the date to UTC time by either adding
+ * or subtracting the time zone offset.
+ * To work around this, we need to either add
+ * or subtract the time zone offset to the Date
+ * object prior to calling toISOString().
+ * This allows us to get an ISO string
+ * that is in the user's time zone.
+ *
+ * Example:
+ * Time zone offset is 240
+ * Meaning: The browser needs to add 240 minutes
+ * to the Date object to get UTC time.
+ * What Ionic does: We subtract 240 minutes
+ * from the Date object. The browser then adds
+ * 240 minutes in toISOString(). The result
+ * is a time that is in the user's time zone
+ * and not UTC.
+ *
+ * Note: Some timezones include minute adjustments
+ * such as 30 or 45 minutes. This is why we use setMinutes
+ * instead of setHours.
+ * Example: India Standard Time
+ * Timezone offset: -330 = -5.5 hours.
+ *
+ * List of timezones with 30 and 45 minute timezones:
+ * https://www.timeanddate.com/time/time-zones-interesting.html
+ */
+export const removeDateTzOffset = (date) => {
+    const tzOffset = date.getTimezoneOffset();
+    date.setMinutes(date.getMinutes() - tzOffset);
+    return date;
+};
+const DATE_AM = removeDateTzOffset(new Date('2022T01:00'));
+const DATE_PM = removeDateTzOffset(new Date('2022T13:00'));
+/**
+ * Formats the locale's string representation of the day period (am/pm) for a given
+ * ref parts day period.
+ *
+ * @param locale The locale to format the day period in.
+ * @param value The date string, in ISO format.
+ * @returns The localized day period (am/pm) representation of the given value.
+ */
+export const getLocalizedDayPeriod = (locale, dayPeriod) => {
+    const date = dayPeriod === 'am' ? DATE_AM : DATE_PM;
+    const localizedDayPeriod = new Intl.DateTimeFormat(locale, {
+        hour: 'numeric',
+        timeZone: 'UTC',
+    })
+        .formatToParts(date)
+        .find((part) => part.type === 'dayPeriod');
+    if (localizedDayPeriod) {
+        return localizedDayPeriod.value;
+    }
+    return getFormattedDayPeriod(dayPeriod);
+};
+/**
+ * Formats the datetime's value to a string, for use in the native input.
+ *
+ * @param value The value to format, either an ISO string or an array thereof.
+ */
+export const formatValue = (value) => {
+    return Array.isArray(value) ? value.join(',') : value;
+};

+ 131 - 0
src/node_modules/@ionic/core/dist/collection/components/datetime/utils/helpers.js

@@ -0,0 +1,131 @@
+/*!
+ * (C) Ionic http://ionicframework.com - MIT License
+ */
+/**
+ * Determines if given year is a
+ * leap year. Returns `true` if year
+ * is a leap year. Returns `false`
+ * otherwise.
+ */
+export const isLeapYear = (year) => {
+    return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
+};
+/**
+ * Determines the hour cycle for a user.
+ * If the hour cycle is explicitly defined, just use that.
+ * Otherwise, we try to derive it from either the specified
+ * locale extension tags or from Intl.DateTimeFormat directly.
+ */
+export const getHourCycle = (locale, hourCycle) => {
+    /**
+     * If developer has explicitly enabled 24-hour time
+     * then return early and do not look at the system default.
+     */
+    if (hourCycle !== undefined) {
+        return hourCycle;
+    }
+    /**
+     * If hourCycle was not specified, check the locale
+     * that is set on the user's device. We first check the
+     * Intl.DateTimeFormat hourCycle option as developers can encode this
+     * option into the locale string. Example: `en-US-u-hc-h23`
+     */
+    const formatted = new Intl.DateTimeFormat(locale, { hour: 'numeric' });
+    const options = formatted.resolvedOptions();
+    if (options.hourCycle !== undefined) {
+        return options.hourCycle;
+    }
+    /**
+     * If hourCycle is not specified (either through lack
+     * of browser support or locale information) then fall
+     * back to this slower hourCycle check.
+     */
+    const date = new Date('5/18/2021 00:00');
+    const parts = formatted.formatToParts(date);
+    const hour = parts.find((p) => p.type === 'hour');
+    if (!hour) {
+        throw new Error('Hour value not found from DateTimeFormat');
+    }
+    /**
+     * Midnight for h11 starts at 0:00am
+     * Midnight for h12 starts at 12:00am
+     * Midnight for h23 starts at 00:00
+     * Midnight for h24 starts at 24:00
+     */
+    switch (hour.value) {
+        case '0':
+            return 'h11';
+        case '12':
+            return 'h12';
+        case '00':
+            return 'h23';
+        case '24':
+            return 'h24';
+        default:
+            throw new Error(`Invalid hour cycle "${hourCycle}"`);
+    }
+};
+/**
+ * Determine if the hour cycle uses a 24-hour format.
+ * Returns true for h23 and h24. Returns false otherwise.
+ * If you don't know the hourCycle, use getHourCycle above
+ * and pass the result into this function.
+ */
+export const is24Hour = (hourCycle) => {
+    return hourCycle === 'h23' || hourCycle === 'h24';
+};
+/**
+ * Given a date object, returns the number
+ * of days in that month.
+ * Month value begin at 1, not 0.
+ * i.e. January = month 1.
+ */
+export const getNumDaysInMonth = (month, year) => {
+    return month === 4 || month === 6 || month === 9 || month === 11
+        ? 30
+        : month === 2
+            ? isLeapYear(year)
+                ? 29
+                : 28
+            : 31;
+};
+/**
+ * Certain locales display month then year while
+ * others display year then month.
+ * We can use Intl.DateTimeFormat to determine
+ * the ordering for each locale.
+ * The formatOptions param can be used to customize
+ * which pieces of a date to compare against the month
+ * with. For example, some locales render dd/mm/yyyy
+ * while others render mm/dd/yyyy. This function can be
+ * used for variations of the same "month first" check.
+ */
+export const isMonthFirstLocale = (locale, formatOptions = {
+    month: 'numeric',
+    year: 'numeric',
+}) => {
+    /**
+     * By setting month and year we guarantee that only
+     * month, year, and literal (slashes '/', for example)
+     * values are included in the formatToParts results.
+     *
+     * The ordering of the parts will be determined by
+     * the locale. So if the month is the first value,
+     * then we know month should be shown first. If the
+     * year is the first value, then we know year should be shown first.
+     *
+     * This ordering can be controlled by customizing the locale property.
+     */
+    const parts = new Intl.DateTimeFormat(locale, formatOptions).formatToParts(new Date());
+    return parts[0].type === 'month';
+};
+/**
+ * Determines if the given locale formats the day period (am/pm) to the
+ * left or right of the hour.
+ * @param locale The locale to check.
+ * @returns `true` if the locale formats the day period to the left of the hour.
+ */
+export const isLocaleDayPeriodRTL = (locale) => {
+    const parts = new Intl.DateTimeFormat(locale, { hour: 'numeric' }).formatToParts(new Date());
+    return parts[0].type === 'dayPeriod';
+};

+ 466 - 0
src/node_modules/@ionic/core/dist/collection/components/datetime/utils/manipulation.js

@@ -0,0 +1,466 @@
+/*!
+ * (C) Ionic http://ionicframework.com - MIT License
+ */
+import { isAfter, isBefore, isSameDay } from "./comparison";
+import { getNumDaysInMonth } from "./helpers";
+import { clampDate, parseAmPm } from "./parse";
+const twoDigit = (val) => {
+    return ('0' + (val !== undefined ? Math.abs(val) : '0')).slice(-2);
+};
+const fourDigit = (val) => {
+    return ('000' + (val !== undefined ? Math.abs(val) : '0')).slice(-4);
+};
+export function convertDataToISO(data) {
+    if (Array.isArray(data)) {
+        return data.map((parts) => convertDataToISO(parts));
+    }
+    // https://www.w3.org/TR/NOTE-datetime
+    let rtn = '';
+    if (data.year !== undefined) {
+        // YYYY
+        rtn = fourDigit(data.year);
+        if (data.month !== undefined) {
+            // YYYY-MM
+            rtn += '-' + twoDigit(data.month);
+            if (data.day !== undefined) {
+                // YYYY-MM-DD
+                rtn += '-' + twoDigit(data.day);
+                if (data.hour !== undefined) {
+                    // YYYY-MM-DDTHH:mm:SS
+                    rtn += `T${twoDigit(data.hour)}:${twoDigit(data.minute)}:00`;
+                }
+            }
+        }
+    }
+    else if (data.hour !== undefined) {
+        // HH:mm
+        rtn = twoDigit(data.hour) + ':' + twoDigit(data.minute);
+    }
+    return rtn;
+}
+/**
+ * Converts an 12 hour value to 24 hours.
+ */
+export const convert12HourTo24Hour = (hour, ampm) => {
+    if (ampm === undefined) {
+        return hour;
+    }
+    /**
+     * If AM and 12am
+     * then return 00:00.
+     * Otherwise just return
+     * the hour since it is
+     * already in 24 hour format.
+     */
+    if (ampm === 'am') {
+        if (hour === 12) {
+            return 0;
+        }
+        return hour;
+    }
+    /**
+     * If PM and 12pm
+     * just return 12:00
+     * since it is already
+     * in 24 hour format.
+     * Otherwise add 12 hours
+     * to the time.
+     */
+    if (hour === 12) {
+        return 12;
+    }
+    return hour + 12;
+};
+export const getStartOfWeek = (refParts) => {
+    const { dayOfWeek } = refParts;
+    if (dayOfWeek === null || dayOfWeek === undefined) {
+        throw new Error('No day of week provided');
+    }
+    return subtractDays(refParts, dayOfWeek);
+};
+export const getEndOfWeek = (refParts) => {
+    const { dayOfWeek } = refParts;
+    if (dayOfWeek === null || dayOfWeek === undefined) {
+        throw new Error('No day of week provided');
+    }
+    return addDays(refParts, 6 - dayOfWeek);
+};
+export const getNextDay = (refParts) => {
+    return addDays(refParts, 1);
+};
+export const getPreviousDay = (refParts) => {
+    return subtractDays(refParts, 1);
+};
+export const getPreviousWeek = (refParts) => {
+    return subtractDays(refParts, 7);
+};
+export const getNextWeek = (refParts) => {
+    return addDays(refParts, 7);
+};
+/**
+ * Given datetime parts, subtract
+ * numDays from the date.
+ * Returns a new DatetimeParts object
+ * Currently can only go backward at most 1 month.
+ */
+export const subtractDays = (refParts, numDays) => {
+    const { month, day, year } = refParts;
+    if (day === null) {
+        throw new Error('No day provided');
+    }
+    const workingParts = {
+        month,
+        day,
+        year,
+    };
+    workingParts.day = day - numDays;
+    /**
+     * If wrapping to previous month
+     * update days and decrement month
+     */
+    if (workingParts.day < 1) {
+        workingParts.month -= 1;
+    }
+    /**
+     * If moving to previous year, reset
+     * month to December and decrement year
+     */
+    if (workingParts.month < 1) {
+        workingParts.month = 12;
+        workingParts.year -= 1;
+    }
+    /**
+     * Determine how many days are in the current
+     * month
+     */
+    if (workingParts.day < 1) {
+        const daysInMonth = getNumDaysInMonth(workingParts.month, workingParts.year);
+        /**
+         * Take num days in month and add the
+         * number of underflow days. This number will
+         * be negative.
+         * Example: 1 week before Jan 2, 2021 is
+         * December 26, 2021 so:
+         * 2 - 7 = -5
+         * 31 + (-5) = 26
+         */
+        workingParts.day = daysInMonth + workingParts.day;
+    }
+    return workingParts;
+};
+/**
+ * Given datetime parts, add
+ * numDays to the date.
+ * Returns a new DatetimeParts object
+ * Currently can only go forward at most 1 month.
+ */
+export const addDays = (refParts, numDays) => {
+    const { month, day, year } = refParts;
+    if (day === null) {
+        throw new Error('No day provided');
+    }
+    const workingParts = {
+        month,
+        day,
+        year,
+    };
+    const daysInMonth = getNumDaysInMonth(month, year);
+    workingParts.day = day + numDays;
+    /**
+     * If wrapping to next month
+     * update days and increment month
+     */
+    if (workingParts.day > daysInMonth) {
+        workingParts.day -= daysInMonth;
+        workingParts.month += 1;
+    }
+    /**
+     * If moving to next year, reset
+     * month to January and increment year
+     */
+    if (workingParts.month > 12) {
+        workingParts.month = 1;
+        workingParts.year += 1;
+    }
+    return workingParts;
+};
+/**
+ * Given DatetimeParts, generate the previous month.
+ */
+export const getPreviousMonth = (refParts) => {
+    /**
+     * If current month is January, wrap backwards
+     *  to December of the previous year.
+     */
+    const month = refParts.month === 1 ? 12 : refParts.month - 1;
+    const year = refParts.month === 1 ? refParts.year - 1 : refParts.year;
+    const numDaysInMonth = getNumDaysInMonth(month, year);
+    const day = numDaysInMonth < refParts.day ? numDaysInMonth : refParts.day;
+    return { month, year, day };
+};
+/**
+ * Given DatetimeParts, generate the next month.
+ */
+export const getNextMonth = (refParts) => {
+    /**
+     * If current month is December, wrap forwards
+     *  to January of the next year.
+     */
+    const month = refParts.month === 12 ? 1 : refParts.month + 1;
+    const year = refParts.month === 12 ? refParts.year + 1 : refParts.year;
+    const numDaysInMonth = getNumDaysInMonth(month, year);
+    const day = numDaysInMonth < refParts.day ? numDaysInMonth : refParts.day;
+    return { month, year, day };
+};
+const changeYear = (refParts, yearDelta) => {
+    const month = refParts.month;
+    const year = refParts.year + yearDelta;
+    const numDaysInMonth = getNumDaysInMonth(month, year);
+    const day = numDaysInMonth < refParts.day ? numDaysInMonth : refParts.day;
+    return { month, year, day };
+};
+/**
+ * Given DatetimeParts, generate the previous year.
+ */
+export const getPreviousYear = (refParts) => {
+    return changeYear(refParts, -1);
+};
+/**
+ * Given DatetimeParts, generate the next year.
+ */
+export const getNextYear = (refParts) => {
+    return changeYear(refParts, 1);
+};
+/**
+ * If PM, then internal value should
+ * be converted to 24-hr time.
+ * Does not apply when public
+ * values are already 24-hr time.
+ */
+export const getInternalHourValue = (hour, use24Hour, ampm) => {
+    if (use24Hour) {
+        return hour;
+    }
+    return convert12HourTo24Hour(hour, ampm);
+};
+/**
+ * Unless otherwise stated, all month values are
+ * 1 indexed instead of the typical 0 index in JS Date.
+ * Example:
+ *   January = Month 0 when using JS Date
+ *   January = Month 1 when using this datetime util
+ */
+/**
+ * Given the current datetime parts and a new AM/PM value
+ * calculate what the hour should be in 24-hour time format.
+ * Used when toggling the AM/PM segment since we store our hours
+ * in 24-hour time format internally.
+ */
+export const calculateHourFromAMPM = (currentParts, newAMPM) => {
+    const { ampm: currentAMPM, hour } = currentParts;
+    let newHour = hour;
+    /**
+     * If going from AM --> PM, need to update the
+     *
+     */
+    if (currentAMPM === 'am' && newAMPM === 'pm') {
+        newHour = convert12HourTo24Hour(newHour, 'pm');
+        /**
+         * If going from PM --> AM
+         */
+    }
+    else if (currentAMPM === 'pm' && newAMPM === 'am') {
+        newHour = Math.abs(newHour - 12);
+    }
+    return newHour;
+};
+/**
+ * Updates parts to ensure that month and day
+ * values are valid. For days that do not exist,
+ * or are outside the min/max bounds, the closest
+ * valid day is used.
+ */
+export const validateParts = (parts, minParts, maxParts) => {
+    const { month, day, year } = parts;
+    const partsCopy = clampDate(Object.assign({}, parts), minParts, maxParts);
+    const numDays = getNumDaysInMonth(month, year);
+    /**
+     * If the max number of days
+     * is greater than the day we want
+     * to set, update the DatetimeParts
+     * day field to be the max days.
+     */
+    if (day !== null && numDays < day) {
+        partsCopy.day = numDays;
+    }
+    /**
+     * If value is same day as min day,
+     * make sure the time value is in bounds.
+     */
+    if (minParts !== undefined && isSameDay(partsCopy, minParts)) {
+        /**
+         * If the hour is out of bounds,
+         * update both the hour and minute.
+         * This is done so that the new time
+         * is closest to what the user selected.
+         */
+        if (partsCopy.hour !== undefined && minParts.hour !== undefined) {
+            if (partsCopy.hour < minParts.hour) {
+                partsCopy.hour = minParts.hour;
+                partsCopy.minute = minParts.minute;
+                /**
+                 * If only the minute is out of bounds,
+                 * set it to the min minute.
+                 */
+            }
+            else if (partsCopy.hour === minParts.hour &&
+                partsCopy.minute !== undefined &&
+                minParts.minute !== undefined &&
+                partsCopy.minute < minParts.minute) {
+                partsCopy.minute = minParts.minute;
+            }
+        }
+    }
+    /**
+     * If value is same day as max day,
+     * make sure the time value is in bounds.
+     */
+    if (maxParts !== undefined && isSameDay(parts, maxParts)) {
+        /**
+         * If the hour is out of bounds,
+         * update both the hour and minute.
+         * This is done so that the new time
+         * is closest to what the user selected.
+         */
+        if (partsCopy.hour !== undefined && maxParts.hour !== undefined) {
+            if (partsCopy.hour > maxParts.hour) {
+                partsCopy.hour = maxParts.hour;
+                partsCopy.minute = maxParts.minute;
+                /**
+                 * If only the minute is out of bounds,
+                 * set it to the max minute.
+                 */
+            }
+            else if (partsCopy.hour === maxParts.hour &&
+                partsCopy.minute !== undefined &&
+                maxParts.minute !== undefined &&
+                partsCopy.minute > maxParts.minute) {
+                partsCopy.minute = maxParts.minute;
+            }
+        }
+    }
+    return partsCopy;
+};
+/**
+ * Returns the closest date to refParts
+ * that also meets the constraints of
+ * the *Values params.
+ */
+export const getClosestValidDate = ({ refParts, monthValues, dayValues, yearValues, hourValues, minuteValues, minParts, maxParts, }) => {
+    const { hour, minute, day, month, year } = refParts;
+    const copyParts = Object.assign(Object.assign({}, refParts), { dayOfWeek: undefined });
+    if (yearValues !== undefined) {
+        // Filters out years that are out of the min/max bounds
+        const filteredYears = yearValues.filter((year) => {
+            if (minParts !== undefined && year < minParts.year) {
+                return false;
+            }
+            if (maxParts !== undefined && year > maxParts.year) {
+                return false;
+            }
+            return true;
+        });
+        copyParts.year = findClosestValue(year, filteredYears);
+    }
+    if (monthValues !== undefined) {
+        // Filters out months that are out of the min/max bounds
+        const filteredMonths = monthValues.filter((month) => {
+            if (minParts !== undefined && copyParts.year === minParts.year && month < minParts.month) {
+                return false;
+            }
+            if (maxParts !== undefined && copyParts.year === maxParts.year && month > maxParts.month) {
+                return false;
+            }
+            return true;
+        });
+        copyParts.month = findClosestValue(month, filteredMonths);
+    }
+    // Day is nullable but cannot be undefined
+    if (day !== null && dayValues !== undefined) {
+        // Filters out days that are out of the min/max bounds
+        const filteredDays = dayValues.filter((day) => {
+            if (minParts !== undefined && isBefore(Object.assign(Object.assign({}, copyParts), { day }), minParts)) {
+                return false;
+            }
+            if (maxParts !== undefined && isAfter(Object.assign(Object.assign({}, copyParts), { day }), maxParts)) {
+                return false;
+            }
+            return true;
+        });
+        copyParts.day = findClosestValue(day, filteredDays);
+    }
+    if (hour !== undefined && hourValues !== undefined) {
+        // Filters out hours that are out of the min/max bounds
+        const filteredHours = hourValues.filter((hour) => {
+            if ((minParts === null || minParts === void 0 ? void 0 : minParts.hour) !== undefined && isSameDay(copyParts, minParts) && hour < minParts.hour) {
+                return false;
+            }
+            if ((maxParts === null || maxParts === void 0 ? void 0 : maxParts.hour) !== undefined && isSameDay(copyParts, maxParts) && hour > maxParts.hour) {
+                return false;
+            }
+            return true;
+        });
+        copyParts.hour = findClosestValue(hour, filteredHours);
+        copyParts.ampm = parseAmPm(copyParts.hour);
+    }
+    if (minute !== undefined && minuteValues !== undefined) {
+        // Filters out minutes that are out of the min/max bounds
+        const filteredMinutes = minuteValues.filter((minute) => {
+            if ((minParts === null || minParts === void 0 ? void 0 : minParts.minute) !== undefined &&
+                isSameDay(copyParts, minParts) &&
+                copyParts.hour === minParts.hour &&
+                minute < minParts.minute) {
+                return false;
+            }
+            if ((maxParts === null || maxParts === void 0 ? void 0 : maxParts.minute) !== undefined &&
+                isSameDay(copyParts, maxParts) &&
+                copyParts.hour === maxParts.hour &&
+                minute > maxParts.minute) {
+                return false;
+            }
+            return true;
+        });
+        copyParts.minute = findClosestValue(minute, filteredMinutes);
+    }
+    return copyParts;
+};
+/**
+ * Finds the value in "values" that is
+ * numerically closest to "reference".
+ * This function assumes that "values" is
+ * already sorted in ascending order.
+ * @param reference The reference number to use
+ * when finding the closest value
+ * @param values The allowed values that will be
+ * searched to find the closest value to "reference"
+ */
+const findClosestValue = (reference, values) => {
+    let closestValue = values[0];
+    let rank = Math.abs(closestValue - reference);
+    for (let i = 1; i < values.length; i++) {
+        const value = values[i];
+        /**
+         * This code prioritizes the first
+         * closest result. Given two values
+         * with the same distance from reference,
+         * this code will prioritize the smaller of
+         * the two values.
+         */
+        const valueRank = Math.abs(value - reference);
+        if (valueRank < rank) {
+            closestValue = value;
+            rank = valueRank;
+        }
+    }
+    return closestValue;
+};

+ 197 - 0
src/node_modules/@ionic/core/dist/collection/components/datetime/utils/parse.js

@@ -0,0 +1,197 @@
+/*!
+ * (C) Ionic http://ionicframework.com - MIT License
+ */
+import { printIonWarning } from "../../../utils/logging/index";
+import { isAfter, isBefore } from "./comparison";
+import { getNumDaysInMonth } from "./helpers";
+const ISO_8601_REGEXP = 
+// eslint-disable-next-line no-useless-escape
+/^(\d{4}|[+\-]\d{6})(?:-(\d{2})(?:-(\d{2}))?)?(?:T(\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{3}))?)?(?:(Z)|([+\-])(\d{2})(?::(\d{2}))?)?)?$/;
+// eslint-disable-next-line no-useless-escape
+const TIME_REGEXP = /^((\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{3}))?)?(?:(Z)|([+\-])(\d{2})(?::(\d{2}))?)?)?$/;
+/**
+ * Use to convert a string of comma separated numbers or
+ * an array of numbers, and clean up any user input
+ */
+export const convertToArrayOfNumbers = (input) => {
+    if (input === undefined) {
+        return;
+    }
+    let processedInput = input;
+    if (typeof input === 'string') {
+        // convert the string to an array of strings
+        // auto remove any whitespace and [] characters
+        processedInput = input.replace(/\[|\]|\s/g, '').split(',');
+    }
+    let values;
+    if (Array.isArray(processedInput)) {
+        // ensure each value is an actual number in the returned array
+        values = processedInput.map((num) => parseInt(num, 10)).filter(isFinite);
+    }
+    else {
+        values = [processedInput];
+    }
+    return values;
+};
+/**
+ * Extracts date information
+ * from a .calendar-day element
+ * into DatetimeParts.
+ */
+export const getPartsFromCalendarDay = (el) => {
+    return {
+        month: parseInt(el.getAttribute('data-month'), 10),
+        day: parseInt(el.getAttribute('data-day'), 10),
+        year: parseInt(el.getAttribute('data-year'), 10),
+        dayOfWeek: parseInt(el.getAttribute('data-day-of-week'), 10),
+    };
+};
+export function parseDate(val) {
+    if (Array.isArray(val)) {
+        const parsedArray = [];
+        for (const valStr of val) {
+            const parsedVal = parseDate(valStr);
+            /**
+             * If any of the values weren't parsed correctly, consider
+             * the entire batch incorrect. This simplifies the type
+             * signatures by having "undefined" be a general error case
+             * instead of returning (Datetime | undefined)[], which is
+             * harder for TS to perform type narrowing on.
+             */
+            if (!parsedVal) {
+                return undefined;
+            }
+            parsedArray.push(parsedVal);
+        }
+        return parsedArray;
+    }
+    // manually parse IS0 cuz Date.parse cannot be trusted
+    // ISO 8601 format: 1994-12-15T13:47:20Z
+    let parse = null;
+    if (val != null && val !== '') {
+        // try parsing for just time first, HH:MM
+        parse = TIME_REGEXP.exec(val);
+        if (parse) {
+            // adjust the array so it fits nicely with the datetime parse
+            parse.unshift(undefined, undefined);
+            parse[2] = parse[3] = undefined;
+        }
+        else {
+            // try parsing for full ISO datetime
+            parse = ISO_8601_REGEXP.exec(val);
+        }
+    }
+    if (parse === null) {
+        // wasn't able to parse the ISO datetime
+        printIonWarning(`Unable to parse date string: ${val}. Please provide a valid ISO 8601 datetime string.`);
+        return undefined;
+    }
+    // ensure all the parse values exist with at least 0
+    for (let i = 1; i < 8; i++) {
+        parse[i] = parse[i] !== undefined ? parseInt(parse[i], 10) : undefined;
+    }
+    // can also get second and millisecond from parse[6] and parse[7] if needed
+    return {
+        year: parse[1],
+        month: parse[2],
+        day: parse[3],
+        hour: parse[4],
+        minute: parse[5],
+        ampm: parse[4] < 12 ? 'am' : 'pm',
+    };
+}
+export const clampDate = (dateParts, minParts, maxParts) => {
+    if (minParts && isBefore(dateParts, minParts)) {
+        return minParts;
+    }
+    else if (maxParts && isAfter(dateParts, maxParts)) {
+        return maxParts;
+    }
+    return dateParts;
+};
+/**
+ * Parses an hour and returns if the value is in the morning (am) or afternoon (pm).
+ * @param hour The hour to format, should be 0-23
+ * @returns `pm` if the hour is greater than or equal to 12, `am` if less than 12.
+ */
+export const parseAmPm = (hour) => {
+    return hour >= 12 ? 'pm' : 'am';
+};
+/**
+ * Takes a max date string and creates a DatetimeParts
+ * object, filling in any missing information.
+ * For example, max="2012" would fill in the missing
+ * month, day, hour, and minute information.
+ */
+export const parseMaxParts = (max, todayParts) => {
+    const result = parseDate(max);
+    /**
+     * If min was not a valid date then return undefined.
+     */
+    if (result === undefined) {
+        return;
+    }
+    const { month, day, year, hour, minute } = result;
+    /**
+     * When passing in `max` or `min`, developers
+     * can pass in any ISO-8601 string. This means
+     * that not all of the date/time fields are defined.
+     * For example, passing max="2012" is valid even though
+     * there is no month, day, hour, or minute data.
+     * However, all of this data is required when clamping the date
+     * so that the correct initial value can be selected. As a result,
+     * we need to fill in any omitted data with the min or max values.
+     */
+    const yearValue = year !== null && year !== void 0 ? year : todayParts.year;
+    const monthValue = month !== null && month !== void 0 ? month : 12;
+    return {
+        month: monthValue,
+        day: day !== null && day !== void 0 ? day : getNumDaysInMonth(monthValue, yearValue),
+        /**
+         * Passing in "HH:mm" is a valid ISO-8601
+         * string, so we just default to the current year
+         * in this case.
+         */
+        year: yearValue,
+        hour: hour !== null && hour !== void 0 ? hour : 23,
+        minute: minute !== null && minute !== void 0 ? minute : 59,
+    };
+};
+/**
+ * Takes a min date string and creates a DatetimeParts
+ * object, filling in any missing information.
+ * For example, min="2012" would fill in the missing
+ * month, day, hour, and minute information.
+ */
+export const parseMinParts = (min, todayParts) => {
+    const result = parseDate(min);
+    /**
+     * If min was not a valid date then return undefined.
+     */
+    if (result === undefined) {
+        return;
+    }
+    const { month, day, year, hour, minute } = result;
+    /**
+     * When passing in `max` or `min`, developers
+     * can pass in any ISO-8601 string. This means
+     * that not all of the date/time fields are defined.
+     * For example, passing max="2012" is valid even though
+     * there is no month, day, hour, or minute data.
+     * However, all of this data is required when clamping the date
+     * so that the correct initial value can be selected. As a result,
+     * we need to fill in any omitted data with the min or max values.
+     */
+    return {
+        month: month !== null && month !== void 0 ? month : 1,
+        day: day !== null && day !== void 0 ? day : 1,
+        /**
+         * Passing in "HH:mm" is a valid ISO-8601
+         * string, so we just default to the current year
+         * in this case.
+         */
+        year: year !== null && year !== void 0 ? year : todayParts.year,
+        hour: hour !== null && hour !== void 0 ? hour : 0,
+        minute: minute !== null && minute !== void 0 ? minute : 0,
+    };
+};

+ 173 - 0
src/node_modules/@ionic/core/dist/collection/components/datetime/utils/state.js

@@ -0,0 +1,173 @@
+/*!
+ * (C) Ionic http://ionicframework.com - MIT License
+ */
+import { printIonError } from "../../../utils/logging/index";
+import { isAfter, isBefore, isSameDay } from "./comparison";
+import { generateDayAriaLabel, getDay } from "./format";
+import { getNextMonth, getPreviousMonth } from "./manipulation";
+export const isYearDisabled = (refYear, minParts, maxParts) => {
+    if (minParts && minParts.year > refYear) {
+        return true;
+    }
+    if (maxParts && maxParts.year < refYear) {
+        return true;
+    }
+    return false;
+};
+/**
+ * Returns true if a given day should
+ * not be interactive according to its value,
+ * or the max/min dates.
+ */
+export const isDayDisabled = (refParts, minParts, maxParts, dayValues) => {
+    /**
+     * If this is a filler date (i.e. padding)
+     * then the date is disabled.
+     */
+    if (refParts.day === null) {
+        return true;
+    }
+    /**
+     * If user passed in a list of acceptable day values
+     * check to make sure that the date we are looking
+     * at is in this array.
+     */
+    if (dayValues !== undefined && !dayValues.includes(refParts.day)) {
+        return true;
+    }
+    /**
+     * Given a min date, perform the following
+     * checks. If any of them are true, then the
+     * day should be disabled:
+     * 1. Is the current year < the min allowed year?
+     * 2. Is the current year === min allowed year,
+     * but the current month < the min allowed month?
+     * 3. Is the current year === min allowed year, the
+     * current month === min allow month, but the current
+     * day < the min allowed day?
+     */
+    if (minParts && isBefore(refParts, minParts)) {
+        return true;
+    }
+    /**
+     * Given a max date, perform the following
+     * checks. If any of them are true, then the
+     * day should be disabled:
+     * 1. Is the current year > the max allowed year?
+     * 2. Is the current year === max allowed year,
+     * but the current month > the max allowed month?
+     * 3. Is the current year === max allowed year, the
+     * current month === max allow month, but the current
+     * day > the max allowed day?
+     */
+    if (maxParts && isAfter(refParts, maxParts)) {
+        return true;
+    }
+    /**
+     * If none of these checks
+     * passed then the date should
+     * be interactive.
+     */
+    return false;
+};
+/**
+ * Given a locale, a date, the selected date(s), and today's date,
+ * generate the state for a given calendar day button.
+ */
+export const getCalendarDayState = (locale, refParts, activeParts, todayParts, minParts, maxParts, dayValues) => {
+    /**
+     * activeParts signals what day(s) are currently selected in the datetime.
+     * If multiple="true", this will be an array, but the logic in this util
+     * is the same whether we have one selected day or many because we're only
+     * calculating the state for one button. So, we treat a single activeParts value
+     * the same as an array of length one.
+     */
+    const activePartsArray = Array.isArray(activeParts) ? activeParts : [activeParts];
+    /**
+     * The day button is active if it is selected, or in other words, if refParts
+     * matches at least one selected date.
+     */
+    const isActive = activePartsArray.find((parts) => isSameDay(refParts, parts)) !== undefined;
+    const isToday = isSameDay(refParts, todayParts);
+    const disabled = isDayDisabled(refParts, minParts, maxParts, dayValues);
+    /**
+     * Note that we always return one object regardless of whether activeParts
+     * was an array, since we pare down to one value for isActive.
+     */
+    return {
+        disabled,
+        isActive,
+        isToday,
+        ariaSelected: isActive ? 'true' : null,
+        ariaLabel: generateDayAriaLabel(locale, isToday, refParts),
+        text: refParts.day != null ? getDay(locale, refParts) : null,
+    };
+};
+/**
+ * Returns `true` if the month is disabled given the
+ * current date value and min/max date constraints.
+ */
+export const isMonthDisabled = (refParts, { minParts, maxParts, }) => {
+    // If the year is disabled then the month is disabled.
+    if (isYearDisabled(refParts.year, minParts, maxParts)) {
+        return true;
+    }
+    // If the date value is before the min date, then the month is disabled.
+    // If the date value is after the max date, then the month is disabled.
+    if ((minParts && isBefore(refParts, minParts)) || (maxParts && isAfter(refParts, maxParts))) {
+        return true;
+    }
+    return false;
+};
+/**
+ * Given a working date, an optional minimum date range,
+ * and an optional maximum date range; determine if the
+ * previous navigation button is disabled.
+ */
+export const isPrevMonthDisabled = (refParts, minParts, maxParts) => {
+    const prevMonth = Object.assign(Object.assign({}, getPreviousMonth(refParts)), { day: null });
+    return isMonthDisabled(prevMonth, {
+        minParts,
+        maxParts,
+    });
+};
+/**
+ * Given a working date and a maximum date range,
+ * determine if the next navigation button is disabled.
+ */
+export const isNextMonthDisabled = (refParts, maxParts) => {
+    const nextMonth = Object.assign(Object.assign({}, getNextMonth(refParts)), { day: null });
+    return isMonthDisabled(nextMonth, {
+        maxParts,
+    });
+};
+/**
+ * Given the value of the highlightedDates property
+ * and an ISO string, return the styles to use for
+ * that date, or undefined if none are found.
+ */
+export const getHighlightStyles = (highlightedDates, dateIsoString, el) => {
+    if (Array.isArray(highlightedDates)) {
+        const dateStringWithoutTime = dateIsoString.split('T')[0];
+        const matchingHighlight = highlightedDates.find((hd) => hd.date === dateStringWithoutTime);
+        if (matchingHighlight) {
+            return {
+                textColor: matchingHighlight.textColor,
+                backgroundColor: matchingHighlight.backgroundColor,
+            };
+        }
+    }
+    else {
+        /**
+         * Wrap in a try-catch to prevent exceptions in the user's function
+         * from interrupting the calendar's rendering.
+         */
+        try {
+            return highlightedDates(dateIsoString);
+        }
+        catch (e) {
+            printIonError('Exception thrown from provided `highlightedDates` callback. Please check your function and try again.', el, e);
+        }
+    }
+    return undefined;
+};

+ 45 - 0
src/node_modules/@ionic/core/dist/collection/components/datetime/utils/validate.js

@@ -0,0 +1,45 @@
+/*!
+ * (C) Ionic http://ionicframework.com - MIT License
+ */
+import { printIonWarning } from "../../../utils/logging/index";
+/**
+ * If a time zone is provided in the format options, the rendered text could
+ * differ from what was selected in the Datetime, which could cause
+ * confusion.
+ */
+export const warnIfTimeZoneProvided = (el, formatOptions) => {
+    var _a, _b, _c, _d;
+    if (((_a = formatOptions === null || formatOptions === void 0 ? void 0 : formatOptions.date) === null || _a === void 0 ? void 0 : _a.timeZone) ||
+        ((_b = formatOptions === null || formatOptions === void 0 ? void 0 : formatOptions.date) === null || _b === void 0 ? void 0 : _b.timeZoneName) ||
+        ((_c = formatOptions === null || formatOptions === void 0 ? void 0 : formatOptions.time) === null || _c === void 0 ? void 0 : _c.timeZone) ||
+        ((_d = formatOptions === null || formatOptions === void 0 ? void 0 : formatOptions.time) === null || _d === void 0 ? void 0 : _d.timeZoneName)) {
+        printIonWarning('Datetime: "timeZone" and "timeZoneName" are not supported in "formatOptions".', el);
+    }
+};
+export const checkForPresentationFormatMismatch = (el, presentation, formatOptions) => {
+    // formatOptions is not required
+    if (!formatOptions)
+        return;
+    // If formatOptions is provided, the date and/or time objects are required, depending on the presentation
+    switch (presentation) {
+        case 'date':
+        case 'month-year':
+        case 'month':
+        case 'year':
+            if (formatOptions.date === undefined) {
+                printIonWarning(`Datetime: The '${presentation}' presentation requires a date object in formatOptions.`, el);
+            }
+            break;
+        case 'time':
+            if (formatOptions.time === undefined) {
+                printIonWarning(`Datetime: The 'time' presentation requires a time object in formatOptions.`, el);
+            }
+            break;
+        case 'date-time':
+        case 'time-date':
+            if (formatOptions.date === undefined && formatOptions.time === undefined) {
+                printIonWarning(`Datetime: The '${presentation}' presentation requires either a date or time object (or both) in formatOptions.`, el);
+            }
+            break;
+    }
+};

+ 422 - 0
src/node_modules/@ionic/core/dist/collection/components/fab-button/fab-button.ios.css

@@ -0,0 +1,422 @@
+/**
+ * Convert a font size to a dynamic font size.
+ * Fonts that participate in Dynamic Type should use
+ * dynamic font sizes.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param unit (optional) - The unit to convert to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * Convert a font size to a dynamic font size but impose
+ * a maximum font size.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param maxScale - The maximum scale of the font (i.e. 2.5 for a maximum 250% scale).
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * Convert a font size to a dynamic font size but impose
+ * a minimum font size.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param minScale - The minimum scale of the font (i.e. 0.8 for a minimum 80% scale).
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * Convert a font size to a dynamic font size but impose
+ * maximum and minimum font sizes.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param minScale - The minimum scale of the font (i.e. 0.8 for a minimum 80% scale).
+ * @param maxScale - The maximum scale of the font (i.e. 2.5 for a maximum 250% scale).
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * A heuristic that applies CSS to tablet
+ * viewports.
+ *
+ * Usage:
+ * @include tablet-viewport() {
+ *   :host {
+ *     background-color: green;
+ *   }
+ * }
+ */
+/**
+ * A heuristic that applies CSS to mobile
+ * viewports (i.e. phones, not tablets).
+ *
+ * Usage:
+ * @include mobile-viewport() {
+ *   :host {
+ *     background-color: blue;
+ *   }
+ * }
+ */
+:host {
+  /**
+   * @prop --background: Background of the button
+   * @prop --background-activated: Background of the button when pressed. Note: setting this will interfere with the Material Design ripple.
+   * @prop --background-activated-opacity: Opacity of the button background when pressed
+   * @prop --background-focused: Background of the button when focused with the tab key
+   * @prop --background-focused-opacity: Opacity of the button background when focused with the tab key
+   * @prop --background-hover: Background of the button on hover
+   * @prop --background-hover-opacity: Opacity of the button background on hover
+   *
+   * @prop --color: Text color of the button
+   * @prop --color-activated: Text color of the button when pressed
+   * @prop --color-focused: Text color of the button when focused with the tab key
+   * @prop --color-hover: Text color of the button on hover
+   *
+   * @prop --transition: Transition of the button
+   *
+   * @prop --close-icon-font-size: Font size of the close icon
+   *
+   * @prop --border-radius: Border radius of the button
+   * @prop --border-width: Border width of the button
+   * @prop --border-style: Border style of the button
+   * @prop --border-color: Border color of the button
+   *
+   * @prop --ripple-color: Color of the button ripple effect
+   *
+   * @prop --box-shadow: Box shadow of the button
+   *
+   * @prop --padding-top: Top padding of the button
+   * @prop --padding-end: Right padding if direction is left-to-right, and left padding if direction is right-to-left of the button
+   * @prop --padding-bottom: Bottom padding of the button
+   * @prop --padding-start: Left padding if direction is left-to-right, and right padding if direction is right-to-left of the button
+   */
+  --color-activated: var(--color);
+  --color-focused: var(--color);
+  --color-hover: var(--color);
+  --background-hover: var(--ion-color-primary-contrast, #fff);
+  --background-hover-opacity: .08;
+  --transition: background-color, opacity 100ms linear;
+  --ripple-color: currentColor;
+  --border-radius: 50%;
+  --border-width: 0;
+  --border-style: none;
+  --border-color: initial;
+  --padding-top: 0;
+  --padding-end: 0;
+  --padding-bottom: 0;
+  --padding-start: 0;
+  margin-left: 0;
+  margin-right: 0;
+  margin-top: 0;
+  margin-bottom: 0;
+  display: block;
+  width: 56px;
+  height: 56px;
+  font-size: 14px;
+  text-align: center;
+  text-overflow: ellipsis;
+  text-transform: none;
+  white-space: nowrap;
+  font-kerning: none;
+}
+
+.button-native {
+  border-radius: var(--border-radius);
+  -webkit-padding-start: var(--padding-start);
+  padding-inline-start: var(--padding-start);
+  -webkit-padding-end: var(--padding-end);
+  padding-inline-end: var(--padding-end);
+  padding-top: var(--padding-top);
+  padding-bottom: var(--padding-bottom);
+  font-family: inherit;
+  font-size: inherit;
+  font-style: inherit;
+  font-weight: inherit;
+  letter-spacing: inherit;
+  text-decoration: inherit;
+  text-indent: inherit;
+  text-overflow: inherit;
+  text-transform: inherit;
+  text-align: inherit;
+  white-space: inherit;
+  color: inherit;
+  display: block;
+  position: relative;
+  width: 100%;
+  height: 100%;
+  transform: var(--transform);
+  transition: var(--transition);
+  border-width: var(--border-width);
+  border-style: var(--border-style);
+  border-color: var(--border-color);
+  outline: none;
+  background: var(--background);
+  background-clip: padding-box;
+  color: var(--color);
+  box-shadow: var(--box-shadow);
+  contain: strict;
+  cursor: pointer;
+  overflow: hidden;
+  z-index: 0;
+  appearance: none;
+  box-sizing: border-box;
+}
+
+::slotted(ion-icon) {
+  line-height: 1;
+}
+
+.button-native::after {
+  left: 0;
+  right: 0;
+  top: 0;
+  bottom: 0;
+  position: absolute;
+  content: "";
+  opacity: 0;
+}
+
+.button-inner {
+  left: 0;
+  right: 0;
+  top: 0;
+  display: flex;
+  position: absolute;
+  flex-flow: row nowrap;
+  flex-shrink: 0;
+  align-items: center;
+  justify-content: center;
+  height: 100%;
+  transition: all ease-in-out 300ms;
+  transition-property: transform, opacity;
+  z-index: 1;
+}
+
+:host(.fab-button-disabled) {
+  cursor: default;
+  opacity: 0.5;
+  pointer-events: none;
+}
+
+@media (any-hover: hover) {
+  :host(:hover) .button-native {
+    color: var(--color-hover);
+  }
+  :host(:hover) .button-native::after {
+    background: var(--background-hover);
+    opacity: var(--background-hover-opacity);
+  }
+}
+:host(.ion-focused) .button-native {
+  color: var(--color-focused);
+}
+:host(.ion-focused) .button-native::after {
+  background: var(--background-focused);
+  opacity: var(--background-focused-opacity);
+}
+
+:host(.ion-activated) .button-native {
+  color: var(--color-activated);
+}
+:host(.ion-activated) .button-native::after {
+  background: var(--background-activated);
+  opacity: var(--background-activated-opacity);
+}
+
+::slotted(ion-icon) {
+  line-height: 1;
+}
+
+:host(.fab-button-small) {
+  -webkit-margin-start: 8px;
+  margin-inline-start: 8px;
+  -webkit-margin-end: 8px;
+  margin-inline-end: 8px;
+  margin-top: 8px;
+  margin-bottom: 8px;
+  width: 40px;
+  height: 40px;
+}
+
+.close-icon {
+  -webkit-margin-start: auto;
+  margin-inline-start: auto;
+  -webkit-margin-end: auto;
+  margin-inline-end: auto;
+  margin-top: 0;
+  margin-bottom: 0;
+  left: 0;
+  right: 0;
+  top: 0;
+  position: absolute;
+  height: 100%;
+  transform: scale(0.4) rotateZ(-45deg);
+  transition: all ease-in-out 300ms;
+  transition-property: transform, opacity;
+  font-size: var(--close-icon-font-size);
+  opacity: 0;
+  z-index: 1;
+}
+
+:host(.fab-button-close-active) .close-icon {
+  transform: scale(1) rotateZ(0deg);
+  opacity: 1;
+}
+
+:host(.fab-button-close-active) .button-inner {
+  transform: scale(0.4) rotateZ(45deg);
+  opacity: 0;
+}
+
+ion-ripple-effect {
+  color: var(--ripple-color);
+}
+
+@supports (backdrop-filter: blur(0)) {
+  :host(.fab-button-translucent) .button-native {
+    backdrop-filter: var(--backdrop-filter);
+  }
+}
+:host(.ion-color) .button-native {
+  background: var(--ion-color-base);
+  color: var(--ion-color-contrast);
+}
+
+/**
+ * Convert a font size to a dynamic font size.
+ * Fonts that participate in Dynamic Type should use
+ * dynamic font sizes.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param unit (optional) - The unit to convert to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * Convert a font size to a dynamic font size but impose
+ * a maximum font size.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param maxScale - The maximum scale of the font (i.e. 2.5 for a maximum 250% scale).
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * Convert a font size to a dynamic font size but impose
+ * a minimum font size.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param minScale - The minimum scale of the font (i.e. 0.8 for a minimum 80% scale).
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * Convert a font size to a dynamic font size but impose
+ * maximum and minimum font sizes.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param minScale - The minimum scale of the font (i.e. 0.8 for a minimum 80% scale).
+ * @param maxScale - The maximum scale of the font (i.e. 2.5 for a maximum 250% scale).
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * A heuristic that applies CSS to tablet
+ * viewports.
+ *
+ * Usage:
+ * @include tablet-viewport() {
+ *   :host {
+ *     background-color: green;
+ *   }
+ * }
+ */
+/**
+ * A heuristic that applies CSS to mobile
+ * viewports (i.e. phones, not tablets).
+ *
+ * Usage:
+ * @include mobile-viewport() {
+ *   :host {
+ *     background-color: blue;
+ *   }
+ * }
+ */
+:host {
+  --background: var(--ion-color-primary, #0054e9);
+  --background-activated: var(--ion-color-primary-shade, #004acd);
+  --background-focused: var(--ion-color-primary-shade, #004acd);
+  --background-hover: var(--ion-color-primary-tint, #1a65eb);
+  --background-activated-opacity: 1;
+  --background-focused-opacity: 1;
+  --background-hover-opacity: 1;
+  --color: var(--ion-color-primary-contrast, #fff);
+  --box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
+  --transition: 0.2s transform cubic-bezier(0.25, 1.11, 0.78, 1.59);
+  --close-icon-font-size: 28px;
+}
+
+:host(.ion-activated) {
+  --box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
+  --transform: scale(1.1);
+  --transition: 0.2s transform ease-out;
+}
+
+::slotted(ion-icon) {
+  font-size: 28px;
+}
+
+:host(.fab-button-in-list) {
+  --background: var(--ion-color-light, #f4f5f8);
+  --background-activated: var(--ion-color-light-shade, #d7d8da);
+  --background-focused: var(--background-activated);
+  --background-hover: var(--ion-color-light-tint, #f5f6f9);
+  --color: var(--ion-color-light-contrast, #000);
+  --color-activated: var(--ion-color-light-contrast, #000);
+  --color-focused: var(--color-activated);
+  --transition: transform 200ms ease 10ms, opacity 200ms ease 10ms;
+}
+
+:host(.fab-button-in-list) ::slotted(ion-icon) {
+  font-size: 18px;
+}
+
+:host(.ion-color.ion-focused) .button-native::after {
+  background: var(--ion-color-shade);
+}
+
+:host(.ion-color.ion-focused) .button-native,
+:host(.ion-color.ion-activated) .button-native {
+  color: var(--ion-color-contrast);
+}
+:host(.ion-color.ion-focused) .button-native::after,
+:host(.ion-color.ion-activated) .button-native::after {
+  background: var(--ion-color-shade);
+}
+
+@media (any-hover: hover) {
+  :host(.ion-color:hover) .button-native {
+    color: var(--ion-color-contrast);
+  }
+  :host(.ion-color:hover) .button-native::after {
+    background: var(--ion-color-tint);
+  }
+}
+@supports (backdrop-filter: blur(0)) {
+  :host(.fab-button-translucent) {
+    --background: rgba(var(--ion-color-primary-rgb, 0, 84, 233), 0.9);
+    --background-hover: rgba(var(--ion-color-primary-rgb, 0, 84, 233), 0.8);
+    --background-focused: rgba(var(--ion-color-primary-rgb, 0, 84, 233), 0.82);
+    --backdrop-filter: saturate(180%) blur(20px);
+  }
+  :host(.fab-button-translucent-in-list) {
+    --background: rgba(var(--ion-color-light-rgb, 244, 245, 248), 0.9);
+    --background-hover: rgba(var(--ion-color-light-rgb, 244, 245, 248), 0.8);
+    --background-focused: rgba(var(--ion-color-light-rgb, 244, 245, 248), 0.82);
+  }
+}
+@supports (backdrop-filter: blur(0)) {
+  @media (any-hover: hover) {
+    :host(.fab-button-translucent.ion-color:hover) .button-native {
+      background: rgba(var(--ion-color-base-rgb), 0.8);
+    }
+  }
+  :host(.ion-color.fab-button-translucent) .button-native {
+    background: rgba(var(--ion-color-base-rgb), 0.9);
+  }
+  :host(.ion-color.ion-focused.fab-button-translucent) .button-native,
+  :host(.ion-color.ion-activated.fab-button-translucent) .button-native {
+    background: var(--ion-color-base);
+  }
+}

+ 392 - 0
src/node_modules/@ionic/core/dist/collection/components/fab-button/fab-button.js

@@ -0,0 +1,392 @@
+/*!
+ * (C) Ionic http://ionicframework.com - MIT License
+ */
+import { Host, h } from "@stencil/core";
+import { inheritAriaAttributes } from "../../utils/helpers";
+import { createColorClasses, hostContext, openURL } from "../../utils/theme";
+import { close } from "ionicons/icons";
+import { getIonMode } from "../../global/ionic-global";
+/**
+ * @virtualProp {"ios" | "md"} mode - The mode determines which platform styles to use.
+ *
+ * @part native - The native HTML button or anchor element that wraps all child elements.
+ * @part close-icon - The close icon that is displayed when a fab list opens (uses ion-icon).
+ */
+export class FabButton {
+    constructor() {
+        this.fab = null;
+        this.inheritedAttributes = {};
+        this.onFocus = () => {
+            this.ionFocus.emit();
+        };
+        this.onBlur = () => {
+            this.ionBlur.emit();
+        };
+        this.onClick = () => {
+            const { fab } = this;
+            if (!fab) {
+                return;
+            }
+            fab.toggle();
+        };
+        this.color = undefined;
+        this.activated = false;
+        this.disabled = false;
+        this.download = undefined;
+        this.href = undefined;
+        this.rel = undefined;
+        this.routerDirection = 'forward';
+        this.routerAnimation = undefined;
+        this.target = undefined;
+        this.show = false;
+        this.translucent = false;
+        this.type = 'button';
+        this.size = undefined;
+        this.closeIcon = close;
+    }
+    connectedCallback() {
+        this.fab = this.el.closest('ion-fab');
+    }
+    componentWillLoad() {
+        this.inheritedAttributes = inheritAriaAttributes(this.el);
+    }
+    render() {
+        const { el, disabled, color, href, activated, show, translucent, size, inheritedAttributes } = this;
+        const inList = hostContext('ion-fab-list', el);
+        const mode = getIonMode(this);
+        const TagType = href === undefined ? 'button' : 'a';
+        const attrs = TagType === 'button'
+            ? { type: this.type }
+            : {
+                download: this.download,
+                href,
+                rel: this.rel,
+                target: this.target,
+            };
+        return (h(Host, { key: 'eb347f7d6749c40637540d84778eb8d1b667a947', onClick: this.onClick, "aria-disabled": disabled ? 'true' : null, class: createColorClasses(color, {
+                [mode]: true,
+                'fab-button-in-list': inList,
+                'fab-button-translucent-in-list': inList && translucent,
+                'fab-button-close-active': activated,
+                'fab-button-show': show,
+                'fab-button-disabled': disabled,
+                'fab-button-translucent': translucent,
+                'ion-activatable': true,
+                'ion-focusable': true,
+                [`fab-button-${size}`]: size !== undefined,
+            }) }, h(TagType, Object.assign({ key: '83e853c8815f41543c848eb2e05ec2bb1716110a' }, attrs, { class: "button-native", part: "native", disabled: disabled, onFocus: this.onFocus, onBlur: this.onBlur, onClick: (ev) => openURL(href, ev, this.routerDirection, this.routerAnimation) }, inheritedAttributes), h("ion-icon", { key: '798deede94de658e4345acf7c2aafe2ab2567b0b', "aria-hidden": "true", icon: this.closeIcon, part: "close-icon", class: "close-icon", lazy: false }), h("span", { key: '99252fde6de1aca73fc240a6da7e29acac9acb18', class: "button-inner" }, h("slot", { key: 'dc73e9b41bf1f0e385e5784f975dfb81e37c8dfb' })), mode === 'md' && h("ion-ripple-effect", { key: '8413e162f44a0350f54dff06cff7aad101de3549' }))));
+    }
+    static get is() { return "ion-fab-button"; }
+    static get encapsulation() { return "shadow"; }
+    static get originalStyleUrls() {
+        return {
+            "ios": ["fab-button.ios.scss"],
+            "md": ["fab-button.md.scss"]
+        };
+    }
+    static get styleUrls() {
+        return {
+            "ios": ["fab-button.ios.css"],
+            "md": ["fab-button.md.css"]
+        };
+    }
+    static get properties() {
+        return {
+            "color": {
+                "type": "string",
+                "mutable": false,
+                "complexType": {
+                    "original": "Color",
+                    "resolved": "\"danger\" | \"dark\" | \"light\" | \"medium\" | \"primary\" | \"secondary\" | \"success\" | \"tertiary\" | \"warning\" | string & Record<never, never> | undefined",
+                    "references": {
+                        "Color": {
+                            "location": "import",
+                            "path": "../../interface",
+                            "id": "src/interface.d.ts::Color"
+                        }
+                    }
+                },
+                "required": false,
+                "optional": true,
+                "docs": {
+                    "tags": [],
+                    "text": "The color to use from your application's color palette.\nDefault options are: `\"primary\"`, `\"secondary\"`, `\"tertiary\"`, `\"success\"`, `\"warning\"`, `\"danger\"`, `\"light\"`, `\"medium\"`, and `\"dark\"`.\nFor more information on colors, see [theming](/docs/theming/basics)."
+                },
+                "attribute": "color",
+                "reflect": true
+            },
+            "activated": {
+                "type": "boolean",
+                "mutable": false,
+                "complexType": {
+                    "original": "boolean",
+                    "resolved": "boolean",
+                    "references": {}
+                },
+                "required": false,
+                "optional": false,
+                "docs": {
+                    "tags": [],
+                    "text": "If `true`, the fab button will be show a close icon."
+                },
+                "attribute": "activated",
+                "reflect": false,
+                "defaultValue": "false"
+            },
+            "disabled": {
+                "type": "boolean",
+                "mutable": false,
+                "complexType": {
+                    "original": "boolean",
+                    "resolved": "boolean",
+                    "references": {}
+                },
+                "required": false,
+                "optional": false,
+                "docs": {
+                    "tags": [],
+                    "text": "If `true`, the user cannot interact with the fab button."
+                },
+                "attribute": "disabled",
+                "reflect": false,
+                "defaultValue": "false"
+            },
+            "download": {
+                "type": "string",
+                "mutable": false,
+                "complexType": {
+                    "original": "string | undefined",
+                    "resolved": "string | undefined",
+                    "references": {}
+                },
+                "required": false,
+                "optional": false,
+                "docs": {
+                    "tags": [],
+                    "text": "This attribute instructs browsers to download a URL instead of navigating to\nit, so the user will be prompted to save it as a local file. If the attribute\nhas a value, it is used as the pre-filled file name in the Save prompt\n(the user can still change the file name if they want)."
+                },
+                "attribute": "download",
+                "reflect": false
+            },
+            "href": {
+                "type": "string",
+                "mutable": false,
+                "complexType": {
+                    "original": "string | undefined",
+                    "resolved": "string | undefined",
+                    "references": {}
+                },
+                "required": false,
+                "optional": false,
+                "docs": {
+                    "tags": [],
+                    "text": "Contains a URL or a URL fragment that the hyperlink points to.\nIf this property is set, an anchor tag will be rendered."
+                },
+                "attribute": "href",
+                "reflect": false
+            },
+            "rel": {
+                "type": "string",
+                "mutable": false,
+                "complexType": {
+                    "original": "string | undefined",
+                    "resolved": "string | undefined",
+                    "references": {}
+                },
+                "required": false,
+                "optional": false,
+                "docs": {
+                    "tags": [],
+                    "text": "Specifies the relationship of the target object to the link object.\nThe value is a space-separated list of [link types](https://developer.mozilla.org/en-US/docs/Web/HTML/Link_types)."
+                },
+                "attribute": "rel",
+                "reflect": false
+            },
+            "routerDirection": {
+                "type": "string",
+                "mutable": false,
+                "complexType": {
+                    "original": "RouterDirection",
+                    "resolved": "\"back\" | \"forward\" | \"root\"",
+                    "references": {
+                        "RouterDirection": {
+                            "location": "import",
+                            "path": "../router/utils/interface",
+                            "id": "src/components/router/utils/interface.ts::RouterDirection"
+                        }
+                    }
+                },
+                "required": false,
+                "optional": false,
+                "docs": {
+                    "tags": [],
+                    "text": "When using a router, it specifies the transition direction when navigating to\nanother page using `href`."
+                },
+                "attribute": "router-direction",
+                "reflect": false,
+                "defaultValue": "'forward'"
+            },
+            "routerAnimation": {
+                "type": "unknown",
+                "mutable": false,
+                "complexType": {
+                    "original": "AnimationBuilder | undefined",
+                    "resolved": "((baseEl: any, opts?: any) => Animation) | undefined",
+                    "references": {
+                        "AnimationBuilder": {
+                            "location": "import",
+                            "path": "../../interface",
+                            "id": "src/interface.d.ts::AnimationBuilder"
+                        }
+                    }
+                },
+                "required": false,
+                "optional": false,
+                "docs": {
+                    "tags": [],
+                    "text": "When using a router, it specifies the transition animation when navigating to\nanother page using `href`."
+                }
+            },
+            "target": {
+                "type": "string",
+                "mutable": false,
+                "complexType": {
+                    "original": "string | undefined",
+                    "resolved": "string | undefined",
+                    "references": {}
+                },
+                "required": false,
+                "optional": false,
+                "docs": {
+                    "tags": [],
+                    "text": "Specifies where to display the linked URL.\nOnly applies when an `href` is provided.\nSpecial keywords: `\"_blank\"`, `\"_self\"`, `\"_parent\"`, `\"_top\"`."
+                },
+                "attribute": "target",
+                "reflect": false
+            },
+            "show": {
+                "type": "boolean",
+                "mutable": false,
+                "complexType": {
+                    "original": "boolean",
+                    "resolved": "boolean",
+                    "references": {}
+                },
+                "required": false,
+                "optional": false,
+                "docs": {
+                    "tags": [],
+                    "text": "If `true`, the fab button will show when in a fab-list."
+                },
+                "attribute": "show",
+                "reflect": false,
+                "defaultValue": "false"
+            },
+            "translucent": {
+                "type": "boolean",
+                "mutable": false,
+                "complexType": {
+                    "original": "boolean",
+                    "resolved": "boolean",
+                    "references": {}
+                },
+                "required": false,
+                "optional": false,
+                "docs": {
+                    "tags": [],
+                    "text": "If `true`, the fab button will be translucent.\nOnly applies when the mode is `\"ios\"` and the device supports\n[`backdrop-filter`](https://developer.mozilla.org/en-US/docs/Web/CSS/backdrop-filter#Browser_compatibility)."
+                },
+                "attribute": "translucent",
+                "reflect": false,
+                "defaultValue": "false"
+            },
+            "type": {
+                "type": "string",
+                "mutable": false,
+                "complexType": {
+                    "original": "'submit' | 'reset' | 'button'",
+                    "resolved": "\"button\" | \"reset\" | \"submit\"",
+                    "references": {}
+                },
+                "required": false,
+                "optional": false,
+                "docs": {
+                    "tags": [],
+                    "text": "The type of the button."
+                },
+                "attribute": "type",
+                "reflect": false,
+                "defaultValue": "'button'"
+            },
+            "size": {
+                "type": "string",
+                "mutable": false,
+                "complexType": {
+                    "original": "'small'",
+                    "resolved": "\"small\" | undefined",
+                    "references": {}
+                },
+                "required": false,
+                "optional": true,
+                "docs": {
+                    "tags": [],
+                    "text": "The size of the button. Set this to `small` in order to have a mini fab button."
+                },
+                "attribute": "size",
+                "reflect": false
+            },
+            "closeIcon": {
+                "type": "string",
+                "mutable": false,
+                "complexType": {
+                    "original": "string",
+                    "resolved": "string",
+                    "references": {}
+                },
+                "required": false,
+                "optional": false,
+                "docs": {
+                    "tags": [],
+                    "text": "The icon name to use for the close icon. This will appear when the fab button\nis pressed. Only applies if it is the main button inside of a fab containing a\nfab list."
+                },
+                "attribute": "close-icon",
+                "reflect": false,
+                "defaultValue": "close"
+            }
+        };
+    }
+    static get events() {
+        return [{
+                "method": "ionFocus",
+                "name": "ionFocus",
+                "bubbles": true,
+                "cancelable": true,
+                "composed": true,
+                "docs": {
+                    "tags": [],
+                    "text": "Emitted when the button has focus."
+                },
+                "complexType": {
+                    "original": "void",
+                    "resolved": "void",
+                    "references": {}
+                }
+            }, {
+                "method": "ionBlur",
+                "name": "ionBlur",
+                "bubbles": true,
+                "cancelable": true,
+                "composed": true,
+                "docs": {
+                    "tags": [],
+                    "text": "Emitted when the button loses focus."
+                },
+                "complexType": {
+                    "original": "void",
+                    "resolved": "void",
+                    "references": {}
+                }
+            }];
+    }
+    static get elementRef() { return "el"; }
+}

+ 393 - 0
src/node_modules/@ionic/core/dist/collection/components/fab-button/fab-button.md.css

@@ -0,0 +1,393 @@
+/**
+ * Convert a font size to a dynamic font size.
+ * Fonts that participate in Dynamic Type should use
+ * dynamic font sizes.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param unit (optional) - The unit to convert to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * Convert a font size to a dynamic font size but impose
+ * a maximum font size.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param maxScale - The maximum scale of the font (i.e. 2.5 for a maximum 250% scale).
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * Convert a font size to a dynamic font size but impose
+ * a minimum font size.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param minScale - The minimum scale of the font (i.e. 0.8 for a minimum 80% scale).
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * Convert a font size to a dynamic font size but impose
+ * maximum and minimum font sizes.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param minScale - The minimum scale of the font (i.e. 0.8 for a minimum 80% scale).
+ * @param maxScale - The maximum scale of the font (i.e. 2.5 for a maximum 250% scale).
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * A heuristic that applies CSS to tablet
+ * viewports.
+ *
+ * Usage:
+ * @include tablet-viewport() {
+ *   :host {
+ *     background-color: green;
+ *   }
+ * }
+ */
+/**
+ * A heuristic that applies CSS to mobile
+ * viewports (i.e. phones, not tablets).
+ *
+ * Usage:
+ * @include mobile-viewport() {
+ *   :host {
+ *     background-color: blue;
+ *   }
+ * }
+ */
+:host {
+  /**
+   * @prop --background: Background of the button
+   * @prop --background-activated: Background of the button when pressed. Note: setting this will interfere with the Material Design ripple.
+   * @prop --background-activated-opacity: Opacity of the button background when pressed
+   * @prop --background-focused: Background of the button when focused with the tab key
+   * @prop --background-focused-opacity: Opacity of the button background when focused with the tab key
+   * @prop --background-hover: Background of the button on hover
+   * @prop --background-hover-opacity: Opacity of the button background on hover
+   *
+   * @prop --color: Text color of the button
+   * @prop --color-activated: Text color of the button when pressed
+   * @prop --color-focused: Text color of the button when focused with the tab key
+   * @prop --color-hover: Text color of the button on hover
+   *
+   * @prop --transition: Transition of the button
+   *
+   * @prop --close-icon-font-size: Font size of the close icon
+   *
+   * @prop --border-radius: Border radius of the button
+   * @prop --border-width: Border width of the button
+   * @prop --border-style: Border style of the button
+   * @prop --border-color: Border color of the button
+   *
+   * @prop --ripple-color: Color of the button ripple effect
+   *
+   * @prop --box-shadow: Box shadow of the button
+   *
+   * @prop --padding-top: Top padding of the button
+   * @prop --padding-end: Right padding if direction is left-to-right, and left padding if direction is right-to-left of the button
+   * @prop --padding-bottom: Bottom padding of the button
+   * @prop --padding-start: Left padding if direction is left-to-right, and right padding if direction is right-to-left of the button
+   */
+  --color-activated: var(--color);
+  --color-focused: var(--color);
+  --color-hover: var(--color);
+  --background-hover: var(--ion-color-primary-contrast, #fff);
+  --background-hover-opacity: .08;
+  --transition: background-color, opacity 100ms linear;
+  --ripple-color: currentColor;
+  --border-radius: 50%;
+  --border-width: 0;
+  --border-style: none;
+  --border-color: initial;
+  --padding-top: 0;
+  --padding-end: 0;
+  --padding-bottom: 0;
+  --padding-start: 0;
+  margin-left: 0;
+  margin-right: 0;
+  margin-top: 0;
+  margin-bottom: 0;
+  display: block;
+  width: 56px;
+  height: 56px;
+  font-size: 14px;
+  text-align: center;
+  text-overflow: ellipsis;
+  text-transform: none;
+  white-space: nowrap;
+  font-kerning: none;
+}
+
+.button-native {
+  border-radius: var(--border-radius);
+  -webkit-padding-start: var(--padding-start);
+  padding-inline-start: var(--padding-start);
+  -webkit-padding-end: var(--padding-end);
+  padding-inline-end: var(--padding-end);
+  padding-top: var(--padding-top);
+  padding-bottom: var(--padding-bottom);
+  font-family: inherit;
+  font-size: inherit;
+  font-style: inherit;
+  font-weight: inherit;
+  letter-spacing: inherit;
+  text-decoration: inherit;
+  text-indent: inherit;
+  text-overflow: inherit;
+  text-transform: inherit;
+  text-align: inherit;
+  white-space: inherit;
+  color: inherit;
+  display: block;
+  position: relative;
+  width: 100%;
+  height: 100%;
+  transform: var(--transform);
+  transition: var(--transition);
+  border-width: var(--border-width);
+  border-style: var(--border-style);
+  border-color: var(--border-color);
+  outline: none;
+  background: var(--background);
+  background-clip: padding-box;
+  color: var(--color);
+  box-shadow: var(--box-shadow);
+  contain: strict;
+  cursor: pointer;
+  overflow: hidden;
+  z-index: 0;
+  appearance: none;
+  box-sizing: border-box;
+}
+
+::slotted(ion-icon) {
+  line-height: 1;
+}
+
+.button-native::after {
+  left: 0;
+  right: 0;
+  top: 0;
+  bottom: 0;
+  position: absolute;
+  content: "";
+  opacity: 0;
+}
+
+.button-inner {
+  left: 0;
+  right: 0;
+  top: 0;
+  display: flex;
+  position: absolute;
+  flex-flow: row nowrap;
+  flex-shrink: 0;
+  align-items: center;
+  justify-content: center;
+  height: 100%;
+  transition: all ease-in-out 300ms;
+  transition-property: transform, opacity;
+  z-index: 1;
+}
+
+:host(.fab-button-disabled) {
+  cursor: default;
+  opacity: 0.5;
+  pointer-events: none;
+}
+
+@media (any-hover: hover) {
+  :host(:hover) .button-native {
+    color: var(--color-hover);
+  }
+  :host(:hover) .button-native::after {
+    background: var(--background-hover);
+    opacity: var(--background-hover-opacity);
+  }
+}
+:host(.ion-focused) .button-native {
+  color: var(--color-focused);
+}
+:host(.ion-focused) .button-native::after {
+  background: var(--background-focused);
+  opacity: var(--background-focused-opacity);
+}
+
+:host(.ion-activated) .button-native {
+  color: var(--color-activated);
+}
+:host(.ion-activated) .button-native::after {
+  background: var(--background-activated);
+  opacity: var(--background-activated-opacity);
+}
+
+::slotted(ion-icon) {
+  line-height: 1;
+}
+
+:host(.fab-button-small) {
+  -webkit-margin-start: 8px;
+  margin-inline-start: 8px;
+  -webkit-margin-end: 8px;
+  margin-inline-end: 8px;
+  margin-top: 8px;
+  margin-bottom: 8px;
+  width: 40px;
+  height: 40px;
+}
+
+.close-icon {
+  -webkit-margin-start: auto;
+  margin-inline-start: auto;
+  -webkit-margin-end: auto;
+  margin-inline-end: auto;
+  margin-top: 0;
+  margin-bottom: 0;
+  left: 0;
+  right: 0;
+  top: 0;
+  position: absolute;
+  height: 100%;
+  transform: scale(0.4) rotateZ(-45deg);
+  transition: all ease-in-out 300ms;
+  transition-property: transform, opacity;
+  font-size: var(--close-icon-font-size);
+  opacity: 0;
+  z-index: 1;
+}
+
+:host(.fab-button-close-active) .close-icon {
+  transform: scale(1) rotateZ(0deg);
+  opacity: 1;
+}
+
+:host(.fab-button-close-active) .button-inner {
+  transform: scale(0.4) rotateZ(45deg);
+  opacity: 0;
+}
+
+ion-ripple-effect {
+  color: var(--ripple-color);
+}
+
+@supports (backdrop-filter: blur(0)) {
+  :host(.fab-button-translucent) .button-native {
+    backdrop-filter: var(--backdrop-filter);
+  }
+}
+:host(.ion-color) .button-native {
+  background: var(--ion-color-base);
+  color: var(--ion-color-contrast);
+}
+
+/**
+ * Convert a font size to a dynamic font size.
+ * Fonts that participate in Dynamic Type should use
+ * dynamic font sizes.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param unit (optional) - The unit to convert to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * Convert a font size to a dynamic font size but impose
+ * a maximum font size.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param maxScale - The maximum scale of the font (i.e. 2.5 for a maximum 250% scale).
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * Convert a font size to a dynamic font size but impose
+ * a minimum font size.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param minScale - The minimum scale of the font (i.e. 0.8 for a minimum 80% scale).
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * Convert a font size to a dynamic font size but impose
+ * maximum and minimum font sizes.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param minScale - The minimum scale of the font (i.e. 0.8 for a minimum 80% scale).
+ * @param maxScale - The maximum scale of the font (i.e. 2.5 for a maximum 250% scale).
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * A heuristic that applies CSS to tablet
+ * viewports.
+ *
+ * Usage:
+ * @include tablet-viewport() {
+ *   :host {
+ *     background-color: green;
+ *   }
+ * }
+ */
+/**
+ * A heuristic that applies CSS to mobile
+ * viewports (i.e. phones, not tablets).
+ *
+ * Usage:
+ * @include mobile-viewport() {
+ *   :host {
+ *     background-color: blue;
+ *   }
+ * }
+ */
+:host {
+  --background: var(--ion-color-primary, #0054e9);
+  --background-activated: transparent;
+  --background-focused: currentColor;
+  --background-hover: currentColor;
+  --background-activated-opacity: 0;
+  --background-focused-opacity: .24;
+  --background-hover-opacity: .08;
+  --color: var(--ion-color-primary-contrast, #fff);
+  --box-shadow: 0 3px 5px -1px rgba(0, 0, 0, 0.2), 0 6px 10px 0 rgba(0, 0, 0, 0.14), 0 1px 18px 0 rgba(0, 0, 0, 0.12);
+  --transition: box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1), background-color 280ms cubic-bezier(0.4, 0, 0.2, 1), color 280ms cubic-bezier(0.4, 0, 0.2, 1), opacity 15ms linear 30ms, transform 270ms cubic-bezier(0, 0, 0.2, 1) 0ms;
+  --close-icon-font-size: 24px;
+}
+
+:host(.ion-activated) {
+  --box-shadow: 0 7px 8px -4px rgba(0, 0, 0, 0.2), 0 12px 17px 2px rgba(0, 0, 0, 0.14), 0 5px 22px 4px rgba(0, 0, 0, 0.12);
+}
+
+::slotted(ion-icon) {
+  font-size: 24px;
+}
+
+:host(.fab-button-in-list) {
+  --color: rgba(var(--ion-text-color-rgb, 0, 0, 0), 0.54);
+  --color-activated: rgba(var(--ion-text-color-rgb, 0, 0, 0), 0.54);
+  --color-focused: var(--color-activated);
+  --background: var(--ion-color-light, #f4f5f8);
+  --background-activated: transparent;
+  --background-focused: var(--ion-color-light-shade, #d7d8da);
+  --background-hover: var(--ion-color-light-tint, #f5f6f9);
+}
+
+:host(.fab-button-in-list) ::slotted(ion-icon) {
+  font-size: 18px;
+}
+
+:host(.ion-color.ion-focused) .button-native {
+  color: var(--ion-color-contrast);
+}
+:host(.ion-color.ion-focused) .button-native::after {
+  background: var(--ion-color-contrast);
+}
+
+:host(.ion-color.ion-activated) .button-native {
+  color: var(--ion-color-contrast);
+}
+:host(.ion-color.ion-activated) .button-native::after {
+  background: transparent;
+}
+
+@media (any-hover: hover) {
+  :host(.ion-color:hover) .button-native {
+    color: var(--ion-color-contrast);
+  }
+  :host(.ion-color:hover) .button-native::after {
+    background: var(--ion-color-contrast);
+  }
+}

+ 66 - 0
src/node_modules/@ionic/core/dist/collection/components/fab-button/test/a11y/fab-button.e2e.js

@@ -0,0 +1,66 @@
+/*!
+ * (C) Ionic http://ionicframework.com - MIT License
+ */
+import AxeBuilder from "@axe-core/playwright";
+import { expect } from "@playwright/test";
+import { configs, test } from "../../../../utils/test/playwright/index";
+configs({ directions: ['ltr'], palettes: ['light', 'dark'] }).forEach(({ title, config }) => {
+    test.describe(title('fab-button: a11y'), () => {
+        test('should not have accessibility violations', async ({ page }) => {
+            await page.setContent(`
+        <ion-fab-button>FAB</ion-fab-button>
+        <ion-fab-button class="ion-activated">FAB</ion-fab-button>
+        `, config);
+            const results = await new AxeBuilder({ page }).analyze();
+            expect(results.violations).toEqual([]);
+        });
+    });
+    test.describe(title('fab-list: contrast'), () => {
+        test('should not have accessibility violations', async ({ page }) => {
+            await page.setContent(`
+        <ion-fab activated="true">
+          <ion-fab-button>
+            Open
+          </ion-fab-button>
+          <ion-fab-list side="bottom">
+            <ion-fab-button>Down</ion-fab-button>
+            <ion-fab-button class="ion-activated">Down</ion-fab-button>
+            <ion-fab-button class="ion-focused">Down</ion-fab-button>
+          </ion-fab-list>
+        </ion-fab>
+        `, config);
+            const results = await new AxeBuilder({ page }).analyze();
+            expect(results.violations).toEqual([]);
+        });
+    });
+    test.describe(title('fab: translucent contrast'), () => {
+        test('should not have accessibility violations', async ({ page }) => {
+            await page.setContent(`
+        <ion-fab-button translucent="true">FAB</ion-fab-button>
+        <ion-fab-button class="ion-focused" translucent="true">FAB</ion-fab-button>
+        <ion-fab activated="true">
+          <ion-fab-button>
+            Open
+          </ion-fab-button>
+          <ion-fab-list side="bottom">
+            <ion-fab-button translucent="true">Down</ion-fab-button>
+            <ion-fab-button class="ion-focused" translucent="true">Down</ion-fab-button>
+          </ion-fab-list>
+        </ion-fab>
+        `, config);
+            const results = await new AxeBuilder({ page }).analyze();
+            expect(results.violations).toEqual([]);
+        });
+    });
+});
+configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
+    test.describe(title('fab-button: aria attributes'), () => {
+        test('should inherit aria attributes to inner button', async ({ page }) => {
+            await page.setContent(`
+        <ion-fab-button aria-label="Hello World">My Button</ion-fab-button>
+      `, config);
+            const nativeButton = page.locator('ion-fab-button button');
+            await expect(nativeButton).toHaveAttribute('aria-label', 'Hello World');
+        });
+    });
+});

+ 202 - 0
src/node_modules/@ionic/core/dist/collection/components/fab-list/fab-list.css

@@ -0,0 +1,202 @@
+/**
+ * Convert a font size to a dynamic font size.
+ * Fonts that participate in Dynamic Type should use
+ * dynamic font sizes.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param unit (optional) - The unit to convert to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * Convert a font size to a dynamic font size but impose
+ * a maximum font size.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param maxScale - The maximum scale of the font (i.e. 2.5 for a maximum 250% scale).
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * Convert a font size to a dynamic font size but impose
+ * a minimum font size.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param minScale - The minimum scale of the font (i.e. 0.8 for a minimum 80% scale).
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * Convert a font size to a dynamic font size but impose
+ * maximum and minimum font sizes.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param minScale - The minimum scale of the font (i.e. 0.8 for a minimum 80% scale).
+ * @param maxScale - The maximum scale of the font (i.e. 2.5 for a maximum 250% scale).
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * A heuristic that applies CSS to tablet
+ * viewports.
+ *
+ * Usage:
+ * @include tablet-viewport() {
+ *   :host {
+ *     background-color: green;
+ *   }
+ * }
+ */
+/**
+ * A heuristic that applies CSS to mobile
+ * viewports (i.e. phones, not tablets).
+ *
+ * Usage:
+ * @include mobile-viewport() {
+ *   :host {
+ *     background-color: blue;
+ *   }
+ * }
+ */
+/**
+ * Convert a font size to a dynamic font size.
+ * Fonts that participate in Dynamic Type should use
+ * dynamic font sizes.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param unit (optional) - The unit to convert to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * Convert a font size to a dynamic font size but impose
+ * a maximum font size.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param maxScale - The maximum scale of the font (i.e. 2.5 for a maximum 250% scale).
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * Convert a font size to a dynamic font size but impose
+ * a minimum font size.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param minScale - The minimum scale of the font (i.e. 0.8 for a minimum 80% scale).
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * Convert a font size to a dynamic font size but impose
+ * maximum and minimum font sizes.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param minScale - The minimum scale of the font (i.e. 0.8 for a minimum 80% scale).
+ * @param maxScale - The maximum scale of the font (i.e. 2.5 for a maximum 250% scale).
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * A heuristic that applies CSS to tablet
+ * viewports.
+ *
+ * Usage:
+ * @include tablet-viewport() {
+ *   :host {
+ *     background-color: green;
+ *   }
+ * }
+ */
+/**
+ * A heuristic that applies CSS to mobile
+ * viewports (i.e. phones, not tablets).
+ *
+ * Usage:
+ * @include mobile-viewport() {
+ *   :host {
+ *     background-color: blue;
+ *   }
+ * }
+ */
+:host {
+  margin-left: 0;
+  margin-right: 0;
+  margin-top: calc(100% + 10px);
+  margin-bottom: calc(100% + 10px);
+  display: none;
+  position: absolute;
+  top: 0;
+  flex-direction: column;
+  align-items: center;
+  /**
+   * The list should be centered relative to the parent
+   * FAB button. We set minimum dimensions so the
+   * FAB list can be centered relative to the small FAB button.
+   * However, the small FAB button adds start/end margin, so we need
+   * to account for that in the FAB list dimensions.
+   */
+  min-width: 56px;
+  min-height: 56px;
+}
+
+:host(.fab-list-active) {
+  display: flex;
+}
+
+::slotted(.fab-button-in-list) {
+  margin-left: 0;
+  margin-right: 0;
+  margin-top: 8px;
+  margin-bottom: 8px;
+  width: 40px;
+  height: 40px;
+  transform: scale(0);
+  opacity: 0;
+  visibility: hidden;
+}
+
+:host(.fab-list-side-top) ::slotted(.fab-button-in-list),
+:host(.fab-list-side-bottom) ::slotted(.fab-button-in-list) {
+  margin-left: 0;
+  margin-right: 0;
+  margin-top: 5px;
+  margin-bottom: 5px;
+}
+
+:host(.fab-list-side-start) ::slotted(.fab-button-in-list),
+:host(.fab-list-side-end) ::slotted(.fab-button-in-list) {
+  -webkit-margin-start: 5px;
+  margin-inline-start: 5px;
+  -webkit-margin-end: 5px;
+  margin-inline-end: 5px;
+  margin-top: 0;
+  margin-bottom: 0;
+}
+
+::slotted(.fab-button-in-list.fab-button-show) {
+  transform: scale(1);
+  opacity: 1;
+  visibility: visible;
+}
+
+:host(.fab-list-side-top) {
+  top: auto;
+  bottom: 0;
+  flex-direction: column-reverse;
+}
+
+:host(.fab-list-side-start) {
+  -webkit-margin-start: calc(100% + 10px);
+  margin-inline-start: calc(100% + 10px);
+  -webkit-margin-end: calc(100% + 10px);
+  margin-inline-end: calc(100% + 10px);
+  margin-top: 0;
+  margin-bottom: 0;
+  flex-direction: row-reverse;
+}
+:host(.fab-list-side-start) {
+  inset-inline-end: 0;
+}
+
+:host(.fab-list-side-end) {
+  -webkit-margin-start: calc(100% + 10px);
+  margin-inline-start: calc(100% + 10px);
+  -webkit-margin-end: calc(100% + 10px);
+  margin-inline-end: calc(100% + 10px);
+  margin-top: 0;
+  margin-bottom: 0;
+  flex-direction: row;
+}
+:host(.fab-list-side-end) {
+  inset-inline-start: 0;
+}

+ 86 - 0
src/node_modules/@ionic/core/dist/collection/components/fab-list/fab-list.js

@@ -0,0 +1,86 @@
+/*!
+ * (C) Ionic http://ionicframework.com - MIT License
+ */
+import { Host, h } from "@stencil/core";
+import { getIonMode } from "../../global/ionic-global";
+export class FabList {
+    constructor() {
+        this.activated = false;
+        this.side = 'bottom';
+    }
+    activatedChanged(activated) {
+        const fabs = Array.from(this.el.querySelectorAll('ion-fab-button'));
+        // if showing the fabs add a timeout, else show immediately
+        const timeout = activated ? 30 : 0;
+        fabs.forEach((fab, i) => {
+            setTimeout(() => (fab.show = activated), i * timeout);
+        });
+    }
+    render() {
+        const mode = getIonMode(this);
+        return (h(Host, { key: 'fa1d195b9950654ba0e984bf61d981c977d05275', class: {
+                [mode]: true,
+                'fab-list-active': this.activated,
+                [`fab-list-side-${this.side}`]: true,
+            } }, h("slot", { key: '2ec738c66c05112e1e2521155d6adfc36d2fd1db' })));
+    }
+    static get is() { return "ion-fab-list"; }
+    static get encapsulation() { return "shadow"; }
+    static get originalStyleUrls() {
+        return {
+            "$": ["fab-list.scss"]
+        };
+    }
+    static get styleUrls() {
+        return {
+            "$": ["fab-list.css"]
+        };
+    }
+    static get properties() {
+        return {
+            "activated": {
+                "type": "boolean",
+                "mutable": false,
+                "complexType": {
+                    "original": "boolean",
+                    "resolved": "boolean",
+                    "references": {}
+                },
+                "required": false,
+                "optional": false,
+                "docs": {
+                    "tags": [],
+                    "text": "If `true`, the fab list will show all fab buttons in the list."
+                },
+                "attribute": "activated",
+                "reflect": false,
+                "defaultValue": "false"
+            },
+            "side": {
+                "type": "string",
+                "mutable": false,
+                "complexType": {
+                    "original": "'start' | 'end' | 'top' | 'bottom'",
+                    "resolved": "\"bottom\" | \"end\" | \"start\" | \"top\"",
+                    "references": {}
+                },
+                "required": false,
+                "optional": false,
+                "docs": {
+                    "tags": [],
+                    "text": "The side the fab list will show on relative to the main fab button."
+                },
+                "attribute": "side",
+                "reflect": false,
+                "defaultValue": "'bottom'"
+            }
+        };
+    }
+    static get elementRef() { return "el"; }
+    static get watchers() {
+        return [{
+                "propName": "activated",
+                "methodName": "activatedChanged"
+            }];
+    }
+}

+ 366 - 0
src/node_modules/@ionic/core/dist/collection/components/fab/fab.css

@@ -0,0 +1,366 @@
+/**
+ * Convert a font size to a dynamic font size.
+ * Fonts that participate in Dynamic Type should use
+ * dynamic font sizes.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param unit (optional) - The unit to convert to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * Convert a font size to a dynamic font size but impose
+ * a maximum font size.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param maxScale - The maximum scale of the font (i.e. 2.5 for a maximum 250% scale).
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * Convert a font size to a dynamic font size but impose
+ * a minimum font size.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param minScale - The minimum scale of the font (i.e. 0.8 for a minimum 80% scale).
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * Convert a font size to a dynamic font size but impose
+ * maximum and minimum font sizes.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param minScale - The minimum scale of the font (i.e. 0.8 for a minimum 80% scale).
+ * @param maxScale - The maximum scale of the font (i.e. 2.5 for a maximum 250% scale).
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * A heuristic that applies CSS to tablet
+ * viewports.
+ *
+ * Usage:
+ * @include tablet-viewport() {
+ *   :host {
+ *     background-color: green;
+ *   }
+ * }
+ */
+/**
+ * A heuristic that applies CSS to mobile
+ * viewports (i.e. phones, not tablets).
+ *
+ * Usage:
+ * @include mobile-viewport() {
+ *   :host {
+ *     background-color: blue;
+ *   }
+ * }
+ */
+/**
+ * Convert a font size to a dynamic font size.
+ * Fonts that participate in Dynamic Type should use
+ * dynamic font sizes.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param unit (optional) - The unit to convert to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * Convert a font size to a dynamic font size but impose
+ * a maximum font size.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param maxScale - The maximum scale of the font (i.e. 2.5 for a maximum 250% scale).
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * Convert a font size to a dynamic font size but impose
+ * a minimum font size.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param minScale - The minimum scale of the font (i.e. 0.8 for a minimum 80% scale).
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * Convert a font size to a dynamic font size but impose
+ * maximum and minimum font sizes.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param minScale - The minimum scale of the font (i.e. 0.8 for a minimum 80% scale).
+ * @param maxScale - The maximum scale of the font (i.e. 2.5 for a maximum 250% scale).
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * A heuristic that applies CSS to tablet
+ * viewports.
+ *
+ * Usage:
+ * @include tablet-viewport() {
+ *   :host {
+ *     background-color: green;
+ *   }
+ * }
+ */
+/**
+ * A heuristic that applies CSS to mobile
+ * viewports (i.e. phones, not tablets).
+ *
+ * Usage:
+ * @include mobile-viewport() {
+ *   :host {
+ *     background-color: blue;
+ *   }
+ * }
+ */
+/**
+ * Convert a font size to a dynamic font size.
+ * Fonts that participate in Dynamic Type should use
+ * dynamic font sizes.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param unit (optional) - The unit to convert to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * Convert a font size to a dynamic font size but impose
+ * a maximum font size.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param maxScale - The maximum scale of the font (i.e. 2.5 for a maximum 250% scale).
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * Convert a font size to a dynamic font size but impose
+ * a minimum font size.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param minScale - The minimum scale of the font (i.e. 0.8 for a minimum 80% scale).
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * Convert a font size to a dynamic font size but impose
+ * maximum and minimum font sizes.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param minScale - The minimum scale of the font (i.e. 0.8 for a minimum 80% scale).
+ * @param maxScale - The maximum scale of the font (i.e. 2.5 for a maximum 250% scale).
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * A heuristic that applies CSS to tablet
+ * viewports.
+ *
+ * Usage:
+ * @include tablet-viewport() {
+ *   :host {
+ *     background-color: green;
+ *   }
+ * }
+ */
+/**
+ * A heuristic that applies CSS to mobile
+ * viewports (i.e. phones, not tablets).
+ *
+ * Usage:
+ * @include mobile-viewport() {
+ *   :host {
+ *     background-color: blue;
+ *   }
+ * }
+ */
+/**
+ * Convert a font size to a dynamic font size.
+ * Fonts that participate in Dynamic Type should use
+ * dynamic font sizes.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param unit (optional) - The unit to convert to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * Convert a font size to a dynamic font size but impose
+ * a maximum font size.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param maxScale - The maximum scale of the font (i.e. 2.5 for a maximum 250% scale).
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * Convert a font size to a dynamic font size but impose
+ * a minimum font size.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param minScale - The minimum scale of the font (i.e. 0.8 for a minimum 80% scale).
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * Convert a font size to a dynamic font size but impose
+ * maximum and minimum font sizes.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param minScale - The minimum scale of the font (i.e. 0.8 for a minimum 80% scale).
+ * @param maxScale - The maximum scale of the font (i.e. 2.5 for a maximum 250% scale).
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * A heuristic that applies CSS to tablet
+ * viewports.
+ *
+ * Usage:
+ * @include tablet-viewport() {
+ *   :host {
+ *     background-color: green;
+ *   }
+ * }
+ */
+/**
+ * A heuristic that applies CSS to mobile
+ * viewports (i.e. phones, not tablets).
+ *
+ * Usage:
+ * @include mobile-viewport() {
+ *   :host {
+ *     background-color: blue;
+ *   }
+ * }
+ */
+:host {
+  position: absolute;
+  width: fit-content;
+  height: fit-content;
+  z-index: 999;
+}
+
+:host(.fab-horizontal-center) {
+  left: 0px;
+  right: 0px;
+  -webkit-margin-start: auto;
+  margin-inline-start: auto;
+  -webkit-margin-end: auto;
+  margin-inline-end: auto;
+}
+
+:host(.fab-horizontal-start) {
+  /* stylelint-disable */
+  left: calc(10px + var(--ion-safe-area-left, 0px));
+  /* stylelint-enable */
+}
+:host-context([dir=rtl]):host(.fab-horizontal-start), :host-context([dir=rtl]).fab-horizontal-start {
+  right: calc(10px + var(--ion-safe-area-right, 0px));
+  left: unset;
+}
+
+@supports selector(:dir(rtl)) {
+  :host(.fab-horizontal-start:dir(rtl)) {
+    right: calc(10px + var(--ion-safe-area-right, 0px));
+    left: unset;
+  }
+}
+
+:host(.fab-horizontal-end) {
+  /* stylelint-disable */
+  right: calc(10px + var(--ion-safe-area-right, 0px));
+  /* stylelint-enable */
+}
+:host-context([dir=rtl]):host(.fab-horizontal-end), :host-context([dir=rtl]).fab-horizontal-end {
+  left: calc(10px + var(--ion-safe-area-left, 0px));
+  right: unset;
+}
+
+@supports selector(:dir(rtl)) {
+  :host(.fab-horizontal-end:dir(rtl)) {
+    left: calc(10px + var(--ion-safe-area-left, 0px));
+    right: unset;
+  }
+}
+
+:host(.fab-vertical-top) {
+  top: 10px;
+}
+
+/**
+ * Reset the top value since edge
+ * styles use margin-top.
+ */
+:host(.fab-vertical-top.fab-edge) {
+  top: 0;
+}
+
+/**
+ * We need to adjust the parent FAB button up by half
+ * its height so that half of it sits on the header. As a result,
+ * we target the slotted ion-fab-button instead of targeting the host.
+ */
+:host(.fab-vertical-top.fab-edge) ::slotted(ion-fab-button) {
+  margin-top: -50%;
+}
+
+/**
+ * The small FAB button adds top and bottom margin. We need to account for
+ * that margin when we adjust the FAB button for edge styles since we
+ * are overriding the margin-top value below.
+ */
+:host(.fab-vertical-top.fab-edge) ::slotted(ion-fab-button.fab-button-small) {
+  margin-top: calc((-100% + 16px) / 2);
+}
+
+/**
+ * Since we are adjusting the FAB button we also need
+ * to adjust the sibling ion-fab-list otherwise there will be
+ * a gap between the parent FAB button and the associated list.
+ */
+:host(.fab-vertical-top.fab-edge) ::slotted(ion-fab-list.fab-list-side-start),
+:host(.fab-vertical-top.fab-edge) ::slotted(ion-fab-list.fab-list-side-end) {
+  margin-top: -50%;
+}
+
+:host(.fab-vertical-top.fab-edge) ::slotted(ion-fab-list.fab-list-side-top),
+:host(.fab-vertical-top.fab-edge) ::slotted(ion-fab-list.fab-list-side-bottom) {
+  margin-top: calc(50% + 10px);
+}
+
+:host(.fab-vertical-bottom) {
+  bottom: 10px;
+}
+
+/**
+ * Reset the bottom value since edge
+ * styles use margin-bottom.
+ */
+:host(.fab-vertical-bottom.fab-edge) {
+  bottom: 0;
+}
+
+/**
+ * We need to adjust the parent FAB button down by half
+ * its height so that half of it sits on the footer. As a result,
+ * we target the slotted ion-fab-button instead of targeting the host.
+ */
+:host(.fab-vertical-bottom.fab-edge) ::slotted(ion-fab-button) {
+  margin-bottom: -50%;
+}
+
+/**
+ * The small FAB button adds top and bottom margin. We need to account for
+ * that margin when we adjust the FAB button for edge styles since we
+ * are overriding the margin-bottom value below.
+ */
+:host(.fab-vertical-bottom.fab-edge) ::slotted(ion-fab-button.fab-button-small) {
+  margin-bottom: calc((-100% + 16px) / 2);
+}
+
+/**
+ * Since we are adjusting the FAB button we also need
+ * to adjust the sibling ion-fab-list otherwise there will be
+ * a gap between the parent FAB button and the associated list.
+ */
+:host(.fab-vertical-bottom.fab-edge) ::slotted(ion-fab-list.fab-list-side-start),
+:host(.fab-vertical-bottom.fab-edge) ::slotted(ion-fab-list.fab-list-side-end) {
+  margin-bottom: -50%;
+}
+
+:host(.fab-vertical-bottom.fab-edge) ::slotted(ion-fab-list.fab-list-side-top),
+:host(.fab-vertical-bottom.fab-edge) ::slotted(ion-fab-list.fab-list-side-bottom) {
+  margin-bottom: calc(50% + 10px);
+}
+
+:host(.fab-vertical-center) {
+  top: 0px;
+  bottom: 0px;
+  margin-top: auto;
+  margin-bottom: auto;
+}

+ 191 - 0
src/node_modules/@ionic/core/dist/collection/components/fab/fab.js

@@ -0,0 +1,191 @@
+/*!
+ * (C) Ionic http://ionicframework.com - MIT License
+ */
+import { Host, h } from "@stencil/core";
+import { getIonMode } from "../../global/ionic-global";
+export class Fab {
+    constructor() {
+        this.horizontal = undefined;
+        this.vertical = undefined;
+        this.edge = false;
+        this.activated = false;
+    }
+    activatedChanged() {
+        const activated = this.activated;
+        const fab = this.getFab();
+        if (fab) {
+            fab.activated = activated;
+        }
+        Array.from(this.el.querySelectorAll('ion-fab-list')).forEach((list) => {
+            list.activated = activated;
+        });
+    }
+    componentDidLoad() {
+        if (this.activated) {
+            this.activatedChanged();
+        }
+    }
+    /**
+     * Close an active FAB list container.
+     */
+    async close() {
+        this.activated = false;
+    }
+    getFab() {
+        return this.el.querySelector('ion-fab-button');
+    }
+    /**
+     * Opens/Closes the FAB list container.
+     * @internal
+     */
+    async toggle() {
+        const hasList = !!this.el.querySelector('ion-fab-list');
+        if (hasList) {
+            this.activated = !this.activated;
+        }
+    }
+    render() {
+        const { horizontal, vertical, edge } = this;
+        const mode = getIonMode(this);
+        return (h(Host, { key: 'cb44cf6486b0a6439b99da87c065b0b52e2514f4', class: {
+                [mode]: true,
+                [`fab-horizontal-${horizontal}`]: horizontal !== undefined,
+                [`fab-vertical-${vertical}`]: vertical !== undefined,
+                'fab-edge': edge,
+            } }, h("slot", { key: '1ed484c7ecb10cd81fbca9a4f5c4049bf82f9f8a' })));
+    }
+    static get is() { return "ion-fab"; }
+    static get encapsulation() { return "shadow"; }
+    static get originalStyleUrls() {
+        return {
+            "$": ["fab.scss"]
+        };
+    }
+    static get styleUrls() {
+        return {
+            "$": ["fab.css"]
+        };
+    }
+    static get properties() {
+        return {
+            "horizontal": {
+                "type": "string",
+                "mutable": false,
+                "complexType": {
+                    "original": "'start' | 'end' | 'center'",
+                    "resolved": "\"center\" | \"end\" | \"start\" | undefined",
+                    "references": {}
+                },
+                "required": false,
+                "optional": true,
+                "docs": {
+                    "tags": [],
+                    "text": "Where to align the fab horizontally in the viewport."
+                },
+                "attribute": "horizontal",
+                "reflect": false
+            },
+            "vertical": {
+                "type": "string",
+                "mutable": false,
+                "complexType": {
+                    "original": "'top' | 'bottom' | 'center'",
+                    "resolved": "\"bottom\" | \"center\" | \"top\" | undefined",
+                    "references": {}
+                },
+                "required": false,
+                "optional": true,
+                "docs": {
+                    "tags": [],
+                    "text": "Where to align the fab vertically in the viewport."
+                },
+                "attribute": "vertical",
+                "reflect": false
+            },
+            "edge": {
+                "type": "boolean",
+                "mutable": false,
+                "complexType": {
+                    "original": "boolean",
+                    "resolved": "boolean",
+                    "references": {}
+                },
+                "required": false,
+                "optional": false,
+                "docs": {
+                    "tags": [],
+                    "text": "If `true`, the fab will display on the edge of the header if\n`vertical` is `\"top\"`, and on the edge of the footer if\nit is `\"bottom\"`. Should be used with a `fixed` slot."
+                },
+                "attribute": "edge",
+                "reflect": false,
+                "defaultValue": "false"
+            },
+            "activated": {
+                "type": "boolean",
+                "mutable": true,
+                "complexType": {
+                    "original": "boolean",
+                    "resolved": "boolean",
+                    "references": {}
+                },
+                "required": false,
+                "optional": false,
+                "docs": {
+                    "tags": [],
+                    "text": "If `true`, both the `ion-fab-button` and all `ion-fab-list` inside `ion-fab` will become active.\nThat means `ion-fab-button` will become a `close` icon and `ion-fab-list` will become visible."
+                },
+                "attribute": "activated",
+                "reflect": false,
+                "defaultValue": "false"
+            }
+        };
+    }
+    static get methods() {
+        return {
+            "close": {
+                "complexType": {
+                    "signature": "() => Promise<void>",
+                    "parameters": [],
+                    "references": {
+                        "Promise": {
+                            "location": "global",
+                            "id": "global::Promise"
+                        }
+                    },
+                    "return": "Promise<void>"
+                },
+                "docs": {
+                    "text": "Close an active FAB list container.",
+                    "tags": []
+                }
+            },
+            "toggle": {
+                "complexType": {
+                    "signature": "() => Promise<void>",
+                    "parameters": [],
+                    "references": {
+                        "Promise": {
+                            "location": "global",
+                            "id": "global::Promise"
+                        }
+                    },
+                    "return": "Promise<void>"
+                },
+                "docs": {
+                    "text": "Opens/Closes the FAB list container.",
+                    "tags": [{
+                            "name": "internal",
+                            "text": undefined
+                        }]
+                }
+            }
+        };
+    }
+    static get elementRef() { return "el"; }
+    static get watchers() {
+        return [{
+                "propName": "activated",
+                "methodName": "activatedChanged"
+            }];
+    }
+}

+ 65 - 0
src/node_modules/@ionic/core/dist/collection/components/fab/test/basic/fab.e2e.js

@@ -0,0 +1,65 @@
+/*!
+ * (C) Ionic http://ionicframework.com - MIT License
+ */
+import { expect } from "@playwright/test";
+import { configs, test } from "../../../../utils/test/playwright/index";
+configs().forEach(({ title, screenshot, config }) => {
+    test.describe(title('fab: basic (visual checks)'), () => {
+        test.beforeEach(async ({ page }) => {
+            await page.goto(`/src/components/fab/test/basic`, config);
+        });
+        test('should not have visual regressions', async ({ page }) => {
+            await page.setIonViewport();
+            await expect(page).toHaveScreenshot(screenshot(`fab-basic`));
+        });
+        test('should not have visual regressions when open', async ({ page }) => {
+            // this fab has multiple buttons on each side, so it covers all the bases
+            const fab = page.locator('#fab5');
+            await fab.click();
+            await page.waitForChanges();
+            /**
+             * fab.screenshot doesn't work since ion-fab's bounding box only covers the
+             * central button. This viewport size crops extra white space while also
+             * containing all the buttons in the open fab.
+             */
+            await page.setViewportSize({
+                width: 320,
+                height: 415,
+            });
+            await expect(page).toHaveScreenshot(screenshot(`fab-open`));
+        });
+    });
+});
+/**
+ * This behavior does not vary
+ * across modes/directions.
+ */
+configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
+    test.describe(title('fab: basic (functionality checks)'), () => {
+        test.beforeEach(async ({ page }) => {
+            await page.goto(`/src/components/fab/test/basic`, config);
+        });
+        test('should toggle active state when clicked', async ({ page }) => {
+            const fab = page.locator('#fab1');
+            const fabList = fab.locator('ion-fab-list');
+            await expect(fabList).not.toHaveClass(/fab-list-active/);
+            // open fab
+            await fab.click();
+            await page.waitForChanges();
+            await expect(fabList).toHaveClass(/fab-list-active/);
+            // close fab
+            await fab.click();
+            await page.waitForChanges();
+            await expect(fabList).not.toHaveClass(/fab-list-active/);
+        });
+        test('should not open when disabled', async ({ page }) => {
+            const fab = page.locator('#fab2');
+            const fabList = fab.locator('ion-fab-list');
+            await expect(fabList).not.toHaveClass(/fab-list-active/);
+            // attempt to open fab
+            await fab.click();
+            await page.waitForChanges();
+            await expect(fabList).not.toHaveClass(/fab-list-active/);
+        });
+    });
+});

+ 17 - 0
src/node_modules/@ionic/core/dist/collection/components/fab/test/custom-size/fab.e2e.js

@@ -0,0 +1,17 @@
+/*!
+ * (C) Ionic http://ionicframework.com - MIT License
+ */
+import { expect } from "@playwright/test";
+import { configs, test, Viewports } from "../../../../utils/test/playwright/index";
+/**
+ * This behavior does not vary across modes
+ */
+configs({ modes: ['ios'] }).forEach(({ title, config, screenshot }) => {
+    test.describe(title('fab: custom size'), () => {
+        test('should position fabs correctly with custom sizes', async ({ page }) => {
+            await page.goto(`/src/components/fab/test/custom-size`, config);
+            await page.setViewportSize(Viewports.tablet.landscape);
+            await expect(page).toHaveScreenshot(screenshot(`fab-custom-size`));
+        });
+    });
+});

+ 63 - 0
src/node_modules/@ionic/core/dist/collection/components/fab/test/safe-area/fab.e2e.js

@@ -0,0 +1,63 @@
+/*!
+ * (C) Ionic http://ionicframework.com - MIT License
+ */
+import { expect } from "@playwright/test";
+import { configs, test } from "../../../../utils/test/playwright/index";
+configs({ modes: ['md'] }).forEach(({ title, screenshot, config }) => {
+    test.describe(title('fab: safe area'), () => {
+        test('should ignore document direction in safe area positioning for start-positioned fab', async ({ page }) => {
+            await page.setContent(`
+          <style>
+            :root {
+              --ion-safe-area-left: 40px;
+              --ion-safe-area-right: 20px;
+            }
+          </style>
+
+          <ion-content>
+            <ion-fab vertical="center" horizontal="start">
+              <ion-fab-button>
+                <ion-icon name="add"></ion-icon>
+              </ion-fab-button>
+            </ion-fab>
+          </ion-content>
+        `, config);
+            /**
+             * We need to capture the entire page to check the fab's position,
+             * but we don't need much extra white space.
+             */
+            await page.setViewportSize({
+                width: 200,
+                height: 200,
+            });
+            await expect(page).toHaveScreenshot(screenshot('fab-safe-area-horizontal-start'));
+        });
+        test('should ignore document direction in safe area positioning for end-positioned fab', async ({ page }) => {
+            await page.setContent(`
+          <style>
+            :root {
+              --ion-safe-area-left: 40px;
+              --ion-safe-area-right: 20px;
+            }
+          </style>
+
+          <ion-content>
+            <ion-fab vertical="center" horizontal="end">
+              <ion-fab-button>
+                <ion-icon name="add"></ion-icon>
+              </ion-fab-button>
+            </ion-fab>
+          </ion-content>
+        `, config);
+            /**
+             * We need to capture the entire page to check the fab's position,
+             * but we don't need much extra white space.
+             */
+            await page.setViewportSize({
+                width: 200,
+                height: 200,
+            });
+            await expect(page).toHaveScreenshot(screenshot('fab-safe-area-horizontal-end'));
+        });
+    });
+});

+ 14 - 0
src/node_modules/@ionic/core/dist/collection/components/fab/test/states/fab.e2e.js

@@ -0,0 +1,14 @@
+/*!
+ * (C) Ionic http://ionicframework.com - MIT License
+ */
+import { expect } from "@playwright/test";
+import { configs, test } from "../../../../utils/test/playwright/index";
+configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
+    test.describe(title('fab: states'), () => {
+        test('should not have visual regressions', async ({ page }) => {
+            await page.goto(`/src/components/fab/test/states`, config);
+            await page.setIonViewport();
+            await expect(page).toHaveScreenshot(screenshot(`fab-states`));
+        });
+    });
+});

+ 28 - 0
src/node_modules/@ionic/core/dist/collection/components/fab/test/translucent/fab.e2e.js

@@ -0,0 +1,28 @@
+/*!
+ * (C) Ionic http://ionicframework.com - MIT License
+ */
+import { expect } from "@playwright/test";
+import { configs, test } from "../../../../utils/test/playwright/index";
+/**
+ * Translucency is only available on ios mode
+ */
+configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
+    test.describe(title('fab: translucent'), () => {
+        test('should not have visual regressions', async ({ page }) => {
+            await page.goto(`/src/components/fab/test/translucent`, config);
+            const fab = page.locator('#fab5');
+            await fab.click();
+            await page.waitForChanges();
+            /**
+             * fab.screenshot doesn't work since ion-fab's bounding box only covers the
+             * central button. This viewport size crops extra white space while also
+             * containing all the buttons in the open fab.
+             */
+            await page.setViewportSize({
+                width: 235,
+                height: 310,
+            });
+            await expect(page).toHaveScreenshot(screenshot(`fab-translucent`));
+        });
+    });
+});

+ 146 - 0
src/node_modules/@ionic/core/dist/collection/components/footer/footer.ios.css

@@ -0,0 +1,146 @@
+/**
+ * Convert a font size to a dynamic font size.
+ * Fonts that participate in Dynamic Type should use
+ * dynamic font sizes.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param unit (optional) - The unit to convert to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * Convert a font size to a dynamic font size but impose
+ * a maximum font size.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param maxScale - The maximum scale of the font (i.e. 2.5 for a maximum 250% scale).
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * Convert a font size to a dynamic font size but impose
+ * a minimum font size.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param minScale - The minimum scale of the font (i.e. 0.8 for a minimum 80% scale).
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * Convert a font size to a dynamic font size but impose
+ * maximum and minimum font sizes.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param minScale - The minimum scale of the font (i.e. 0.8 for a minimum 80% scale).
+ * @param maxScale - The maximum scale of the font (i.e. 2.5 for a maximum 250% scale).
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * A heuristic that applies CSS to tablet
+ * viewports.
+ *
+ * Usage:
+ * @include tablet-viewport() {
+ *   :host {
+ *     background-color: green;
+ *   }
+ * }
+ */
+/**
+ * A heuristic that applies CSS to mobile
+ * viewports (i.e. phones, not tablets).
+ *
+ * Usage:
+ * @include mobile-viewport() {
+ *   :host {
+ *     background-color: blue;
+ *   }
+ * }
+ */
+ion-footer {
+  display: block;
+  position: relative;
+  order: 1;
+  width: 100%;
+  z-index: 10;
+}
+
+ion-footer.footer-toolbar-padding ion-toolbar:last-of-type {
+  padding-bottom: var(--ion-safe-area-bottom, 0);
+}
+
+/**
+ * Convert a font size to a dynamic font size.
+ * Fonts that participate in Dynamic Type should use
+ * dynamic font sizes.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param unit (optional) - The unit to convert to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * Convert a font size to a dynamic font size but impose
+ * a maximum font size.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param maxScale - The maximum scale of the font (i.e. 2.5 for a maximum 250% scale).
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * Convert a font size to a dynamic font size but impose
+ * a minimum font size.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param minScale - The minimum scale of the font (i.e. 0.8 for a minimum 80% scale).
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * Convert a font size to a dynamic font size but impose
+ * maximum and minimum font sizes.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param minScale - The minimum scale of the font (i.e. 0.8 for a minimum 80% scale).
+ * @param maxScale - The maximum scale of the font (i.e. 2.5 for a maximum 250% scale).
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * A heuristic that applies CSS to tablet
+ * viewports.
+ *
+ * Usage:
+ * @include tablet-viewport() {
+ *   :host {
+ *     background-color: green;
+ *   }
+ * }
+ */
+/**
+ * A heuristic that applies CSS to mobile
+ * viewports (i.e. phones, not tablets).
+ *
+ * Usage:
+ * @include mobile-viewport() {
+ *   :host {
+ *     background-color: blue;
+ *   }
+ * }
+ */
+.footer-ios ion-toolbar:first-of-type {
+  --border-width: 0.55px 0 0;
+}
+
+@supports (backdrop-filter: blur(0)) {
+  .footer-background {
+    left: 0;
+    right: 0;
+    top: 0;
+    bottom: 0;
+    position: absolute;
+    backdrop-filter: saturate(180%) blur(20px);
+  }
+  .footer-translucent-ios ion-toolbar {
+    --opacity: .8;
+  }
+}
+.footer-ios.ion-no-border ion-toolbar:first-of-type {
+  --border-width: 0;
+}
+
+.footer-collapse-fade ion-toolbar {
+  --opacity-scale: inherit;
+}

+ 151 - 0
src/node_modules/@ionic/core/dist/collection/components/footer/footer.js

@@ -0,0 +1,151 @@
+/*!
+ * (C) Ionic http://ionicframework.com - MIT License
+ */
+import { Host, h } from "@stencil/core";
+import { findIonContent, getScrollElement, printIonContentErrorMsg } from "../../utils/content/index";
+import { createKeyboardController } from "../../utils/keyboard/keyboard-controller";
+import { getIonMode } from "../../global/ionic-global";
+import { handleFooterFade } from "./footer.utils";
+/**
+ * @virtualProp {"ios" | "md"} mode - The mode determines which platform styles to use.
+ */
+export class Footer {
+    constructor() {
+        this.keyboardCtrl = null;
+        this.checkCollapsibleFooter = () => {
+            const mode = getIonMode(this);
+            if (mode !== 'ios') {
+                return;
+            }
+            const { collapse } = this;
+            const hasFade = collapse === 'fade';
+            this.destroyCollapsibleFooter();
+            if (hasFade) {
+                const pageEl = this.el.closest('ion-app,ion-page,.ion-page,page-inner');
+                const contentEl = pageEl ? findIonContent(pageEl) : null;
+                if (!contentEl) {
+                    printIonContentErrorMsg(this.el);
+                    return;
+                }
+                this.setupFadeFooter(contentEl);
+            }
+        };
+        this.setupFadeFooter = async (contentEl) => {
+            const scrollEl = (this.scrollEl = await getScrollElement(contentEl));
+            /**
+             * Handle fading of toolbars on scroll
+             */
+            this.contentScrollCallback = () => {
+                handleFooterFade(scrollEl, this.el);
+            };
+            scrollEl.addEventListener('scroll', this.contentScrollCallback);
+            handleFooterFade(scrollEl, this.el);
+        };
+        this.keyboardVisible = false;
+        this.collapse = undefined;
+        this.translucent = false;
+    }
+    componentDidLoad() {
+        this.checkCollapsibleFooter();
+    }
+    componentDidUpdate() {
+        this.checkCollapsibleFooter();
+    }
+    async connectedCallback() {
+        this.keyboardCtrl = await createKeyboardController(async (keyboardOpen, waitForResize) => {
+            /**
+             * If the keyboard is hiding, then we need to wait
+             * for the webview to resize. Otherwise, the footer
+             * will flicker before the webview resizes.
+             */
+            if (keyboardOpen === false && waitForResize !== undefined) {
+                await waitForResize;
+            }
+            this.keyboardVisible = keyboardOpen; // trigger re-render by updating state
+        });
+    }
+    disconnectedCallback() {
+        if (this.keyboardCtrl) {
+            this.keyboardCtrl.destroy();
+        }
+    }
+    destroyCollapsibleFooter() {
+        if (this.scrollEl && this.contentScrollCallback) {
+            this.scrollEl.removeEventListener('scroll', this.contentScrollCallback);
+            this.contentScrollCallback = undefined;
+        }
+    }
+    render() {
+        const { translucent, collapse } = this;
+        const mode = getIonMode(this);
+        const tabs = this.el.closest('ion-tabs');
+        const tabBar = tabs === null || tabs === void 0 ? void 0 : tabs.querySelector(':scope > ion-tab-bar');
+        return (h(Host, { key: '5da19dc38ba73e1ddfd1bef3ebd485105d77c751', role: "contentinfo", class: {
+                [mode]: true,
+                // Used internally for styling
+                [`footer-${mode}`]: true,
+                [`footer-translucent`]: translucent,
+                [`footer-translucent-${mode}`]: translucent,
+                ['footer-toolbar-padding']: !this.keyboardVisible && (!tabBar || tabBar.slot !== 'bottom'),
+                [`footer-collapse-${collapse}`]: collapse !== undefined,
+            } }, mode === 'ios' && translucent && h("div", { key: 'fafad08090a33d8c4e8a5b63d61929dcb89aab47', class: "footer-background" }), h("slot", { key: 'e0a443d346afa55e4317c0bc1263fdbe3c619559' })));
+    }
+    static get is() { return "ion-footer"; }
+    static get originalStyleUrls() {
+        return {
+            "ios": ["footer.ios.scss"],
+            "md": ["footer.md.scss"]
+        };
+    }
+    static get styleUrls() {
+        return {
+            "ios": ["footer.ios.css"],
+            "md": ["footer.md.css"]
+        };
+    }
+    static get properties() {
+        return {
+            "collapse": {
+                "type": "string",
+                "mutable": false,
+                "complexType": {
+                    "original": "'fade'",
+                    "resolved": "\"fade\" | undefined",
+                    "references": {}
+                },
+                "required": false,
+                "optional": true,
+                "docs": {
+                    "tags": [],
+                    "text": "Describes the scroll effect that will be applied to the footer.\nOnly applies in iOS mode."
+                },
+                "attribute": "collapse",
+                "reflect": false
+            },
+            "translucent": {
+                "type": "boolean",
+                "mutable": false,
+                "complexType": {
+                    "original": "boolean",
+                    "resolved": "boolean",
+                    "references": {}
+                },
+                "required": false,
+                "optional": false,
+                "docs": {
+                    "tags": [],
+                    "text": "If `true`, the footer will be translucent.\nOnly applies when the mode is `\"ios\"` and the device supports\n[`backdrop-filter`](https://developer.mozilla.org/en-US/docs/Web/CSS/backdrop-filter#Browser_compatibility).\n\nNote: In order to scroll content behind the footer, the `fullscreen`\nattribute needs to be set on the content."
+                },
+                "attribute": "translucent",
+                "reflect": false,
+                "defaultValue": "false"
+            }
+        };
+    }
+    static get states() {
+        return {
+            "keyboardVisible": {}
+        };
+    }
+    static get elementRef() { return "el"; }
+}

+ 129 - 0
src/node_modules/@ionic/core/dist/collection/components/footer/footer.md.css

@@ -0,0 +1,129 @@
+/**
+ * Convert a font size to a dynamic font size.
+ * Fonts that participate in Dynamic Type should use
+ * dynamic font sizes.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param unit (optional) - The unit to convert to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * Convert a font size to a dynamic font size but impose
+ * a maximum font size.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param maxScale - The maximum scale of the font (i.e. 2.5 for a maximum 250% scale).
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * Convert a font size to a dynamic font size but impose
+ * a minimum font size.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param minScale - The minimum scale of the font (i.e. 0.8 for a minimum 80% scale).
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * Convert a font size to a dynamic font size but impose
+ * maximum and minimum font sizes.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param minScale - The minimum scale of the font (i.e. 0.8 for a minimum 80% scale).
+ * @param maxScale - The maximum scale of the font (i.e. 2.5 for a maximum 250% scale).
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * A heuristic that applies CSS to tablet
+ * viewports.
+ *
+ * Usage:
+ * @include tablet-viewport() {
+ *   :host {
+ *     background-color: green;
+ *   }
+ * }
+ */
+/**
+ * A heuristic that applies CSS to mobile
+ * viewports (i.e. phones, not tablets).
+ *
+ * Usage:
+ * @include mobile-viewport() {
+ *   :host {
+ *     background-color: blue;
+ *   }
+ * }
+ */
+ion-footer {
+  display: block;
+  position: relative;
+  order: 1;
+  width: 100%;
+  z-index: 10;
+}
+
+ion-footer.footer-toolbar-padding ion-toolbar:last-of-type {
+  padding-bottom: var(--ion-safe-area-bottom, 0);
+}
+
+/**
+ * Convert a font size to a dynamic font size.
+ * Fonts that participate in Dynamic Type should use
+ * dynamic font sizes.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param unit (optional) - The unit to convert to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * Convert a font size to a dynamic font size but impose
+ * a maximum font size.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param maxScale - The maximum scale of the font (i.e. 2.5 for a maximum 250% scale).
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * Convert a font size to a dynamic font size but impose
+ * a minimum font size.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param minScale - The minimum scale of the font (i.e. 0.8 for a minimum 80% scale).
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * Convert a font size to a dynamic font size but impose
+ * maximum and minimum font sizes.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param minScale - The minimum scale of the font (i.e. 0.8 for a minimum 80% scale).
+ * @param maxScale - The maximum scale of the font (i.e. 2.5 for a maximum 250% scale).
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * A heuristic that applies CSS to tablet
+ * viewports.
+ *
+ * Usage:
+ * @include tablet-viewport() {
+ *   :host {
+ *     background-color: green;
+ *   }
+ * }
+ */
+/**
+ * A heuristic that applies CSS to mobile
+ * viewports (i.e. phones, not tablets).
+ *
+ * Usage:
+ * @include mobile-viewport() {
+ *   :host {
+ *     background-color: blue;
+ *   }
+ * }
+ */
+.footer-md {
+  box-shadow: 0 2px 4px -1px rgba(0, 0, 0, 0.2), 0 4px 5px 0 rgba(0, 0, 0, 0.14), 0 1px 10px 0 rgba(0, 0, 0, 0.12);
+}
+
+.footer-md.ion-no-border {
+  box-shadow: none;
+}

+ 33 - 0
src/node_modules/@ionic/core/dist/collection/components/footer/footer.utils.js

@@ -0,0 +1,33 @@
+/*!
+ * (C) Ionic http://ionicframework.com - MIT License
+ */
+import { readTask, writeTask } from "@stencil/core";
+import { clamp } from "../../utils/helpers";
+export const handleFooterFade = (scrollEl, baseEl) => {
+    readTask(() => {
+        const scrollTop = scrollEl.scrollTop;
+        const maxScroll = scrollEl.scrollHeight - scrollEl.clientHeight;
+        /**
+         * Toolbar background will fade
+         * out over fadeDuration in pixels.
+         */
+        const fadeDuration = 10;
+        /**
+         * Begin fading out maxScroll - 30px
+         * from the bottom of the content.
+         * Also determine how close we are
+         * to starting the fade. If we are
+         * before the starting point, the
+         * scale value will get clamped to 0.
+         * If we are after the maxScroll (rubber
+         * band scrolling), the scale value will
+         * get clamped to 1.
+         */
+        const fadeStart = maxScroll - fadeDuration;
+        const distanceToStart = scrollTop - fadeStart;
+        const scale = clamp(0, 1 - distanceToStart / fadeDuration, 1);
+        writeTask(() => {
+            baseEl.style.setProperty('--opacity-scale', scale.toString());
+        });
+    });
+};

+ 61 - 0
src/node_modules/@ionic/core/dist/collection/components/footer/test/basic/footer.e2e.js

@@ -0,0 +1,61 @@
+/*!
+ * (C) Ionic http://ionicframework.com - MIT License
+ */
+import { expect } from "@playwright/test";
+import { configs, test } from "../../../../utils/test/playwright/index";
+configs().forEach(({ title, screenshot, config }) => {
+    test.describe(title('footer: rendering'), () => {
+        test('should not have visual regressions with basic footer', async ({ page }) => {
+            await page.setContent(`
+          <ion-footer>
+            <ion-toolbar>
+              <ion-title>Footer - Default</ion-title>
+            </ion-toolbar>
+          </ion-footer>
+        `, config);
+            const footer = page.locator('ion-footer');
+            await expect(footer).toHaveScreenshot(screenshot(`footer-diff`));
+        });
+    });
+});
+/**
+ * This behavior does not vary
+ * across directions.
+ */
+configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
+    test.describe(title('footer: feature rendering'), () => {
+        test('should not have visual regressions with no border', async ({ page }) => {
+            await page.setContent(`
+        <ion-footer class="ion-no-border">
+          <ion-toolbar>
+            <ion-title>Footer - No Border</ion-title>
+          </ion-toolbar>
+        </ion-footer>
+      `, config);
+            const footer = page.locator('ion-footer');
+            await expect(footer).toHaveScreenshot(screenshot(`footer-no-border-diff`));
+        });
+    });
+});
+/**
+ * This behavior only exists on
+ * iOS mode and does not vary across directions.
+ */
+configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
+    test.describe(title('footer: translucent'), () => {
+        test('should not have visual regressions with translucent footer', async ({ page }) => {
+            await page.setContent(`
+        <ion-footer translucent="true">
+          <div style="position: absolute; top: 0; left: 0; right: 0; bottom: 0">
+            <img style="transform: rotate(145deg) scale(1.5)" src="/src/components/footer/test/img.jpg" />
+          </div>
+          <ion-toolbar>
+            <ion-title>Footer - Translucent</ion-title>
+          </ion-toolbar>
+        </ion-footer>
+      `, config);
+            const footer = page.locator('ion-footer');
+            await expect(footer).toHaveScreenshot(screenshot(`footer-translucent-diff`));
+        });
+    });
+});

+ 21 - 0
src/node_modules/@ionic/core/dist/collection/components/footer/test/fade/footer.e2e.js

@@ -0,0 +1,21 @@
+/*!
+ * (C) Ionic http://ionicframework.com - MIT License
+ */
+import { expect } from "@playwright/test";
+import { configs, test } from "../../../../utils/test/playwright/index";
+/**
+ * Fade effect is only available in iOS mode.
+ */
+configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
+    test.describe(title('footer: fade'), () => {
+        test('should not have visual regressions with fade footer', async ({ page }) => {
+            await page.goto('/src/components/footer/test/fade', config);
+            const footer = page.locator('ion-footer');
+            await expect(footer).toHaveScreenshot(screenshot(`footer-fade-blurred-diff`));
+            const content = page.locator('ion-content');
+            await content.evaluate((el) => el.scrollToBottom(0));
+            await page.waitForChanges();
+            await expect(footer).toHaveScreenshot(screenshot(`footer-fade-not-blurred-diff`));
+        });
+    });
+});

+ 26 - 0
src/node_modules/@ionic/core/dist/collection/components/footer/test/scroll-target/footer.e2e.js

@@ -0,0 +1,26 @@
+/*!
+ * (C) Ionic http://ionicframework.com - MIT License
+ */
+import { expect } from "@playwright/test";
+import { configs, test } from "../../../../utils/test/playwright/index";
+/**
+ * Translucent effect is only available in iOS mode.
+ */
+configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
+    test.describe(title('footer: scroll-target'), () => {
+        /**
+         * This test suite verifies that the fade effect for iOS is working correctly
+         * when the `ion-footer` is using a custom scroll target with the `.ion-content-scroll-host`
+         * selector.
+         */
+        test('should not have visual regressions with custom scroll target footer', async ({ page }) => {
+            await page.goto('/src/components/footer/test/scroll-target', config);
+            const footer = page.locator('ion-footer');
+            await expect(footer).toHaveScreenshot(screenshot(`footer-fade-scroll-target-blurred-diff`));
+            const scrollTarget = page.locator('#scroll-target');
+            await scrollTarget.evaluate((el) => (el.scrollTop = el.scrollHeight));
+            await page.waitForChanges();
+            await expect(footer).toHaveScreenshot(screenshot(`footer-fade-scroll-target-not-blurred-diff`));
+        });
+    });
+});

+ 17 - 0
src/node_modules/@ionic/core/dist/collection/components/footer/test/with-tabs/footer.e2e.js

@@ -0,0 +1,17 @@
+/*!
+ * (C) Ionic http://ionicframework.com - MIT License
+ */
+import { expect } from "@playwright/test";
+import { configs, test } from "../../../../utils/test/playwright/index";
+/**
+ * This does not test LTR vs. RTL layout.
+ */
+configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
+    test.describe(title('footer: with tabs'), () => {
+        test('should not have extra padding when near a tab bar', async ({ page }) => {
+            await page.goto('/src/components/footer/test/with-tabs', config);
+            const footer = page.locator('[tab="tab-one"] ion-footer');
+            await expect(footer).toHaveScreenshot(screenshot(`footer-with-tabs`));
+        });
+    });
+});

+ 235 - 0
src/node_modules/@ionic/core/dist/collection/components/grid/grid.css

@@ -0,0 +1,235 @@
+/**
+ * Convert a font size to a dynamic font size.
+ * Fonts that participate in Dynamic Type should use
+ * dynamic font sizes.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param unit (optional) - The unit to convert to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * Convert a font size to a dynamic font size but impose
+ * a maximum font size.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param maxScale - The maximum scale of the font (i.e. 2.5 for a maximum 250% scale).
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * Convert a font size to a dynamic font size but impose
+ * a minimum font size.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param minScale - The minimum scale of the font (i.e. 0.8 for a minimum 80% scale).
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * Convert a font size to a dynamic font size but impose
+ * maximum and minimum font sizes.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param minScale - The minimum scale of the font (i.e. 0.8 for a minimum 80% scale).
+ * @param maxScale - The maximum scale of the font (i.e. 2.5 for a maximum 250% scale).
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * A heuristic that applies CSS to tablet
+ * viewports.
+ *
+ * Usage:
+ * @include tablet-viewport() {
+ *   :host {
+ *     background-color: green;
+ *   }
+ * }
+ */
+/**
+ * A heuristic that applies CSS to mobile
+ * viewports (i.e. phones, not tablets).
+ *
+ * Usage:
+ * @include mobile-viewport() {
+ *   :host {
+ *     background-color: blue;
+ *   }
+ * }
+ */
+/**
+ * Convert a font size to a dynamic font size.
+ * Fonts that participate in Dynamic Type should use
+ * dynamic font sizes.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param unit (optional) - The unit to convert to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * Convert a font size to a dynamic font size but impose
+ * a maximum font size.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param maxScale - The maximum scale of the font (i.e. 2.5 for a maximum 250% scale).
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * Convert a font size to a dynamic font size but impose
+ * a minimum font size.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param minScale - The minimum scale of the font (i.e. 0.8 for a minimum 80% scale).
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * Convert a font size to a dynamic font size but impose
+ * maximum and minimum font sizes.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param minScale - The minimum scale of the font (i.e. 0.8 for a minimum 80% scale).
+ * @param maxScale - The maximum scale of the font (i.e. 2.5 for a maximum 250% scale).
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * A heuristic that applies CSS to tablet
+ * viewports.
+ *
+ * Usage:
+ * @include tablet-viewport() {
+ *   :host {
+ *     background-color: green;
+ *   }
+ * }
+ */
+/**
+ * A heuristic that applies CSS to mobile
+ * viewports (i.e. phones, not tablets).
+ *
+ * Usage:
+ * @include mobile-viewport() {
+ *   :host {
+ *     background-color: blue;
+ *   }
+ * }
+ */
+/**
+ * A heuristic that applies CSS to tablet
+ * viewports.
+ *
+ * Usage:
+ * @include tablet-viewport() {
+ *   :host {
+ *     background-color: green;
+ *   }
+ * }
+ */
+/**
+ * A heuristic that applies CSS to mobile
+ * viewports (i.e. phones, not tablets).
+ *
+ * Usage:
+ * @include mobile-viewport() {
+ *   :host {
+ *     background-color: blue;
+ *   }
+ * }
+ */
+:host {
+  /**
+   * @prop --ion-grid-padding: Padding for the Grid
+   * @prop --ion-grid-padding-xs: Padding for the Grid on xs screens
+   * @prop --ion-grid-padding-sm: Padding for the Grid on sm screens
+   * @prop --ion-grid-padding-md: Padding for the Grid on md screens
+   * @prop --ion-grid-padding-lg: Padding for the Grid on lg screens
+   * @prop --ion-grid-padding-xl: Padding for the Grid on xl screens
+   *
+   * @prop --ion-grid-width: Width of the fixed Grid
+   * @prop --ion-grid-width-xs: Width of the fixed Grid on xs screens
+   * @prop --ion-grid-width-sm: Width of the fixed Grid on sm screens
+   * @prop --ion-grid-width-md: Width of the fixed Grid on md screens
+   * @prop --ion-grid-width-lg: Width of the fixed Grid on lg screens
+   * @prop --ion-grid-width-xl: Width of the fixed Grid on xl screens
+   */
+  -webkit-padding-start: var(--ion-grid-padding-xs, var(--ion-grid-padding, 5px));
+  padding-inline-start: var(--ion-grid-padding-xs, var(--ion-grid-padding, 5px));
+  -webkit-padding-end: var(--ion-grid-padding-xs, var(--ion-grid-padding, 5px));
+  padding-inline-end: var(--ion-grid-padding-xs, var(--ion-grid-padding, 5px));
+  padding-top: var(--ion-grid-padding-xs, var(--ion-grid-padding, 5px));
+  padding-bottom: var(--ion-grid-padding-xs, var(--ion-grid-padding, 5px));
+  -webkit-margin-start: auto;
+  margin-inline-start: auto;
+  -webkit-margin-end: auto;
+  margin-inline-end: auto;
+  display: block;
+  flex: 1;
+}
+@media (min-width: 576px) {
+  :host {
+    -webkit-padding-start: var(--ion-grid-padding-sm, var(--ion-grid-padding, 5px));
+    padding-inline-start: var(--ion-grid-padding-sm, var(--ion-grid-padding, 5px));
+    -webkit-padding-end: var(--ion-grid-padding-sm, var(--ion-grid-padding, 5px));
+    padding-inline-end: var(--ion-grid-padding-sm, var(--ion-grid-padding, 5px));
+    padding-top: var(--ion-grid-padding-sm, var(--ion-grid-padding, 5px));
+    padding-bottom: var(--ion-grid-padding-sm, var(--ion-grid-padding, 5px));
+  }
+}
+@media (min-width: 768px) {
+  :host {
+    -webkit-padding-start: var(--ion-grid-padding-md, var(--ion-grid-padding, 5px));
+    padding-inline-start: var(--ion-grid-padding-md, var(--ion-grid-padding, 5px));
+    -webkit-padding-end: var(--ion-grid-padding-md, var(--ion-grid-padding, 5px));
+    padding-inline-end: var(--ion-grid-padding-md, var(--ion-grid-padding, 5px));
+    padding-top: var(--ion-grid-padding-md, var(--ion-grid-padding, 5px));
+    padding-bottom: var(--ion-grid-padding-md, var(--ion-grid-padding, 5px));
+  }
+}
+@media (min-width: 992px) {
+  :host {
+    -webkit-padding-start: var(--ion-grid-padding-lg, var(--ion-grid-padding, 5px));
+    padding-inline-start: var(--ion-grid-padding-lg, var(--ion-grid-padding, 5px));
+    -webkit-padding-end: var(--ion-grid-padding-lg, var(--ion-grid-padding, 5px));
+    padding-inline-end: var(--ion-grid-padding-lg, var(--ion-grid-padding, 5px));
+    padding-top: var(--ion-grid-padding-lg, var(--ion-grid-padding, 5px));
+    padding-bottom: var(--ion-grid-padding-lg, var(--ion-grid-padding, 5px));
+  }
+}
+@media (min-width: 1200px) {
+  :host {
+    -webkit-padding-start: var(--ion-grid-padding-xl, var(--ion-grid-padding, 5px));
+    padding-inline-start: var(--ion-grid-padding-xl, var(--ion-grid-padding, 5px));
+    -webkit-padding-end: var(--ion-grid-padding-xl, var(--ion-grid-padding, 5px));
+    padding-inline-end: var(--ion-grid-padding-xl, var(--ion-grid-padding, 5px));
+    padding-top: var(--ion-grid-padding-xl, var(--ion-grid-padding, 5px));
+    padding-bottom: var(--ion-grid-padding-xl, var(--ion-grid-padding, 5px));
+  }
+}
+
+:host(.grid-fixed) {
+  width: var(--ion-grid-width-xs, var(--ion-grid-width, 100%));
+  max-width: 100%;
+}
+@media (min-width: 576px) {
+  :host(.grid-fixed) {
+    width: var(--ion-grid-width-sm, var(--ion-grid-width, 540px));
+  }
+}
+@media (min-width: 768px) {
+  :host(.grid-fixed) {
+    width: var(--ion-grid-width-md, var(--ion-grid-width, 720px));
+  }
+}
+@media (min-width: 992px) {
+  :host(.grid-fixed) {
+    width: var(--ion-grid-width-lg, var(--ion-grid-width, 960px));
+  }
+}
+@media (min-width: 1200px) {
+  :host(.grid-fixed) {
+    width: var(--ion-grid-width-xl, var(--ion-grid-width, 1140px));
+  }
+}
+
+:host(.ion-no-padding) {
+  --ion-grid-column-padding: 0;
+  --ion-grid-column-padding-xs: 0;
+  --ion-grid-column-padding-sm: 0;
+  --ion-grid-column-padding-md: 0;
+  --ion-grid-column-padding-lg: 0;
+  --ion-grid-column-padding-xl: 0;
+}

+ 51 - 0
src/node_modules/@ionic/core/dist/collection/components/grid/grid.js

@@ -0,0 +1,51 @@
+/*!
+ * (C) Ionic http://ionicframework.com - MIT License
+ */
+import { Host, h } from "@stencil/core";
+import { getIonMode } from "../../global/ionic-global";
+export class Grid {
+    constructor() {
+        this.fixed = false;
+    }
+    render() {
+        const mode = getIonMode(this);
+        return (h(Host, { key: '930ce78b02f8360fbca08a35d364d2c09128c6c8', class: {
+                [mode]: true,
+                'grid-fixed': this.fixed,
+            } }, h("slot", { key: 'c47bf7ef2197f5ebc42d3e2c55044276fb0db393' })));
+    }
+    static get is() { return "ion-grid"; }
+    static get encapsulation() { return "shadow"; }
+    static get originalStyleUrls() {
+        return {
+            "$": ["grid.scss"]
+        };
+    }
+    static get styleUrls() {
+        return {
+            "$": ["grid.css"]
+        };
+    }
+    static get properties() {
+        return {
+            "fixed": {
+                "type": "boolean",
+                "mutable": false,
+                "complexType": {
+                    "original": "boolean",
+                    "resolved": "boolean",
+                    "references": {}
+                },
+                "required": false,
+                "optional": false,
+                "docs": {
+                    "tags": [],
+                    "text": "If `true`, the grid will have a fixed width based on the screen size."
+                },
+                "attribute": "fixed",
+                "reflect": false,
+                "defaultValue": "false"
+            }
+        };
+    }
+}

+ 17 - 0
src/node_modules/@ionic/core/dist/collection/components/grid/test/basic/grid.e2e.js

@@ -0,0 +1,17 @@
+/*!
+ * (C) Ionic http://ionicframework.com - MIT License
+ */
+import { expect } from "@playwright/test";
+import { configs, test } from "../../../../utils/test/playwright/index";
+/**
+ * ion-grid does not have different styling per-mode
+ */
+configs({ modes: ['md'] }).forEach(({ title, screenshot, config }) => {
+    test.describe(title('grid: basic'), () => {
+        test('should not have visual regressions', async ({ page }) => {
+            await page.goto(`/src/components/grid/test/basic`, config);
+            await page.setIonViewport();
+            await expect(page).toHaveScreenshot(screenshot(`grid-basic`));
+        });
+    });
+});

+ 17 - 0
src/node_modules/@ionic/core/dist/collection/components/grid/test/offsets/grid.e2e.js

@@ -0,0 +1,17 @@
+/*!
+ * (C) Ionic http://ionicframework.com - MIT License
+ */
+import { expect } from "@playwright/test";
+import { configs, test } from "../../../../utils/test/playwright/index";
+/**
+ * ion-grid does not have different styling per-mode
+ */
+configs({ modes: ['md'] }).forEach(({ title, screenshot, config }) => {
+    test.describe(title('grid: offsets'), () => {
+        test('should not have visual regressions', async ({ page }) => {
+            await page.goto(`/src/components/grid/test/offsets`, config);
+            await page.setIonViewport();
+            await expect(page).toHaveScreenshot(screenshot(`grid-offsets`));
+        });
+    });
+});

+ 17 - 0
src/node_modules/@ionic/core/dist/collection/components/grid/test/padding/grid.e2e.js

@@ -0,0 +1,17 @@
+/*!
+ * (C) Ionic http://ionicframework.com - MIT License
+ */
+import { expect } from "@playwright/test";
+import { configs, test } from "../../../../utils/test/playwright/index";
+/**
+ * ion-grid does not have different styling per-mode
+ */
+configs({ modes: ['md'] }).forEach(({ title, screenshot, config }) => {
+    test.describe(title('grid: padding'), () => {
+        test('should not have visual regressions', async ({ page }) => {
+            await page.goto(`/src/components/grid/test/padding`, config);
+            await page.setIonViewport();
+            await expect(page).toHaveScreenshot(screenshot(`grid-padding`));
+        });
+    });
+});

+ 17 - 0
src/node_modules/@ionic/core/dist/collection/components/grid/test/sizes/grid.e2e.js

@@ -0,0 +1,17 @@
+/*!
+ * (C) Ionic http://ionicframework.com - MIT License
+ */
+import { expect } from "@playwright/test";
+import { configs, test } from "../../../../utils/test/playwright/index";
+/**
+ * ion-grid does not have different styling per-mode
+ */
+configs({ modes: ['md'] }).forEach(({ title, screenshot, config }) => {
+    test.describe(title('grid: sizes'), () => {
+        test('should not have visual regressions', async ({ page }) => {
+            await page.goto(`/src/components/grid/test/sizes`, config);
+            await page.setIonViewport();
+            await expect(page).toHaveScreenshot(screenshot(`grid-sizes`));
+        });
+    });
+});

+ 250 - 0
src/node_modules/@ionic/core/dist/collection/components/header/header.ios.css

@@ -0,0 +1,250 @@
+/**
+ * Convert a font size to a dynamic font size.
+ * Fonts that participate in Dynamic Type should use
+ * dynamic font sizes.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param unit (optional) - The unit to convert to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * Convert a font size to a dynamic font size but impose
+ * a maximum font size.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param maxScale - The maximum scale of the font (i.e. 2.5 for a maximum 250% scale).
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * Convert a font size to a dynamic font size but impose
+ * a minimum font size.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param minScale - The minimum scale of the font (i.e. 0.8 for a minimum 80% scale).
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * Convert a font size to a dynamic font size but impose
+ * maximum and minimum font sizes.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param minScale - The minimum scale of the font (i.e. 0.8 for a minimum 80% scale).
+ * @param maxScale - The maximum scale of the font (i.e. 2.5 for a maximum 250% scale).
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * A heuristic that applies CSS to tablet
+ * viewports.
+ *
+ * Usage:
+ * @include tablet-viewport() {
+ *   :host {
+ *     background-color: green;
+ *   }
+ * }
+ */
+/**
+ * A heuristic that applies CSS to mobile
+ * viewports (i.e. phones, not tablets).
+ *
+ * Usage:
+ * @include mobile-viewport() {
+ *   :host {
+ *     background-color: blue;
+ *   }
+ * }
+ */
+ion-header {
+  display: block;
+  position: relative;
+  order: -1;
+  width: 100%;
+  z-index: 10;
+}
+
+ion-header ion-toolbar:first-of-type {
+  padding-top: var(--ion-safe-area-top, 0);
+}
+
+/**
+ * Convert a font size to a dynamic font size.
+ * Fonts that participate in Dynamic Type should use
+ * dynamic font sizes.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param unit (optional) - The unit to convert to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * Convert a font size to a dynamic font size but impose
+ * a maximum font size.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param maxScale - The maximum scale of the font (i.e. 2.5 for a maximum 250% scale).
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * Convert a font size to a dynamic font size but impose
+ * a minimum font size.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param minScale - The minimum scale of the font (i.e. 0.8 for a minimum 80% scale).
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * Convert a font size to a dynamic font size but impose
+ * maximum and minimum font sizes.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param minScale - The minimum scale of the font (i.e. 0.8 for a minimum 80% scale).
+ * @param maxScale - The maximum scale of the font (i.e. 2.5 for a maximum 250% scale).
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * A heuristic that applies CSS to tablet
+ * viewports.
+ *
+ * Usage:
+ * @include tablet-viewport() {
+ *   :host {
+ *     background-color: green;
+ *   }
+ * }
+ */
+/**
+ * A heuristic that applies CSS to mobile
+ * viewports (i.e. phones, not tablets).
+ *
+ * Usage:
+ * @include mobile-viewport() {
+ *   :host {
+ *     background-color: blue;
+ *   }
+ * }
+ */
+.header-ios ion-toolbar:last-of-type {
+  --border-width: 0 0 0.55px;
+}
+
+@supports (backdrop-filter: blur(0)) {
+  .header-background {
+    left: 0;
+    right: 0;
+    top: 0;
+    bottom: 0;
+    position: absolute;
+    backdrop-filter: saturate(180%) blur(20px);
+  }
+  .header-translucent-ios ion-toolbar {
+    --opacity: .8;
+  }
+  /**
+   * Disable the saturation otherwise it distorts the content
+   * background color when large header is not collapsed
+   */
+  .header-collapse-condense-inactive .header-background {
+    backdrop-filter: blur(20px);
+  }
+}
+.header-ios.ion-no-border ion-toolbar:last-of-type {
+  --border-width: 0;
+}
+
+.header-collapse-fade ion-toolbar {
+  --opacity-scale: inherit;
+}
+
+.header-collapse-condense {
+  z-index: 9;
+}
+
+.header-collapse-condense ion-toolbar {
+  position: sticky;
+  top: 0;
+}
+
+.header-collapse-condense ion-toolbar:first-of-type {
+  /**
+   * The toolbar that contains the collapsible
+   * large title should not have safe area padding applied.
+   */
+  padding-top: 0px;
+  z-index: 1;
+}
+
+/**
+ * Large title toolbar should just use the content background
+ * since it needs to blend in with the header above it.
+ */
+.header-collapse-condense ion-toolbar {
+  --background: var(--ion-background-color, #fff);
+  z-index: 0;
+}
+
+.header-collapse-condense ion-toolbar:last-of-type {
+  --border-width: 0px;
+}
+
+.header-collapse-condense ion-toolbar ion-searchbar {
+  padding-top: 0px;
+  padding-bottom: 13px;
+}
+
+.header-collapse-main {
+  --opacity-scale: 1;
+}
+
+.header-collapse-main ion-toolbar {
+  --opacity-scale: inherit;
+}
+
+.header-collapse-main ion-toolbar.in-toolbar ion-title,
+.header-collapse-main ion-toolbar.in-toolbar ion-buttons {
+  transition: all 0.2s ease-in-out;
+}
+
+.header-collapse-condense-inactive:not(.header-collapse-condense) ion-toolbar.in-toolbar ion-title,
+.header-collapse-condense-inactive:not(.header-collapse-condense) ion-toolbar.in-toolbar ion-buttons.buttons-collapse {
+  opacity: 0;
+  pointer-events: none;
+}
+
+/**
+ * There is a bug in Safari where changing
+ * the opacity of an element in a scrollable container
+ * while rubber-banding causes the scroll position
+ * to jump to the top
+ */
+.header-collapse-condense-inactive.header-collapse-condense ion-toolbar.in-toolbar ion-title,
+.header-collapse-condense-inactive.header-collapse-condense ion-toolbar.in-toolbar ion-buttons.buttons-collapse {
+  visibility: hidden;
+}
+
+/**
+ * The main header is only hidden once the collapsible large
+ * title is configured. As a result, if the main header loads
+ * before the collapsible large title is configured then the
+ * main header will be visible briefly before being hidden
+ * by the collapsible large title.
+ *
+ * The following selector ensures that any main header
+ * on a page with a collapsible large title is hidden
+ * before the collapsible large title is configured.
+ * Once the collapsible large title is configured the main
+ * header will have the ".header-collapse-main" class, and
+ * this selector will no longer apply.
+ *
+ * The :has(...) part of the selector ensures a couple things:
+ * 1. This will only apply within a page view since the content
+ * must be a subsequent-sibling of the header (~ ion-content).
+ * 2. This will only apply when that content has a collapse header (ion-header[collapse="condense"])
+ *
+ * We use opacity: 0 to avoid a layout shift.
+ * We target both the attribute and the class in the event that the attribute
+ * is not reflected on the host in some frameworks.
+ *
+ * Both headers should be scoped to iOS mode otherwise an MD app that uses an
+ * iOS header may cause other MD headers to be unexpectedly hidden.
+ */
+ion-header.header-ios:not(.header-collapse-main):has(~ ion-content ion-header.header-ios[collapse=condense],
+~ ion-content ion-header.header-ios.header-collapse-condense) {
+  opacity: 0;
+}

+ 205 - 0
src/node_modules/@ionic/core/dist/collection/components/header/header.js

@@ -0,0 +1,205 @@
+/*!
+ * (C) Ionic http://ionicframework.com - MIT License
+ */
+import { Host, h, writeTask } from "@stencil/core";
+import { findIonContent, getScrollElement, printIonContentErrorMsg } from "../../utils/content/index";
+import { inheritAriaAttributes } from "../../utils/helpers";
+import { hostContext } from "../../utils/theme";
+import { getIonMode } from "../../global/ionic-global";
+import { cloneElement, createHeaderIndex, handleContentScroll, handleHeaderFade, handleToolbarIntersection, setHeaderActive, setToolbarBackgroundOpacity, } from "./header.utils";
+/**
+ * @virtualProp {"ios" | "md"} mode - The mode determines which platform styles to use.
+ */
+export class Header {
+    constructor() {
+        this.inheritedAttributes = {};
+        this.setupFadeHeader = async (contentEl, condenseHeader) => {
+            const scrollEl = (this.scrollEl = await getScrollElement(contentEl));
+            /**
+             * Handle fading of toolbars on scroll
+             */
+            this.contentScrollCallback = () => {
+                handleHeaderFade(this.scrollEl, this.el, condenseHeader);
+            };
+            scrollEl.addEventListener('scroll', this.contentScrollCallback);
+            handleHeaderFade(this.scrollEl, this.el, condenseHeader);
+        };
+        this.collapse = undefined;
+        this.translucent = false;
+    }
+    componentWillLoad() {
+        this.inheritedAttributes = inheritAriaAttributes(this.el);
+    }
+    componentDidLoad() {
+        this.checkCollapsibleHeader();
+    }
+    componentDidUpdate() {
+        this.checkCollapsibleHeader();
+    }
+    disconnectedCallback() {
+        this.destroyCollapsibleHeader();
+    }
+    async checkCollapsibleHeader() {
+        const mode = getIonMode(this);
+        if (mode !== 'ios') {
+            return;
+        }
+        const { collapse } = this;
+        const hasCondense = collapse === 'condense';
+        const hasFade = collapse === 'fade';
+        this.destroyCollapsibleHeader();
+        if (hasCondense) {
+            const pageEl = this.el.closest('ion-app,ion-page,.ion-page,page-inner');
+            const contentEl = pageEl ? findIonContent(pageEl) : null;
+            // Cloned elements are always needed in iOS transition
+            writeTask(() => {
+                const title = cloneElement('ion-title');
+                title.size = 'large';
+                cloneElement('ion-back-button');
+            });
+            await this.setupCondenseHeader(contentEl, pageEl);
+        }
+        else if (hasFade) {
+            const pageEl = this.el.closest('ion-app,ion-page,.ion-page,page-inner');
+            const contentEl = pageEl ? findIonContent(pageEl) : null;
+            if (!contentEl) {
+                printIonContentErrorMsg(this.el);
+                return;
+            }
+            const condenseHeader = contentEl.querySelector('ion-header[collapse="condense"]');
+            await this.setupFadeHeader(contentEl, condenseHeader);
+        }
+    }
+    destroyCollapsibleHeader() {
+        if (this.intersectionObserver) {
+            this.intersectionObserver.disconnect();
+            this.intersectionObserver = undefined;
+        }
+        if (this.scrollEl && this.contentScrollCallback) {
+            this.scrollEl.removeEventListener('scroll', this.contentScrollCallback);
+            this.contentScrollCallback = undefined;
+        }
+        if (this.collapsibleMainHeader) {
+            this.collapsibleMainHeader.classList.remove('header-collapse-main');
+            this.collapsibleMainHeader = undefined;
+        }
+    }
+    async setupCondenseHeader(contentEl, pageEl) {
+        if (!contentEl || !pageEl) {
+            printIonContentErrorMsg(this.el);
+            return;
+        }
+        if (typeof IntersectionObserver === 'undefined') {
+            return;
+        }
+        this.scrollEl = await getScrollElement(contentEl);
+        const headers = pageEl.querySelectorAll('ion-header');
+        this.collapsibleMainHeader = Array.from(headers).find((header) => header.collapse !== 'condense');
+        if (!this.collapsibleMainHeader) {
+            return;
+        }
+        const mainHeaderIndex = createHeaderIndex(this.collapsibleMainHeader);
+        const scrollHeaderIndex = createHeaderIndex(this.el);
+        if (!mainHeaderIndex || !scrollHeaderIndex) {
+            return;
+        }
+        setHeaderActive(mainHeaderIndex, false);
+        setToolbarBackgroundOpacity(mainHeaderIndex.el, 0);
+        /**
+         * Handle interaction between toolbar collapse and
+         * showing/hiding content in the primary ion-header
+         * as well as progressively showing/hiding the main header
+         * border as the top-most toolbar collapses or expands.
+         */
+        const toolbarIntersection = (ev) => {
+            handleToolbarIntersection(ev, mainHeaderIndex, scrollHeaderIndex, this.scrollEl);
+        };
+        this.intersectionObserver = new IntersectionObserver(toolbarIntersection, {
+            root: contentEl,
+            threshold: [0.25, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1],
+        });
+        this.intersectionObserver.observe(scrollHeaderIndex.toolbars[scrollHeaderIndex.toolbars.length - 1].el);
+        /**
+         * Handle scaling of large iOS titles and
+         * showing/hiding border on last toolbar
+         * in primary header
+         */
+        this.contentScrollCallback = () => {
+            handleContentScroll(this.scrollEl, scrollHeaderIndex, contentEl);
+        };
+        this.scrollEl.addEventListener('scroll', this.contentScrollCallback);
+        writeTask(() => {
+            if (this.collapsibleMainHeader !== undefined) {
+                this.collapsibleMainHeader.classList.add('header-collapse-main');
+            }
+        });
+    }
+    render() {
+        const { translucent, inheritedAttributes } = this;
+        const mode = getIonMode(this);
+        const collapse = this.collapse || 'none';
+        // banner role must be at top level, so remove role if inside a menu
+        const roleType = hostContext('ion-menu', this.el) ? 'none' : 'banner';
+        return (h(Host, Object.assign({ key: 'c687314ef290793a9d633ad20cfc5eeb47621e31', role: roleType, class: {
+                [mode]: true,
+                // Used internally for styling
+                [`header-${mode}`]: true,
+                [`header-translucent`]: this.translucent,
+                [`header-collapse-${collapse}`]: true,
+                [`header-translucent-${mode}`]: this.translucent,
+            } }, inheritedAttributes), mode === 'ios' && translucent && h("div", { key: 'b429996046082405a91e7c23f95516db0b736f12', class: "header-background" }), h("slot", { key: 'e17a8965f8d3a33c1bfcb056c153d8242e5229fa' })));
+    }
+    static get is() { return "ion-header"; }
+    static get originalStyleUrls() {
+        return {
+            "ios": ["header.ios.scss"],
+            "md": ["header.md.scss"]
+        };
+    }
+    static get styleUrls() {
+        return {
+            "ios": ["header.ios.css"],
+            "md": ["header.md.css"]
+        };
+    }
+    static get properties() {
+        return {
+            "collapse": {
+                "type": "string",
+                "mutable": false,
+                "complexType": {
+                    "original": "'condense' | 'fade'",
+                    "resolved": "\"condense\" | \"fade\" | undefined",
+                    "references": {}
+                },
+                "required": false,
+                "optional": true,
+                "docs": {
+                    "tags": [],
+                    "text": "Describes the scroll effect that will be applied to the header.\nOnly applies in iOS mode.\n\nTypically used for [Collapsible Large Titles](https://ionicframework.com/docs/api/title#collapsible-large-titles)"
+                },
+                "attribute": "collapse",
+                "reflect": false
+            },
+            "translucent": {
+                "type": "boolean",
+                "mutable": false,
+                "complexType": {
+                    "original": "boolean",
+                    "resolved": "boolean",
+                    "references": {}
+                },
+                "required": false,
+                "optional": false,
+                "docs": {
+                    "tags": [],
+                    "text": "If `true`, the header will be translucent.\nOnly applies when the mode is `\"ios\"` and the device supports\n[`backdrop-filter`](https://developer.mozilla.org/en-US/docs/Web/CSS/backdrop-filter#Browser_compatibility).\n\nNote: In order to scroll content behind the header, the `fullscreen`\nattribute needs to be set on the content."
+                },
+                "attribute": "translucent",
+                "reflect": false,
+                "defaultValue": "false"
+            }
+        };
+    }
+    static get elementRef() { return "el"; }
+}

+ 133 - 0
src/node_modules/@ionic/core/dist/collection/components/header/header.md.css

@@ -0,0 +1,133 @@
+/**
+ * Convert a font size to a dynamic font size.
+ * Fonts that participate in Dynamic Type should use
+ * dynamic font sizes.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param unit (optional) - The unit to convert to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * Convert a font size to a dynamic font size but impose
+ * a maximum font size.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param maxScale - The maximum scale of the font (i.e. 2.5 for a maximum 250% scale).
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * Convert a font size to a dynamic font size but impose
+ * a minimum font size.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param minScale - The minimum scale of the font (i.e. 0.8 for a minimum 80% scale).
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * Convert a font size to a dynamic font size but impose
+ * maximum and minimum font sizes.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param minScale - The minimum scale of the font (i.e. 0.8 for a minimum 80% scale).
+ * @param maxScale - The maximum scale of the font (i.e. 2.5 for a maximum 250% scale).
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * A heuristic that applies CSS to tablet
+ * viewports.
+ *
+ * Usage:
+ * @include tablet-viewport() {
+ *   :host {
+ *     background-color: green;
+ *   }
+ * }
+ */
+/**
+ * A heuristic that applies CSS to mobile
+ * viewports (i.e. phones, not tablets).
+ *
+ * Usage:
+ * @include mobile-viewport() {
+ *   :host {
+ *     background-color: blue;
+ *   }
+ * }
+ */
+ion-header {
+  display: block;
+  position: relative;
+  order: -1;
+  width: 100%;
+  z-index: 10;
+}
+
+ion-header ion-toolbar:first-of-type {
+  padding-top: var(--ion-safe-area-top, 0);
+}
+
+/**
+ * Convert a font size to a dynamic font size.
+ * Fonts that participate in Dynamic Type should use
+ * dynamic font sizes.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param unit (optional) - The unit to convert to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * Convert a font size to a dynamic font size but impose
+ * a maximum font size.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param maxScale - The maximum scale of the font (i.e. 2.5 for a maximum 250% scale).
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * Convert a font size to a dynamic font size but impose
+ * a minimum font size.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param minScale - The minimum scale of the font (i.e. 0.8 for a minimum 80% scale).
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * Convert a font size to a dynamic font size but impose
+ * maximum and minimum font sizes.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param minScale - The minimum scale of the font (i.e. 0.8 for a minimum 80% scale).
+ * @param maxScale - The maximum scale of the font (i.e. 2.5 for a maximum 250% scale).
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * A heuristic that applies CSS to tablet
+ * viewports.
+ *
+ * Usage:
+ * @include tablet-viewport() {
+ *   :host {
+ *     background-color: green;
+ *   }
+ * }
+ */
+/**
+ * A heuristic that applies CSS to mobile
+ * viewports (i.e. phones, not tablets).
+ *
+ * Usage:
+ * @include mobile-viewport() {
+ *   :host {
+ *     background-color: blue;
+ *   }
+ * }
+ */
+.header-md {
+  box-shadow: 0 2px 4px -1px rgba(0, 0, 0, 0.2), 0 4px 5px 0 rgba(0, 0, 0, 0.14), 0 1px 10px 0 rgba(0, 0, 0, 0.12);
+}
+
+.header-collapse-condense {
+  display: none;
+}
+
+.header-md.ion-no-border {
+  box-shadow: none;
+}

+ 180 - 0
src/node_modules/@ionic/core/dist/collection/components/header/header.utils.js

@@ -0,0 +1,180 @@
+/*!
+ * (C) Ionic http://ionicframework.com - MIT License
+ */
+import { readTask, writeTask } from "@stencil/core";
+import { clamp } from "../../utils/helpers";
+const TRANSITION = 'all 0.2s ease-in-out';
+export const cloneElement = (tagName) => {
+    const getCachedEl = document.querySelector(`${tagName}.ion-cloned-element`);
+    if (getCachedEl !== null) {
+        return getCachedEl;
+    }
+    const clonedEl = document.createElement(tagName);
+    clonedEl.classList.add('ion-cloned-element');
+    clonedEl.style.setProperty('display', 'none');
+    document.body.appendChild(clonedEl);
+    return clonedEl;
+};
+export const createHeaderIndex = (headerEl) => {
+    if (!headerEl) {
+        return;
+    }
+    const toolbars = headerEl.querySelectorAll('ion-toolbar');
+    return {
+        el: headerEl,
+        toolbars: Array.from(toolbars).map((toolbar) => {
+            const ionTitleEl = toolbar.querySelector('ion-title');
+            return {
+                el: toolbar,
+                background: toolbar.shadowRoot.querySelector('.toolbar-background'),
+                ionTitleEl,
+                innerTitleEl: ionTitleEl ? ionTitleEl.shadowRoot.querySelector('.toolbar-title') : null,
+                ionButtonsEl: Array.from(toolbar.querySelectorAll('ion-buttons')),
+            };
+        }),
+    };
+};
+export const handleContentScroll = (scrollEl, scrollHeaderIndex, contentEl) => {
+    readTask(() => {
+        const scrollTop = scrollEl.scrollTop;
+        const scale = clamp(1, 1 + -scrollTop / 500, 1.1);
+        // Native refresher should not cause titles to scale
+        const nativeRefresher = contentEl.querySelector('ion-refresher.refresher-native');
+        if (nativeRefresher === null) {
+            writeTask(() => {
+                scaleLargeTitles(scrollHeaderIndex.toolbars, scale);
+            });
+        }
+    });
+};
+export const setToolbarBackgroundOpacity = (headerEl, opacity) => {
+    /**
+     * Fading in the backdrop opacity
+     * should happen after the large title
+     * has collapsed, so it is handled
+     * by handleHeaderFade()
+     */
+    if (headerEl.collapse === 'fade') {
+        return;
+    }
+    if (opacity === undefined) {
+        headerEl.style.removeProperty('--opacity-scale');
+    }
+    else {
+        headerEl.style.setProperty('--opacity-scale', opacity.toString());
+    }
+};
+const handleToolbarBorderIntersection = (ev, mainHeaderIndex, scrollTop) => {
+    if (!ev[0].isIntersecting) {
+        return;
+    }
+    /**
+     * There is a bug in Safari where overflow scrolling on a non-body element
+     * does not always reset the scrollTop position to 0 when letting go. It will
+     * set to 1 once the rubber band effect has ended. This causes the background to
+     * appear slightly on certain app setups.
+     *
+     * Additionally, we check if user is rubber banding (scrolling is negative)
+     * as this can mean they are using pull to refresh. Once the refresher starts,
+     * the content is transformed which can cause the intersection observer to erroneously
+     * fire here as well.
+     */
+    const scale = ev[0].intersectionRatio > 0.9 || scrollTop <= 0 ? 0 : ((1 - ev[0].intersectionRatio) * 100) / 75;
+    setToolbarBackgroundOpacity(mainHeaderIndex.el, scale === 1 ? undefined : scale);
+};
+/**
+ * If toolbars are intersecting, hide the scrollable toolbar content
+ * and show the primary toolbar content. If the toolbars are not intersecting,
+ * hide the primary toolbar content and show the scrollable toolbar content
+ */
+export const handleToolbarIntersection = (ev, // TODO(FW-2832): type (IntersectionObserverEntry[] triggers errors which should be sorted)
+mainHeaderIndex, scrollHeaderIndex, scrollEl) => {
+    writeTask(() => {
+        const scrollTop = scrollEl.scrollTop;
+        handleToolbarBorderIntersection(ev, mainHeaderIndex, scrollTop);
+        const event = ev[0];
+        const intersection = event.intersectionRect;
+        const intersectionArea = intersection.width * intersection.height;
+        const rootArea = event.rootBounds.width * event.rootBounds.height;
+        const isPageHidden = intersectionArea === 0 && rootArea === 0;
+        const leftDiff = Math.abs(intersection.left - event.boundingClientRect.left);
+        const rightDiff = Math.abs(intersection.right - event.boundingClientRect.right);
+        const isPageTransitioning = intersectionArea > 0 && (leftDiff >= 5 || rightDiff >= 5);
+        if (isPageHidden || isPageTransitioning) {
+            return;
+        }
+        if (event.isIntersecting) {
+            setHeaderActive(mainHeaderIndex, false);
+            setHeaderActive(scrollHeaderIndex);
+        }
+        else {
+            /**
+             * There is a bug with IntersectionObserver on Safari
+             * where `event.isIntersecting === false` when cancelling
+             * a swipe to go back gesture. Checking the intersection
+             * x, y, width, and height provides a workaround. This bug
+             * does not happen when using Safari + Web Animations,
+             * only Safari + CSS Animations.
+             */
+            const hasValidIntersection = (intersection.x === 0 && intersection.y === 0) || (intersection.width !== 0 && intersection.height !== 0);
+            if (hasValidIntersection && scrollTop > 0) {
+                setHeaderActive(mainHeaderIndex);
+                setHeaderActive(scrollHeaderIndex, false);
+                setToolbarBackgroundOpacity(mainHeaderIndex.el);
+            }
+        }
+    });
+};
+export const setHeaderActive = (headerIndex, active = true) => {
+    const headerEl = headerIndex.el;
+    if (active) {
+        headerEl.classList.remove('header-collapse-condense-inactive');
+        headerEl.removeAttribute('aria-hidden');
+    }
+    else {
+        headerEl.classList.add('header-collapse-condense-inactive');
+        headerEl.setAttribute('aria-hidden', 'true');
+    }
+};
+export const scaleLargeTitles = (toolbars = [], scale = 1, transition = false) => {
+    toolbars.forEach((toolbar) => {
+        const ionTitle = toolbar.ionTitleEl;
+        const titleDiv = toolbar.innerTitleEl;
+        if (!ionTitle || ionTitle.size !== 'large') {
+            return;
+        }
+        titleDiv.style.transition = transition ? TRANSITION : '';
+        titleDiv.style.transform = `scale3d(${scale}, ${scale}, 1)`;
+    });
+};
+export const handleHeaderFade = (scrollEl, baseEl, condenseHeader) => {
+    readTask(() => {
+        const scrollTop = scrollEl.scrollTop;
+        const baseElHeight = baseEl.clientHeight;
+        const fadeStart = condenseHeader ? condenseHeader.clientHeight : 0;
+        /**
+         * If we are using fade header with a condense
+         * header, then the toolbar backgrounds should
+         * not begin to fade in until the condense
+         * header has fully collapsed.
+         *
+         * Additionally, the main content should not
+         * overflow out of the container until the
+         * condense header has fully collapsed. When
+         * using just the condense header the content
+         * should overflow out of the container.
+         */
+        if (condenseHeader !== null && scrollTop < fadeStart) {
+            baseEl.style.setProperty('--opacity-scale', '0');
+            scrollEl.style.setProperty('clip-path', `inset(${baseElHeight}px 0px 0px 0px)`);
+            return;
+        }
+        const distanceToStart = scrollTop - fadeStart;
+        const fadeDuration = 10;
+        const scale = clamp(0, distanceToStart / fadeDuration, 1);
+        writeTask(() => {
+            scrollEl.style.removeProperty('clip-path');
+            baseEl.style.setProperty('--opacity-scale', scale.toString());
+        });
+    });
+};

+ 29 - 0
src/node_modules/@ionic/core/dist/collection/components/header/test/a11y/header.e2e.js

@@ -0,0 +1,29 @@
+/*!
+ * (C) Ionic http://ionicframework.com - MIT License
+ */
+import AxeBuilder from "@axe-core/playwright";
+import { expect } from "@playwright/test";
+import { configs, test } from "../../../../utils/test/playwright/index";
+configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => {
+    test.describe(title('header: a11y'), () => {
+        test('should not have accessibility violations', async ({ page }) => {
+            await page.goto(`/src/components/header/test/a11y`, config);
+            const headers = page.locator('ion-header');
+            await expect(headers.first()).toHaveAttribute('role', 'banner');
+            await expect(headers.last()).toHaveAttribute('role', 'none');
+            const results = await new AxeBuilder({ page }).analyze();
+            expect(results.violations).toEqual([]);
+        });
+        test('should allow for custom role', async ({ page }) => {
+            /**
+             * Note: This example should not be used in production.
+             * This only serves to check that `role` can be customized.
+             */
+            await page.setContent(`
+        <ion-header role="heading"></ion-header>
+      `, config);
+            const header = page.locator('ion-header');
+            await expect(header).toHaveAttribute('role', 'heading');
+        });
+    });
+});

+ 147 - 0
src/node_modules/@ionic/core/dist/collection/components/header/test/basic/header.e2e.js

@@ -0,0 +1,147 @@
+/*!
+ * (C) Ionic http://ionicframework.com - MIT License
+ */
+import { expect } from "@playwright/test";
+import { configs, test } from "../../../../utils/test/playwright/index";
+configs().forEach(({ title, screenshot, config }) => {
+    test.describe(title('header: rendering'), () => {
+        test('should not have visual regressions with basic header', async ({ page }) => {
+            await page.setContent(`
+        <ion-header>
+          <ion-toolbar>
+            <ion-title>Header - Default</ion-title>
+          </ion-toolbar>
+        </ion-header>
+      `, config);
+            const header = page.locator('ion-header');
+            await expect(header).toHaveScreenshot(screenshot(`header-diff`));
+        });
+    });
+});
+configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
+    test.describe(title('header: feature rendering'), () => {
+        test('should not have visual regressions with no border', async ({ page }) => {
+            await page.setContent(`
+        <ion-header class="ion-no-border">
+          <ion-toolbar>
+            <ion-title>Header - No Border</ion-title>
+          </ion-toolbar>
+        </ion-header>
+      `, config);
+            const header = page.locator('ion-header');
+            await expect(header).toHaveScreenshot(screenshot(`header-no-border-diff`));
+        });
+    });
+});
+/**
+ * Translucent effect is only available in iOS mode.
+ */
+configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
+    test.describe(title('header: translucent'), () => {
+        test('should not have visual regressions with translucent header', async ({ page }) => {
+            await page.setContent(`
+        <ion-header translucent="true">
+          <div style="position: absolute; top: 0; left: 0; right: 0; bottom: 0">
+            <img style="transform: rotate(145deg) scale(1.5)" src="/src/components/header/test/img.jpg" />
+          </div>
+          <ion-toolbar>
+            <ion-title>Header - Translucent</ion-title>
+          </ion-toolbar>
+        </ion-header>
+      `, config);
+            const header = page.locator('ion-header');
+            await expect(header).toHaveScreenshot(screenshot(`header-translucent-diff`));
+        });
+        test('should not have visual regressions with translucent header with color', async ({ page }) => {
+            await page.setContent(`
+        <ion-header translucent="true">
+          <div style="position: absolute; top: 0; left: 0; right: 0; bottom: 0">
+            <img style="transform: rotate(145deg) scale(1.5)" src="/src/components/header/test/img.jpg" />
+          </div>
+          <ion-toolbar color="tertiary">
+            <ion-title>Header - Translucent</ion-title>
+          </ion-toolbar>
+        </ion-header>
+      `, config);
+            const header = page.locator('ion-header');
+            await expect(header).toHaveScreenshot(screenshot(`header-translucent-color-diff`));
+        });
+    });
+});
+/**
+ * This test only impacts MD applications
+ */
+configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
+    test.describe(title('header: translucent'), () => {
+        test('should not hide MD headers when using a descendant iOS header in an MD app', async ({ page }) => {
+            test.info().annotations.push({
+                type: 'issue',
+                description: 'https://github.com/ionic-team/ionic-framework/issues/28867',
+            });
+            await page.setContent(`
+        <ion-header id="main-header">
+          <ion-toolbar>
+            <ion-title>Header</ion-title>
+          </ion-toolbar>
+        </ion-header>
+        <ion-content>
+          <ion-header collapse="condense">
+            <ion-toolbar>
+              <ion-title size="large">Header</ion-title>
+            </ion-toolbar>
+          </ion-header>
+
+          <ion-header mode="ios">
+            <ion-toolbar>
+              <ion-title>Welcome</ion-title>
+            </ion-toolbar>
+          </ion-header>
+        </ion-content>
+      `, config);
+            const header = page.locator('ion-header#main-header');
+            /**
+             * The existence of the iOS header in an MD app should not cause the main MD header
+             * to be hidden. We do not have toHaveVisible because the behavior that hides
+             * the header under correct circumstances does it using opacity: 0.
+             * Playwright considers an element with opacity: 0 to still be visible
+             * because it has a non-zero bounding box.
+             */
+            await expect(header).toHaveScreenshot(screenshot('header-md-visibility-ios-descendant'));
+        });
+        test('should not hide MD headers when using a root iOS header in an MD app', async ({ page }) => {
+            test.info().annotations.push({
+                type: 'issue',
+                description: 'https://github.com/ionic-team/ionic-framework/issues/28867',
+            });
+            await page.setContent(`
+        <ion-header id="main-header" mode="ios">
+          <ion-toolbar>
+            <ion-title>Header</ion-title>
+          </ion-toolbar>
+        </ion-header>
+        <ion-content>
+          <ion-header collapse="condense">
+            <ion-toolbar>
+              <ion-title size="large">Header</ion-title>
+            </ion-toolbar>
+          </ion-header>
+
+          <ion-header>
+            <ion-toolbar>
+              <ion-title>Welcome</ion-title>
+            </ion-toolbar>
+          </ion-header>
+        </ion-content>
+      `, config);
+            const header = page.locator('ion-header#main-header');
+            /**
+             * The existence of the iOS header in an MD app should not cause the main MD header
+             * to be hidden. We do not have toHaveVisible because the behavior that hides
+             * the header under correct circumstances does it using opacity: 0.
+             * Playwright considers an element with opacity: 0 to still be visible
+             * because it has a non-zero bounding box.
+             */
+            await expect(header).toHaveScreenshot(screenshot('header-md-visibility-ios-main'));
+        });
+    });
+});

+ 33 - 0
src/node_modules/@ionic/core/dist/collection/components/header/test/condense/header.e2e.js

@@ -0,0 +1,33 @@
+/*!
+ * (C) Ionic http://ionicframework.com - MIT License
+ */
+import { expect } from "@playwright/test";
+import { configs, test } from "../../../../utils/test/playwright/index";
+configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
+    test.describe(title('header: condense'), () => {
+        test('should be hidden from screen readers when collapsed', async ({ page }) => {
+            await page.goto('/src/components/header/test/condense', config);
+            const largeTitleHeader = page.locator('#largeTitleHeader');
+            const smallTitleHeader = page.locator('#smallTitleHeader');
+            const content = page.locator('ion-content');
+            await expect(smallTitleHeader).toHaveAttribute('aria-hidden', 'true');
+            await expect(largeTitleHeader).toHaveScreenshot(screenshot(`header-condense-large-title-initial-diff`));
+            await content.evaluate(async (el) => {
+                await el.scrollToBottom();
+            });
+            await page.locator('#largeTitleHeader.header-collapse-condense-inactive').waitFor();
+            await expect(smallTitleHeader).toHaveScreenshot(screenshot(`header-condense-large-title-collapsed-diff`));
+            /**
+             * Playwright can't do .not.toHaveAttribute() because a value is expected,
+             * and toHaveAttribute can't accept a value of type null.
+             */
+            const ariaHidden = await smallTitleHeader.getAttribute('aria-hidden');
+            expect(ariaHidden).toBeNull();
+            await content.evaluate(async (el) => {
+                await el.scrollToTop();
+            });
+            await page.locator('#smallTitleHeader.header-collapse-condense-inactive').waitFor();
+            await expect(smallTitleHeader).toHaveAttribute('aria-hidden', 'true');
+        });
+    });
+});

+ 21 - 0
src/node_modules/@ionic/core/dist/collection/components/header/test/fade/header.e2e.js

@@ -0,0 +1,21 @@
+/*!
+ * (C) Ionic http://ionicframework.com - MIT License
+ */
+import { expect } from "@playwright/test";
+import { configs, test } from "../../../../utils/test/playwright/index";
+/**
+ * Translucent effect is only available in iOS mode.
+ */
+configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
+    test.describe(title('header: fade'), () => {
+        test('should not have visual regressions with fade header', async ({ page }) => {
+            await page.goto('/src/components/header/test/fade', config);
+            const header = page.locator('ion-header');
+            await expect(header).toHaveScreenshot(screenshot(`header-fade-not-blurred-diff`));
+            const content = page.locator('ion-content');
+            await content.evaluate((el) => el.scrollToBottom(0));
+            await page.waitForChanges();
+            await expect(header).toHaveScreenshot(screenshot(`header-fade-blurred-diff`));
+        });
+    });
+});

+ 26 - 0
src/node_modules/@ionic/core/dist/collection/components/header/test/scroll-target/header.e2e.js

@@ -0,0 +1,26 @@
+/*!
+ * (C) Ionic http://ionicframework.com - MIT License
+ */
+import { expect } from "@playwright/test";
+import { configs, test } from "../../../../utils/test/playwright/index";
+/**
+ * Translucent effect is only available in iOS mode.
+ */
+configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
+    test.describe(title('header: scroll-target'), () => {
+        /**
+         * This test suite verifies that the fade effect for iOS is working correctly
+         * when the `ion-header` is using a custom scroll target with the `.ion-content-scroll-host`
+         * selector.
+         */
+        test('should not have visual regressions with custom scroll target header', async ({ page }) => {
+            await page.goto('/src/components/header/test/scroll-target', config);
+            const header = page.locator('ion-header');
+            await expect(header).toHaveScreenshot(screenshot(`header-scroll-target-not-blurred-diff`));
+            const scrollTarget = page.locator('#scroll-target');
+            await scrollTarget.evaluate((el) => (el.scrollTop = el.scrollHeight));
+            await page.waitForChanges();
+            await expect(header).toHaveScreenshot(screenshot(`header-scroll-target-blurred-diff`));
+        });
+    });
+});

+ 16 - 0
src/node_modules/@ionic/core/dist/collection/components/icon/test/basic/icon.e2e.js

@@ -0,0 +1,16 @@
+/*!
+ * (C) Ionic http://ionicframework.com - MIT License
+ */
+import { expect } from "@playwright/test";
+import { configs, test } from "../../../../utils/test/playwright/index";
+configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
+    test.describe(title('icon: basic'), () => {
+        test('should render icon when passed', async ({ page }) => {
+            await page.setContent(`
+        <ion-icon name="star"></ion-icon>
+      `, config);
+            const icon = page.locator('ion-icon');
+            await expect(icon).toHaveScreenshot(screenshot(`icon`));
+        });
+    });
+});

+ 5 - 0
src/node_modules/@ionic/core/dist/collection/components/icon/test/dir/heart-broken.svg

@@ -0,0 +1,5 @@
+<!-- Generated by IcoMoon.io -->
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
+<title>heart-broken</title>
+<path d="M23.6 2c4.637 0 8.4 3.764 8.4 8.401 0 9.132-9.87 11.964-15.999 21.232-6.485-9.326-16.001-11.799-16.001-21.232 0-4.637 3.763-8.401 8.4-8.401 1.886 0 3.625 0.86 5.025 2.12l-2.425 3.88 7 4-4 10 11-12-7-4 1.934-2.901c1.107-0.68 2.35-1.099 3.665-1.099z"></path>
+</svg>

+ 23 - 0
src/node_modules/@ionic/core/dist/collection/components/icon/test/dir/icon.e2e.js

@@ -0,0 +1,23 @@
+/*!
+ * (C) Ionic http://ionicframework.com - MIT License
+ */
+import { expect } from "@playwright/test";
+import { configs, test } from "../../../../utils/test/playwright/index";
+configs({ modes: ['md'] }).forEach(({ title, screenshot, config }) => {
+    test.describe(title('icon: rtl'), () => {
+        test('should flip icon when rtl is active', async ({ page }) => {
+            await page.setContent(`
+        <ion-icon name="cut" flip-rtl="true"></ion-icon>
+      `, config);
+            const icon = page.locator('ion-icon');
+            await expect(icon).toHaveScreenshot(screenshot(`icon-flip`));
+        });
+        test('should not flip icon when rtl is active', async ({ page }) => {
+            await page.setContent(`
+        <ion-icon name="cut" flip-rtl="false"></ion-icon>
+      `, config);
+            const icon = page.locator('ion-icon');
+            await expect(icon).toHaveScreenshot(screenshot(`icon-no-flip`));
+        });
+    });
+});

+ 12 - 0
src/node_modules/@ionic/core/dist/collection/components/img/img.css

@@ -0,0 +1,12 @@
+:host {
+  display: block;
+  object-fit: contain;
+}
+
+img {
+  display: block;
+  width: 100%;
+  height: 100%;
+  object-fit: inherit;
+  object-position: inherit;
+}

+ 204 - 0
src/node_modules/@ionic/core/dist/collection/components/img/img.js

@@ -0,0 +1,204 @@
+/*!
+ * (C) Ionic http://ionicframework.com - MIT License
+ */
+import { Host, h } from "@stencil/core";
+import { inheritAttributes } from "../../utils/helpers";
+import { getIonMode } from "../../global/ionic-global";
+/**
+ * @part image - The inner `img` element.
+ */
+export class Img {
+    constructor() {
+        this.inheritedAttributes = {};
+        this.onLoad = () => {
+            this.ionImgDidLoad.emit();
+        };
+        this.onError = () => {
+            this.ionError.emit();
+        };
+        this.loadSrc = undefined;
+        this.loadError = undefined;
+        this.alt = undefined;
+        this.src = undefined;
+    }
+    srcChanged() {
+        this.addIO();
+    }
+    componentWillLoad() {
+        this.inheritedAttributes = inheritAttributes(this.el, ['draggable']);
+    }
+    componentDidLoad() {
+        this.addIO();
+    }
+    addIO() {
+        if (this.src === undefined) {
+            return;
+        }
+        if (typeof window !== 'undefined' &&
+            'IntersectionObserver' in window &&
+            'IntersectionObserverEntry' in window &&
+            'isIntersecting' in window.IntersectionObserverEntry.prototype) {
+            this.removeIO();
+            this.io = new IntersectionObserver((data) => {
+                /**
+                 * On slower devices, it is possible for an intersection observer entry to contain multiple
+                 * objects in the array. This happens when quickly scrolling an image into view and then out of
+                 * view. In this case, the last object represents the current state of the component.
+                 */
+                if (data[data.length - 1].isIntersecting) {
+                    this.load();
+                    this.removeIO();
+                }
+            });
+            this.io.observe(this.el);
+        }
+        else {
+            // fall back to setTimeout for Safari and IE
+            setTimeout(() => this.load(), 200);
+        }
+    }
+    load() {
+        this.loadError = this.onError;
+        this.loadSrc = this.src;
+        this.ionImgWillLoad.emit();
+    }
+    removeIO() {
+        if (this.io) {
+            this.io.disconnect();
+            this.io = undefined;
+        }
+    }
+    render() {
+        const { loadSrc, alt, onLoad, loadError, inheritedAttributes } = this;
+        const { draggable } = inheritedAttributes;
+        return (h(Host, { key: '14d24d65ec8e5522192ca58035264971b1ab883b', class: getIonMode(this) }, h("img", { key: '345ba155a5fdce5e66c397a599b7333d37d9cb1d', decoding: "async", src: loadSrc, alt: alt, onLoad: onLoad, onError: loadError, part: "image", draggable: isDraggable(draggable) })));
+    }
+    static get is() { return "ion-img"; }
+    static get encapsulation() { return "shadow"; }
+    static get originalStyleUrls() {
+        return {
+            "$": ["img.scss"]
+        };
+    }
+    static get styleUrls() {
+        return {
+            "$": ["img.css"]
+        };
+    }
+    static get properties() {
+        return {
+            "alt": {
+                "type": "string",
+                "mutable": false,
+                "complexType": {
+                    "original": "string",
+                    "resolved": "string | undefined",
+                    "references": {}
+                },
+                "required": false,
+                "optional": true,
+                "docs": {
+                    "tags": [],
+                    "text": "This attribute defines the alternative text describing the image.\nUsers will see this text displayed if the image URL is wrong,\nthe image is not in one of the supported formats, or if the image is not yet downloaded."
+                },
+                "attribute": "alt",
+                "reflect": false
+            },
+            "src": {
+                "type": "string",
+                "mutable": false,
+                "complexType": {
+                    "original": "string",
+                    "resolved": "string | undefined",
+                    "references": {}
+                },
+                "required": false,
+                "optional": true,
+                "docs": {
+                    "tags": [],
+                    "text": "The image URL. This attribute is mandatory for the `<img>` element."
+                },
+                "attribute": "src",
+                "reflect": false
+            }
+        };
+    }
+    static get states() {
+        return {
+            "loadSrc": {},
+            "loadError": {}
+        };
+    }
+    static get events() {
+        return [{
+                "method": "ionImgWillLoad",
+                "name": "ionImgWillLoad",
+                "bubbles": true,
+                "cancelable": true,
+                "composed": true,
+                "docs": {
+                    "tags": [],
+                    "text": "Emitted when the img src has been set"
+                },
+                "complexType": {
+                    "original": "void",
+                    "resolved": "void",
+                    "references": {}
+                }
+            }, {
+                "method": "ionImgDidLoad",
+                "name": "ionImgDidLoad",
+                "bubbles": true,
+                "cancelable": true,
+                "composed": true,
+                "docs": {
+                    "tags": [],
+                    "text": "Emitted when the image has finished loading"
+                },
+                "complexType": {
+                    "original": "void",
+                    "resolved": "void",
+                    "references": {}
+                }
+            }, {
+                "method": "ionError",
+                "name": "ionError",
+                "bubbles": true,
+                "cancelable": true,
+                "composed": true,
+                "docs": {
+                    "tags": [],
+                    "text": "Emitted when the img fails to load"
+                },
+                "complexType": {
+                    "original": "void",
+                    "resolved": "void",
+                    "references": {}
+                }
+            }];
+    }
+    static get elementRef() { return "el"; }
+    static get watchers() {
+        return [{
+                "propName": "src",
+                "methodName": "srcChanged"
+            }];
+    }
+}
+/**
+ * Enumerated strings must be set as booleans
+ * as Stencil will not render 'false' in the DOM.
+ * The need to explicitly render draggable="true"
+ * as only certain elements are draggable by default.
+ * https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/draggable.
+ */
+const isDraggable = (draggable) => {
+    switch (draggable) {
+        case 'true':
+            return true;
+        case 'false':
+            return false;
+        default:
+            return undefined;
+    }
+};

+ 73 - 0
src/node_modules/@ionic/core/dist/collection/components/img/test/basic/img.e2e.js

@@ -0,0 +1,73 @@
+/*!
+ * (C) Ionic http://ionicframework.com - MIT License
+ */
+import { expect } from "@playwright/test";
+import { configs, test } from "../../../../utils/test/playwright/index";
+configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
+    test.describe(title('img: basic'), () => {
+        // TODO FW-3596
+        test.describe.skip('image successfully loads', () => {
+            let ionImgWillLoad;
+            let ionImgDidLoad;
+            test.beforeEach(async ({ page }) => {
+                await page.route('**/*', (route) => {
+                    if (route.request().resourceType() === 'image') {
+                        return route.fulfill({
+                            status: 200,
+                            contentType: 'image/png',
+                            body: Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIwAAAABJRU5ErkJggg==', 'base64'),
+                        });
+                    }
+                    return route.continue();
+                });
+                /**
+                 * We render the img intentionally without providing a source,
+                 * to allow the event spies to be set-up before the events
+                 * can be emitted.
+                 *
+                 * Later we will assign an image source to load.
+                 */
+                await page.setContent('<ion-img></ion-img>', config);
+                ionImgDidLoad = await page.spyOnEvent('ionImgDidLoad');
+                ionImgWillLoad = await page.spyOnEvent('ionImgWillLoad');
+                const ionImg = page.locator('ion-img');
+                await ionImg.evaluate((el) => {
+                    el.src = 'https://via.placeholder.com/150';
+                    return el;
+                });
+            });
+            test('should emit ionImgWillLoad', async () => {
+                await ionImgWillLoad.next();
+                expect(ionImgWillLoad).toHaveReceivedEventTimes(1);
+            });
+            test('should emit ionImgDidLoad', async () => {
+                await ionImgDidLoad.next();
+                expect(ionImgWillLoad).toHaveReceivedEventTimes(1);
+            });
+        });
+        test.describe('image fails to load', () => {
+            let ionError;
+            test.beforeEach(async ({ page }) => {
+                await page.route('**/*', (route) => route.request().resourceType() === 'image' ? route.abort() : route.continue());
+                /**
+                 * We render the img intentionally without providing a source,
+                 * to allow the event spies to be set-up before the events
+                 * can be emitted.
+                 *
+                 * Later we will assign an image source to load.
+                 */
+                await page.setContent('<ion-img></ion-img>', config);
+                ionError = await page.spyOnEvent('ionError');
+                const ionImg = page.locator('ion-img');
+                await ionImg.evaluate((el) => {
+                    el.src = 'https://via.placeholder.com/150';
+                    return el;
+                });
+            });
+            test('should emit ionError', async () => {
+                await ionError.next();
+                expect(ionError).toHaveReceivedEventTimes(1);
+            });
+        });
+    });
+});

+ 21 - 0
src/node_modules/@ionic/core/dist/collection/components/img/test/draggable/img.e2e.js

@@ -0,0 +1,21 @@
+/*!
+ * (C) Ionic http://ionicframework.com - MIT License
+ */
+import { expect } from "@playwright/test";
+import { configs, test } from "../../../../utils/test/playwright/index";
+/**
+ * This behavior does not vary across modes/directions.
+ */
+configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => {
+    test.describe(title('img: draggable'), () => {
+        test('should correctly set draggable attribute on inner img element', async ({ page }) => {
+            await page.goto('/src/components/img/test/draggable', config);
+            const imgDraggableTrue = page.locator('#img-draggable-true img');
+            await expect(imgDraggableTrue).toHaveAttribute('draggable', 'true');
+            const imgDraggableFalse = page.locator('#img-draggable-false img');
+            await expect(imgDraggableFalse).toHaveAttribute('draggable', 'false');
+            const imgDraggableUnset = page.locator('#img-draggable-unset img');
+            expect(await imgDraggableUnset.getAttribute('draggable')).toBeNull();
+        });
+    });
+});

+ 156 - 0
src/node_modules/@ionic/core/dist/collection/components/infinite-scroll-content/infinite-scroll-content.ios.css

@@ -0,0 +1,156 @@
+/**
+ * Convert a font size to a dynamic font size.
+ * Fonts that participate in Dynamic Type should use
+ * dynamic font sizes.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param unit (optional) - The unit to convert to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * Convert a font size to a dynamic font size but impose
+ * a maximum font size.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param maxScale - The maximum scale of the font (i.e. 2.5 for a maximum 250% scale).
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * Convert a font size to a dynamic font size but impose
+ * a minimum font size.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param minScale - The minimum scale of the font (i.e. 0.8 for a minimum 80% scale).
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * Convert a font size to a dynamic font size but impose
+ * maximum and minimum font sizes.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param minScale - The minimum scale of the font (i.e. 0.8 for a minimum 80% scale).
+ * @param maxScale - The maximum scale of the font (i.e. 2.5 for a maximum 250% scale).
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * A heuristic that applies CSS to tablet
+ * viewports.
+ *
+ * Usage:
+ * @include tablet-viewport() {
+ *   :host {
+ *     background-color: green;
+ *   }
+ * }
+ */
+/**
+ * A heuristic that applies CSS to mobile
+ * viewports (i.e. phones, not tablets).
+ *
+ * Usage:
+ * @include mobile-viewport() {
+ *   :host {
+ *     background-color: blue;
+ *   }
+ * }
+ */
+ion-infinite-scroll-content {
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  min-height: 84px;
+  text-align: center;
+  user-select: none;
+}
+
+.infinite-loading {
+  margin-left: 0;
+  margin-right: 0;
+  margin-top: 0;
+  margin-bottom: 32px;
+  display: none;
+  width: 100%;
+}
+
+.infinite-loading-text {
+  -webkit-margin-start: 32px;
+  margin-inline-start: 32px;
+  -webkit-margin-end: 32px;
+  margin-inline-end: 32px;
+  margin-top: 4px;
+  margin-bottom: 0;
+}
+
+.infinite-scroll-loading ion-infinite-scroll-content > .infinite-loading {
+  display: block;
+}
+
+/**
+ * Convert a font size to a dynamic font size.
+ * Fonts that participate in Dynamic Type should use
+ * dynamic font sizes.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param unit (optional) - The unit to convert to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * Convert a font size to a dynamic font size but impose
+ * a maximum font size.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param maxScale - The maximum scale of the font (i.e. 2.5 for a maximum 250% scale).
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * Convert a font size to a dynamic font size but impose
+ * a minimum font size.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param minScale - The minimum scale of the font (i.e. 0.8 for a minimum 80% scale).
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * Convert a font size to a dynamic font size but impose
+ * maximum and minimum font sizes.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param minScale - The minimum scale of the font (i.e. 0.8 for a minimum 80% scale).
+ * @param maxScale - The maximum scale of the font (i.e. 2.5 for a maximum 250% scale).
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * A heuristic that applies CSS to tablet
+ * viewports.
+ *
+ * Usage:
+ * @include tablet-viewport() {
+ *   :host {
+ *     background-color: green;
+ *   }
+ * }
+ */
+/**
+ * A heuristic that applies CSS to mobile
+ * viewports (i.e. phones, not tablets).
+ *
+ * Usage:
+ * @include mobile-viewport() {
+ *   :host {
+ *     background-color: blue;
+ *   }
+ * }
+ */
+.infinite-scroll-content-ios .infinite-loading-text {
+  color: var(--ion-color-step-600, var(--ion-text-color-step-400, #666666));
+}
+
+.infinite-scroll-content-ios .infinite-loading-spinner .spinner-lines-ios line,
+.infinite-scroll-content-ios .infinite-loading-spinner .spinner-lines-small-ios line,
+.infinite-scroll-content-ios .infinite-loading-spinner .spinner-crescent circle {
+  stroke: var(--ion-color-step-600, var(--ion-text-color-step-400, #666666));
+}
+
+.infinite-scroll-content-ios .infinite-loading-spinner .spinner-bubbles circle,
+.infinite-scroll-content-ios .infinite-loading-spinner .spinner-circles circle,
+.infinite-scroll-content-ios .infinite-loading-spinner .spinner-dots circle {
+  fill: var(--ion-color-step-600, var(--ion-text-color-step-400, #666666));
+}

+ 99 - 0
src/node_modules/@ionic/core/dist/collection/components/infinite-scroll-content/infinite-scroll-content.js

@@ -0,0 +1,99 @@
+/*!
+ * (C) Ionic http://ionicframework.com - MIT License
+ */
+import { Host, h } from "@stencil/core";
+import { ENABLE_HTML_CONTENT_DEFAULT } from "../../utils/config";
+import { sanitizeDOMString } from "../../utils/sanitization/index";
+import { config } from "../../global/config";
+import { getIonMode } from "../../global/ionic-global";
+export class InfiniteScrollContent {
+    constructor() {
+        this.customHTMLEnabled = config.get('innerHTMLTemplatesEnabled', ENABLE_HTML_CONTENT_DEFAULT);
+        this.loadingSpinner = undefined;
+        this.loadingText = undefined;
+    }
+    componentDidLoad() {
+        if (this.loadingSpinner === undefined) {
+            const mode = getIonMode(this);
+            this.loadingSpinner = config.get('infiniteLoadingSpinner', config.get('spinner', mode === 'ios' ? 'lines' : 'crescent'));
+        }
+    }
+    renderLoadingText() {
+        const { customHTMLEnabled, loadingText } = this;
+        if (customHTMLEnabled) {
+            return h("div", { class: "infinite-loading-text", innerHTML: sanitizeDOMString(loadingText) });
+        }
+        return h("div", { class: "infinite-loading-text" }, this.loadingText);
+    }
+    render() {
+        const mode = getIonMode(this);
+        return (h(Host, { key: '060278bf9cb0321e182352f9613be4ebbb028259', class: {
+                [mode]: true,
+                // Used internally for styling
+                [`infinite-scroll-content-${mode}`]: true,
+            } }, h("div", { key: '07d3cada920145f979ad315bd187fb878e0c3da3', class: "infinite-loading" }, this.loadingSpinner && (h("div", { key: '6254f175d7543d09f3dd47cd0589a2809182cd8c', class: "infinite-loading-spinner" }, h("ion-spinner", { key: 'a6a816d1c65b60b786333b209b63492aa716a283', name: this.loadingSpinner }))), this.loadingText !== undefined && this.renderLoadingText())));
+    }
+    static get is() { return "ion-infinite-scroll-content"; }
+    static get originalStyleUrls() {
+        return {
+            "ios": ["infinite-scroll-content.ios.scss"],
+            "md": ["infinite-scroll-content.md.scss"]
+        };
+    }
+    static get styleUrls() {
+        return {
+            "ios": ["infinite-scroll-content.ios.css"],
+            "md": ["infinite-scroll-content.md.css"]
+        };
+    }
+    static get properties() {
+        return {
+            "loadingSpinner": {
+                "type": "string",
+                "mutable": true,
+                "complexType": {
+                    "original": "SpinnerTypes | null",
+                    "resolved": "\"bubbles\" | \"circles\" | \"circular\" | \"crescent\" | \"dots\" | \"lines\" | \"lines-sharp\" | \"lines-sharp-small\" | \"lines-small\" | null | undefined",
+                    "references": {
+                        "SpinnerTypes": {
+                            "location": "import",
+                            "path": "../spinner/spinner-configs",
+                            "id": "src/components/spinner/spinner-configs.ts::SpinnerTypes"
+                        }
+                    }
+                },
+                "required": false,
+                "optional": true,
+                "docs": {
+                    "tags": [],
+                    "text": "An animated SVG spinner that shows while loading."
+                },
+                "attribute": "loading-spinner",
+                "reflect": false
+            },
+            "loadingText": {
+                "type": "string",
+                "mutable": false,
+                "complexType": {
+                    "original": "string | IonicSafeString",
+                    "resolved": "IonicSafeString | string | undefined",
+                    "references": {
+                        "IonicSafeString": {
+                            "location": "import",
+                            "path": "../../utils/sanitization",
+                            "id": "src/utils/sanitization/index.ts::IonicSafeString"
+                        }
+                    }
+                },
+                "required": false,
+                "optional": true,
+                "docs": {
+                    "tags": [],
+                    "text": "Optional text to display while loading.\n`loadingText` can accept either plaintext or HTML as a string.\nTo display characters normally reserved for HTML, they\nmust be escaped. For example `<Ionic>` would become\n`&lt;Ionic&gt;`\n\nFor more information: [Security Documentation](https://ionicframework.com/docs/faq/security)\n\nThis property accepts custom HTML as a string.\nContent is parsed as plaintext by default.\n`innerHTMLTemplatesEnabled` must be set to `true` in the Ionic config\nbefore custom HTML can be used."
+                },
+                "attribute": "loading-text",
+                "reflect": false
+            }
+        };
+    }
+}

+ 156 - 0
src/node_modules/@ionic/core/dist/collection/components/infinite-scroll-content/infinite-scroll-content.md.css

@@ -0,0 +1,156 @@
+/**
+ * Convert a font size to a dynamic font size.
+ * Fonts that participate in Dynamic Type should use
+ * dynamic font sizes.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param unit (optional) - The unit to convert to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * Convert a font size to a dynamic font size but impose
+ * a maximum font size.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param maxScale - The maximum scale of the font (i.e. 2.5 for a maximum 250% scale).
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * Convert a font size to a dynamic font size but impose
+ * a minimum font size.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param minScale - The minimum scale of the font (i.e. 0.8 for a minimum 80% scale).
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * Convert a font size to a dynamic font size but impose
+ * maximum and minimum font sizes.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param minScale - The minimum scale of the font (i.e. 0.8 for a minimum 80% scale).
+ * @param maxScale - The maximum scale of the font (i.e. 2.5 for a maximum 250% scale).
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * A heuristic that applies CSS to tablet
+ * viewports.
+ *
+ * Usage:
+ * @include tablet-viewport() {
+ *   :host {
+ *     background-color: green;
+ *   }
+ * }
+ */
+/**
+ * A heuristic that applies CSS to mobile
+ * viewports (i.e. phones, not tablets).
+ *
+ * Usage:
+ * @include mobile-viewport() {
+ *   :host {
+ *     background-color: blue;
+ *   }
+ * }
+ */
+ion-infinite-scroll-content {
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  min-height: 84px;
+  text-align: center;
+  user-select: none;
+}
+
+.infinite-loading {
+  margin-left: 0;
+  margin-right: 0;
+  margin-top: 0;
+  margin-bottom: 32px;
+  display: none;
+  width: 100%;
+}
+
+.infinite-loading-text {
+  -webkit-margin-start: 32px;
+  margin-inline-start: 32px;
+  -webkit-margin-end: 32px;
+  margin-inline-end: 32px;
+  margin-top: 4px;
+  margin-bottom: 0;
+}
+
+.infinite-scroll-loading ion-infinite-scroll-content > .infinite-loading {
+  display: block;
+}
+
+/**
+ * Convert a font size to a dynamic font size.
+ * Fonts that participate in Dynamic Type should use
+ * dynamic font sizes.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param unit (optional) - The unit to convert to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * Convert a font size to a dynamic font size but impose
+ * a maximum font size.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param maxScale - The maximum scale of the font (i.e. 2.5 for a maximum 250% scale).
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * Convert a font size to a dynamic font size but impose
+ * a minimum font size.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param minScale - The minimum scale of the font (i.e. 0.8 for a minimum 80% scale).
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * Convert a font size to a dynamic font size but impose
+ * maximum and minimum font sizes.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param minScale - The minimum scale of the font (i.e. 0.8 for a minimum 80% scale).
+ * @param maxScale - The maximum scale of the font (i.e. 2.5 for a maximum 250% scale).
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * A heuristic that applies CSS to tablet
+ * viewports.
+ *
+ * Usage:
+ * @include tablet-viewport() {
+ *   :host {
+ *     background-color: green;
+ *   }
+ * }
+ */
+/**
+ * A heuristic that applies CSS to mobile
+ * viewports (i.e. phones, not tablets).
+ *
+ * Usage:
+ * @include mobile-viewport() {
+ *   :host {
+ *     background-color: blue;
+ *   }
+ * }
+ */
+.infinite-scroll-content-md .infinite-loading-text {
+  color: var(--ion-color-step-600, var(--ion-text-color-step-400, #666666));
+}
+
+.infinite-scroll-content-md .infinite-loading-spinner .spinner-lines-md line,
+.infinite-scroll-content-md .infinite-loading-spinner .spinner-lines-small-md line,
+.infinite-scroll-content-md .infinite-loading-spinner .spinner-crescent circle {
+  stroke: var(--ion-color-step-600, var(--ion-text-color-step-400, #666666));
+}
+
+.infinite-scroll-content-md .infinite-loading-spinner .spinner-bubbles circle,
+.infinite-scroll-content-md .infinite-loading-spinner .spinner-circles circle,
+.infinite-scroll-content-md .infinite-loading-spinner .spinner-dots circle {
+  fill: var(--ion-color-step-600, var(--ion-text-color-step-400, #666666));
+}

+ 37 - 0
src/node_modules/@ionic/core/dist/collection/components/infinite-scroll-content/test/infinite-scroll-content.spec.js

@@ -0,0 +1,37 @@
+/*!
+ * (C) Ionic http://ionicframework.com - MIT License
+ */
+import { newSpecPage } from "@stencil/core/testing";
+import { config } from "../../../global/config";
+import { InfiniteScrollContent } from "../infinite-scroll-content";
+describe('infinite-scroll-content: custom html', () => {
+    it('should not allow for custom html by default', async () => {
+        const page = await newSpecPage({
+            components: [InfiniteScrollContent],
+            html: `<ion-infinite-scroll-content loading-text="<button class='custom-html'>Custom Text</button>"></ion-infinite-scroll-content>`,
+        });
+        const content = page.body.querySelector('.infinite-loading-text');
+        expect(content.textContent).toContain('Custom Text');
+        expect(content.querySelector('button.custom-html')).toBe(null);
+    });
+    it('should allow for custom html', async () => {
+        config.reset({ innerHTMLTemplatesEnabled: true });
+        const page = await newSpecPage({
+            components: [InfiniteScrollContent],
+            html: `<ion-infinite-scroll-content loading-text="<button class='custom-html'>Custom Text</button>"></ion-infinite-scroll-content>`,
+        });
+        const content = page.body.querySelector('.infinite-loading-text');
+        expect(content.textContent).toContain('Custom Text');
+        expect(content.querySelector('button.custom-html')).not.toBe(null);
+    });
+    it('should not allow for custom html', async () => {
+        config.reset({ innerHTMLTemplatesEnabled: false });
+        const page = await newSpecPage({
+            components: [InfiniteScrollContent],
+            html: `<ion-infinite-scroll-content loading-text="<button class='custom-html'>Custom Text2</button>"></ion-infinite-scroll-content>`,
+        });
+        const content = page.body.querySelector('.infinite-loading-text');
+        expect(content.textContent).toContain('Custom Text');
+        expect(content.querySelector('button.custom-html')).toBe(null);
+    });
+});

+ 1 - 0
src/node_modules/@ionic/core/dist/collection/components/infinite-scroll/infinite-scroll-interface.js

@@ -0,0 +1 @@
+export {};

+ 8 - 0
src/node_modules/@ionic/core/dist/collection/components/infinite-scroll/infinite-scroll.css

@@ -0,0 +1,8 @@
+ion-infinite-scroll {
+  display: none;
+  width: 100%;
+}
+
+.infinite-scroll-enabled {
+  display: block;
+}

+ 299 - 0
src/node_modules/@ionic/core/dist/collection/components/infinite-scroll/infinite-scroll.js

@@ -0,0 +1,299 @@
+/*!
+ * (C) Ionic http://ionicframework.com - MIT License
+ */
+import { Host, h, readTask, writeTask } from "@stencil/core";
+import { findClosestIonContent, getScrollElement, printIonContentErrorMsg } from "../../utils/content/index";
+import { getIonMode } from "../../global/ionic-global";
+export class InfiniteScroll {
+    constructor() {
+        this.thrPx = 0;
+        this.thrPc = 0;
+        /**
+         * didFire exists so that ionInfinite
+         * does not fire multiple times if
+         * users continue to scroll after
+         * scrolling into the infinite
+         * scroll threshold.
+         */
+        this.didFire = false;
+        this.isBusy = false;
+        this.onScroll = () => {
+            const scrollEl = this.scrollEl;
+            if (!scrollEl || !this.canStart()) {
+                return 1;
+            }
+            const infiniteHeight = this.el.offsetHeight;
+            if (infiniteHeight === 0) {
+                // if there is no height of this element then do nothing
+                return 2;
+            }
+            const scrollTop = scrollEl.scrollTop;
+            const scrollHeight = scrollEl.scrollHeight;
+            const height = scrollEl.offsetHeight;
+            const threshold = this.thrPc !== 0 ? height * this.thrPc : this.thrPx;
+            const distanceFromInfinite = this.position === 'bottom'
+                ? scrollHeight - infiniteHeight - scrollTop - threshold - height
+                : scrollTop - infiniteHeight - threshold;
+            if (distanceFromInfinite < 0) {
+                if (!this.didFire) {
+                    this.isLoading = true;
+                    this.didFire = true;
+                    this.ionInfinite.emit();
+                    return 3;
+                }
+            }
+            return 4;
+        };
+        this.isLoading = false;
+        this.threshold = '15%';
+        this.disabled = false;
+        this.position = 'bottom';
+    }
+    thresholdChanged() {
+        const val = this.threshold;
+        if (val.lastIndexOf('%') > -1) {
+            this.thrPx = 0;
+            this.thrPc = parseFloat(val) / 100;
+        }
+        else {
+            this.thrPx = parseFloat(val);
+            this.thrPc = 0;
+        }
+    }
+    disabledChanged() {
+        const disabled = this.disabled;
+        if (disabled) {
+            this.isLoading = false;
+            this.isBusy = false;
+        }
+        this.enableScrollEvents(!disabled);
+    }
+    async connectedCallback() {
+        const contentEl = findClosestIonContent(this.el);
+        if (!contentEl) {
+            printIonContentErrorMsg(this.el);
+            return;
+        }
+        this.scrollEl = await getScrollElement(contentEl);
+        this.thresholdChanged();
+        this.disabledChanged();
+        if (this.position === 'top') {
+            writeTask(() => {
+                if (this.scrollEl) {
+                    this.scrollEl.scrollTop = this.scrollEl.scrollHeight - this.scrollEl.clientHeight;
+                }
+            });
+        }
+    }
+    disconnectedCallback() {
+        this.enableScrollEvents(false);
+        this.scrollEl = undefined;
+    }
+    /**
+     * Call `complete()` within the `ionInfinite` output event handler when
+     * your async operation has completed. For example, the `loading`
+     * state is while the app is performing an asynchronous operation,
+     * such as receiving more data from an AJAX request to add more items
+     * to a data list. Once the data has been received and UI updated, you
+     * then call this method to signify that the loading has completed.
+     * This method will change the infinite scroll's state from `loading`
+     * to `enabled`.
+     */
+    async complete() {
+        const scrollEl = this.scrollEl;
+        if (!this.isLoading || !scrollEl) {
+            return;
+        }
+        this.isLoading = false;
+        if (this.position === 'top') {
+            /**
+             * New content is being added at the top, but the scrollTop position stays the same,
+             * which causes a scroll jump visually. This algorithm makes sure to prevent this.
+             * (Frame 1)
+             *    - complete() is called, but the UI hasn't had time to update yet.
+             *    - Save the current content dimensions.
+             *    - Wait for the next frame using _dom.read, so the UI will be updated.
+             * (Frame 2)
+             *    - Read the new content dimensions.
+             *    - Calculate the height difference and the new scroll position.
+             *    - Delay the scroll position change until other possible dom reads are done using _dom.write to be performant.
+             * (Still frame 2, if I'm correct)
+             *    - Change the scroll position (= visually maintain the scroll position).
+             *    - Change the state to re-enable the InfiniteScroll.
+             *    - This should be after changing the scroll position, or it could
+             *    cause the InfiniteScroll to be triggered again immediately.
+             * (Frame 3)
+             *    Done.
+             */
+            this.isBusy = true;
+            // ******** DOM READ ****************
+            // Save the current content dimensions before the UI updates
+            const prev = scrollEl.scrollHeight - scrollEl.scrollTop;
+            // ******** DOM READ ****************
+            requestAnimationFrame(() => {
+                readTask(() => {
+                    // UI has updated, save the new content dimensions
+                    const scrollHeight = scrollEl.scrollHeight;
+                    // New content was added on top, so the scroll position should be changed immediately to prevent it from jumping around
+                    const newScrollTop = scrollHeight - prev;
+                    // ******** DOM WRITE ****************
+                    requestAnimationFrame(() => {
+                        writeTask(() => {
+                            scrollEl.scrollTop = newScrollTop;
+                            this.isBusy = false;
+                            this.didFire = false;
+                        });
+                    });
+                });
+            });
+        }
+        else {
+            this.didFire = false;
+        }
+    }
+    canStart() {
+        return !this.disabled && !this.isBusy && !!this.scrollEl && !this.isLoading;
+    }
+    enableScrollEvents(shouldListen) {
+        if (this.scrollEl) {
+            if (shouldListen) {
+                this.scrollEl.addEventListener('scroll', this.onScroll);
+            }
+            else {
+                this.scrollEl.removeEventListener('scroll', this.onScroll);
+            }
+        }
+    }
+    render() {
+        const mode = getIonMode(this);
+        const disabled = this.disabled;
+        return (h(Host, { key: '1444429a86950c449953cbf578436cc8cabf40ec', class: {
+                [mode]: true,
+                'infinite-scroll-loading': this.isLoading,
+                'infinite-scroll-enabled': !disabled,
+            } }));
+    }
+    static get is() { return "ion-infinite-scroll"; }
+    static get originalStyleUrls() {
+        return {
+            "$": ["infinite-scroll.scss"]
+        };
+    }
+    static get styleUrls() {
+        return {
+            "$": ["infinite-scroll.css"]
+        };
+    }
+    static get properties() {
+        return {
+            "threshold": {
+                "type": "string",
+                "mutable": false,
+                "complexType": {
+                    "original": "string",
+                    "resolved": "string",
+                    "references": {}
+                },
+                "required": false,
+                "optional": false,
+                "docs": {
+                    "tags": [],
+                    "text": "The threshold distance from the bottom\nof the content to call the `infinite` output event when scrolled.\nThe threshold value can be either a percent, or\nin pixels. For example, use the value of `10%` for the `infinite`\noutput event to get called when the user has scrolled 10%\nfrom the bottom of the page. Use the value `100px` when the\nscroll is within 100 pixels from the bottom of the page."
+                },
+                "attribute": "threshold",
+                "reflect": false,
+                "defaultValue": "'15%'"
+            },
+            "disabled": {
+                "type": "boolean",
+                "mutable": false,
+                "complexType": {
+                    "original": "boolean",
+                    "resolved": "boolean",
+                    "references": {}
+                },
+                "required": false,
+                "optional": false,
+                "docs": {
+                    "tags": [],
+                    "text": "If `true`, the infinite scroll will be hidden and scroll event listeners\nwill be removed.\n\nSet this to true to disable the infinite scroll from actively\ntrying to receive new data while scrolling. This is useful\nwhen it is known that there is no more data that can be added, and\nthe infinite scroll is no longer needed."
+                },
+                "attribute": "disabled",
+                "reflect": false,
+                "defaultValue": "false"
+            },
+            "position": {
+                "type": "string",
+                "mutable": false,
+                "complexType": {
+                    "original": "'top' | 'bottom'",
+                    "resolved": "\"bottom\" | \"top\"",
+                    "references": {}
+                },
+                "required": false,
+                "optional": false,
+                "docs": {
+                    "tags": [],
+                    "text": "The position of the infinite scroll element.\nThe value can be either `top` or `bottom`."
+                },
+                "attribute": "position",
+                "reflect": false,
+                "defaultValue": "'bottom'"
+            }
+        };
+    }
+    static get states() {
+        return {
+            "isLoading": {}
+        };
+    }
+    static get events() {
+        return [{
+                "method": "ionInfinite",
+                "name": "ionInfinite",
+                "bubbles": true,
+                "cancelable": true,
+                "composed": true,
+                "docs": {
+                    "tags": [],
+                    "text": "Emitted when the scroll reaches\nthe threshold distance. From within your infinite handler,\nyou must call the infinite scroll's `complete()` method when\nyour async operation has completed."
+                },
+                "complexType": {
+                    "original": "void",
+                    "resolved": "void",
+                    "references": {}
+                }
+            }];
+    }
+    static get methods() {
+        return {
+            "complete": {
+                "complexType": {
+                    "signature": "() => Promise<void>",
+                    "parameters": [],
+                    "references": {
+                        "Promise": {
+                            "location": "global",
+                            "id": "global::Promise"
+                        }
+                    },
+                    "return": "Promise<void>"
+                },
+                "docs": {
+                    "text": "Call `complete()` within the `ionInfinite` output event handler when\nyour async operation has completed. For example, the `loading`\nstate is while the app is performing an asynchronous operation,\nsuch as receiving more data from an AJAX request to add more items\nto a data list. Once the data has been received and UI updated, you\nthen call this method to signify that the loading has completed.\nThis method will change the infinite scroll's state from `loading`\nto `enabled`.",
+                    "tags": []
+                }
+            }
+        };
+    }
+    static get elementRef() { return "el"; }
+    static get watchers() {
+        return [{
+                "propName": "threshold",
+                "methodName": "thresholdChanged"
+            }, {
+                "propName": "disabled",
+                "methodName": "disabledChanged"
+            }];
+    }
+}

+ 19 - 0
src/node_modules/@ionic/core/dist/collection/components/infinite-scroll/test/basic/infinite-scroll.e2e.js

@@ -0,0 +1,19 @@
+/*!
+ * (C) Ionic http://ionicframework.com - MIT License
+ */
+import { expect } from "@playwright/test";
+import { configs, test } from "../../../../utils/test/playwright/index";
+configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
+    test.describe(title('infinite-scroll: basic'), () => {
+        test('should load more items when scrolled to the bottom', async ({ page }) => {
+            await page.goto('/src/components/infinite-scroll/test/basic', config);
+            const ionInfiniteComplete = await page.spyOnEvent('ionInfiniteComplete');
+            const content = page.locator('ion-content');
+            const items = page.locator('ion-item');
+            expect(await items.count()).toBe(30);
+            await content.evaluate((el) => el.scrollToBottom(0));
+            await ionInfiniteComplete.next();
+            expect(await items.count()).toBe(60);
+        });
+    });
+});

+ 19 - 0
src/node_modules/@ionic/core/dist/collection/components/infinite-scroll/test/scroll-target/infinite-scroll.e2e.js

@@ -0,0 +1,19 @@
+/*!
+ * (C) Ionic http://ionicframework.com - MIT License
+ */
+import { expect } from "@playwright/test";
+import { configs, test } from "../../../../utils/test/playwright/index";
+configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
+    test.describe(title('infinite-scroll: scroll-target'), () => {
+        test('should load more items when scroll target is scrolled to the bottom', async ({ page }) => {
+            await page.goto('/src/components/infinite-scroll/test/scroll-target', config);
+            const ionInfiniteComplete = await page.spyOnEvent('ionInfiniteComplete');
+            const content = page.locator('#scroll-target');
+            const items = page.locator('ion-item');
+            expect(await items.count()).toBe(30);
+            await content.evaluate((el) => (el.scrollTop = el.scrollHeight));
+            await ionInfiniteComplete.next();
+            expect(await items.count()).toBe(60);
+        });
+    });
+});

+ 31 - 0
src/node_modules/@ionic/core/dist/collection/components/infinite-scroll/test/small-dom-update/infinite-scroll.e2e.js

@@ -0,0 +1,31 @@
+/*!
+ * (C) Ionic http://ionicframework.com - MIT License
+ */
+import { expect } from "@playwright/test";
+import { configs, test } from "../../../../utils/test/playwright/index";
+configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
+    test.describe(title('infinite-scroll: appending small amounts to dom'), () => {
+        test('should load more after remaining in threshold', async ({ page }) => {
+            await page.goto('/src/components/infinite-scroll/test/small-dom-update', config);
+            const ionInfiniteComplete = await page.spyOnEvent('ionInfiniteComplete');
+            const content = page.locator('ion-content');
+            const items = page.locator('#list .item');
+            expect(await items.count()).toBe(30);
+            await content.evaluate((el) => el.scrollToBottom(0));
+            await ionInfiniteComplete.next();
+            /**
+             * Even after appending we'll still be within
+             * the infinite scroll's threshold
+             */
+            expect(await items.count()).toBe(33);
+            await content.evaluate((el) => el.scrollToBottom(0));
+            await ionInfiniteComplete.next();
+            /**
+             * Scrolling down again without leaving
+             * the threshold should still trigger
+             * infinite scroll again.
+             */
+            expect(await items.count()).toBe(36);
+        });
+    });
+});

+ 19 - 0
src/node_modules/@ionic/core/dist/collection/components/infinite-scroll/test/top/infinite-scroll.e2e.js

@@ -0,0 +1,19 @@
+/*!
+ * (C) Ionic http://ionicframework.com - MIT License
+ */
+import { expect } from "@playwright/test";
+import { configs, test } from "../../../../utils/test/playwright/index";
+configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
+    test.describe(title('infinite-scroll: top'), () => {
+        test('should load more items when scrolled to the top', async ({ page }) => {
+            await page.goto('/src/components/infinite-scroll/test/top', config);
+            const ionInfiniteComplete = await page.spyOnEvent('ionInfiniteComplete');
+            const content = page.locator('ion-content');
+            const items = page.locator('ion-item');
+            expect(await items.count()).toBe(30);
+            await content.evaluate((el) => el.scrollToTop(0));
+            await ionInfiniteComplete.next();
+            expect(await items.count()).toBe(60);
+        });
+    });
+});

+ 0 - 0
src/node_modules/@ionic/core/dist/collection/components/input-password-toggle/input-password-toggle.css


+ 183 - 0
src/node_modules/@ionic/core/dist/collection/components/input-password-toggle/input-password-toggle.js

@@ -0,0 +1,183 @@
+/*!
+ * (C) Ionic http://ionicframework.com - MIT License
+ */
+import { Host, h } from "@stencil/core";
+import { printIonWarning } from "../../utils/logging/index";
+import { createColorClasses } from "../../utils/theme";
+import { eyeOff, eye } from "ionicons/icons";
+import { getIonMode } from "../../global/ionic-global";
+/**
+ * @virtualProp {"ios" | "md"} mode - The mode determines which platform styles to use.
+ */
+export class InputPasswordToggle {
+    constructor() {
+        this.togglePasswordVisibility = () => {
+            const { inputElRef } = this;
+            if (!inputElRef) {
+                return;
+            }
+            inputElRef.type = inputElRef.type === 'text' ? 'password' : 'text';
+        };
+        this.color = undefined;
+        this.showIcon = undefined;
+        this.hideIcon = undefined;
+        this.type = 'password';
+    }
+    /**
+     * Whenever the input type changes we need to re-run validation to ensure the password
+     * toggle is being used with the correct input type. If the application changes the type
+     * outside of this component we also need to re-render so the correct icon is shown.
+     */
+    onTypeChange(newValue) {
+        if (newValue !== 'text' && newValue !== 'password') {
+            printIonWarning(`ion-input-password-toggle only supports inputs of type "text" or "password". Input of type "${newValue}" is not compatible.`, this.el);
+            return;
+        }
+    }
+    connectedCallback() {
+        const { el } = this;
+        const inputElRef = (this.inputElRef = el.closest('ion-input'));
+        if (!inputElRef) {
+            printIonWarning('No ancestor ion-input found for ion-input-password-toggle. This component must be slotted inside of an ion-input.', el);
+            return;
+        }
+        /**
+         * Important: Set the type in connectedCallback because the default value
+         * of this.type may not always be accurate. Usually inputs have the "password" type
+         * but it is possible to have the input to initially have the "text" type. In that scenario
+         * the wrong icon will show briefly before switching to the correct icon. Setting the
+         * type here allows us to avoid that flicker.
+         */
+        this.type = inputElRef.type;
+    }
+    disconnectedCallback() {
+        this.inputElRef = null;
+    }
+    render() {
+        var _a, _b;
+        const { color, type } = this;
+        const mode = getIonMode(this);
+        const showPasswordIcon = (_a = this.showIcon) !== null && _a !== void 0 ? _a : eye;
+        const hidePasswordIcon = (_b = this.hideIcon) !== null && _b !== void 0 ? _b : eyeOff;
+        const isPasswordVisible = type === 'text';
+        return (h(Host, { key: 'ed1c29726ce0c91548f0e2ada61e3f8b5c813d2d', class: createColorClasses(color, {
+                [mode]: true,
+            }) }, h("ion-button", { key: '9698eccdaedb86cf12d20acc53660371b3af3c55', mode: mode, color: color, fill: "clear", shape: "round", "aria-checked": isPasswordVisible ? 'true' : 'false', "aria-label": "show password", role: "switch", type: "button", onPointerDown: (ev) => {
+                /**
+                 * This prevents mobile browsers from
+                 * blurring the input when the password toggle
+                 * button is activated.
+                 */
+                ev.preventDefault();
+            }, onClick: this.togglePasswordVisibility }, h("ion-icon", { key: '1f2119c30b56c800d9af44e6499445a0ebb466cf', slot: "icon-only", "aria-hidden": "true", icon: isPasswordVisible ? hidePasswordIcon : showPasswordIcon }))));
+    }
+    static get is() { return "ion-input-password-toggle"; }
+    static get encapsulation() { return "shadow"; }
+    static get originalStyleUrls() {
+        return {
+            "ios": ["input-password-toggle.scss"],
+            "md": ["input-password-toggle.scss"]
+        };
+    }
+    static get styleUrls() {
+        return {
+            "ios": ["input-password-toggle.css"],
+            "md": ["input-password-toggle.css"]
+        };
+    }
+    static get properties() {
+        return {
+            "color": {
+                "type": "string",
+                "mutable": false,
+                "complexType": {
+                    "original": "Color",
+                    "resolved": "\"danger\" | \"dark\" | \"light\" | \"medium\" | \"primary\" | \"secondary\" | \"success\" | \"tertiary\" | \"warning\" | string & Record<never, never> | undefined",
+                    "references": {
+                        "Color": {
+                            "location": "import",
+                            "path": "../../interface",
+                            "id": "src/interface.d.ts::Color"
+                        }
+                    }
+                },
+                "required": false,
+                "optional": true,
+                "docs": {
+                    "tags": [],
+                    "text": "The color to use from your application's color palette.\nDefault options are: `\"primary\"`, `\"secondary\"`, `\"tertiary\"`, `\"success\"`, `\"warning\"`, `\"danger\"`, `\"light\"`, `\"medium\"`, and `\"dark\"`.\nFor more information on colors, see [theming](/docs/theming/basics)."
+                },
+                "attribute": "color",
+                "reflect": true
+            },
+            "showIcon": {
+                "type": "string",
+                "mutable": false,
+                "complexType": {
+                    "original": "string",
+                    "resolved": "string | undefined",
+                    "references": {}
+                },
+                "required": false,
+                "optional": true,
+                "docs": {
+                    "tags": [],
+                    "text": "The icon that can be used to represent showing a password. If not set, the \"eye\" Ionicon will be used."
+                },
+                "attribute": "show-icon",
+                "reflect": false
+            },
+            "hideIcon": {
+                "type": "string",
+                "mutable": false,
+                "complexType": {
+                    "original": "string",
+                    "resolved": "string | undefined",
+                    "references": {}
+                },
+                "required": false,
+                "optional": true,
+                "docs": {
+                    "tags": [],
+                    "text": "The icon that can be used to represent hiding a password. If not set, the \"eyeOff\" Ionicon will be used."
+                },
+                "attribute": "hide-icon",
+                "reflect": false
+            },
+            "type": {
+                "type": "string",
+                "mutable": true,
+                "complexType": {
+                    "original": "TextFieldTypes",
+                    "resolved": "\"date\" | \"datetime-local\" | \"email\" | \"month\" | \"number\" | \"password\" | \"search\" | \"tel\" | \"text\" | \"time\" | \"url\" | \"week\"",
+                    "references": {
+                        "TextFieldTypes": {
+                            "location": "import",
+                            "path": "../../interface",
+                            "id": "src/interface.d.ts::TextFieldTypes"
+                        }
+                    }
+                },
+                "required": false,
+                "optional": false,
+                "docs": {
+                    "tags": [{
+                            "name": "internal",
+                            "text": undefined
+                        }],
+                    "text": ""
+                },
+                "attribute": "type",
+                "reflect": false,
+                "defaultValue": "'password'"
+            }
+        };
+    }
+    static get elementRef() { return "el"; }
+    static get watchers() {
+        return [{
+                "propName": "type",
+                "methodName": "onTypeChange"
+            }];
+    }
+}

+ 21 - 0
src/node_modules/@ionic/core/dist/collection/components/input-password-toggle/test/a11y/input-password-toggle.e2e.js

@@ -0,0 +1,21 @@
+/*!
+ * (C) Ionic http://ionicframework.com - MIT License
+ */
+import AxeBuilder from "@axe-core/playwright";
+import { expect } from "@playwright/test";
+import { configs, test } from "../../../../utils/test/playwright/index";
+configs({ directions: ['ltr'] }).forEach(({ title, config }) => {
+    test.describe(title('input password toggle: a11y'), () => {
+        test('should not have accessibility violations', async ({ page }) => {
+            await page.setContent(`
+          <main>
+            <ion-input label="input" type="password">
+              <ion-input-password-toggle slot="end"></ion-input-password-toggle>
+            </ion-input>
+          </main>
+        `, config);
+            const results = await new AxeBuilder({ page }).analyze();
+            expect(results.violations).toEqual([]);
+        });
+    });
+});

+ 38 - 0
src/node_modules/@ionic/core/dist/collection/components/input-password-toggle/test/basic/input-password-toggle.e2e.js

@@ -0,0 +1,38 @@
+/*!
+ * (C) Ionic http://ionicframework.com - MIT License
+ */
+import { expect } from "@playwright/test";
+import { configs, test } from "../../../../utils/test/playwright/index";
+configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
+    test.describe(title('input password toggle: states'), () => {
+        test('should be hidden when inside of a readonly input', async ({ page }) => {
+            await page.setContent(`
+          <ion-input label="input" type="password" readonly="true">
+            <ion-input-password-toggle slot="end"></ion-input-password-toggle>
+          </ion-input>
+        `, config);
+            const inputPasswordToggle = page.locator('ion-input-password-toggle');
+            await expect(inputPasswordToggle).toBeHidden();
+        });
+        test('should be hidden when inside of a disabled input', async ({ page }) => {
+            await page.setContent(`
+          <ion-input label="input" type="password" disabled="true">
+            <ion-input-password-toggle slot="end"></ion-input-password-toggle>
+          </ion-input>
+        `, config);
+            const inputPasswordToggle = page.locator('ion-input-password-toggle');
+            await expect(inputPasswordToggle).toBeHidden();
+        });
+    });
+    test.describe(title('input password toggle: rendering'), () => {
+        test('should not have visual regressions', async ({ page }) => {
+            await page.setContent(`
+          <ion-input label="input" type="password">
+            <ion-input-password-toggle slot="end"></ion-input-password-toggle>
+          </ion-input>
+        `, config);
+            const inputPasswordToggle = page.locator('ion-input-password-toggle');
+            await expect(inputPasswordToggle).toHaveScreenshot(screenshot('input-password-toggle'));
+        });
+    });
+});

+ 76 - 0
src/node_modules/@ionic/core/dist/collection/components/input-password-toggle/test/input-password-toggle.spec.js

@@ -0,0 +1,76 @@
+/*!
+ * (C) Ionic http://ionicframework.com - MIT License
+ */
+import { h } from "@stencil/core";
+import { newSpecPage } from "@stencil/core/testing";
+import { Input } from "../../input/input";
+import { InputPasswordToggle } from "../input-password-toggle";
+import { Button } from "../../button/button";
+import { initialize } from "../../../global/ionic-global";
+describe('input password toggle', () => {
+    it('should toggle input type when clicked', async () => {
+        const page = await newSpecPage({
+            components: [Input, InputPasswordToggle, Button],
+            template: () => (h("ion-input", { type: "password" }, h("ion-input-password-toggle", { slot: "end" }))),
+        });
+        const inputPasswordToggle = page.body.querySelector('ion-input-password-toggle');
+        const button = inputPasswordToggle.shadowRoot.querySelector('ion-button');
+        const input = page.body.querySelector('ion-input');
+        expect(input.type).toBe('password');
+        button.click();
+        await page.waitForChanges();
+        expect(input.type).toBe('text');
+        button.click();
+        await page.waitForChanges();
+        expect(input.type).toBe('password');
+    });
+    it('should render custom icons', async () => {
+        const page = await newSpecPage({
+            components: [Input, InputPasswordToggle, Button],
+            template: () => (h("ion-input", { type: "password" }, h("ion-input-password-toggle", { showIcon: "show", hideIcon: "hide", slot: "end" }))),
+        });
+        const inputPasswordToggle = page.body.querySelector('ion-input-password-toggle');
+        const button = inputPasswordToggle.shadowRoot.querySelector('ion-button');
+        const icon = inputPasswordToggle.shadowRoot.querySelector('ion-icon');
+        // Grab the attribute to test since we are not actually passing in a valid SVG
+        expect(icon.getAttribute('icon')).toBe('show');
+        button.click();
+        await page.waitForChanges();
+        expect(icon.getAttribute('icon')).toBe('hide');
+    });
+    it('changing the type on the input should update the icon used in password toggle', async () => {
+        const page = await newSpecPage({
+            components: [Input, InputPasswordToggle, Button],
+            template: () => (h("ion-input", { type: "password" }, h("ion-input-password-toggle", { showIcon: "show", hideIcon: "hide", slot: "end" }))),
+        });
+        const inputPasswordToggle = page.body.querySelector('ion-input-password-toggle');
+        const input = page.body.querySelector('ion-input');
+        const icon = inputPasswordToggle.shadowRoot.querySelector('ion-icon');
+        // Grab the attribute to test since we are not actually passing in a valid SVG
+        expect(icon.getAttribute('icon')).toBe('show');
+        input.type = 'text';
+        await page.waitForChanges();
+        expect(icon.getAttribute('icon')).toBe('hide');
+        input.type = 'password';
+        await page.waitForChanges();
+        expect(icon.getAttribute('icon')).toBe('show');
+    });
+    it('should inherit the mode and color to internal ionic components', async () => {
+        /**
+         * This initialize script tells Stencil how to determine the mode on components.
+         * This is required for any getIonMode internal logic to function properly in spec tests.
+         */
+        initialize();
+        const page = await newSpecPage({
+            components: [Input, InputPasswordToggle, Button],
+            template: () => (h("ion-input", { type: "password", color: "primary" }, h("ion-input-password-toggle", { slot: "end", mode: "ios", color: "danger" }))),
+        });
+        const inputPasswordToggle = page.body.querySelector('ion-input-password-toggle');
+        const button = inputPasswordToggle.shadowRoot.querySelector('ion-button');
+        await page.waitForChanges();
+        // mode is a virtual prop so we need to access it as an attribute
+        expect(button.getAttribute('mode')).toBe('ios');
+        // color is an actual prop so we can access the element property
+        expect(button.color).toBe('danger');
+    });
+});

+ 1 - 0
src/node_modules/@ionic/core/dist/collection/components/input/input-interface.js

@@ -0,0 +1 @@
+export {};

+ 751 - 0
src/node_modules/@ionic/core/dist/collection/components/input/input.ios.css

@@ -0,0 +1,751 @@
+/**
+ * Convert a font size to a dynamic font size.
+ * Fonts that participate in Dynamic Type should use
+ * dynamic font sizes.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param unit (optional) - The unit to convert to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * Convert a font size to a dynamic font size but impose
+ * a maximum font size.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param maxScale - The maximum scale of the font (i.e. 2.5 for a maximum 250% scale).
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * Convert a font size to a dynamic font size but impose
+ * a minimum font size.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param minScale - The minimum scale of the font (i.e. 0.8 for a minimum 80% scale).
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * Convert a font size to a dynamic font size but impose
+ * maximum and minimum font sizes.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param minScale - The minimum scale of the font (i.e. 0.8 for a minimum 80% scale).
+ * @param maxScale - The maximum scale of the font (i.e. 2.5 for a maximum 250% scale).
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * A heuristic that applies CSS to tablet
+ * viewports.
+ *
+ * Usage:
+ * @include tablet-viewport() {
+ *   :host {
+ *     background-color: green;
+ *   }
+ * }
+ */
+/**
+ * A heuristic that applies CSS to mobile
+ * viewports (i.e. phones, not tablets).
+ *
+ * Usage:
+ * @include mobile-viewport() {
+ *   :host {
+ *     background-color: blue;
+ *   }
+ * }
+ */
+:host {
+  /**
+   * @prop --background: Background of the input
+   *
+   * @prop --color: Color of the input text
+   *
+   * @prop --padding-top: Top padding of the input
+   * @prop --padding-end: Right padding if direction is left-to-right, and left padding if direction is right-to-left of the input
+   * @prop --padding-bottom: Bottom padding of the input
+   * @prop --padding-start: Left padding if direction is left-to-right, and right padding if direction is right-to-left of the input
+   *
+   * @prop --placeholder-color: Color of the input placeholder text
+   * @prop --placeholder-font-style: Font style of the input placeholder text
+   * @prop --placeholder-font-weight: Font weight of the input placeholder text
+   * @prop --placeholder-opacity: Opacity of the input placeholder text
+   *
+   * @prop --highlight-height: The height of the highlight on the input. Only applies to md mode.
+   * @prop --highlight-color-focused: The color of the highlight on the input when focused
+   * @prop --highlight-color-valid: The color of the highlight on the input when valid
+   * @prop --highlight-color-invalid: The color of the highlight on the input when invalid
+   *
+   * @prop --border-color: Color of the border below the input when using helper text, error text, or counter
+   * @prop --border-radius: Radius of the input. A large radius may display unevenly when using fill="outline"; if needed, use shape="round" instead or increase --padding-start.
+   * @prop --border-style: Style of the border below the input when using helper text, error text, or counter
+   * @prop --border-width: Width of the border below the input when using helper text, error text, or counter
+   */
+  --placeholder-color: initial;
+  --placeholder-font-style: initial;
+  --placeholder-font-weight: initial;
+  --placeholder-opacity: var(--ion-placeholder-opacity, 0.6);
+  --padding-top: 0px;
+  --padding-end: 0px;
+  --padding-bottom: 0px;
+  --padding-start: 0px;
+  --background: transparent;
+  --color: initial;
+  --border-style: solid;
+  --highlight-color-focused: var(--ion-color-primary, #0054e9);
+  --highlight-color-valid: var(--ion-color-success, #2dd55b);
+  --highlight-color-invalid: var(--ion-color-danger, #c5000f);
+  /**
+   * This is a private API that is used to switch
+   * out the highlight color based on the state
+   * of the component without having to write
+   * different selectors for different fill variants.
+   */
+  --highlight-color: var(--highlight-color-focused);
+  display: block;
+  position: relative;
+  width: 100%;
+  min-height: 44px;
+  /* stylelint-disable-next-line all */
+  padding: 0 !important;
+  color: var(--color);
+  font-family: var(--ion-font-family, inherit);
+  z-index: 2;
+}
+
+:host-context(ion-item)[slot=start],
+:host-context(ion-item)[slot=end] {
+  width: auto;
+}
+
+:host(.ion-color) {
+  --highlight-color-focused: var(--ion-color-base);
+}
+
+/**
+ * Since the label sits on top of the element,
+ * the component needs to be taller otherwise the
+ * label will appear too close to the input text.
+ */
+:host(.input-label-placement-floating),
+:host(.input-label-placement-stacked) {
+  min-height: 56px;
+}
+
+.native-input {
+  padding-left: 0;
+  padding-right: 0;
+  padding-top: 0;
+  padding-bottom: 0;
+  font-family: inherit;
+  font-size: inherit;
+  font-style: inherit;
+  font-weight: inherit;
+  letter-spacing: inherit;
+  text-decoration: inherit;
+  text-indent: inherit;
+  text-overflow: inherit;
+  text-transform: inherit;
+  text-align: inherit;
+  white-space: inherit;
+  color: inherit;
+  display: inline-block;
+  position: relative;
+  flex: 1;
+  width: 100%;
+  max-width: 100%;
+  max-height: 100%;
+  border: 0;
+  outline: none;
+  background: transparent;
+  box-sizing: border-box;
+  appearance: none;
+  /**
+   * This ensures the input
+   * remains on top of any decoration
+   * that we render (particularly the
+   * outline border when fill="outline").
+   * If we did not do this then Axe would
+   * be unable to determine the color
+   * contrast of the input.
+   */
+  z-index: 1;
+}
+.native-input::placeholder {
+  color: var(--placeholder-color);
+  font-family: inherit;
+  font-style: var(--placeholder-font-style);
+  font-weight: var(--placeholder-font-weight);
+  opacity: var(--placeholder-opacity);
+}
+.native-input:-webkit-autofill {
+  background-color: transparent;
+}
+.native-input:invalid {
+  box-shadow: none;
+}
+.native-input::-ms-clear {
+  display: none;
+}
+
+.cloned-input {
+  top: 0;
+  bottom: 0;
+  position: absolute;
+  pointer-events: none;
+}
+.cloned-input {
+  inset-inline-start: 0;
+}
+
+/**
+ * The cloned input needs to be disabled on
+ * Android otherwise the viewport will still
+ * shift when running scroll assist.
+ */
+.cloned-input:disabled {
+  opacity: 1;
+}
+
+.input-clear-icon {
+  -webkit-margin-start: auto;
+  margin-inline-start: auto;
+  -webkit-margin-end: auto;
+  margin-inline-end: auto;
+  margin-top: auto;
+  margin-bottom: auto;
+  padding-left: 0;
+  padding-right: 0;
+  padding-top: 0;
+  padding-bottom: 0;
+  background-position: center;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 30px;
+  height: 30px;
+  border: 0;
+  outline: none;
+  background-color: transparent;
+  background-repeat: no-repeat;
+  color: var(--ion-color-step-600, var(--ion-text-color-step-400, #666666));
+  visibility: hidden;
+  appearance: none;
+}
+
+:host(.in-item-color) .input-clear-icon {
+  color: inherit;
+}
+
+/**
+ * Normally, we would not want to use :focus
+ * here because that would mean tapping the button
+ * on mobile would focus it (and keep it focused).
+ * However, the clear button always disappears after
+ * being activated, so we never get to that state.
+ */
+.input-clear-icon:focus {
+  opacity: 0.5;
+}
+
+:host(.has-value) .input-clear-icon {
+  visibility: visible;
+}
+
+.input-wrapper {
+  -webkit-padding-start: var(--padding-start);
+  padding-inline-start: var(--padding-start);
+  -webkit-padding-end: var(--padding-end);
+  padding-inline-end: var(--padding-end);
+  padding-top: var(--padding-top);
+  padding-bottom: var(--padding-bottom);
+  border-radius: var(--border-radius);
+  display: flex;
+  position: relative;
+  flex-grow: 1;
+  align-items: stretch;
+  height: inherit;
+  min-height: inherit;
+  transition: background-color 15ms linear;
+  background: var(--background);
+  line-height: normal;
+}
+
+.native-wrapper {
+  display: flex;
+  position: relative;
+  flex-grow: 1;
+  align-items: center;
+  width: 100%;
+}
+
+:host(.ion-touched.ion-invalid) {
+  --highlight-color: var(--highlight-color-invalid);
+}
+
+/**
+ * The component highlight is only shown
+ * on focus, so we can safely set the valid
+ * color state when valid. If we
+ * set it when .has-focus is present then
+ * the highlight color would change
+ * from the valid color to the component's
+ * color during the transition after the
+ * component loses focus.
+ */
+:host(.ion-valid) {
+  --highlight-color: var(--highlight-color-valid);
+}
+
+.input-bottom {
+  /**
+   * The bottom content should take on the start and end
+   * padding so it is always aligned with either the label
+   * or the start of the text input.
+   */
+  -webkit-padding-start: var(--padding-start);
+  padding-inline-start: var(--padding-start);
+  -webkit-padding-end: var(--padding-end);
+  padding-inline-end: var(--padding-end);
+  padding-top: 5px;
+  padding-bottom: 0;
+  display: flex;
+  justify-content: space-between;
+  border-top: var(--border-width) var(--border-style) var(--border-color);
+  font-size: 0.75rem;
+}
+
+/**
+ * If the input has a validity state, the
+ * border and label should reflect that as a color.
+ * The invalid state should show if the input is
+ * invalid and has already been touched.
+ * The valid state should show if the input
+ * is valid, has already been touched, and
+ * is currently focused. Do not show the valid
+ * highlight when the input is blurred.
+ */
+:host(.has-focus.ion-valid),
+:host(.ion-touched.ion-invalid) {
+  --border-color: var(--highlight-color);
+}
+
+/**
+ * Error text should only be shown when .ion-invalid is
+ * present on the input. Otherwise the helper text should
+ * be shown.
+ */
+.input-bottom .error-text {
+  display: none;
+  color: var(--highlight-color-invalid);
+}
+
+.input-bottom .helper-text {
+  display: block;
+  color: var(--ion-color-step-550, var(--ion-text-color-step-450, #737373));
+}
+
+:host(.ion-touched.ion-invalid) .input-bottom .error-text {
+  display: block;
+}
+
+:host(.ion-touched.ion-invalid) .input-bottom .helper-text {
+  display: none;
+}
+
+.input-bottom .counter {
+  /**
+   * Counter should always be at
+   * the end of the container even
+   * when no helper/error texts are used.
+   */
+  -webkit-margin-start: auto;
+  margin-inline-start: auto;
+  color: var(--ion-color-step-550, var(--ion-text-color-step-450, #737373));
+  white-space: nowrap;
+  padding-inline-start: 16px;
+}
+
+:host(.has-focus) input {
+  caret-color: var(--highlight-color);
+}
+
+.label-text-wrapper {
+  /**
+   * This causes the label to take up
+   * the entire height of its container
+   * while still keeping the text centered.
+   */
+  display: flex;
+  align-items: center;
+  /**
+   * Label text should not extend
+   * beyond the bounds of the input.
+   * However, we do not set the max
+   * width to 100% because then
+   * only the label would show and users
+   * would not be able to see what they are typing.
+   */
+  max-width: 200px;
+  transition: color 150ms cubic-bezier(0.4, 0, 0.2, 1), transform 150ms cubic-bezier(0.4, 0, 0.2, 1);
+  /**
+   * This ensures that double tapping this text
+   * clicks the <label> and focuses the input
+   * when a screen reader is enabled.
+   */
+  pointer-events: none;
+}
+
+/**
+ * We need to use two elements instead of
+ * one. The .label-text-wrapper is responsible
+ * for centering the label text vertically regardless
+ * of the input height using flexbox.
+ *
+ * The .label-text element is responsible for controlling
+ * overflow when label-placement="fixed".
+ * We want the ellipses to show up when the
+ * fixed label overflows, but text-overflow: ellipsis only
+ * works on block-level elements. A flex item is
+ * considered blockified (https://www.w3.org/TR/css-display-3/#blockify).
+ */
+.label-text,
+::slotted([slot=label]) {
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  overflow: hidden;
+}
+
+/**
+ * If no label text is placed into the slot
+ * then the element should be hidden otherwise
+ * there will be additional margins added.
+ */
+.label-text-wrapper-hidden,
+.input-outline-notch-hidden {
+  display: none;
+}
+
+.input-wrapper input {
+  /**
+   * When the floating label appears on top of the
+   * input, we need to fade the input out so that the
+   * label does not overlap with the placeholder.
+   */
+  transition: opacity 150ms cubic-bezier(0.4, 0, 0.2, 1);
+}
+
+/**
+ * Label is on the left of the input in LTR and
+ * on the right in RTL.
+ */
+:host(.input-label-placement-start) .input-wrapper {
+  flex-direction: row;
+}
+
+:host(.input-label-placement-start) .label-text-wrapper {
+  /**
+   * The margin between the label and
+   * the input should be on the end
+   * when the label sits at the start.
+   */
+  -webkit-margin-start: 0;
+  margin-inline-start: 0;
+  -webkit-margin-end: 16px;
+  margin-inline-end: 16px;
+  margin-top: 0;
+  margin-bottom: 0;
+}
+
+/**
+ * Label is on the right of the input in LTR and
+ * on the left in RTL.
+ */
+:host(.input-label-placement-end) .input-wrapper {
+  flex-direction: row-reverse;
+}
+
+/**
+ * The margin between the label and
+ * the input should be on the start
+ * when the label sits at the end.
+ */
+:host(.input-label-placement-end) .label-text-wrapper {
+  -webkit-margin-start: 16px;
+  margin-inline-start: 16px;
+  -webkit-margin-end: 0;
+  margin-inline-end: 0;
+  margin-top: 0;
+  margin-bottom: 0;
+}
+
+:host(.input-label-placement-fixed) .label-text-wrapper {
+  /**
+   * The margin between the label and
+   * the input should be on the end
+   * when the label sits at the start.
+   */
+  -webkit-margin-start: 0;
+  margin-inline-start: 0;
+  -webkit-margin-end: 16px;
+  margin-inline-end: 16px;
+  margin-top: 0;
+  margin-bottom: 0;
+}
+
+/**
+ * Label is on the left of the input in LTR and
+ * on the right in RTL. Label also has a fixed width.
+ */
+:host(.input-label-placement-fixed) .label-text {
+  flex: 0 0 100px;
+  width: 100px;
+  min-width: 100px;
+  max-width: 200px;
+}
+
+/**
+ * Stacked: Label sits above the input and is scaled down.
+ * Floating: Label sits over the input when the input has no
+ * value and is blurred. Label sits above the input and is scaled
+ * down when the input is focused or has a value.
+ *
+ */
+:host(.input-label-placement-stacked) .input-wrapper,
+:host(.input-label-placement-floating) .input-wrapper {
+  flex-direction: column;
+  align-items: start;
+}
+
+/**
+ * Ensures that the label animates
+ * up and to the left in LTR or
+ * up and to the right in RTL.
+ */
+:host(.input-label-placement-stacked) .label-text-wrapper,
+:host(.input-label-placement-floating) .label-text-wrapper {
+  transform-origin: left top;
+  max-width: 100%;
+  /**
+   * The 2 ensures the label
+   * remains on top of any browser
+   * autofill background too.
+   */
+  z-index: 2;
+}
+:host-context([dir=rtl]):host(.input-label-placement-stacked) .label-text-wrapper, :host-context([dir=rtl]).input-label-placement-stacked .label-text-wrapper, :host-context([dir=rtl]):host(.input-label-placement-floating) .label-text-wrapper, :host-context([dir=rtl]).input-label-placement-floating .label-text-wrapper {
+  transform-origin: right top;
+}
+
+@supports selector(:dir(rtl)) {
+  :host(.input-label-placement-stacked:dir(rtl)) .label-text-wrapper, :host(.input-label-placement-floating:dir(rtl)) .label-text-wrapper {
+    transform-origin: right top;
+  }
+}
+
+/**
+ * Ensures the input does not
+ * overlap the label.
+ */
+:host(.input-label-placement-stacked) input,
+:host(.input-label-placement-floating) input {
+  margin-left: 0;
+  margin-right: 0;
+  margin-top: 1px;
+  margin-bottom: 0;
+}
+
+/**
+ * This makes the label sit over the input
+ * when the input is blurred and has no value.
+ */
+:host(.input-label-placement-floating) .label-text-wrapper {
+  transform: translateY(100%) scale(1);
+}
+
+/**
+ * The input should be hidden when the label
+ * is on top of the input. This prevents the label
+ * from overlapping any placeholder value.
+ */
+:host(.input-label-placement-floating) input {
+  opacity: 0;
+}
+
+:host(.has-focus.input-label-placement-floating) input,
+:host(.has-value.input-label-placement-floating) input {
+  opacity: 1;
+}
+
+/**
+ * This makes the label sit above the input.
+ */
+:host(.label-floating) .label-text-wrapper {
+  transform: translateY(50%) scale(0.75);
+  /**
+   * Label text should not extend
+   * beyond the bounds of the input.
+   */
+  max-width: calc(100% / 0.75);
+}
+
+::slotted([slot=start]:last-of-type) {
+  margin-inline-end: 16px;
+  margin-inline-start: 0;
+}
+
+::slotted([slot=end]:first-of-type) {
+  margin-inline-start: 16px;
+  margin-inline-end: 0;
+}
+
+/**
+ * The input password toggle component should be hidden when the input is readonly/disabled
+ * because it is not possible to edit a password.
+ */
+:host([disabled]) ::slotted(ion-input-password-toggle),
+:host([readonly]) ::slotted(ion-input-password-toggle) {
+  display: none;
+}
+
+/**
+ * Convert a font size to a dynamic font size.
+ * Fonts that participate in Dynamic Type should use
+ * dynamic font sizes.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param unit (optional) - The unit to convert to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * Convert a font size to a dynamic font size but impose
+ * a maximum font size.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param maxScale - The maximum scale of the font (i.e. 2.5 for a maximum 250% scale).
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * Convert a font size to a dynamic font size but impose
+ * a minimum font size.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param minScale - The minimum scale of the font (i.e. 0.8 for a minimum 80% scale).
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * Convert a font size to a dynamic font size but impose
+ * maximum and minimum font sizes.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param minScale - The minimum scale of the font (i.e. 0.8 for a minimum 80% scale).
+ * @param maxScale - The maximum scale of the font (i.e. 2.5 for a maximum 250% scale).
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * A heuristic that applies CSS to tablet
+ * viewports.
+ *
+ * Usage:
+ * @include tablet-viewport() {
+ *   :host {
+ *     background-color: green;
+ *   }
+ * }
+ */
+/**
+ * A heuristic that applies CSS to mobile
+ * viewports (i.e. phones, not tablets).
+ *
+ * Usage:
+ * @include mobile-viewport() {
+ *   :host {
+ *     background-color: blue;
+ *   }
+ * }
+ */
+/**
+ * Convert a font size to a dynamic font size.
+ * Fonts that participate in Dynamic Type should use
+ * dynamic font sizes.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param unit (optional) - The unit to convert to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * Convert a font size to a dynamic font size but impose
+ * a maximum font size.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param maxScale - The maximum scale of the font (i.e. 2.5 for a maximum 250% scale).
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * Convert a font size to a dynamic font size but impose
+ * a minimum font size.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param minScale - The minimum scale of the font (i.e. 0.8 for a minimum 80% scale).
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * Convert a font size to a dynamic font size but impose
+ * maximum and minimum font sizes.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param minScale - The minimum scale of the font (i.e. 0.8 for a minimum 80% scale).
+ * @param maxScale - The maximum scale of the font (i.e. 2.5 for a maximum 250% scale).
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * A heuristic that applies CSS to tablet
+ * viewports.
+ *
+ * Usage:
+ * @include tablet-viewport() {
+ *   :host {
+ *     background-color: green;
+ *   }
+ * }
+ */
+/**
+ * A heuristic that applies CSS to mobile
+ * viewports (i.e. phones, not tablets).
+ *
+ * Usage:
+ * @include mobile-viewport() {
+ *   :host {
+ *     background-color: blue;
+ *   }
+ * }
+ */
+:host {
+  --border-width: 0.55px;
+  --border-color: var(--ion-item-border-color, var(--ion-border-color, var(--ion-color-step-250, var(--ion-background-color-step-250, #c8c7cc))));
+  --highlight-height: 0px;
+  font-size: inherit;
+}
+
+.input-clear-icon ion-icon {
+  width: 18px;
+  height: 18px;
+}
+
+:host(.input-disabled) {
+  opacity: 0.3;
+}
+
+/**
+ * Slotted buttons have a lot of default padding that can
+ * cause them to look misaligned from other pieces such
+ * as the control's label, especially when using a clear
+ * fill. We also make them circular to ensure that non-
+ * clear buttons and the focus/hover state on clear ones
+ * don't look too crowded.
+ */
+::slotted(ion-button[slot=start].button-has-icon-only),
+::slotted(ion-button[slot=end].button-has-icon-only) {
+  --border-radius: 50%;
+  --padding-start: 0;
+  --padding-end: 0;
+  --padding-top: 0;
+  --padding-bottom: 0;
+  aspect-ratio: 1;
+}

+ 1202 - 0
src/node_modules/@ionic/core/dist/collection/components/input/input.js

@@ -0,0 +1,1202 @@
+/*!
+ * (C) Ionic http://ionicframework.com - MIT License
+ */
+import { Build, Host, forceUpdate, h } from "@stencil/core";
+import { createNotchController } from "../../utils/forms/index";
+import { inheritAriaAttributes, debounceEvent, inheritAttributes, componentOnReady } from "../../utils/helpers";
+import { createSlotMutationController } from "../../utils/slot-mutation-controller";
+import { createColorClasses, hostContext } from "../../utils/theme";
+import { closeCircle, closeSharp } from "ionicons/icons";
+import { getIonMode } from "../../global/ionic-global";
+import { getCounterText } from "./input.utils";
+/**
+ * @virtualProp {"ios" | "md"} mode - The mode determines which platform styles to use.
+ *
+ * @slot label - The label text to associate with the input. Use the `labelPlacement` property to control where the label is placed relative to the input. Use this if you need to render a label with custom HTML. (EXPERIMENTAL)
+ * @slot start - Content to display at the leading edge of the input. (EXPERIMENTAL)
+ * @slot end - Content to display at the trailing edge of the input. (EXPERIMENTAL)
+ */
+export class Input {
+    constructor() {
+        this.inputId = `ion-input-${inputIds++}`;
+        this.inheritedAttributes = {};
+        this.isComposing = false;
+        /**
+         * `true` if the input was cleared as a result of the user typing
+         * with `clearOnEdit` enabled.
+         *
+         * Resets when the input loses focus.
+         */
+        this.didInputClearOnEdit = false;
+        this.onInput = (ev) => {
+            const input = ev.target;
+            if (input) {
+                this.value = input.value || '';
+            }
+            this.emitInputChange(ev);
+        };
+        this.onChange = (ev) => {
+            this.emitValueChange(ev);
+        };
+        this.onBlur = (ev) => {
+            this.hasFocus = false;
+            if (this.focusedValue !== this.value) {
+                /**
+                 * Emits the `ionChange` event when the input value
+                 * is different than the value when the input was focused.
+                 */
+                this.emitValueChange(ev);
+            }
+            this.didInputClearOnEdit = false;
+            this.ionBlur.emit(ev);
+        };
+        this.onFocus = (ev) => {
+            this.hasFocus = true;
+            this.focusedValue = this.value;
+            this.ionFocus.emit(ev);
+        };
+        this.onKeydown = (ev) => {
+            this.checkClearOnEdit(ev);
+        };
+        this.onCompositionStart = () => {
+            this.isComposing = true;
+        };
+        this.onCompositionEnd = () => {
+            this.isComposing = false;
+        };
+        this.clearTextInput = (ev) => {
+            if (this.clearInput && !this.readonly && !this.disabled && ev) {
+                ev.preventDefault();
+                ev.stopPropagation();
+                // Attempt to focus input again after pressing clear button
+                this.setFocus();
+            }
+            this.value = '';
+            this.emitInputChange(ev);
+        };
+        this.hasFocus = false;
+        this.color = undefined;
+        this.autocapitalize = 'off';
+        this.autocomplete = 'off';
+        this.autocorrect = 'off';
+        this.autofocus = false;
+        this.clearInput = false;
+        this.clearInputIcon = undefined;
+        this.clearOnEdit = undefined;
+        this.counter = false;
+        this.counterFormatter = undefined;
+        this.debounce = undefined;
+        this.disabled = false;
+        this.enterkeyhint = undefined;
+        this.errorText = undefined;
+        this.fill = undefined;
+        this.inputmode = undefined;
+        this.helperText = undefined;
+        this.label = undefined;
+        this.labelPlacement = 'start';
+        this.max = undefined;
+        this.maxlength = undefined;
+        this.min = undefined;
+        this.minlength = undefined;
+        this.multiple = undefined;
+        this.name = this.inputId;
+        this.pattern = undefined;
+        this.placeholder = undefined;
+        this.readonly = false;
+        this.required = false;
+        this.shape = undefined;
+        this.spellcheck = false;
+        this.step = undefined;
+        this.type = 'text';
+        this.value = '';
+    }
+    debounceChanged() {
+        const { ionInput, debounce, originalIonInput } = this;
+        /**
+         * If debounce is undefined, we have to manually revert the ionInput emitter in case
+         * debounce used to be set to a number. Otherwise, the event would stay debounced.
+         */
+        this.ionInput = debounce === undefined ? originalIonInput !== null && originalIonInput !== void 0 ? originalIonInput : ionInput : debounceEvent(ionInput, debounce);
+    }
+    /**
+     * Whenever the type on the input changes we need
+     * to update the internal type prop on the password
+     * toggle so that that correct icon is shown.
+     */
+    onTypeChange() {
+        const passwordToggle = this.el.querySelector('ion-input-password-toggle');
+        if (passwordToggle) {
+            passwordToggle.type = this.type;
+        }
+    }
+    /**
+     * Update the native input element when the value changes
+     */
+    valueChanged() {
+        const nativeInput = this.nativeInput;
+        const value = this.getValue();
+        if (nativeInput && nativeInput.value !== value && !this.isComposing) {
+            /**
+             * Assigning the native input's value on attribute
+             * value change, allows `ionInput` implementations
+             * to override the control's value.
+             *
+             * Used for patterns such as input trimming (removing whitespace),
+             * or input masking.
+             */
+            nativeInput.value = value;
+        }
+    }
+    componentWillLoad() {
+        this.inheritedAttributes = Object.assign(Object.assign({}, inheritAriaAttributes(this.el)), inheritAttributes(this.el, ['tabindex', 'title', 'data-form-type']));
+    }
+    connectedCallback() {
+        const { el } = this;
+        this.slotMutationController = createSlotMutationController(el, ['label', 'start', 'end'], () => forceUpdate(this));
+        this.notchController = createNotchController(el, () => this.notchSpacerEl, () => this.labelSlot);
+        this.debounceChanged();
+        if (Build.isBrowser) {
+            document.dispatchEvent(new CustomEvent('ionInputDidLoad', {
+                detail: this.el,
+            }));
+        }
+    }
+    componentDidLoad() {
+        this.originalIonInput = this.ionInput;
+        /**
+         * Set the type on the password toggle in the event that this input's
+         * type was set async and does not match the default type for the password toggle.
+         * This can happen when the type is bound using a JS framework binding syntax
+         * such as [type] in Angular.
+         */
+        this.onTypeChange();
+        this.debounceChanged();
+    }
+    componentDidRender() {
+        var _a;
+        (_a = this.notchController) === null || _a === void 0 ? void 0 : _a.calculateNotchWidth();
+    }
+    disconnectedCallback() {
+        if (Build.isBrowser) {
+            document.dispatchEvent(new CustomEvent('ionInputDidUnload', {
+                detail: this.el,
+            }));
+        }
+        if (this.slotMutationController) {
+            this.slotMutationController.destroy();
+            this.slotMutationController = undefined;
+        }
+        if (this.notchController) {
+            this.notchController.destroy();
+            this.notchController = undefined;
+        }
+    }
+    /**
+     * Sets focus on the native `input` in `ion-input`. Use this method instead of the global
+     * `input.focus()`.
+     *
+     * Developers who wish to focus an input when a page enters
+     * should call `setFocus()` in the `ionViewDidEnter()` lifecycle method.
+     *
+     * Developers who wish to focus an input when an overlay is presented
+     * should call `setFocus` after `didPresent` has resolved.
+     *
+     * See [managing focus](/docs/developing/managing-focus) for more information.
+     */
+    async setFocus() {
+        if (this.nativeInput) {
+            this.nativeInput.focus();
+        }
+    }
+    /**
+     * Returns the native `<input>` element used under the hood.
+     */
+    async getInputElement() {
+        /**
+         * If this gets called in certain early lifecycle hooks (ex: Vue onMounted),
+         * nativeInput won't be defined yet with the custom elements build, so wait for it to load in.
+         */
+        if (!this.nativeInput) {
+            await new Promise((resolve) => componentOnReady(this.el, resolve));
+        }
+        return Promise.resolve(this.nativeInput);
+    }
+    /**
+     * Emits an `ionChange` event.
+     *
+     * This API should be called for user committed changes.
+     * This API should not be used for external value changes.
+     */
+    emitValueChange(event) {
+        const { value } = this;
+        // Checks for both null and undefined values
+        const newValue = value == null ? value : value.toString();
+        // Emitting a value change should update the internal state for tracking the focused value
+        this.focusedValue = newValue;
+        this.ionChange.emit({ value: newValue, event });
+    }
+    /**
+     * Emits an `ionInput` event.
+     */
+    emitInputChange(event) {
+        const { value } = this;
+        // Checks for both null and undefined values
+        const newValue = value == null ? value : value.toString();
+        this.ionInput.emit({ value: newValue, event });
+    }
+    shouldClearOnEdit() {
+        const { type, clearOnEdit } = this;
+        return clearOnEdit === undefined ? type === 'password' : clearOnEdit;
+    }
+    getValue() {
+        return typeof this.value === 'number' ? this.value.toString() : (this.value || '').toString();
+    }
+    checkClearOnEdit(ev) {
+        if (!this.shouldClearOnEdit()) {
+            return;
+        }
+        /**
+         * The following keys do not modify the
+         * contents of the input. As a result, pressing
+         * them should not edit the input.
+         *
+         * We can't check to see if the value of the input
+         * was changed because we call checkClearOnEdit
+         * in a keydown listener, and the key has not yet
+         * been added to the input.
+         */
+        const IGNORED_KEYS = ['Enter', 'Tab', 'Shift', 'Meta', 'Alt', 'Control'];
+        const pressedIgnoredKey = IGNORED_KEYS.includes(ev.key);
+        /**
+         * Clear the input if the control has not been previously cleared during focus.
+         * Do not clear if the user hitting enter to submit a form.
+         */
+        if (!this.didInputClearOnEdit && this.hasValue() && !pressedIgnoredKey) {
+            this.value = '';
+            this.emitInputChange(ev);
+        }
+        /**
+         * Pressing an IGNORED_KEYS first and
+         * then an allowed key will cause the input to not
+         * be cleared.
+         */
+        if (!pressedIgnoredKey) {
+            this.didInputClearOnEdit = true;
+        }
+    }
+    hasValue() {
+        return this.getValue().length > 0;
+    }
+    /**
+     * Renders the helper text or error text values
+     */
+    renderHintText() {
+        const { helperText, errorText } = this;
+        return [h("div", { class: "helper-text" }, helperText), h("div", { class: "error-text" }, errorText)];
+    }
+    renderCounter() {
+        const { counter, maxlength, counterFormatter, value } = this;
+        if (counter !== true || maxlength === undefined) {
+            return;
+        }
+        return h("div", { class: "counter" }, getCounterText(value, maxlength, counterFormatter));
+    }
+    /**
+     * Responsible for rendering helper text,
+     * error text, and counter. This element should only
+     * be rendered if hint text is set or counter is enabled.
+     */
+    renderBottomContent() {
+        const { counter, helperText, errorText, maxlength } = this;
+        /**
+         * undefined and empty string values should
+         * be treated as not having helper/error text.
+         */
+        const hasHintText = !!helperText || !!errorText;
+        const hasCounter = counter === true && maxlength !== undefined;
+        if (!hasHintText && !hasCounter) {
+            return;
+        }
+        return (h("div", { class: "input-bottom" }, this.renderHintText(), this.renderCounter()));
+    }
+    renderLabel() {
+        const { label } = this;
+        return (h("div", { class: {
+                'label-text-wrapper': true,
+                'label-text-wrapper-hidden': !this.hasLabel,
+            } }, label === undefined ? h("slot", { name: "label" }) : h("div", { class: "label-text" }, label)));
+    }
+    /**
+     * Gets any content passed into the `label` slot,
+     * not the <slot> definition.
+     */
+    get labelSlot() {
+        return this.el.querySelector('[slot="label"]');
+    }
+    /**
+     * Returns `true` if label content is provided
+     * either by a prop or a content. If you want
+     * to get the plaintext value of the label use
+     * the `labelText` getter instead.
+     */
+    get hasLabel() {
+        return this.label !== undefined || this.labelSlot !== null;
+    }
+    /**
+     * Renders the border container
+     * when fill="outline".
+     */
+    renderLabelContainer() {
+        const mode = getIonMode(this);
+        const hasOutlineFill = mode === 'md' && this.fill === 'outline';
+        if (hasOutlineFill) {
+            /**
+             * The outline fill has a special outline
+             * that appears around the input and the label.
+             * Certain stacked and floating label placements cause the
+             * label to translate up and create a "cut out"
+             * inside of that border by using the notch-spacer element.
+             */
+            return [
+                h("div", { class: "input-outline-container" }, h("div", { class: "input-outline-start" }), h("div", { class: {
+                        'input-outline-notch': true,
+                        'input-outline-notch-hidden': !this.hasLabel,
+                    } }, h("div", { class: "notch-spacer", "aria-hidden": "true", ref: (el) => (this.notchSpacerEl = el) }, this.label)), h("div", { class: "input-outline-end" })),
+                this.renderLabel(),
+            ];
+        }
+        /**
+         * If not using the outline style,
+         * we can render just the label.
+         */
+        return this.renderLabel();
+    }
+    render() {
+        const { disabled, fill, readonly, shape, inputId, labelPlacement, el, hasFocus, clearInputIcon } = this;
+        const mode = getIonMode(this);
+        const value = this.getValue();
+        const inItem = hostContext('ion-item', this.el);
+        const shouldRenderHighlight = mode === 'md' && fill !== 'outline' && !inItem;
+        const defaultClearIcon = mode === 'ios' ? closeCircle : closeSharp;
+        const clearIconData = clearInputIcon !== null && clearInputIcon !== void 0 ? clearInputIcon : defaultClearIcon;
+        const hasValue = this.hasValue();
+        const hasStartEndSlots = el.querySelector('[slot="start"], [slot="end"]') !== null;
+        /**
+         * If the label is stacked, it should always sit above the input.
+         * For floating labels, the label should move above the input if
+         * the input has a value, is focused, or has anything in either
+         * the start or end slot.
+         *
+         * If there is content in the start slot, the label would overlap
+         * it if not forced to float. This is also applied to the end slot
+         * because with the default or solid fills, the input is not
+         * vertically centered in the container, but the label is. This
+         * causes the slots and label to appear vertically offset from each
+         * other when the label isn't floating above the input. This doesn't
+         * apply to the outline fill, but this was not accounted for to keep
+         * things consistent.
+         *
+         * TODO(FW-5592): Remove hasStartEndSlots condition
+         */
+        const labelShouldFloat = labelPlacement === 'stacked' || (labelPlacement === 'floating' && (hasValue || hasFocus || hasStartEndSlots));
+        return (h(Host, { key: '907ce98a82b5cfae5a08504cd79e00a2330b7444', class: createColorClasses(this.color, {
+                [mode]: true,
+                'has-value': hasValue,
+                'has-focus': hasFocus,
+                'label-floating': labelShouldFloat,
+                [`input-fill-${fill}`]: fill !== undefined,
+                [`input-shape-${shape}`]: shape !== undefined,
+                [`input-label-placement-${labelPlacement}`]: true,
+                'in-item': inItem,
+                'in-item-color': hostContext('ion-item.ion-color', this.el),
+                'input-disabled': disabled,
+            }) }, h("label", { key: '59d5bb45d2a5b828bba0ed8687a632a551e2f4d8', class: "input-wrapper", htmlFor: inputId }, this.renderLabelContainer(), h("div", { key: 'f93f129d08246d0e9a601c100d718534d6403853', class: "native-wrapper" }, h("slot", { key: '54eeb1a6bace662b7eb0d7e27180ea3d7e3a3729', name: "start" }), h("input", Object.assign({ key: 'b3e0be55bc1a4a539ae3b0fdcf7fc078723cca16', class: "native-input", ref: (input) => (this.nativeInput = input), id: inputId, disabled: disabled, autoCapitalize: this.autocapitalize, autoComplete: this.autocomplete, autoCorrect: this.autocorrect, autoFocus: this.autofocus, enterKeyHint: this.enterkeyhint, inputMode: this.inputmode, min: this.min, max: this.max, minLength: this.minlength, maxLength: this.maxlength, multiple: this.multiple, name: this.name, pattern: this.pattern, placeholder: this.placeholder || '', readOnly: readonly, required: this.required, spellcheck: this.spellcheck, step: this.step, type: this.type, value: value, onInput: this.onInput, onChange: this.onChange, onBlur: this.onBlur, onFocus: this.onFocus, onKeyDown: this.onKeydown, onCompositionstart: this.onCompositionStart, onCompositionend: this.onCompositionEnd }, this.inheritedAttributes)), this.clearInput && !readonly && !disabled && (h("button", { key: '5f6373504a6d0d074bfbf875c794d45ea2748175', "aria-label": "reset", type: "button", class: "input-clear-icon", onPointerDown: (ev) => {
+                /**
+                 * This prevents mobile browsers from
+                 * blurring the input when the clear
+                 * button is activated.
+                 */
+                ev.preventDefault();
+            }, onFocusin: (ev) => {
+                /**
+                 * Prevent the focusin event from bubbling otherwise it will cause the focusin
+                 * event listener in scroll assist to fire. When this fires, focus will be moved
+                 * back to the input even if the clear button was never tapped. This poses issues
+                 * for screen readers as it means users would be unable to swipe past the clear button.
+                 */
+                ev.stopPropagation();
+            }, onClick: this.clearTextInput }, h("ion-icon", { key: '230d77973aa83458ceb32bf52e3abe9bc322cfe6', "aria-hidden": "true", icon: clearIconData }))), h("slot", { key: '9d69ac6e8a3c4b2b303dba2478f82695d5755ed2', name: "end" })), shouldRenderHighlight && h("div", { key: 'ac61f16237ce731e0745ab72d0fc3f066252464a', class: "input-highlight" })), this.renderBottomContent()));
+    }
+    static get is() { return "ion-input"; }
+    static get encapsulation() { return "scoped"; }
+    static get originalStyleUrls() {
+        return {
+            "ios": ["input.ios.scss"],
+            "md": ["input.md.scss"]
+        };
+    }
+    static get styleUrls() {
+        return {
+            "ios": ["input.ios.css"],
+            "md": ["input.md.css"]
+        };
+    }
+    static get properties() {
+        return {
+            "color": {
+                "type": "string",
+                "mutable": false,
+                "complexType": {
+                    "original": "Color",
+                    "resolved": "\"danger\" | \"dark\" | \"light\" | \"medium\" | \"primary\" | \"secondary\" | \"success\" | \"tertiary\" | \"warning\" | string & Record<never, never> | undefined",
+                    "references": {
+                        "Color": {
+                            "location": "import",
+                            "path": "../../interface",
+                            "id": "src/interface.d.ts::Color"
+                        }
+                    }
+                },
+                "required": false,
+                "optional": true,
+                "docs": {
+                    "tags": [],
+                    "text": "The color to use from your application's color palette.\nDefault options are: `\"primary\"`, `\"secondary\"`, `\"tertiary\"`, `\"success\"`, `\"warning\"`, `\"danger\"`, `\"light\"`, `\"medium\"`, and `\"dark\"`.\nFor more information on colors, see [theming](/docs/theming/basics)."
+                },
+                "attribute": "color",
+                "reflect": true
+            },
+            "autocapitalize": {
+                "type": "string",
+                "mutable": false,
+                "complexType": {
+                    "original": "string",
+                    "resolved": "string",
+                    "references": {}
+                },
+                "required": false,
+                "optional": false,
+                "docs": {
+                    "tags": [],
+                    "text": "Indicates whether and how the text value should be automatically capitalized as it is entered/edited by the user.\nAvailable options: `\"off\"`, `\"none\"`, `\"on\"`, `\"sentences\"`, `\"words\"`, `\"characters\"`."
+                },
+                "attribute": "autocapitalize",
+                "reflect": false,
+                "defaultValue": "'off'"
+            },
+            "autocomplete": {
+                "type": "string",
+                "mutable": false,
+                "complexType": {
+                    "original": "AutocompleteTypes",
+                    "resolved": "\"name\" | \"email\" | \"tel\" | \"url\" | \"on\" | \"off\" | \"honorific-prefix\" | \"given-name\" | \"additional-name\" | \"family-name\" | \"honorific-suffix\" | \"nickname\" | \"username\" | \"new-password\" | \"current-password\" | \"one-time-code\" | \"organization-title\" | \"organization\" | \"street-address\" | \"address-line1\" | \"address-line2\" | \"address-line3\" | \"address-level4\" | \"address-level3\" | \"address-level2\" | \"address-level1\" | \"country\" | \"country-name\" | \"postal-code\" | \"cc-name\" | \"cc-given-name\" | \"cc-additional-name\" | \"cc-family-name\" | \"cc-number\" | \"cc-exp\" | \"cc-exp-month\" | \"cc-exp-year\" | \"cc-csc\" | \"cc-type\" | \"transaction-currency\" | \"transaction-amount\" | \"language\" | \"bday\" | \"bday-day\" | \"bday-month\" | \"bday-year\" | \"sex\" | \"tel-country-code\" | \"tel-national\" | \"tel-area-code\" | \"tel-local\" | \"tel-extension\" | \"impp\" | \"photo\"",
+                    "references": {
+                        "AutocompleteTypes": {
+                            "location": "import",
+                            "path": "../../interface",
+                            "id": "src/interface.d.ts::AutocompleteTypes"
+                        }
+                    }
+                },
+                "required": false,
+                "optional": false,
+                "docs": {
+                    "tags": [],
+                    "text": "Indicates whether the value of the control can be automatically completed by the browser."
+                },
+                "attribute": "autocomplete",
+                "reflect": false,
+                "defaultValue": "'off'"
+            },
+            "autocorrect": {
+                "type": "string",
+                "mutable": false,
+                "complexType": {
+                    "original": "'on' | 'off'",
+                    "resolved": "\"off\" | \"on\"",
+                    "references": {}
+                },
+                "required": false,
+                "optional": false,
+                "docs": {
+                    "tags": [],
+                    "text": "Whether auto correction should be enabled when the user is entering/editing the text value."
+                },
+                "attribute": "autocorrect",
+                "reflect": false,
+                "defaultValue": "'off'"
+            },
+            "autofocus": {
+                "type": "boolean",
+                "mutable": false,
+                "complexType": {
+                    "original": "boolean",
+                    "resolved": "boolean",
+                    "references": {}
+                },
+                "required": false,
+                "optional": false,
+                "docs": {
+                    "tags": [],
+                    "text": "Sets the [`autofocus` attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/autofocus) on the native input element.\n\nThis may not be sufficient for the element to be focused on page load. See [managing focus](/docs/developing/managing-focus) for more information."
+                },
+                "attribute": "autofocus",
+                "reflect": false,
+                "defaultValue": "false"
+            },
+            "clearInput": {
+                "type": "boolean",
+                "mutable": false,
+                "complexType": {
+                    "original": "boolean",
+                    "resolved": "boolean",
+                    "references": {}
+                },
+                "required": false,
+                "optional": false,
+                "docs": {
+                    "tags": [],
+                    "text": "If `true`, a clear icon will appear in the input when there is a value. Clicking it clears the input."
+                },
+                "attribute": "clear-input",
+                "reflect": false,
+                "defaultValue": "false"
+            },
+            "clearInputIcon": {
+                "type": "string",
+                "mutable": false,
+                "complexType": {
+                    "original": "string",
+                    "resolved": "string | undefined",
+                    "references": {}
+                },
+                "required": false,
+                "optional": true,
+                "docs": {
+                    "tags": [],
+                    "text": "The icon to use for the clear button. Only applies when `clearInput` is set to `true`."
+                },
+                "attribute": "clear-input-icon",
+                "reflect": false
+            },
+            "clearOnEdit": {
+                "type": "boolean",
+                "mutable": false,
+                "complexType": {
+                    "original": "boolean",
+                    "resolved": "boolean | undefined",
+                    "references": {}
+                },
+                "required": false,
+                "optional": true,
+                "docs": {
+                    "tags": [],
+                    "text": "If `true`, the value will be cleared after focus upon edit. Defaults to `true` when `type` is `\"password\"`, `false` for all other types."
+                },
+                "attribute": "clear-on-edit",
+                "reflect": false
+            },
+            "counter": {
+                "type": "boolean",
+                "mutable": false,
+                "complexType": {
+                    "original": "boolean",
+                    "resolved": "boolean",
+                    "references": {}
+                },
+                "required": false,
+                "optional": false,
+                "docs": {
+                    "tags": [],
+                    "text": "If `true`, a character counter will display the ratio of characters used and the total character limit. Developers must also set the `maxlength` property for the counter to be calculated correctly."
+                },
+                "attribute": "counter",
+                "reflect": false,
+                "defaultValue": "false"
+            },
+            "counterFormatter": {
+                "type": "unknown",
+                "mutable": false,
+                "complexType": {
+                    "original": "(inputLength: number, maxLength: number) => string",
+                    "resolved": "((inputLength: number, maxLength: number) => string) | undefined",
+                    "references": {}
+                },
+                "required": false,
+                "optional": true,
+                "docs": {
+                    "tags": [],
+                    "text": "A callback used to format the counter text.\nBy default the counter text is set to \"itemLength / maxLength\".\n\nSee https://ionicframework.com/docs/troubleshooting/runtime#accessing-this\nif you need to access `this` from within the callback."
+                }
+            },
+            "debounce": {
+                "type": "number",
+                "mutable": false,
+                "complexType": {
+                    "original": "number",
+                    "resolved": "number | undefined",
+                    "references": {}
+                },
+                "required": false,
+                "optional": true,
+                "docs": {
+                    "tags": [],
+                    "text": "Set the amount of time, in milliseconds, to wait to trigger the `ionInput` event after each keystroke."
+                },
+                "attribute": "debounce",
+                "reflect": false
+            },
+            "disabled": {
+                "type": "boolean",
+                "mutable": false,
+                "complexType": {
+                    "original": "boolean",
+                    "resolved": "boolean",
+                    "references": {}
+                },
+                "required": false,
+                "optional": false,
+                "docs": {
+                    "tags": [],
+                    "text": "If `true`, the user cannot interact with the input."
+                },
+                "attribute": "disabled",
+                "reflect": true,
+                "defaultValue": "false"
+            },
+            "enterkeyhint": {
+                "type": "string",
+                "mutable": false,
+                "complexType": {
+                    "original": "'enter' | 'done' | 'go' | 'next' | 'previous' | 'search' | 'send'",
+                    "resolved": "\"done\" | \"enter\" | \"go\" | \"next\" | \"previous\" | \"search\" | \"send\" | undefined",
+                    "references": {}
+                },
+                "required": false,
+                "optional": true,
+                "docs": {
+                    "tags": [],
+                    "text": "A hint to the browser for which enter key to display.\nPossible values: `\"enter\"`, `\"done\"`, `\"go\"`, `\"next\"`,\n`\"previous\"`, `\"search\"`, and `\"send\"`."
+                },
+                "attribute": "enterkeyhint",
+                "reflect": false
+            },
+            "errorText": {
+                "type": "string",
+                "mutable": false,
+                "complexType": {
+                    "original": "string",
+                    "resolved": "string | undefined",
+                    "references": {}
+                },
+                "required": false,
+                "optional": true,
+                "docs": {
+                    "tags": [],
+                    "text": "Text that is placed under the input and displayed when an error is detected."
+                },
+                "attribute": "error-text",
+                "reflect": false
+            },
+            "fill": {
+                "type": "string",
+                "mutable": false,
+                "complexType": {
+                    "original": "'outline' | 'solid'",
+                    "resolved": "\"outline\" | \"solid\" | undefined",
+                    "references": {}
+                },
+                "required": false,
+                "optional": true,
+                "docs": {
+                    "tags": [],
+                    "text": "The fill for the item. If `\"solid\"` the item will have a background. If\n`\"outline\"` the item will be transparent with a border. Only available in `md` mode."
+                },
+                "attribute": "fill",
+                "reflect": false
+            },
+            "inputmode": {
+                "type": "string",
+                "mutable": false,
+                "complexType": {
+                    "original": "'none' | 'text' | 'tel' | 'url' | 'email' | 'numeric' | 'decimal' | 'search'",
+                    "resolved": "\"decimal\" | \"email\" | \"none\" | \"numeric\" | \"search\" | \"tel\" | \"text\" | \"url\" | undefined",
+                    "references": {}
+                },
+                "required": false,
+                "optional": true,
+                "docs": {
+                    "tags": [],
+                    "text": "A hint to the browser for which keyboard to display.\nPossible values: `\"none\"`, `\"text\"`, `\"tel\"`, `\"url\"`,\n`\"email\"`, `\"numeric\"`, `\"decimal\"`, and `\"search\"`."
+                },
+                "attribute": "inputmode",
+                "reflect": false
+            },
+            "helperText": {
+                "type": "string",
+                "mutable": false,
+                "complexType": {
+                    "original": "string",
+                    "resolved": "string | undefined",
+                    "references": {}
+                },
+                "required": false,
+                "optional": true,
+                "docs": {
+                    "tags": [],
+                    "text": "Text that is placed under the input and displayed when no error is detected."
+                },
+                "attribute": "helper-text",
+                "reflect": false
+            },
+            "label": {
+                "type": "string",
+                "mutable": false,
+                "complexType": {
+                    "original": "string",
+                    "resolved": "string | undefined",
+                    "references": {}
+                },
+                "required": false,
+                "optional": true,
+                "docs": {
+                    "tags": [],
+                    "text": "The visible label associated with the input.\n\nUse this if you need to render a plaintext label.\n\nThe `label` property will take priority over the `label` slot if both are used."
+                },
+                "attribute": "label",
+                "reflect": false
+            },
+            "labelPlacement": {
+                "type": "string",
+                "mutable": false,
+                "complexType": {
+                    "original": "'start' | 'end' | 'floating' | 'stacked' | 'fixed'",
+                    "resolved": "\"end\" | \"fixed\" | \"floating\" | \"stacked\" | \"start\"",
+                    "references": {}
+                },
+                "required": false,
+                "optional": false,
+                "docs": {
+                    "tags": [],
+                    "text": "Where to place the label relative to the input.\n`\"start\"`: The label will appear to the left of the input in LTR and to the right in RTL.\n`\"end\"`: The label will appear to the right of the input in LTR and to the left in RTL.\n`\"floating\"`: The label will appear smaller and above the input when the input is focused or it has a value. Otherwise it will appear on top of the input.\n`\"stacked\"`: The label will appear smaller and above the input regardless even when the input is blurred or has no value.\n`\"fixed\"`: The label has the same behavior as `\"start\"` except it also has a fixed width. Long text will be truncated with ellipses (\"...\")."
+                },
+                "attribute": "label-placement",
+                "reflect": false,
+                "defaultValue": "'start'"
+            },
+            "max": {
+                "type": "any",
+                "mutable": false,
+                "complexType": {
+                    "original": "string | number",
+                    "resolved": "number | string | undefined",
+                    "references": {}
+                },
+                "required": false,
+                "optional": true,
+                "docs": {
+                    "tags": [],
+                    "text": "The maximum value, which must not be less than its minimum (min attribute) value."
+                },
+                "attribute": "max",
+                "reflect": false
+            },
+            "maxlength": {
+                "type": "number",
+                "mutable": false,
+                "complexType": {
+                    "original": "number",
+                    "resolved": "number | undefined",
+                    "references": {}
+                },
+                "required": false,
+                "optional": true,
+                "docs": {
+                    "tags": [],
+                    "text": "If the value of the type attribute is `text`, `email`, `search`, `password`, `tel`, or `url`, this attribute specifies the maximum number of characters that the user can enter."
+                },
+                "attribute": "maxlength",
+                "reflect": false
+            },
+            "min": {
+                "type": "any",
+                "mutable": false,
+                "complexType": {
+                    "original": "string | number",
+                    "resolved": "number | string | undefined",
+                    "references": {}
+                },
+                "required": false,
+                "optional": true,
+                "docs": {
+                    "tags": [],
+                    "text": "The minimum value, which must not be greater than its maximum (max attribute) value."
+                },
+                "attribute": "min",
+                "reflect": false
+            },
+            "minlength": {
+                "type": "number",
+                "mutable": false,
+                "complexType": {
+                    "original": "number",
+                    "resolved": "number | undefined",
+                    "references": {}
+                },
+                "required": false,
+                "optional": true,
+                "docs": {
+                    "tags": [],
+                    "text": "If the value of the type attribute is `text`, `email`, `search`, `password`, `tel`, or `url`, this attribute specifies the minimum number of characters that the user can enter."
+                },
+                "attribute": "minlength",
+                "reflect": false
+            },
+            "multiple": {
+                "type": "boolean",
+                "mutable": false,
+                "complexType": {
+                    "original": "boolean",
+                    "resolved": "boolean | undefined",
+                    "references": {}
+                },
+                "required": false,
+                "optional": true,
+                "docs": {
+                    "tags": [],
+                    "text": "If `true`, the user can enter more than one value. This attribute applies when the type attribute is set to `\"email\"`, otherwise it is ignored."
+                },
+                "attribute": "multiple",
+                "reflect": false
+            },
+            "name": {
+                "type": "string",
+                "mutable": false,
+                "complexType": {
+                    "original": "string",
+                    "resolved": "string",
+                    "references": {}
+                },
+                "required": false,
+                "optional": false,
+                "docs": {
+                    "tags": [],
+                    "text": "The name of the control, which is submitted with the form data."
+                },
+                "attribute": "name",
+                "reflect": false,
+                "defaultValue": "this.inputId"
+            },
+            "pattern": {
+                "type": "string",
+                "mutable": false,
+                "complexType": {
+                    "original": "string",
+                    "resolved": "string | undefined",
+                    "references": {}
+                },
+                "required": false,
+                "optional": true,
+                "docs": {
+                    "tags": [],
+                    "text": "A regular expression that the value is checked against. The pattern must match the entire value, not just some subset. Use the title attribute to describe the pattern to help the user. This attribute applies when the value of the type attribute is `\"text\"`, `\"search\"`, `\"tel\"`, `\"url\"`, `\"email\"`, `\"date\"`, or `\"password\"`, otherwise it is ignored. When the type attribute is `\"date\"`, `pattern` will only be used in browsers that do not support the `\"date\"` input type natively. See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/date for more information."
+                },
+                "attribute": "pattern",
+                "reflect": false
+            },
+            "placeholder": {
+                "type": "string",
+                "mutable": false,
+                "complexType": {
+                    "original": "string",
+                    "resolved": "string | undefined",
+                    "references": {}
+                },
+                "required": false,
+                "optional": true,
+                "docs": {
+                    "tags": [],
+                    "text": "Instructional text that shows before the input has a value.\nThis property applies only when the `type` property is set to `\"email\"`,\n`\"number\"`, `\"password\"`, `\"search\"`, `\"tel\"`, `\"text\"`, or `\"url\"`, otherwise it is ignored."
+                },
+                "attribute": "placeholder",
+                "reflect": false
+            },
+            "readonly": {
+                "type": "boolean",
+                "mutable": false,
+                "complexType": {
+                    "original": "boolean",
+                    "resolved": "boolean",
+                    "references": {}
+                },
+                "required": false,
+                "optional": false,
+                "docs": {
+                    "tags": [],
+                    "text": "If `true`, the user cannot modify the value."
+                },
+                "attribute": "readonly",
+                "reflect": true,
+                "defaultValue": "false"
+            },
+            "required": {
+                "type": "boolean",
+                "mutable": false,
+                "complexType": {
+                    "original": "boolean",
+                    "resolved": "boolean",
+                    "references": {}
+                },
+                "required": false,
+                "optional": false,
+                "docs": {
+                    "tags": [],
+                    "text": "If `true`, the user must fill in a value before submitting a form."
+                },
+                "attribute": "required",
+                "reflect": false,
+                "defaultValue": "false"
+            },
+            "shape": {
+                "type": "string",
+                "mutable": false,
+                "complexType": {
+                    "original": "'round'",
+                    "resolved": "\"round\" | undefined",
+                    "references": {}
+                },
+                "required": false,
+                "optional": true,
+                "docs": {
+                    "tags": [],
+                    "text": "The shape of the input. If \"round\" it will have an increased border radius."
+                },
+                "attribute": "shape",
+                "reflect": false
+            },
+            "spellcheck": {
+                "type": "boolean",
+                "mutable": false,
+                "complexType": {
+                    "original": "boolean",
+                    "resolved": "boolean",
+                    "references": {}
+                },
+                "required": false,
+                "optional": false,
+                "docs": {
+                    "tags": [],
+                    "text": "If `true`, the element will have its spelling and grammar checked."
+                },
+                "attribute": "spellcheck",
+                "reflect": false,
+                "defaultValue": "false"
+            },
+            "step": {
+                "type": "string",
+                "mutable": false,
+                "complexType": {
+                    "original": "string",
+                    "resolved": "string | undefined",
+                    "references": {}
+                },
+                "required": false,
+                "optional": true,
+                "docs": {
+                    "tags": [],
+                    "text": "Works with the min and max attributes to limit the increments at which a value can be set.\nPossible values are: `\"any\"` or a positive floating point number."
+                },
+                "attribute": "step",
+                "reflect": false
+            },
+            "type": {
+                "type": "string",
+                "mutable": false,
+                "complexType": {
+                    "original": "TextFieldTypes",
+                    "resolved": "\"date\" | \"datetime-local\" | \"email\" | \"month\" | \"number\" | \"password\" | \"search\" | \"tel\" | \"text\" | \"time\" | \"url\" | \"week\"",
+                    "references": {
+                        "TextFieldTypes": {
+                            "location": "import",
+                            "path": "../../interface",
+                            "id": "src/interface.d.ts::TextFieldTypes"
+                        }
+                    }
+                },
+                "required": false,
+                "optional": false,
+                "docs": {
+                    "tags": [],
+                    "text": "The type of control to display. The default type is text."
+                },
+                "attribute": "type",
+                "reflect": false,
+                "defaultValue": "'text'"
+            },
+            "value": {
+                "type": "any",
+                "mutable": true,
+                "complexType": {
+                    "original": "string | number | null",
+                    "resolved": "null | number | string | undefined",
+                    "references": {}
+                },
+                "required": false,
+                "optional": true,
+                "docs": {
+                    "tags": [],
+                    "text": "The value of the input."
+                },
+                "attribute": "value",
+                "reflect": false,
+                "defaultValue": "''"
+            }
+        };
+    }
+    static get states() {
+        return {
+            "hasFocus": {}
+        };
+    }
+    static get events() {
+        return [{
+                "method": "ionInput",
+                "name": "ionInput",
+                "bubbles": true,
+                "cancelable": true,
+                "composed": true,
+                "docs": {
+                    "tags": [],
+                    "text": "The `ionInput` event is fired each time the user modifies the input's value.\nUnlike the `ionChange` event, the `ionInput` event is fired for each alteration\nto the input's value. This typically happens for each keystroke as the user types.\n\nFor elements that accept text input (`type=text`, `type=tel`, etc.), the interface\nis [`InputEvent`](https://developer.mozilla.org/en-US/docs/Web/API/InputEvent); for others,\nthe interface is [`Event`](https://developer.mozilla.org/en-US/docs/Web/API/Event). If\nthe input is cleared on edit, the type is `null`."
+                },
+                "complexType": {
+                    "original": "InputInputEventDetail",
+                    "resolved": "InputInputEventDetail",
+                    "references": {
+                        "InputInputEventDetail": {
+                            "location": "import",
+                            "path": "./input-interface",
+                            "id": "src/components/input/input-interface.ts::InputInputEventDetail"
+                        }
+                    }
+                }
+            }, {
+                "method": "ionChange",
+                "name": "ionChange",
+                "bubbles": true,
+                "cancelable": true,
+                "composed": true,
+                "docs": {
+                    "tags": [],
+                    "text": "The `ionChange` event is fired when the user modifies the input's value.\nUnlike the `ionInput` event, the `ionChange` event is only fired when changes\nare committed, not as the user types.\n\nDepending on the way the users interacts with the element, the `ionChange`\nevent fires at a different moment:\n- When the user commits the change explicitly (e.g. by selecting a date\nfrom a date picker for `<ion-input type=\"date\">`, pressing the \"Enter\" key, etc.).\n- When the element loses focus after its value has changed: for elements\nwhere the user's interaction is typing.\n\nThis event will not emit when programmatically setting the `value` property."
+                },
+                "complexType": {
+                    "original": "InputChangeEventDetail",
+                    "resolved": "InputChangeEventDetail",
+                    "references": {
+                        "InputChangeEventDetail": {
+                            "location": "import",
+                            "path": "./input-interface",
+                            "id": "src/components/input/input-interface.ts::InputChangeEventDetail"
+                        }
+                    }
+                }
+            }, {
+                "method": "ionBlur",
+                "name": "ionBlur",
+                "bubbles": true,
+                "cancelable": true,
+                "composed": true,
+                "docs": {
+                    "tags": [],
+                    "text": "Emitted when the input loses focus."
+                },
+                "complexType": {
+                    "original": "FocusEvent",
+                    "resolved": "FocusEvent",
+                    "references": {
+                        "FocusEvent": {
+                            "location": "global",
+                            "id": "global::FocusEvent"
+                        }
+                    }
+                }
+            }, {
+                "method": "ionFocus",
+                "name": "ionFocus",
+                "bubbles": true,
+                "cancelable": true,
+                "composed": true,
+                "docs": {
+                    "tags": [],
+                    "text": "Emitted when the input has focus."
+                },
+                "complexType": {
+                    "original": "FocusEvent",
+                    "resolved": "FocusEvent",
+                    "references": {
+                        "FocusEvent": {
+                            "location": "global",
+                            "id": "global::FocusEvent"
+                        }
+                    }
+                }
+            }];
+    }
+    static get methods() {
+        return {
+            "setFocus": {
+                "complexType": {
+                    "signature": "() => Promise<void>",
+                    "parameters": [],
+                    "references": {
+                        "Promise": {
+                            "location": "global",
+                            "id": "global::Promise"
+                        }
+                    },
+                    "return": "Promise<void>"
+                },
+                "docs": {
+                    "text": "Sets focus on the native `input` in `ion-input`. Use this method instead of the global\n`input.focus()`.\n\nDevelopers who wish to focus an input when a page enters\nshould call `setFocus()` in the `ionViewDidEnter()` lifecycle method.\n\nDevelopers who wish to focus an input when an overlay is presented\nshould call `setFocus` after `didPresent` has resolved.\n\nSee [managing focus](/docs/developing/managing-focus) for more information.",
+                    "tags": []
+                }
+            },
+            "getInputElement": {
+                "complexType": {
+                    "signature": "() => Promise<HTMLInputElement>",
+                    "parameters": [],
+                    "references": {
+                        "Promise": {
+                            "location": "global",
+                            "id": "global::Promise"
+                        },
+                        "HTMLInputElement": {
+                            "location": "global",
+                            "id": "global::HTMLInputElement"
+                        }
+                    },
+                    "return": "Promise<HTMLInputElement>"
+                },
+                "docs": {
+                    "text": "Returns the native `<input>` element used under the hood.",
+                    "tags": []
+                }
+            }
+        };
+    }
+    static get elementRef() { return "el"; }
+    static get watchers() {
+        return [{
+                "propName": "debounce",
+                "methodName": "debounceChanged"
+            }, {
+                "propName": "type",
+                "methodName": "onTypeChange"
+            }, {
+                "propName": "value",
+                "methodName": "valueChanged"
+            }];
+    }
+}
+let inputIds = 0;

+ 1276 - 0
src/node_modules/@ionic/core/dist/collection/components/input/input.md.css

@@ -0,0 +1,1276 @@
+/**
+ * Convert a font size to a dynamic font size.
+ * Fonts that participate in Dynamic Type should use
+ * dynamic font sizes.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param unit (optional) - The unit to convert to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * Convert a font size to a dynamic font size but impose
+ * a maximum font size.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param maxScale - The maximum scale of the font (i.e. 2.5 for a maximum 250% scale).
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * Convert a font size to a dynamic font size but impose
+ * a minimum font size.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param minScale - The minimum scale of the font (i.e. 0.8 for a minimum 80% scale).
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * Convert a font size to a dynamic font size but impose
+ * maximum and minimum font sizes.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param minScale - The minimum scale of the font (i.e. 0.8 for a minimum 80% scale).
+ * @param maxScale - The maximum scale of the font (i.e. 2.5 for a maximum 250% scale).
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * A heuristic that applies CSS to tablet
+ * viewports.
+ *
+ * Usage:
+ * @include tablet-viewport() {
+ *   :host {
+ *     background-color: green;
+ *   }
+ * }
+ */
+/**
+ * A heuristic that applies CSS to mobile
+ * viewports (i.e. phones, not tablets).
+ *
+ * Usage:
+ * @include mobile-viewport() {
+ *   :host {
+ *     background-color: blue;
+ *   }
+ * }
+ */
+:host {
+  /**
+   * @prop --background: Background of the input
+   *
+   * @prop --color: Color of the input text
+   *
+   * @prop --padding-top: Top padding of the input
+   * @prop --padding-end: Right padding if direction is left-to-right, and left padding if direction is right-to-left of the input
+   * @prop --padding-bottom: Bottom padding of the input
+   * @prop --padding-start: Left padding if direction is left-to-right, and right padding if direction is right-to-left of the input
+   *
+   * @prop --placeholder-color: Color of the input placeholder text
+   * @prop --placeholder-font-style: Font style of the input placeholder text
+   * @prop --placeholder-font-weight: Font weight of the input placeholder text
+   * @prop --placeholder-opacity: Opacity of the input placeholder text
+   *
+   * @prop --highlight-height: The height of the highlight on the input. Only applies to md mode.
+   * @prop --highlight-color-focused: The color of the highlight on the input when focused
+   * @prop --highlight-color-valid: The color of the highlight on the input when valid
+   * @prop --highlight-color-invalid: The color of the highlight on the input when invalid
+   *
+   * @prop --border-color: Color of the border below the input when using helper text, error text, or counter
+   * @prop --border-radius: Radius of the input. A large radius may display unevenly when using fill="outline"; if needed, use shape="round" instead or increase --padding-start.
+   * @prop --border-style: Style of the border below the input when using helper text, error text, or counter
+   * @prop --border-width: Width of the border below the input when using helper text, error text, or counter
+   */
+  --placeholder-color: initial;
+  --placeholder-font-style: initial;
+  --placeholder-font-weight: initial;
+  --placeholder-opacity: var(--ion-placeholder-opacity, 0.6);
+  --padding-top: 0px;
+  --padding-end: 0px;
+  --padding-bottom: 0px;
+  --padding-start: 0px;
+  --background: transparent;
+  --color: initial;
+  --border-style: solid;
+  --highlight-color-focused: var(--ion-color-primary, #0054e9);
+  --highlight-color-valid: var(--ion-color-success, #2dd55b);
+  --highlight-color-invalid: var(--ion-color-danger, #c5000f);
+  /**
+   * This is a private API that is used to switch
+   * out the highlight color based on the state
+   * of the component without having to write
+   * different selectors for different fill variants.
+   */
+  --highlight-color: var(--highlight-color-focused);
+  display: block;
+  position: relative;
+  width: 100%;
+  min-height: 44px;
+  /* stylelint-disable-next-line all */
+  padding: 0 !important;
+  color: var(--color);
+  font-family: var(--ion-font-family, inherit);
+  z-index: 2;
+}
+
+:host-context(ion-item)[slot=start],
+:host-context(ion-item)[slot=end] {
+  width: auto;
+}
+
+:host(.ion-color) {
+  --highlight-color-focused: var(--ion-color-base);
+}
+
+/**
+ * Since the label sits on top of the element,
+ * the component needs to be taller otherwise the
+ * label will appear too close to the input text.
+ */
+:host(.input-label-placement-floating),
+:host(.input-label-placement-stacked) {
+  min-height: 56px;
+}
+
+.native-input {
+  padding-left: 0;
+  padding-right: 0;
+  padding-top: 0;
+  padding-bottom: 0;
+  font-family: inherit;
+  font-size: inherit;
+  font-style: inherit;
+  font-weight: inherit;
+  letter-spacing: inherit;
+  text-decoration: inherit;
+  text-indent: inherit;
+  text-overflow: inherit;
+  text-transform: inherit;
+  text-align: inherit;
+  white-space: inherit;
+  color: inherit;
+  display: inline-block;
+  position: relative;
+  flex: 1;
+  width: 100%;
+  max-width: 100%;
+  max-height: 100%;
+  border: 0;
+  outline: none;
+  background: transparent;
+  box-sizing: border-box;
+  appearance: none;
+  /**
+   * This ensures the input
+   * remains on top of any decoration
+   * that we render (particularly the
+   * outline border when fill="outline").
+   * If we did not do this then Axe would
+   * be unable to determine the color
+   * contrast of the input.
+   */
+  z-index: 1;
+}
+.native-input::placeholder {
+  color: var(--placeholder-color);
+  font-family: inherit;
+  font-style: var(--placeholder-font-style);
+  font-weight: var(--placeholder-font-weight);
+  opacity: var(--placeholder-opacity);
+}
+.native-input:-webkit-autofill {
+  background-color: transparent;
+}
+.native-input:invalid {
+  box-shadow: none;
+}
+.native-input::-ms-clear {
+  display: none;
+}
+
+.cloned-input {
+  top: 0;
+  bottom: 0;
+  position: absolute;
+  pointer-events: none;
+}
+.cloned-input {
+  inset-inline-start: 0;
+}
+
+/**
+ * The cloned input needs to be disabled on
+ * Android otherwise the viewport will still
+ * shift when running scroll assist.
+ */
+.cloned-input:disabled {
+  opacity: 1;
+}
+
+.input-clear-icon {
+  -webkit-margin-start: auto;
+  margin-inline-start: auto;
+  -webkit-margin-end: auto;
+  margin-inline-end: auto;
+  margin-top: auto;
+  margin-bottom: auto;
+  padding-left: 0;
+  padding-right: 0;
+  padding-top: 0;
+  padding-bottom: 0;
+  background-position: center;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 30px;
+  height: 30px;
+  border: 0;
+  outline: none;
+  background-color: transparent;
+  background-repeat: no-repeat;
+  color: var(--ion-color-step-600, var(--ion-text-color-step-400, #666666));
+  visibility: hidden;
+  appearance: none;
+}
+
+:host(.in-item-color) .input-clear-icon {
+  color: inherit;
+}
+
+/**
+ * Normally, we would not want to use :focus
+ * here because that would mean tapping the button
+ * on mobile would focus it (and keep it focused).
+ * However, the clear button always disappears after
+ * being activated, so we never get to that state.
+ */
+.input-clear-icon:focus {
+  opacity: 0.5;
+}
+
+:host(.has-value) .input-clear-icon {
+  visibility: visible;
+}
+
+.input-wrapper {
+  -webkit-padding-start: var(--padding-start);
+  padding-inline-start: var(--padding-start);
+  -webkit-padding-end: var(--padding-end);
+  padding-inline-end: var(--padding-end);
+  padding-top: var(--padding-top);
+  padding-bottom: var(--padding-bottom);
+  border-radius: var(--border-radius);
+  display: flex;
+  position: relative;
+  flex-grow: 1;
+  align-items: stretch;
+  height: inherit;
+  min-height: inherit;
+  transition: background-color 15ms linear;
+  background: var(--background);
+  line-height: normal;
+}
+
+.native-wrapper {
+  display: flex;
+  position: relative;
+  flex-grow: 1;
+  align-items: center;
+  width: 100%;
+}
+
+:host(.ion-touched.ion-invalid) {
+  --highlight-color: var(--highlight-color-invalid);
+}
+
+/**
+ * The component highlight is only shown
+ * on focus, so we can safely set the valid
+ * color state when valid. If we
+ * set it when .has-focus is present then
+ * the highlight color would change
+ * from the valid color to the component's
+ * color during the transition after the
+ * component loses focus.
+ */
+:host(.ion-valid) {
+  --highlight-color: var(--highlight-color-valid);
+}
+
+.input-bottom {
+  /**
+   * The bottom content should take on the start and end
+   * padding so it is always aligned with either the label
+   * or the start of the text input.
+   */
+  -webkit-padding-start: var(--padding-start);
+  padding-inline-start: var(--padding-start);
+  -webkit-padding-end: var(--padding-end);
+  padding-inline-end: var(--padding-end);
+  padding-top: 5px;
+  padding-bottom: 0;
+  display: flex;
+  justify-content: space-between;
+  border-top: var(--border-width) var(--border-style) var(--border-color);
+  font-size: 0.75rem;
+}
+
+/**
+ * If the input has a validity state, the
+ * border and label should reflect that as a color.
+ * The invalid state should show if the input is
+ * invalid and has already been touched.
+ * The valid state should show if the input
+ * is valid, has already been touched, and
+ * is currently focused. Do not show the valid
+ * highlight when the input is blurred.
+ */
+:host(.has-focus.ion-valid),
+:host(.ion-touched.ion-invalid) {
+  --border-color: var(--highlight-color);
+}
+
+/**
+ * Error text should only be shown when .ion-invalid is
+ * present on the input. Otherwise the helper text should
+ * be shown.
+ */
+.input-bottom .error-text {
+  display: none;
+  color: var(--highlight-color-invalid);
+}
+
+.input-bottom .helper-text {
+  display: block;
+  color: var(--ion-color-step-550, var(--ion-text-color-step-450, #737373));
+}
+
+:host(.ion-touched.ion-invalid) .input-bottom .error-text {
+  display: block;
+}
+
+:host(.ion-touched.ion-invalid) .input-bottom .helper-text {
+  display: none;
+}
+
+.input-bottom .counter {
+  /**
+   * Counter should always be at
+   * the end of the container even
+   * when no helper/error texts are used.
+   */
+  -webkit-margin-start: auto;
+  margin-inline-start: auto;
+  color: var(--ion-color-step-550, var(--ion-text-color-step-450, #737373));
+  white-space: nowrap;
+  padding-inline-start: 16px;
+}
+
+:host(.has-focus) input {
+  caret-color: var(--highlight-color);
+}
+
+.label-text-wrapper {
+  /**
+   * This causes the label to take up
+   * the entire height of its container
+   * while still keeping the text centered.
+   */
+  display: flex;
+  align-items: center;
+  /**
+   * Label text should not extend
+   * beyond the bounds of the input.
+   * However, we do not set the max
+   * width to 100% because then
+   * only the label would show and users
+   * would not be able to see what they are typing.
+   */
+  max-width: 200px;
+  transition: color 150ms cubic-bezier(0.4, 0, 0.2, 1), transform 150ms cubic-bezier(0.4, 0, 0.2, 1);
+  /**
+   * This ensures that double tapping this text
+   * clicks the <label> and focuses the input
+   * when a screen reader is enabled.
+   */
+  pointer-events: none;
+}
+
+/**
+ * We need to use two elements instead of
+ * one. The .label-text-wrapper is responsible
+ * for centering the label text vertically regardless
+ * of the input height using flexbox.
+ *
+ * The .label-text element is responsible for controlling
+ * overflow when label-placement="fixed".
+ * We want the ellipses to show up when the
+ * fixed label overflows, but text-overflow: ellipsis only
+ * works on block-level elements. A flex item is
+ * considered blockified (https://www.w3.org/TR/css-display-3/#blockify).
+ */
+.label-text,
+::slotted([slot=label]) {
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  overflow: hidden;
+}
+
+/**
+ * If no label text is placed into the slot
+ * then the element should be hidden otherwise
+ * there will be additional margins added.
+ */
+.label-text-wrapper-hidden,
+.input-outline-notch-hidden {
+  display: none;
+}
+
+.input-wrapper input {
+  /**
+   * When the floating label appears on top of the
+   * input, we need to fade the input out so that the
+   * label does not overlap with the placeholder.
+   */
+  transition: opacity 150ms cubic-bezier(0.4, 0, 0.2, 1);
+}
+
+/**
+ * Label is on the left of the input in LTR and
+ * on the right in RTL.
+ */
+:host(.input-label-placement-start) .input-wrapper {
+  flex-direction: row;
+}
+
+:host(.input-label-placement-start) .label-text-wrapper {
+  /**
+   * The margin between the label and
+   * the input should be on the end
+   * when the label sits at the start.
+   */
+  -webkit-margin-start: 0;
+  margin-inline-start: 0;
+  -webkit-margin-end: 16px;
+  margin-inline-end: 16px;
+  margin-top: 0;
+  margin-bottom: 0;
+}
+
+/**
+ * Label is on the right of the input in LTR and
+ * on the left in RTL.
+ */
+:host(.input-label-placement-end) .input-wrapper {
+  flex-direction: row-reverse;
+}
+
+/**
+ * The margin between the label and
+ * the input should be on the start
+ * when the label sits at the end.
+ */
+:host(.input-label-placement-end) .label-text-wrapper {
+  -webkit-margin-start: 16px;
+  margin-inline-start: 16px;
+  -webkit-margin-end: 0;
+  margin-inline-end: 0;
+  margin-top: 0;
+  margin-bottom: 0;
+}
+
+:host(.input-label-placement-fixed) .label-text-wrapper {
+  /**
+   * The margin between the label and
+   * the input should be on the end
+   * when the label sits at the start.
+   */
+  -webkit-margin-start: 0;
+  margin-inline-start: 0;
+  -webkit-margin-end: 16px;
+  margin-inline-end: 16px;
+  margin-top: 0;
+  margin-bottom: 0;
+}
+
+/**
+ * Label is on the left of the input in LTR and
+ * on the right in RTL. Label also has a fixed width.
+ */
+:host(.input-label-placement-fixed) .label-text {
+  flex: 0 0 100px;
+  width: 100px;
+  min-width: 100px;
+  max-width: 200px;
+}
+
+/**
+ * Stacked: Label sits above the input and is scaled down.
+ * Floating: Label sits over the input when the input has no
+ * value and is blurred. Label sits above the input and is scaled
+ * down when the input is focused or has a value.
+ *
+ */
+:host(.input-label-placement-stacked) .input-wrapper,
+:host(.input-label-placement-floating) .input-wrapper {
+  flex-direction: column;
+  align-items: start;
+}
+
+/**
+ * Ensures that the label animates
+ * up and to the left in LTR or
+ * up and to the right in RTL.
+ */
+:host(.input-label-placement-stacked) .label-text-wrapper,
+:host(.input-label-placement-floating) .label-text-wrapper {
+  transform-origin: left top;
+  max-width: 100%;
+  /**
+   * The 2 ensures the label
+   * remains on top of any browser
+   * autofill background too.
+   */
+  z-index: 2;
+}
+:host-context([dir=rtl]):host(.input-label-placement-stacked) .label-text-wrapper, :host-context([dir=rtl]).input-label-placement-stacked .label-text-wrapper, :host-context([dir=rtl]):host(.input-label-placement-floating) .label-text-wrapper, :host-context([dir=rtl]).input-label-placement-floating .label-text-wrapper {
+  transform-origin: right top;
+}
+
+@supports selector(:dir(rtl)) {
+  :host(.input-label-placement-stacked:dir(rtl)) .label-text-wrapper, :host(.input-label-placement-floating:dir(rtl)) .label-text-wrapper {
+    transform-origin: right top;
+  }
+}
+
+/**
+ * Ensures the input does not
+ * overlap the label.
+ */
+:host(.input-label-placement-stacked) input,
+:host(.input-label-placement-floating) input {
+  margin-left: 0;
+  margin-right: 0;
+  margin-top: 1px;
+  margin-bottom: 0;
+}
+
+/**
+ * This makes the label sit over the input
+ * when the input is blurred and has no value.
+ */
+:host(.input-label-placement-floating) .label-text-wrapper {
+  transform: translateY(100%) scale(1);
+}
+
+/**
+ * The input should be hidden when the label
+ * is on top of the input. This prevents the label
+ * from overlapping any placeholder value.
+ */
+:host(.input-label-placement-floating) input {
+  opacity: 0;
+}
+
+:host(.has-focus.input-label-placement-floating) input,
+:host(.has-value.input-label-placement-floating) input {
+  opacity: 1;
+}
+
+/**
+ * This makes the label sit above the input.
+ */
+:host(.label-floating) .label-text-wrapper {
+  transform: translateY(50%) scale(0.75);
+  /**
+   * Label text should not extend
+   * beyond the bounds of the input.
+   */
+  max-width: calc(100% / 0.75);
+}
+
+::slotted([slot=start]:last-of-type) {
+  margin-inline-end: 16px;
+  margin-inline-start: 0;
+}
+
+::slotted([slot=end]:first-of-type) {
+  margin-inline-start: 16px;
+  margin-inline-end: 0;
+}
+
+/**
+ * The input password toggle component should be hidden when the input is readonly/disabled
+ * because it is not possible to edit a password.
+ */
+:host([disabled]) ::slotted(ion-input-password-toggle),
+:host([readonly]) ::slotted(ion-input-password-toggle) {
+  display: none;
+}
+
+/**
+ * Convert a font size to a dynamic font size.
+ * Fonts that participate in Dynamic Type should use
+ * dynamic font sizes.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param unit (optional) - The unit to convert to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * Convert a font size to a dynamic font size but impose
+ * a maximum font size.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param maxScale - The maximum scale of the font (i.e. 2.5 for a maximum 250% scale).
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * Convert a font size to a dynamic font size but impose
+ * a minimum font size.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param minScale - The minimum scale of the font (i.e. 0.8 for a minimum 80% scale).
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * Convert a font size to a dynamic font size but impose
+ * maximum and minimum font sizes.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param minScale - The minimum scale of the font (i.e. 0.8 for a minimum 80% scale).
+ * @param maxScale - The maximum scale of the font (i.e. 2.5 for a maximum 250% scale).
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * A heuristic that applies CSS to tablet
+ * viewports.
+ *
+ * Usage:
+ * @include tablet-viewport() {
+ *   :host {
+ *     background-color: green;
+ *   }
+ * }
+ */
+/**
+ * A heuristic that applies CSS to mobile
+ * viewports (i.e. phones, not tablets).
+ *
+ * Usage:
+ * @include mobile-viewport() {
+ *   :host {
+ *     background-color: blue;
+ *   }
+ * }
+ */
+/**
+ * Convert a font size to a dynamic font size.
+ * Fonts that participate in Dynamic Type should use
+ * dynamic font sizes.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param unit (optional) - The unit to convert to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * Convert a font size to a dynamic font size but impose
+ * a maximum font size.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param maxScale - The maximum scale of the font (i.e. 2.5 for a maximum 250% scale).
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * Convert a font size to a dynamic font size but impose
+ * a minimum font size.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param minScale - The minimum scale of the font (i.e. 0.8 for a minimum 80% scale).
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * Convert a font size to a dynamic font size but impose
+ * maximum and minimum font sizes.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param minScale - The minimum scale of the font (i.e. 0.8 for a minimum 80% scale).
+ * @param maxScale - The maximum scale of the font (i.e. 2.5 for a maximum 250% scale).
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * A heuristic that applies CSS to tablet
+ * viewports.
+ *
+ * Usage:
+ * @include tablet-viewport() {
+ *   :host {
+ *     background-color: green;
+ *   }
+ * }
+ */
+/**
+ * A heuristic that applies CSS to mobile
+ * viewports (i.e. phones, not tablets).
+ *
+ * Usage:
+ * @include mobile-viewport() {
+ *   :host {
+ *     background-color: blue;
+ *   }
+ * }
+ */
+/**
+ * Convert a font size to a dynamic font size.
+ * Fonts that participate in Dynamic Type should use
+ * dynamic font sizes.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param unit (optional) - The unit to convert to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * Convert a font size to a dynamic font size but impose
+ * a maximum font size.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param maxScale - The maximum scale of the font (i.e. 2.5 for a maximum 250% scale).
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * Convert a font size to a dynamic font size but impose
+ * a minimum font size.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param minScale - The minimum scale of the font (i.e. 0.8 for a minimum 80% scale).
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * Convert a font size to a dynamic font size but impose
+ * maximum and minimum font sizes.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param minScale - The minimum scale of the font (i.e. 0.8 for a minimum 80% scale).
+ * @param maxScale - The maximum scale of the font (i.e. 2.5 for a maximum 250% scale).
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * A heuristic that applies CSS to tablet
+ * viewports.
+ *
+ * Usage:
+ * @include tablet-viewport() {
+ *   :host {
+ *     background-color: green;
+ *   }
+ * }
+ */
+/**
+ * A heuristic that applies CSS to mobile
+ * viewports (i.e. phones, not tablets).
+ *
+ * Usage:
+ * @include mobile-viewport() {
+ *   :host {
+ *     background-color: blue;
+ *   }
+ * }
+ */
+:host(.input-fill-solid) {
+  --background: var(--ion-color-step-50, var(--ion-background-color-step-50, #f2f2f2));
+  --border-color: var(--ion-color-step-500, var(--ion-background-color-step-500, gray));
+  --border-radius: 4px;
+  --padding-start: 16px;
+  --padding-end: 16px;
+  min-height: 56px;
+}
+
+/**
+ * The solid fill style has a border
+ * at the bottom of the input wrapper.
+ * As a result, the border on the "bottom
+ * content" is not needed.
+ */
+:host(.input-fill-solid) .input-wrapper {
+  border-bottom: var(--border-width) var(--border-style) var(--border-color);
+}
+
+/**
+ * If the input has a validity state, the
+ * border should reflect that as a color.
+ */
+:host(.has-focus.input-fill-solid.ion-valid),
+:host(.input-fill-solid.ion-touched.ion-invalid) {
+  --border-color: var(--highlight-color);
+}
+
+:host(.input-fill-solid) .input-bottom {
+  border-top: none;
+}
+
+/**
+ * Background and border should be
+ * slightly darker on hover.
+ */
+@media (any-hover: hover) {
+  :host(.input-fill-solid:hover) {
+    --background: var(--ion-color-step-100, var(--ion-background-color-step-100, #e6e6e6));
+    --border-color: var(--ion-color-step-750, var(--ion-background-color-step-750, #404040));
+  }
+}
+/**
+ * Background and border should be
+ * much darker on focus.
+ */
+:host(.input-fill-solid.has-focus) {
+  --background: var(--ion-color-step-150, var(--ion-background-color-step-150, #d9d9d9));
+  --border-color: var(--ion-color-step-750, var(--ion-background-color-step-750, #404040));
+}
+
+:host(.input-fill-solid) .input-wrapper {
+  /**
+   * Only the top left and top right borders should.
+   * have a radius when using a solid fill.
+   */
+  border-start-start-radius: var(--border-radius);
+  border-start-end-radius: var(--border-radius);
+  border-end-end-radius: 0px;
+  border-end-start-radius: 0px;
+}
+
+:host(.label-floating.input-fill-solid.input-label-placement-floating) .label-text-wrapper {
+  /**
+   * Label text should not extend
+   * beyond the bounds of the input.
+   */
+  max-width: calc(100% / 0.75);
+}
+
+/**
+ * Convert a font size to a dynamic font size.
+ * Fonts that participate in Dynamic Type should use
+ * dynamic font sizes.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param unit (optional) - The unit to convert to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * Convert a font size to a dynamic font size but impose
+ * a maximum font size.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param maxScale - The maximum scale of the font (i.e. 2.5 for a maximum 250% scale).
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * Convert a font size to a dynamic font size but impose
+ * a minimum font size.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param minScale - The minimum scale of the font (i.e. 0.8 for a minimum 80% scale).
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * Convert a font size to a dynamic font size but impose
+ * maximum and minimum font sizes.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param minScale - The minimum scale of the font (i.e. 0.8 for a minimum 80% scale).
+ * @param maxScale - The maximum scale of the font (i.e. 2.5 for a maximum 250% scale).
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * A heuristic that applies CSS to tablet
+ * viewports.
+ *
+ * Usage:
+ * @include tablet-viewport() {
+ *   :host {
+ *     background-color: green;
+ *   }
+ * }
+ */
+/**
+ * A heuristic that applies CSS to mobile
+ * viewports (i.e. phones, not tablets).
+ *
+ * Usage:
+ * @include mobile-viewport() {
+ *   :host {
+ *     background-color: blue;
+ *   }
+ * }
+ */
+:host(.input-fill-outline) {
+  --border-color: var(--ion-color-step-300, var(--ion-background-color-step-300, #b3b3b3));
+  --border-radius: 4px;
+  --padding-start: 16px;
+  --padding-end: 16px;
+  min-height: 56px;
+}
+
+:host(.input-fill-outline.input-shape-round) {
+  --border-radius: 28px;
+  --padding-start: 32px;
+  --padding-end: 32px;
+}
+
+/**
+ * If the input has a validity state, the
+ * border should reflect that as a color.
+ */
+:host(.has-focus.input-fill-outline.ion-valid),
+:host(.input-fill-outline.ion-touched.ion-invalid) {
+  --border-color: var(--highlight-color);
+}
+
+/**
+ * Border should be
+ * slightly darker on hover.
+ */
+@media (any-hover: hover) {
+  :host(.input-fill-outline:hover) {
+    --border-color: var(--ion-color-step-750, var(--ion-background-color-step-750, #404040));
+  }
+}
+/**
+ * The border should get thicker
+ * and take on component color when
+ * the input is focused.
+ */
+:host(.input-fill-outline.has-focus) {
+  --border-width: var(--highlight-height);
+  --border-color: var(--highlight-color);
+}
+
+/**
+ * The bottom content should never have
+ * a border with the outline style.
+ */
+:host(.input-fill-outline) .input-bottom {
+  border-top: none;
+}
+
+/**
+ * Outline inputs do not have a bottom border.
+ * Instead, they have a border that wraps the
+ * input + label.
+ */
+:host(.input-fill-outline) .input-wrapper {
+  border-bottom: none;
+}
+
+:host(.input-fill-outline.input-label-placement-stacked) .label-text-wrapper,
+:host(.input-fill-outline.input-label-placement-floating) .label-text-wrapper {
+  transform-origin: left top;
+  position: absolute;
+  /**
+   * Label text should not extend
+   * beyond the bounds of the input.
+   */
+  max-width: calc(100% - var(--padding-start) - var(--padding-end));
+}
+:host-context([dir=rtl]):host(.input-fill-outline.input-label-placement-stacked) .label-text-wrapper, :host-context([dir=rtl]).input-fill-outline.input-label-placement-stacked .label-text-wrapper, :host-context([dir=rtl]):host(.input-fill-outline.input-label-placement-floating) .label-text-wrapper, :host-context([dir=rtl]).input-fill-outline.input-label-placement-floating .label-text-wrapper {
+  transform-origin: right top;
+}
+
+@supports selector(:dir(rtl)) {
+  :host(.input-fill-outline.input-label-placement-stacked:dir(rtl)) .label-text-wrapper, :host(.input-fill-outline.input-label-placement-floating:dir(rtl)) .label-text-wrapper {
+    transform-origin: right top;
+  }
+}
+
+/**
+ * The label should appear on top of an outline
+ * container that overlaps it so it is always clickable.
+ */
+:host(.input-fill-outline) .label-text-wrapper,
+:host(.input-fill-outline) .label-text-wrapper {
+  position: relative;
+}
+
+/**
+ * This makes the label sit above the input.
+ */
+:host(.label-floating.input-fill-outline) .label-text-wrapper {
+  transform: translateY(-32%) scale(0.75);
+  margin-left: 0;
+  margin-right: 0;
+  margin-top: 0;
+  margin-bottom: 0;
+  /**
+   * Label text should not extend
+   * beyond the bounds of the input.
+   */
+  max-width: calc((100% - var(--padding-start) - var(--padding-end) - 8px) / 0.75);
+}
+
+/**
+ * This ensures that the input does not
+ * overlap the floating label while still
+ * remaining visually centered.
+ */
+:host(.input-fill-outline.input-label-placement-stacked) input,
+:host(.input-fill-outline.input-label-placement-floating) input {
+  margin-left: 0;
+  margin-right: 0;
+  margin-top: 6px;
+  margin-bottom: 6px;
+}
+
+:host(.input-fill-outline) .input-outline-container {
+  left: 0;
+  right: 0;
+  top: 0;
+  bottom: 0;
+  display: flex;
+  position: absolute;
+  width: 100%;
+  height: 100%;
+}
+
+:host(.input-fill-outline) .input-outline-start,
+:host(.input-fill-outline) .input-outline-end {
+  pointer-events: none;
+}
+
+/**
+ * By default, each piece of the container should have
+ * a top and bottom border. This gives the appearance
+ * of a unified container with a border.
+ */
+:host(.input-fill-outline) .input-outline-start,
+:host(.input-fill-outline) .input-outline-notch,
+:host(.input-fill-outline) .input-outline-end {
+  border-top: var(--border-width) var(--border-style) var(--border-color);
+  border-bottom: var(--border-width) var(--border-style) var(--border-color);
+}
+
+/**
+ * Ensures long labels do not cause the notch to flow
+ * out of bounds.
+ */
+:host(.input-fill-outline) .input-outline-notch {
+  max-width: calc(100% - var(--padding-start) - var(--padding-end));
+}
+
+/**
+ * This element ensures that the notch used
+ * the size of the scaled text so that the
+ * border cut out is the correct width.
+ * The text in this element should not
+ * be interactive.
+ */
+:host(.input-fill-outline) .notch-spacer {
+  /**
+   * We need $input-md-floating-label-padding of padding on the right.
+   * However, we also subtracted $input-md-floating-label-padding from
+   * the width of .input-outline-start
+   * to create space, so we need to take
+   * that into consideration here.
+   */
+  -webkit-padding-end: 8px;
+  padding-inline-end: 8px;
+  font-size: calc(1em * 0.75);
+  opacity: 0;
+  pointer-events: none;
+  /**
+   * The spacer currently inherits
+   * border-box sizing from the Ionic reset styles.
+   * However, we do not want to include padding in
+   * the calculation of the element dimensions.
+   * This code can be removed if input is updated
+   * to use the Shadow DOM.
+   */
+  box-sizing: content-box;
+}
+
+:host(.input-fill-outline) .input-outline-start {
+  border-start-start-radius: var(--border-radius);
+  border-start-end-radius: 0px;
+  border-end-end-radius: 0px;
+  border-end-start-radius: var(--border-radius);
+  -webkit-border-start: var(--border-width) var(--border-style) var(--border-color);
+  border-inline-start: var(--border-width) var(--border-style) var(--border-color);
+  /**
+   * There should be spacing between the translated text
+   * and .input-outline-start. However, we can't add this
+   * spacing onto the notch because it would cause the
+   * label to look like it is not aligned with the
+   * text input. Instead, we subtract a few pixels from
+   * this element.
+   */
+  width: calc(var(--padding-start) - 4px);
+}
+
+:host(.input-fill-outline) .input-outline-end {
+  -webkit-border-end: var(--border-width) var(--border-style) var(--border-color);
+  border-inline-end: var(--border-width) var(--border-style) var(--border-color);
+  border-start-start-radius: 0px;
+  border-start-end-radius: var(--border-radius);
+  border-end-end-radius: var(--border-radius);
+  border-end-start-radius: 0px;
+  /**
+   * The ending outline fragment
+   * should take up the remaining free space.
+   */
+  flex-grow: 1;
+}
+
+/**
+ * When the input either has focus or a value,
+ * there should be a "cut out" at the top for
+ * the floating/stacked label. We simulate this "cut out"
+ * by removing the top border from the notch fragment.
+ */
+:host(.label-floating.input-fill-outline) .input-outline-notch {
+  border-top: none;
+}
+
+/**
+ * Convert a font size to a dynamic font size.
+ * Fonts that participate in Dynamic Type should use
+ * dynamic font sizes.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param unit (optional) - The unit to convert to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * Convert a font size to a dynamic font size but impose
+ * a maximum font size.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param maxScale - The maximum scale of the font (i.e. 2.5 for a maximum 250% scale).
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * Convert a font size to a dynamic font size but impose
+ * a minimum font size.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param minScale - The minimum scale of the font (i.e. 0.8 for a minimum 80% scale).
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * Convert a font size to a dynamic font size but impose
+ * maximum and minimum font sizes.
+ * @param size - The initial font size including the unit (i.e. px or pt)
+ * @param minScale - The minimum scale of the font (i.e. 0.8 for a minimum 80% scale).
+ * @param maxScale - The maximum scale of the font (i.e. 2.5 for a maximum 250% scale).
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
+ * convert to a unit other than $baselineUnit.
+ */
+/**
+ * A heuristic that applies CSS to tablet
+ * viewports.
+ *
+ * Usage:
+ * @include tablet-viewport() {
+ *   :host {
+ *     background-color: green;
+ *   }
+ * }
+ */
+/**
+ * A heuristic that applies CSS to mobile
+ * viewports (i.e. phones, not tablets).
+ *
+ * Usage:
+ * @include mobile-viewport() {
+ *   :host {
+ *     background-color: blue;
+ *   }
+ * }
+ */
+:host {
+  --border-width: 1px;
+  --border-color: var(--ion-item-border-color, var(--ion-border-color, var(--ion-color-step-150, var(--ion-background-color-step-150, rgba(0, 0, 0, 0.13)))));
+  --highlight-height: 2px;
+  font-size: inherit;
+}
+
+.input-clear-icon ion-icon {
+  width: 22px;
+  height: 22px;
+}
+
+:host(.input-disabled) {
+  opacity: 0.38;
+}
+
+/**
+ * If the input has a validity state, the
+ * border and label should reflect that as a color.
+ */
+:host(.has-focus.ion-valid),
+:host(.ion-touched.ion-invalid) {
+  --border-color: var(--highlight-color);
+}
+
+.input-bottom .counter {
+  letter-spacing: 0.0333333333em;
+}
+
+/**
+ * When the input is focused the label should
+ * take on the highlight color. This should
+ * only apply to floating or stacked labels.
+ */
+:host(.input-label-placement-floating.has-focus) .label-text-wrapper,
+:host(.input-label-placement-stacked.has-focus) .label-text-wrapper {
+  color: var(--highlight-color);
+}
+
+:host(.has-focus.input-label-placement-floating.ion-valid) .label-text-wrapper,
+:host(.input-label-placement-floating.ion-touched.ion-invalid) .label-text-wrapper,
+:host(.has-focus.input-label-placement-stacked.ion-valid) .label-text-wrapper,
+:host(.input-label-placement-stacked.ion-touched.ion-invalid) .label-text-wrapper {
+  color: var(--highlight-color);
+}
+
+.input-highlight {
+  bottom: -1px;
+  position: absolute;
+  width: 100%;
+  height: var(--highlight-height);
+  transform: scale(0);
+  transition: transform 200ms;
+  background: var(--highlight-color);
+}
+.input-highlight {
+  inset-inline-start: 0;
+}
+
+:host(.has-focus) .input-highlight {
+  transform: scale(1);
+}
+
+/**
+ * Adjust the highlight up by 1px
+ * so it is not cut off by the
+ * the item's line (if one is present).
+ */
+:host(.in-item) .input-highlight {
+  bottom: 0;
+}
+:host(.in-item) .input-highlight {
+  inset-inline-start: 0;
+}
+
+:host(.input-shape-round) {
+  --border-radius: 16px;
+}
+
+/**
+ * Slotted buttons have a lot of default padding that can
+ * cause them to look misaligned from other pieces such
+ * as the control's label, especially when using a clear
+ * fill. We also make them circular to ensure that non-
+ * clear buttons and the focus/hover state on clear ones
+ * don't look too crowded.
+ */
+::slotted(ion-button[slot=start].button-has-icon-only),
+::slotted(ion-button[slot=end].button-has-icon-only) {
+  --border-radius: 50%;
+  --padding-start: 8px;
+  --padding-end: 8px;
+  --padding-top: 8px;
+  --padding-bottom: 8px;
+  aspect-ratio: 1;
+  min-height: 40px;
+}

Some files were not shown because too many files changed in this diff