voyager-ionic-core 8.8.2 → 8.8.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/components/ion-action-sheet.js +1 -1
  2. package/components/ion-checkbox.js +1 -1
  3. package/components/ion-content.js +1 -1
  4. package/components/ion-datetime.js +1 -1
  5. package/components/ion-input-otp.js +1 -1
  6. package/components/ion-modal.js +1 -1
  7. package/components/ion-picker-column.js +1 -1
  8. package/components/ion-radio-group.js +1 -1
  9. package/components/ion-select-modal.js +1 -1
  10. package/components/ion-select-popover.js +1 -1
  11. package/components/ion-select.js +1 -1
  12. package/components/p-0z8QSI5b.js +4 -0
  13. package/components/{p-ApmKVjaE.js → p-BGHGpkPX.js} +1 -1
  14. package/components/p-BlNv564p.js +4 -0
  15. package/components/p-D-cP12ZN.js +4 -0
  16. package/components/p-D3Ti70Hx.js +4 -0
  17. package/components/{p-Bk2zuNWT.js → p-DvOO1fxp.js} +1 -1
  18. package/components/p-FBcnjE5W.js +4 -0
  19. package/components/p-SBseW5KJ.js +4 -0
  20. package/css/palettes/dark.always.css +1 -1
  21. package/css/palettes/dark.always.css.map +1 -1
  22. package/css/palettes/dark.class.css +1 -1
  23. package/css/palettes/dark.class.css.map +1 -1
  24. package/css/palettes/dark.system.css +1 -1
  25. package/css/palettes/dark.system.css.map +1 -1
  26. package/css/palettes/high-contrast-dark.always.css +1 -1
  27. package/css/palettes/high-contrast-dark.always.css.map +1 -1
  28. package/css/palettes/high-contrast-dark.class.css +1 -1
  29. package/css/palettes/high-contrast-dark.class.css.map +1 -1
  30. package/css/palettes/high-contrast-dark.system.css +1 -1
  31. package/css/palettes/high-contrast-dark.system.css.map +1 -1
  32. package/dist/cjs/ion-action-sheet.cjs.entry.js +4 -4
  33. package/dist/cjs/ion-app_8.cjs.entry.js +1 -1
  34. package/dist/cjs/ion-checkbox.cjs.entry.js +39 -32
  35. package/dist/cjs/ion-datetime_3.cjs.entry.js +17 -6
  36. package/dist/cjs/ion-input-otp.cjs.entry.js +21 -6
  37. package/dist/cjs/ion-modal.cjs.entry.js +99 -45
  38. package/dist/cjs/ion-picker-column.cjs.entry.js +4 -4
  39. package/dist/cjs/ion-radio_2.cjs.entry.js +13 -1
  40. package/dist/cjs/ion-select-modal.cjs.entry.js +18 -7
  41. package/dist/cjs/ion-select_3.cjs.entry.js +18 -7
  42. package/dist/collection/components/action-sheet/action-sheet.js +4 -4
  43. package/dist/collection/components/checkbox/checkbox.js +39 -32
  44. package/dist/collection/components/content/content.css +1 -1
  45. package/dist/collection/components/datetime/datetime.js +17 -6
  46. package/dist/collection/components/input-otp/input-otp.js +21 -6
  47. package/dist/collection/components/modal/modal.js +73 -44
  48. package/dist/collection/components/modal/safe-area-utils.js +27 -2
  49. package/dist/collection/components/picker-column/picker-column.js +5 -5
  50. package/dist/collection/components/radio-group/radio-group.js +13 -1
  51. package/dist/collection/components/radio-group/test/fixtures.js +2 -2
  52. package/dist/collection/components/select-modal/select-modal.js +18 -7
  53. package/dist/collection/components/select-modal/test/fixtures.js +4 -0
  54. package/dist/collection/components/select-popover/select-popover.js +18 -7
  55. package/dist/collection/components/select-popover/test/fixtures.js +4 -0
  56. package/dist/docs.json +1 -1
  57. package/dist/esm/ion-action-sheet.entry.js +4 -4
  58. package/dist/esm/ion-app_8.entry.js +1 -1
  59. package/dist/esm/ion-checkbox.entry.js +39 -32
  60. package/dist/esm/ion-datetime_3.entry.js +17 -6
  61. package/dist/esm/ion-input-otp.entry.js +21 -6
  62. package/dist/esm/ion-modal.entry.js +99 -45
  63. package/dist/esm/ion-picker-column.entry.js +5 -5
  64. package/dist/esm/ion-radio_2.entry.js +13 -1
  65. package/dist/esm/ion-select-modal.entry.js +18 -7
  66. package/dist/esm/ion-select_3.entry.js +18 -7
  67. package/dist/ionic/ionic.esm.js +1 -1
  68. package/dist/ionic/p-078037da.entry.js +4 -0
  69. package/dist/ionic/p-23ec35e4.entry.js +4 -0
  70. package/dist/ionic/p-268a3397.entry.js +4 -0
  71. package/dist/ionic/p-28a9e720.entry.js +4 -0
  72. package/dist/ionic/p-4c67ce4c.entry.js +4 -0
  73. package/dist/ionic/p-87125490.entry.js +4 -0
  74. package/dist/ionic/{p-4dd5e8e0.entry.js → p-8fda6a62.entry.js} +1 -1
  75. package/dist/ionic/{p-9eac4eb1.entry.js → p-aa812c4b.entry.js} +1 -1
  76. package/dist/ionic/p-cb27fe68.entry.js +4 -0
  77. package/dist/ionic/p-ce2edb36.entry.js +4 -0
  78. package/dist/types/components/checkbox/checkbox.d.ts +0 -1
  79. package/dist/types/components/datetime/datetime.d.ts +1 -0
  80. package/dist/types/components/modal/modal.d.ts +41 -3
  81. package/dist/types/components/modal/safe-area-utils.d.ts +16 -0
  82. package/dist/types/components/radio-group/test/fixtures.d.ts +1 -1
  83. package/dist/types/components/select-modal/select-modal.d.ts +1 -0
  84. package/dist/types/components/select-modal/test/fixtures.d.ts +1 -0
  85. package/dist/types/components/select-popover/select-popover.d.ts +1 -0
  86. package/dist/types/components/select-popover/test/fixtures.d.ts +1 -0
  87. package/hydrate/index.js +199 -87
  88. package/hydrate/index.mjs +199 -87
  89. package/package.json +4 -4
  90. package/components/p-1KVKSLu5.js +0 -4
  91. package/components/p-BI7WNErr.js +0 -4
  92. package/components/p-C7AoMl7c.js +0 -4
  93. package/components/p-D6pdfDIA.js +0 -4
  94. package/components/p-cvHphHJA.js +0 -4
  95. package/components/p-e-EDj2CO.js +0 -4
  96. package/dist/ionic/p-2d4eb1b4.entry.js +0 -4
  97. package/dist/ionic/p-3e143d1d.entry.js +0 -4
  98. package/dist/ionic/p-51c11c47.entry.js +0 -4
  99. package/dist/ionic/p-5681dde4.entry.js +0 -4
  100. package/dist/ionic/p-9fae83d8.entry.js +0 -4
  101. package/dist/ionic/p-c744307d.entry.js +0 -4
  102. package/dist/ionic/p-cb78f5a0.entry.js +0 -4
  103. package/dist/ionic/p-e6c5f060.entry.js +0 -4
@@ -22,6 +22,10 @@ const selectModalMdCss = () => `.sc-ion-select-modal-md-h{height:100%}ion-list.s
22
22
  const SelectModal = class {
23
23
  constructor(hostRef) {
24
24
  index.registerInstance(this, hostRef);
25
+ // Tracks the option that received Enter-keydown so keyup only
26
+ // dismisses when the press started on the same option. Prevents
27
+ // Enter on the triggering ion-select from auto-dismissing.
28
+ this.pendingEnterTarget = null;
25
29
  /**
26
30
  * The text to display on the cancel button.
27
31
  */
@@ -71,15 +75,22 @@ const SelectModal = class {
71
75
  return (index.h("ion-radio-group", { value: checked, onIonChange: (ev) => this.callOptionHandler(ev) }, this.options.map((option) => (index.h("ion-item", { lines: "none", class: Object.assign({
72
76
  // TODO FW-4784
73
77
  'item-radio-checked': option.value === checked
74
- }, theme.getClassMap(option.cssClass)) }, index.h("ion-radio", { value: option.value, disabled: option.disabled, justify: "start", labelPlacement: "end", onClick: () => this.closeModal(), onKeyUp: (ev) => {
78
+ }, theme.getClassMap(option.cssClass)) }, index.h("ion-radio", { value: option.value, disabled: option.disabled, justify: "start", labelPlacement: "end", onClick: () => this.closeModal(), onKeyDown: (ev) => {
79
+ if (ev.key === 'Enter' && !ev.repeat) {
80
+ this.pendingEnterTarget = ev.currentTarget;
81
+ }
82
+ }, onKeyUp: (ev) => {
75
83
  if (ev.key === ' ') {
76
- /**
77
- * Selecting a radio option with keyboard navigation,
78
- * either through the Enter or Space keys, should
79
- * dismiss the modal.
80
- */
84
+ // Space selects and dismisses in one press.
81
85
  this.closeModal();
82
86
  }
87
+ else if (ev.key === 'Enter') {
88
+ const shouldClose = this.pendingEnterTarget === ev.currentTarget;
89
+ this.pendingEnterTarget = null;
90
+ if (shouldClose) {
91
+ this.closeModal();
92
+ }
93
+ }
83
94
  } }, option.text))))));
84
95
  }
85
96
  renderCheckboxOptions() {
@@ -94,7 +105,7 @@ const SelectModal = class {
94
105
  } }, option.text))));
95
106
  }
96
107
  render() {
97
- return (index.h(index.Host, { key: 'f8a4cd6ff23ff01eaa1bdaf3c046814e7b30b23b', class: ionicGlobal.getIonMode(this) }, index.h("ion-header", { key: '9e29a7e57ad5cf332641111882f16852187ec8ba' }, index.h("ion-toolbar", { key: 'e6af5d6eabbf4b10799fc8a0b8f91d29b12d41f5' }, this.header !== undefined && index.h("ion-title", { key: '6056e52d15dbf307571d25e0305d67228a79237d' }, this.header), index.h("ion-buttons", { key: 'c9aa4fb2e21a93f3a95c5a8f0ba8b7d5553c5a72', slot: "end" }, index.h("ion-button", { key: '5ffbf512719bcb053b652fc96b1b6154d0593095', onClick: () => this.closeModal() }, this.cancelText)))), index.h("ion-content", { key: '0ec9098798a4e6de7a83a0a7e9d10bdcd7c98a78' }, index.h("ion-list", { key: 'd60b1700d3c2f8655951632de810900707a101f0' }, this.multiple === true ? this.renderCheckboxOptions() : this.renderRadioOptions()))));
108
+ return (index.h(index.Host, { key: 'fda0bf6f93cd5ec9f3c64f88a52de849e0e140a2', class: ionicGlobal.getIonMode(this) }, index.h("ion-header", { key: '27c0b17175a53db9ff159feeeb96451a3f011dab' }, index.h("ion-toolbar", { key: '91a4155ebc317fbc9f1bb3e26a7e94754b953c9b' }, this.header !== undefined && index.h("ion-title", { key: 'f6dae8e4e381f322cc90efefd9bb6ef81d4d2f3e' }, this.header), index.h("ion-buttons", { key: 'e7760532fb2e7e7385ed6e62097d92d96ff20148', slot: "end" }, index.h("ion-button", { key: '4999b6fc46cba138186546dca67b7950855e6fb7', onClick: () => this.closeModal() }, this.cancelText)))), index.h("ion-content", { key: 'c73f80a4bc25b9061ea65cf11e5d811c1a4d8704' }, index.h("ion-list", { key: 'b21905d15b36ad5eb45845e768918d2763cf48b1' }, this.multiple === true ? this.renderCheckboxOptions() : this.renderRadioOptions()))));
98
109
  }
99
110
  get el() { return index.getElement(this); }
100
111
  };
@@ -892,6 +892,10 @@ const selectPopoverMdCss = () => `.sc-ion-select-popover-md-h ion-list.sc-ion-se
892
892
  const SelectPopover = class {
893
893
  constructor(hostRef) {
894
894
  index.registerInstance(this, hostRef);
895
+ // Tracks the option that received Enter-keydown so keyup only
896
+ // dismisses when the press started on the same option. Prevents
897
+ // Enter on the triggering ion-select from auto-dismissing.
898
+ this.pendingEnterTarget = null;
895
899
  /**
896
900
  * An array of options for the popover
897
901
  */
@@ -969,21 +973,28 @@ const SelectPopover = class {
969
973
  return (index.h("ion-radio-group", { value: checked, onIonChange: (ev) => this.callOptionHandler(ev) }, options.map((option) => (index.h("ion-item", { class: Object.assign({
970
974
  // TODO FW-4784
971
975
  'item-radio-checked': option.value === checked
972
- }, theme.getClassMap(option.cssClass)) }, index.h("ion-radio", { value: option.value, disabled: option.disabled, onClick: () => this.dismissParentPopover(), onKeyUp: (ev) => {
976
+ }, theme.getClassMap(option.cssClass)) }, index.h("ion-radio", { value: option.value, disabled: option.disabled, onClick: () => this.dismissParentPopover(), onKeyDown: (ev) => {
977
+ if (ev.key === 'Enter' && !ev.repeat) {
978
+ this.pendingEnterTarget = ev.currentTarget;
979
+ }
980
+ }, onKeyUp: (ev) => {
973
981
  if (ev.key === ' ') {
974
- /**
975
- * Selecting a radio option with keyboard navigation,
976
- * either through the Enter or Space keys, should
977
- * dismiss the popover.
978
- */
982
+ // Space selects and dismisses in one press.
979
983
  this.dismissParentPopover();
980
984
  }
985
+ else if (ev.key === 'Enter') {
986
+ const shouldDismiss = this.pendingEnterTarget === ev.currentTarget;
987
+ this.pendingEnterTarget = null;
988
+ if (shouldDismiss) {
989
+ this.dismissParentPopover();
990
+ }
991
+ }
981
992
  } }, option.text))))));
982
993
  }
983
994
  render() {
984
995
  const { header, message, options, subHeader } = this;
985
996
  const hasSubHeaderOrMessage = subHeader !== undefined || message !== undefined;
986
- return (index.h(index.Host, { key: '0c9845a40d3fc392b0a7d64e2a6ed27d94bb7634', class: ionicGlobal.getIonMode(this) }, index.h("ion-list", { key: '84a30f6661b0f8c00e6fa199658ed2adbcf27358' }, header !== undefined && index.h("ion-list-header", { key: '13f5f56bbfbc06751fa516291a2da72629b60ece' }, header), hasSubHeaderOrMessage && (index.h("ion-item", { key: '3d39d18e720e798bbde334e79e6832091c7dfb81' }, index.h("ion-label", { key: 'd3051b0d140120b44bf5e79572f6f287e7cfb03a', class: "ion-text-wrap" }, subHeader !== undefined && index.h("h3", { key: 'b16805956f3316f8ec703c123b76f717488e8637' }, subHeader), message !== undefined && index.h("p", { key: '2215ac4ab4146a14e75a79192e319a8016286b5f' }, message)))), this.renderOptions(options))));
997
+ return (index.h(index.Host, { key: 'e7449a1ecfcdbf45a79f8e26a00253c4e146448a', class: ionicGlobal.getIonMode(this) }, index.h("ion-list", { key: '52abdfc8668c3429a0dcefef8ddedb6647fdd894' }, header !== undefined && index.h("ion-list-header", { key: '978e5c03728756feafcc60a0e10e6ec59bf2ae11' }, header), hasSubHeaderOrMessage && (index.h("ion-item", { key: 'e93c44e7f07a76def16e4b11f0fb4780d84ed402' }, index.h("ion-label", { key: 'bba1aac43b0bc7f4f00978dd8301985233f3725c', class: "ion-text-wrap" }, subHeader !== undefined && index.h("h3", { key: 'ad96f6017cf2cc5219540bded2c4f1ca3b532de2' }, subHeader), message !== undefined && index.h("p", { key: '3fd038921dc40c4d0c29734433984b279ccaeec3' }, message)))), this.renderOptions(options))));
987
998
  }
988
999
  get el() { return index.getElement(this); }
989
1000
  };
@@ -372,7 +372,7 @@ export class ActionSheet {
372
372
  if (isRadio) {
373
373
  htmlAttrs['aria-checked'] = isActiveRadio ? 'true' : 'false';
374
374
  }
375
- return (h("button", Object.assign({}, htmlAttrs, { role: isRadio ? 'radio' : undefined, type: "button", id: buttonId, class: Object.assign(Object.assign({}, buttonClass(b)), { 'action-sheet-selected': isActiveRadio }), onClick: () => {
375
+ return (h("button", Object.assign({}, htmlAttrs, { role: isRadio ? 'radio' : undefined, type: "button", id: buttonId, class: Object.assign(Object.assign({}, buttonClass(b)), (isRadio && { 'action-sheet-selected': isActiveRadio })), onClick: () => {
376
376
  if (isRadio) {
377
377
  this.selectRadioButton(b);
378
378
  }
@@ -387,12 +387,12 @@ export class ActionSheet {
387
387
  const cancelButton = allButtons.find((b) => b.role === 'cancel');
388
388
  const buttons = allButtons.filter((b) => b.role !== 'cancel');
389
389
  const headerID = `action-sheet-${overlayIndex}-header`;
390
- return (h(Host, Object.assign({ key: '173fcff5b1da7c33c267de4667591c946b8c8d03', role: "dialog", "aria-modal": "true", "aria-labelledby": header !== undefined ? headerID : null, tabindex: "-1" }, htmlAttributes, { style: {
390
+ return (h(Host, Object.assign({ key: 'a56ee2ab59c763036140dbd10306a708c26e3c17', role: "dialog", "aria-modal": "true", "aria-labelledby": header !== undefined ? headerID : null, tabindex: "-1" }, htmlAttributes, { style: {
391
391
  zIndex: `${20000 + this.overlayIndex}`,
392
- }, class: Object.assign(Object.assign({ [mode]: true }, getClassMap(this.cssClass)), { 'overlay-hidden': true, 'action-sheet-translucent': this.translucent }), onIonActionSheetWillDismiss: this.dispatchCancelHandler, onIonBackdropTap: this.onBackdropTap }), h("ion-backdrop", { key: '521ede659f747864f6c974e09016436eceb7158c', tappable: this.backdropDismiss }), h("div", { key: '7a7946fc434bc444f16a70638f5e948c69d33fcd', tabindex: "0", "aria-hidden": "true" }), h("div", { key: 'bcff39a580489dbafa255842e57aa8602c6d0f18', class: "action-sheet-wrapper ion-overlay-wrapper", ref: (el) => (this.wrapperEl = el) }, h("div", { key: '84bba13ce14261f0f0daa3f9c77648c9e7f36e0e', class: "action-sheet-container" }, h("div", { key: 'd9c8ac404fd6719a7adf8cb36549f67616f9a0c4', class: "action-sheet-group", ref: (el) => (this.groupEl = el), role: hasRadioButtons ? 'radiogroup' : undefined }, header !== undefined && (h("div", { key: '180433a8ad03ef5c54728a1a8f34715b6921d658', id: headerID, class: {
392
+ }, class: Object.assign(Object.assign({ [mode]: true }, getClassMap(this.cssClass)), { 'overlay-hidden': true, 'action-sheet-translucent': this.translucent }), onIonActionSheetWillDismiss: this.dispatchCancelHandler, onIonBackdropTap: this.onBackdropTap }), h("ion-backdrop", { key: 'c32eb4281fd6348c7d3989a3f509c211263048e6', tappable: this.backdropDismiss }), h("div", { key: '7f0123114a876fc7cfff3cfb564aded4a7017797', tabindex: "0", "aria-hidden": "true" }), h("div", { key: '645b1d5fde39a8907f21983d66e6ecb7a99aa05d', class: "action-sheet-wrapper ion-overlay-wrapper", ref: (el) => (this.wrapperEl = el) }, h("div", { key: 'a78fb02848462d1a4f9356ac4fa1c43a2e5d90e4', class: "action-sheet-container" }, h("div", { key: '5e846f53e067b211b985d6e1512b72b9d7c1a3aa', class: "action-sheet-group", ref: (el) => (this.groupEl = el), role: hasRadioButtons ? 'radiogroup' : undefined }, header !== undefined && (h("div", { key: 'a90a0e096e1b2fa78b9adb9253c0a517f16e62cb', id: headerID, class: {
393
393
  'action-sheet-title': true,
394
394
  'action-sheet-has-sub-title': this.subHeader !== undefined,
395
- } }, header, this.subHeader && h("div", { key: '7138e79e61b1a8f42bc5a9175c57fa2f15d7ec5a', class: "action-sheet-sub-title" }, this.subHeader))), this.renderActionSheetButtons(buttons)), cancelButton && (h("div", { key: 'b617c722f5b8028d73ed34b69310f312c65f34a7', class: "action-sheet-group action-sheet-group-cancel" }, h("button", Object.assign({ key: 'd0dd876fc48815df3710413c201c0b445a8e16c0' }, cancelButton.htmlAttributes, { type: "button", class: buttonClass(cancelButton), onClick: () => this.buttonClick(cancelButton) }), h("span", { key: 'e7b960157cc6fc5fe92a12090b2be55e8ae072e4', class: "action-sheet-button-inner" }, cancelButton.icon && (h("ion-icon", { key: '05498ffc60cab911dbff0ecbc6168dea59ada9a5', icon: cancelButton.icon, "aria-hidden": "true", lazy: false, class: "action-sheet-icon" })), cancelButton.text), mode === 'md' && h("ion-ripple-effect", { key: '3d401346cea301be4ca03671f7370f6f4b0b6bde' })))))), h("div", { key: '971f3c5fcc07f36c28eb469a47ec0290c692e139', tabindex: "0", "aria-hidden": "true" })));
395
+ } }, header, this.subHeader && h("div", { key: '40f00b12341625c548546de1885b9c9d93bc169c', class: "action-sheet-sub-title" }, this.subHeader))), this.renderActionSheetButtons(buttons)), cancelButton && (h("div", { key: 'ef6974cb63089623df08087274b82745443cee8c', class: "action-sheet-group action-sheet-group-cancel" }, h("button", Object.assign({ key: 'b02911a6491d60f9dcb5da7d942392a9e96552c1' }, cancelButton.htmlAttributes, { type: "button", class: buttonClass(cancelButton), onClick: () => this.buttonClick(cancelButton) }), h("span", { key: '1187433e676eda55e52b5ae328a8e68bba22deb6', class: "action-sheet-button-inner" }, cancelButton.icon && (h("ion-icon", { key: '079ab2a6bd40b996950053617f1c1c8207ecb1f1', icon: cancelButton.icon, "aria-hidden": "true", lazy: false, class: "action-sheet-icon" })), cancelButton.text), mode === 'md' && h("ion-ripple-effect", { key: '3bc473add8ac299f202f8c359d26708872c02f52' })))))), h("div", { key: '9b1ae7b4e3649e9b85632f0d65627ca81499e68d', tabindex: "0", "aria-hidden": "true" })));
396
396
  }
397
397
  static get is() { return "ion-action-sheet"; }
398
398
  static get encapsulation() { return "scoped"; }
@@ -113,48 +113,55 @@ export class Checkbox {
113
113
  this.onDivLabelClick = (ev) => {
114
114
  ev.stopPropagation();
115
115
  };
116
- this.onSlotChange = () => {
117
- this.hasLabelContent = this.el.textContent !== '';
118
- };
119
116
  }
120
117
  connectedCallback() {
121
118
  const { el } = this;
122
- // Watch for class changes to update validation state.
123
119
  if (Build.isBrowser && typeof MutationObserver !== 'undefined') {
124
- this.validationObserver = new MutationObserver(() => {
125
- const newIsInvalid = checkInvalidState(el);
126
- if (this.isInvalid !== newIsInvalid) {
127
- this.isInvalid = newIsInvalid;
128
- /**
129
- * Screen readers tend to announce changes
130
- * to `aria-describedby` when the attribute
131
- * is changed during a blur event for a
132
- * native form control.
133
- * However, the announcement can be spotty
134
- * when using a non-native form control
135
- * and `forceUpdate()`.
136
- * This is due to `forceUpdate()` internally
137
- * rescheduling the DOM update to a lower
138
- * priority queue regardless if it's called
139
- * inside a Promise or not, thus causing
140
- * the screen reader to potentially miss the
141
- * change.
142
- * By using a State variable inside a Promise,
143
- * it guarantees a re-render immediately at
144
- * a higher priority.
145
- */
146
- Promise.resolve().then(() => {
147
- this.hintTextId = this.getHintTextId();
148
- });
120
+ this.validationObserver = new MutationObserver((mutations) => {
121
+ // Watch for label content changes
122
+ if (mutations.some((mutation) => mutation.type === 'characterData' || mutation.type === 'childList')) {
123
+ this.hasLabelContent = this.el.textContent !== '';
124
+ }
125
+ // Watch for class changes to update validation state.
126
+ if (mutations.some((mutation) => mutation.type === 'attributes' && mutation.target === el)) {
127
+ const newIsInvalid = checkInvalidState(el);
128
+ if (this.isInvalid !== newIsInvalid) {
129
+ this.isInvalid = newIsInvalid;
130
+ /**
131
+ * Screen readers tend to announce changes
132
+ * to `aria-describedby` when the attribute
133
+ * is changed during a blur event for a
134
+ * native form control.
135
+ * However, the announcement can be spotty
136
+ * when using a non-native form control
137
+ * and `forceUpdate()`.
138
+ * This is due to `forceUpdate()` internally
139
+ * rescheduling the DOM update to a lower
140
+ * priority queue regardless if it's called
141
+ * inside a Promise or not, thus causing
142
+ * the screen reader to potentially miss the
143
+ * change.
144
+ * By using a State variable inside a Promise,
145
+ * it guarantees a re-render immediately at
146
+ * a higher priority.
147
+ */
148
+ Promise.resolve().then(() => {
149
+ this.hintTextId = this.getHintTextId();
150
+ });
151
+ }
149
152
  }
150
153
  });
151
154
  this.validationObserver.observe(el, {
152
155
  attributes: true,
153
156
  attributeFilter: ['class'],
157
+ characterData: true,
158
+ childList: true,
159
+ subtree: true,
154
160
  });
155
161
  }
156
162
  // Always set initial state
157
163
  this.isInvalid = checkInvalidState(el);
164
+ this.hasLabelContent = this.el.textContent !== '';
158
165
  }
159
166
  componentWillLoad() {
160
167
  this.inheritedAttributes = Object.assign({}, inheritAriaAttributes(this.el));
@@ -204,7 +211,7 @@ export class Checkbox {
204
211
  renderHiddenInput(true, el, name, checked ? value : '', disabled);
205
212
  // The host element must have a checkbox role to ensure proper VoiceOver
206
213
  // support in Safari for accessibility.
207
- return (h(Host, { key: '78eb720868106bcbe8357e50ebae2ab2fce8bdd6', role: "checkbox", "aria-checked": indeterminate ? 'mixed' : `${checked}`, "aria-describedby": this.hintTextId, "aria-invalid": this.isInvalid ? 'true' : undefined, "aria-labelledby": this.hasLabelContent ? this.inputLabelId : null, "aria-label": inheritedAttributes['aria-label'] || null, "aria-disabled": disabled ? 'true' : null, "aria-required": required ? 'true' : undefined, tabindex: disabled ? undefined : 0, onKeyDown: this.onKeyDown, onFocus: this.onFocus, onBlur: this.onBlur, onClick: this.onClick, class: createColorClasses(color, {
214
+ return (h(Host, { key: '0da370f94c5cdf3b08bc9008395558334a300f35', role: "checkbox", "aria-checked": indeterminate ? 'mixed' : `${checked}`, "aria-describedby": this.hintTextId, "aria-invalid": this.isInvalid ? 'true' : undefined, "aria-labelledby": this.hasLabelContent ? this.inputLabelId : null, "aria-label": inheritedAttributes['aria-label'] || null, "aria-disabled": disabled ? 'true' : null, "aria-required": required ? 'true' : undefined, tabindex: disabled ? undefined : 0, onKeyDown: this.onKeyDown, onFocus: this.onFocus, onBlur: this.onBlur, onClick: this.onClick, class: createColorClasses(color, {
208
215
  [mode]: true,
209
216
  'in-item': hostContext('ion-item', el),
210
217
  'checkbox-checked': checked,
@@ -214,10 +221,10 @@ export class Checkbox {
214
221
  [`checkbox-justify-${justify}`]: justify !== undefined,
215
222
  [`checkbox-alignment-${alignment}`]: alignment !== undefined,
216
223
  [`checkbox-label-placement-${labelPlacement}`]: true,
217
- }) }, h("label", { key: '3bd09a96e126bc72a0589a9c9ff8459cb60e1084', class: "checkbox-wrapper", htmlFor: inputId }, h("input", Object.assign({ key: 'f55c566f36dc6b0f069c75dccdebd2c5b3e7385e', type: "checkbox", checked: checked ? true : undefined, disabled: disabled, id: inputId, onChange: this.toggleChecked, required: required }, inheritedAttributes)), h("div", { key: '11db1139eabfe0b83688c574b81d1a6e8b7ae8c6', class: {
224
+ }) }, h("label", { key: '991f1763356671230af119a5fbdc22d0a39974e7', class: "checkbox-wrapper", htmlFor: inputId }, h("input", Object.assign({ key: '982f8a7f84d013b272b17607936355d2b6c251f4', type: "checkbox", checked: checked ? true : undefined, disabled: disabled, id: inputId, onChange: this.toggleChecked, required: required }, inheritedAttributes)), h("div", { key: 'c8f9e8baa20ac68e69fd3c6fcf0e7a26a1084d83', class: {
218
225
  'label-text-wrapper': true,
219
226
  'label-text-wrapper-hidden': !this.hasLabelContent,
220
- }, part: "label", id: this.inputLabelId, onClick: this.onDivLabelClick }, h("slot", { key: '6b3dd09e86063e2bc48014a1715cd788038ca01d', onSlotchange: this.onSlotChange }), this.renderHintText()), h("div", { key: 'bb2c75c8a893fd81e213c6b2ba807d5cba5a4966', class: "native-wrapper" }, h("svg", { key: '028a4c7d211c3697a91743d6242f202209e14c1a', class: "checkbox-icon", viewBox: "0 0 24 24", part: "container", "aria-hidden": "true" }, path)))));
227
+ }, part: "label", id: this.inputLabelId, onClick: this.onDivLabelClick }, h("slot", { key: '6018205e0a73dec826c7881d687f1c2ca8dcb0ab' }), this.renderHintText()), h("div", { key: '57530b9d6ff59ee7ab98f960cd65d66ee87cfd1d', class: "native-wrapper" }, h("svg", { key: '63d719154ff44459e9ca448e3f5d7de94d9ab248', class: "checkbox-icon", viewBox: "0 0 24 24", part: "container", "aria-hidden": "true" }, path)))));
221
228
  }
222
229
  getSVGPath(mode, indeterminate) {
223
230
  let path = indeterminate ? (h("path", { d: "M6 12L18 12", part: "mark" })) : (h("path", { d: "M5.9,12.5l3.8,3.8l8.8-8.8", part: "mark" }));
@@ -116,7 +116,7 @@
116
116
  -webkit-padding-end: var(--padding-end);
117
117
  padding-inline-end: var(--padding-end);
118
118
  padding-top: calc(var(--padding-top) + var(--offset-top));
119
- padding-bottom: calc(var(--padding-bottom) + var(--keyboard-offset) + var(--offset-bottom));
119
+ padding-bottom: calc(var(--padding-bottom) + var(--keyboard-offset) + var(--offset-bottom) + var(--ion-content-safe-area-padding-bottom, 0px));
120
120
  position: absolute;
121
121
  color: var(--color);
122
122
  box-sizing: border-box;
@@ -622,6 +622,12 @@ export class Datetime {
622
622
  this.el.classList.add('datetime-ready');
623
623
  });
624
624
  };
625
+ this.loadTimeoutCleanup = () => {
626
+ if (this.loadTimeout) {
627
+ clearTimeout(this.loadTimeout);
628
+ this.loadTimeout = undefined;
629
+ }
630
+ };
625
631
  this.processValue = (value) => {
626
632
  const hasValue = value !== null && value !== undefined && value !== '' && (!Array.isArray(value) || value.length > 0);
627
633
  const valueToProcess = hasValue ? parseDate(value) : this.defaultParts;
@@ -769,9 +775,10 @@ export class Datetime {
769
775
  if (!prevMonth) {
770
776
  return;
771
777
  }
778
+ const left = prevMonth.offsetWidth * 2;
772
779
  calendarBodyRef.scrollTo({
773
780
  top: 0,
774
- left: 0,
781
+ left: left * (isRTL(this.el) ? 1 : -1),
775
782
  behavior: 'smooth',
776
783
  });
777
784
  };
@@ -893,15 +900,16 @@ export class Datetime {
893
900
  }
894
901
  connectedCallback() {
895
902
  this.clearFocusVisible = startFocusVisible(this.el).destroy;
903
+ this.loadTimeout = setTimeout(() => {
904
+ this.ensureReadyIfVisible();
905
+ }, 100);
896
906
  }
897
907
  disconnectedCallback() {
898
908
  if (this.clearFocusVisible) {
899
909
  this.clearFocusVisible();
900
910
  this.clearFocusVisible = undefined;
901
911
  }
902
- if (this.loadTimeout) {
903
- clearTimeout(this.loadTimeout);
904
- }
912
+ this.loadTimeoutCleanup();
905
913
  }
906
914
  initializeListeners() {
907
915
  this.initializeCalendarListener();
@@ -949,7 +957,10 @@ export class Datetime {
949
957
  * we still initialize listeners and mark the component as ready.
950
958
  *
951
959
  * We schedule this after everything has had a chance to run.
960
+ *
961
+ * We also clean up the load timeout to ensure that we don't have multiple timeouts running.
952
962
  */
963
+ this.loadTimeoutCleanup();
953
964
  this.loadTimeout = setTimeout(() => {
954
965
  this.ensureReadyIfVisible();
955
966
  }, 100);
@@ -1708,7 +1719,7 @@ export class Datetime {
1708
1719
  const hasDatePresentation = presentation === 'date' || presentation === 'date-time' || presentation === 'time-date';
1709
1720
  const hasWheelVariant = hasDatePresentation && preferWheel;
1710
1721
  renderHiddenInput(true, el, name, formatValue(value), disabled);
1711
- return (h(Host, { key: '191a6d7ce7bc2d57bfaaebd8aee31e3c36f2b56a', "aria-disabled": disabled ? 'true' : null, onFocus: this.onFocus, onBlur: this.onBlur, class: Object.assign({}, createColorClasses(color, {
1722
+ return (h(Host, { key: '59e0811aa273e88dfb8e4b703e6824088a457380', "aria-disabled": disabled ? 'true' : null, onFocus: this.onFocus, onBlur: this.onBlur, class: Object.assign({}, createColorClasses(color, {
1712
1723
  [mode]: true,
1713
1724
  ['datetime-readonly']: readonly,
1714
1725
  ['datetime-disabled']: disabled,
@@ -1718,7 +1729,7 @@ export class Datetime {
1718
1729
  [`datetime-size-${size}`]: true,
1719
1730
  [`datetime-prefer-wheel`]: hasWheelVariant,
1720
1731
  [`datetime-grid`]: isGridStyle,
1721
- })) }, h("div", { key: '5c75290394cf7dc37c7dcba6372af003a50a9c04', class: "intersection-tracker", ref: (el) => (this.intersectionTrackerRef = el) }), this.renderDatetime(mode)));
1732
+ })) }, h("div", { key: '3753ff3dde3085070916c3de83687a219a49e553', class: "intersection-tracker", ref: (el) => (this.intersectionTrackerRef = el) }), this.renderDatetime(mode)));
1722
1733
  }
1723
1734
  static get is() { return "ion-datetime"; }
1724
1735
  static get encapsulation() { return "shadow"; }
@@ -130,9 +130,18 @@ export class InputOTP {
130
130
  * - Tab: Allows normal tab navigation between components
131
131
  */
132
132
  this.onKeyDown = (index) => (event) => {
133
- const { length } = this;
133
+ const { disabled, length, readonly } = this;
134
134
  const rtl = isRTL(this.el);
135
135
  const input = event.target;
136
+ if (disabled) {
137
+ return;
138
+ }
139
+ if (readonly) {
140
+ if (event.key === 'Backspace' || event.key === 'Delete') {
141
+ event.preventDefault();
142
+ return;
143
+ }
144
+ }
136
145
  // Meta shortcuts are used to copy, paste, and select text
137
146
  // We don't want to handle these keys here
138
147
  const metaShortcuts = ['a', 'c', 'v', 'x', 'r', 'z', 'y'];
@@ -195,10 +204,13 @@ export class InputOTP {
195
204
  */
196
205
  this.onInput = (index) => (event) => {
197
206
  var _a, _b;
198
- const { length, validKeyPattern } = this;
207
+ const { disabled, length, readonly, validKeyPattern } = this;
199
208
  const input = event.target;
200
209
  const value = input.value;
201
210
  const previousValue = this.previousInputValues[index] || '';
211
+ if (disabled || readonly) {
212
+ return;
213
+ }
202
214
  // 1. Autofill handling
203
215
  // If the length of the value increases by more than 1 from the previous
204
216
  // value, treat this as autofill. This is to prevent the case where the
@@ -317,8 +329,11 @@ export class InputOTP {
317
329
  */
318
330
  this.onPaste = (event) => {
319
331
  var _a, _b;
320
- const { inputRefs, length, validKeyPattern } = this;
332
+ const { disabled, inputRefs, length, readonly, validKeyPattern } = this;
321
333
  event.preventDefault();
334
+ if (disabled || readonly) {
335
+ return;
336
+ }
322
337
  const pastedText = (_a = event.clipboardData) === null || _a === void 0 ? void 0 : _a.getData('text');
323
338
  // If there is no pasted text, still emit the input change event
324
339
  // because this is how the native input element behaves
@@ -605,7 +620,7 @@ export class InputOTP {
605
620
  const tabbableIndex = this.getTabbableIndex();
606
621
  const pattern = this.getPattern();
607
622
  const hasDescription = ((_b = (_a = el.querySelector('.input-otp-description')) === null || _a === void 0 ? void 0 : _a.textContent) === null || _b === void 0 ? void 0 : _b.trim()) !== '';
608
- return (h(Host, { key: 'f15a29fb17b681ef55885ca36d3d5f899cbaca83', class: createColorClasses(color, {
623
+ return (h(Host, { key: '5c1386ae6b8038ec33ca94fd818c9353b1b37f75', class: createColorClasses(color, {
609
624
  [mode]: true,
610
625
  'has-focus': hasFocus,
611
626
  [`input-otp-size-${size}`]: true,
@@ -613,10 +628,10 @@ export class InputOTP {
613
628
  [`input-otp-fill-${fill}`]: true,
614
629
  'input-otp-disabled': disabled,
615
630
  'input-otp-readonly': readonly,
616
- }) }, h("div", Object.assign({ key: 'd7e1d4edd8aafcf2ed4313301287282e90fc7e82', role: "group", "aria-label": "One-time password input", class: "input-otp-group" }, inheritedAttributes), Array.from({ length }).map((_, index) => (h(Fragment, null, h("div", { class: "native-wrapper" }, h("input", { class: "native-input", id: `${inputId}-${index}`, "aria-label": `Input ${index + 1} of ${length}`, type: "text", autoCapitalize: autocapitalize, inputmode: inputmode, pattern: pattern, disabled: disabled, readOnly: readonly, tabIndex: index === tabbableIndex ? 0 : -1, value: inputValues[index] || '', autocomplete: "one-time-code", ref: (el) => (inputRefs[index] = el), onInput: this.onInput(index), onBlur: this.onBlur, onFocus: this.onFocus(index), onKeyDown: this.onKeyDown(index), onPaste: this.onPaste })), this.showSeparator(index) && h("div", { class: "input-otp-separator" }))))), h("div", { key: '3724a3159d02860971879a906092f9965f5a7c47', class: {
631
+ }) }, h("div", Object.assign({ key: '9a19129688e55095f8386826c73ef3f9744becff', role: "group", "aria-label": "One-time password input", class: "input-otp-group" }, inheritedAttributes), Array.from({ length }).map((_, index) => (h(Fragment, null, h("div", { class: "native-wrapper" }, h("input", { class: "native-input", id: `${inputId}-${index}`, "aria-label": `Input ${index + 1} of ${length}`, type: "text", autoCapitalize: autocapitalize, inputmode: inputmode, pattern: pattern, disabled: disabled, readOnly: readonly, tabIndex: index === tabbableIndex ? 0 : -1, value: inputValues[index] || '', autocomplete: "one-time-code", ref: (el) => (inputRefs[index] = el), onInput: this.onInput(index), onBlur: this.onBlur, onFocus: this.onFocus(index), onKeyDown: this.onKeyDown(index), onPaste: this.onPaste })), this.showSeparator(index) && h("div", { class: "input-otp-separator" }))))), h("div", { key: '7853819c3610c4691191f1836b947bf4ec17939d', class: {
617
632
  'input-otp-description': true,
618
633
  'input-otp-description-hidden': !hasDescription,
619
- } }, h("slot", { key: '11baa2624926a08274508afe0833d9237a8dc35c' }))));
634
+ } }, h("slot", { key: 'f4674d47d3d3991f21a0a79321ebc323968071dc' }))));
620
635
  }
621
636
  static get is() { return "ion-input-otp"; }
622
637
  static get encapsulation() { return "scoped"; }
@@ -21,7 +21,7 @@ import { mdEnterAnimation } from "./animations/md.enter";
21
21
  import { mdLeaveAnimation } from "./animations/md.leave";
22
22
  import { createSheetGesture } from "./gestures/sheet";
23
23
  import { createSwipeToCloseGesture, SwipeToCloseDefaults } from "./gestures/swipe-to-close";
24
- import { getInitialSafeAreaConfig, getPositionBasedSafeAreaConfig, applySafeAreaOverrides, clearSafeAreaOverrides, getRootSafeAreaTop, } from "./safe-area-utils";
24
+ import { getInitialSafeAreaConfig, getPositionBasedSafeAreaConfig, applySafeAreaOverrides, clearSafeAreaOverrides, getRootSafeAreaTop, hasCustomModalDimensions, } from "./safe-area-utils";
25
25
  import { setCardStatusBarDark, setCardStatusBarDefault } from "./utils";
26
26
  // TODO(FW-2832): types
27
27
  /**
@@ -251,12 +251,10 @@ export class Modal {
251
251
  // since the viewport may have crossed the centered-dialog breakpoint.
252
252
  if (!context.isSheetModal && !context.isCardModal) {
253
253
  this.updateSafeAreaOverrides();
254
- // Re-evaluate fullscreen safe-area padding: clear first, then re-apply
255
- if (this.wrapperEl) {
256
- this.wrapperEl.style.removeProperty('height');
257
- this.wrapperEl.style.removeProperty('padding-bottom');
258
- }
259
- this.applyFullscreenSafeArea();
254
+ // Re-evaluate fullscreen safe-area padding: clear first, then re-apply.
255
+ const { contentEl, hasFooter } = this.findContentAndFooter();
256
+ this.clearContentSafeAreaPadding(contentEl);
257
+ this.applyFullscreenSafeAreaTo(contentEl, hasFooter);
260
258
  }
261
259
  }, 50); // Debounce to avoid excessive calls during active resizing
262
260
  }
@@ -1011,6 +1009,11 @@ export class Modal {
1011
1009
  }
1012
1010
  /**
1013
1011
  * Creates the context object for safe-area utilities.
1012
+ *
1013
+ * `hasCustomDimensions` is only set by `setInitialSafeAreaOverrides()`
1014
+ * because it is only read by `getInitialSafeAreaConfig()`. Other callers
1015
+ * (resize handler, post-animation update, fullscreen-padding apply) would
1016
+ * pay a `getComputedStyle()` cost for a value they never consult.
1014
1017
  */
1015
1018
  getSafeAreaContext() {
1016
1019
  return {
@@ -1032,7 +1035,7 @@ export class Modal {
1032
1035
  * sheets to prevent header content from getting double-offset padding).
1033
1036
  */
1034
1037
  setInitialSafeAreaOverrides() {
1035
- const context = this.getSafeAreaContext();
1038
+ const context = Object.assign(Object.assign({}, this.getSafeAreaContext()), { hasCustomDimensions: hasCustomModalDimensions(this.el) });
1036
1039
  const safeAreaConfig = getInitialSafeAreaConfig(context);
1037
1040
  applySafeAreaOverrides(this.el, safeAreaConfig);
1038
1041
  // Set the internal offset property with the resolved root safe-area-top value
@@ -1072,59 +1075,85 @@ export class Modal {
1072
1075
  applySafeAreaOverrides(el, safeAreaConfig);
1073
1076
  }
1074
1077
  /**
1075
- * Applies padding-bottom to fullscreen modal wrapper to prevent
1076
- * content from overlapping system navigation bar.
1078
+ * Applies safe-area-bottom scroll padding to ion-content inside
1079
+ * fullscreen modals that have no ion-footer. This prevents content
1080
+ * from being hidden behind the system navigation bar while keeping
1081
+ * the modal background edge-to-edge (no visible gap).
1077
1082
  */
1078
1083
  applyFullscreenSafeArea() {
1079
- const { wrapperEl, el } = this;
1080
- if (!wrapperEl)
1081
- return;
1082
1084
  const context = this.getSafeAreaContext();
1083
1085
  if (context.isSheetModal || context.isCardModal)
1084
1086
  return;
1085
- // Check for standard Ionic layout children (ion-content, ion-footer),
1086
- // searching one level deep for wrapped components (e.g.,
1087
- // <app-footer><ion-footer>...</ion-footer></app-footer>).
1088
- // Note: uses a manual loop instead of querySelector(':scope > ...') because
1089
- // Stencil's mock-doc (used in spec tests) does not support :scope.
1090
- let hasContent = false;
1087
+ const { contentEl, hasFooter } = this.findContentAndFooter();
1088
+ this.applyFullscreenSafeAreaTo(contentEl, hasFooter);
1089
+ }
1090
+ /**
1091
+ * Sets --ion-content-safe-area-padding-bottom on the given ion-content
1092
+ * when no footer is present, so ion-content's .inner-scroll includes
1093
+ * safe-area-bottom in its scroll padding. This keeps the modal background
1094
+ * edge-to-edge while ensuring content scrolls clear of the system nav bar.
1095
+ *
1096
+ * --ion-content-safe-area-padding-bottom is an internal CSS property used
1097
+ * only by this code path. It is not part of ion-content's public API and
1098
+ * should not be set by consumers. The default of 0px makes it a no-op
1099
+ * when unset, which is the expected state for ion-content used outside of
1100
+ * a fullscreen modal without a footer.
1101
+ */
1102
+ applyFullscreenSafeAreaTo(contentEl, hasFooter) {
1103
+ // Only apply for standard Ionic layouts (has ion-content but no
1104
+ // ion-footer). When a footer is present it handles its own safe-area
1105
+ // padding. Custom modals with raw HTML are developer-controlled.
1106
+ if (!contentEl || hasFooter)
1107
+ return;
1108
+ contentEl.style.setProperty('--ion-content-safe-area-padding-bottom', 'var(--ion-safe-area-bottom, 0px)');
1109
+ }
1110
+ /**
1111
+ * Removes the internal --ion-content-safe-area-padding-bottom property
1112
+ * from an already-located ion-content. Callers do their own
1113
+ * findContentAndFooter() so they can also read hasFooter if needed.
1114
+ */
1115
+ clearContentSafeAreaPadding(contentEl) {
1116
+ if (!contentEl)
1117
+ return;
1118
+ contentEl.style.removeProperty('--ion-content-safe-area-padding-bottom');
1119
+ }
1120
+ /**
1121
+ * Finds ion-content and ion-footer among direct children and one level of
1122
+ * grandchildren (for wrapped components like <app-footer><ion-footer>).
1123
+ *
1124
+ * Intentionally does NOT use findIonContent() or querySelector() because
1125
+ * those search the full subtree and would match ion-content inside nested
1126
+ * routes/pages. We only want direct slot children (+ one wrapper level).
1127
+ *
1128
+ * Uses a manual loop instead of querySelector(':scope > ...') because
1129
+ * Stencil's mock-doc (used in spec tests) does not support :scope.
1130
+ */
1131
+ findContentAndFooter() {
1132
+ let contentEl = null;
1091
1133
  let hasFooter = false;
1092
- for (const child of Array.from(el.children)) {
1134
+ for (const child of Array.from(this.el.children)) {
1093
1135
  if (child.tagName === 'ION-CONTENT')
1094
- hasContent = true;
1136
+ contentEl = child;
1095
1137
  if (child.tagName === 'ION-FOOTER')
1096
1138
  hasFooter = true;
1097
1139
  for (const grandchild of Array.from(child.children)) {
1098
- if (grandchild.tagName === 'ION-CONTENT')
1099
- hasContent = true;
1140
+ if (grandchild.tagName === 'ION-CONTENT' && !contentEl)
1141
+ contentEl = grandchild;
1100
1142
  if (grandchild.tagName === 'ION-FOOTER')
1101
1143
  hasFooter = true;
1102
1144
  }
1103
1145
  }
1104
- // Only apply wrapper padding for standard Ionic layouts (has ion-content
1105
- // but no ion-footer). Custom modals with raw HTML are fully
1106
- // developer-controlled and should not be modified.
1107
- if (!hasContent || hasFooter)
1108
- return;
1109
- // Reduce wrapper height by safe-area and add equivalent padding so the
1110
- // total visual size stays the same but the flex content area shrinks.
1111
- // Using height + padding instead of box-sizing: border-box avoids
1112
- // breaking custom modals that set --border-width (border-box would
1113
- // include the border inside the height, changing the layout).
1114
- wrapperEl.style.setProperty('height', 'calc(var(--height) - var(--ion-safe-area-bottom, 0px))');
1115
- wrapperEl.style.setProperty('padding-bottom', 'var(--ion-safe-area-bottom, 0px)');
1146
+ return { contentEl, hasFooter };
1116
1147
  }
1117
1148
  /**
1118
- * Clears all safe-area overrides and padding from wrapper.
1149
+ * Clears all safe-area overrides and padding.
1119
1150
  */
1120
1151
  cleanupSafeAreaOverrides() {
1121
1152
  clearSafeAreaOverrides(this.el);
1122
1153
  // Remove internal sheet offset property
1123
1154
  this.el.style.removeProperty('--ion-modal-offset-top');
1124
- if (this.wrapperEl) {
1125
- this.wrapperEl.style.removeProperty('height');
1126
- this.wrapperEl.style.removeProperty('padding-bottom');
1127
- }
1155
+ const { contentEl } = this.findContentAndFooter();
1156
+ this.clearContentSafeAreaPadding(contentEl);
1128
1157
  }
1129
1158
  render() {
1130
1159
  const { handle, isSheetModal, presentingElement, htmlAttributes, handleBehavior, inheritedAttributes, focusTrap, expandToScroll, } = this;
@@ -1133,20 +1162,20 @@ export class Modal {
1133
1162
  const isCardModal = presentingElement !== undefined && mode === 'ios';
1134
1163
  const isHandleCycle = handleBehavior === 'cycle';
1135
1164
  const isSheetModalWithHandle = isSheetModal && showHandle;
1136
- return (h(Host, Object.assign({ key: '1a53e8f87532abccc169ca4b24973a39c5f9ba16', "no-router": true,
1165
+ return (h(Host, Object.assign({ key: '4bf38aa67df9a3f977163bba5423960bbafd16de', "no-router": true,
1137
1166
  // Allow the modal to be navigable when the handle is focusable
1138
1167
  tabIndex: isHandleCycle && isSheetModalWithHandle ? 0 : -1 }, htmlAttributes, { style: {
1139
1168
  zIndex: `${20000 + this.overlayIndex}`,
1140
- }, class: Object.assign({ [mode]: true, ['modal-default']: !isCardModal && !isSheetModal, [`modal-card`]: isCardModal, [`modal-sheet`]: isSheetModal, [`modal-no-expand-scroll`]: isSheetModal && !expandToScroll, 'overlay-hidden': true, [FOCUS_TRAP_DISABLE_CLASS]: focusTrap === false }, getClassMap(this.cssClass)), onIonBackdropTap: this.onBackdropTap, onIonModalDidPresent: this.onLifecycle, onIonModalWillPresent: this.onLifecycle, onIonModalWillDismiss: this.onLifecycle, onIonModalDidDismiss: this.onLifecycle, onFocus: this.onModalFocus }), h("ion-backdrop", { key: 'fa8e0a436c0d458331402e1850f87af3dc97b582', ref: (el) => (this.backdropEl = el), visible: this.showBackdrop, tappable: this.backdropDismiss, part: "backdrop" }), mode === 'ios' && h("div", { key: 'f00de6027d3c8b5bc93db3b0f7a50a87628d40bb', class: "modal-shadow" }), h("div", Object.assign({ key: 'ae5e33bd6c58e541edb2edbca92420ea02dd5175',
1169
+ }, class: Object.assign({ [mode]: true, ['modal-default']: !isCardModal && !isSheetModal, [`modal-card`]: isCardModal, [`modal-sheet`]: isSheetModal, [`modal-no-expand-scroll`]: isSheetModal && !expandToScroll, 'overlay-hidden': true, [FOCUS_TRAP_DISABLE_CLASS]: focusTrap === false }, getClassMap(this.cssClass)), onIonBackdropTap: this.onBackdropTap, onIonModalDidPresent: this.onLifecycle, onIonModalWillPresent: this.onLifecycle, onIonModalWillDismiss: this.onLifecycle, onIonModalDidDismiss: this.onLifecycle, onFocus: this.onModalFocus }), h("ion-backdrop", { key: '866da40cc5fc8d3e36637098fb3066a5bc9f4e0f', ref: (el) => (this.backdropEl = el), visible: this.showBackdrop, tappable: this.backdropDismiss, part: "backdrop" }), mode === 'ios' && h("div", { key: '5a2a05514ea8592c8feb0465e504aa7c7af17963', class: "modal-shadow" }), h("div", Object.assign({ key: '4d327115306451f57d190b06ab8cbb6191a6f1d7',
1141
1170
  /*
1142
1171
  role and aria-modal must be used on the
1143
1172
  same element. They must also be set inside the
1144
1173
  shadow DOM otherwise ion-button will not be highlighted
1145
1174
  when using VoiceOver: https://bugs.webkit.org/show_bug.cgi?id=247134
1146
1175
  */
1147
- role: "dialog" }, inheritedAttributes, { "aria-modal": "true", class: "modal-wrapper ion-overlay-wrapper", part: "content", ref: (el) => (this.wrapperEl = el) }), showHandle && (h("button", { key: '141cdd8f8522331f4b764e2a4d79ec6596b1eb3a', class: "modal-handle",
1176
+ role: "dialog" }, inheritedAttributes, { "aria-modal": "true", class: "modal-wrapper ion-overlay-wrapper", part: "content", ref: (el) => (this.wrapperEl = el) }), showHandle && (h("button", { key: 'd1882835cc049232c0d957e3ba1e79676a07d179', class: "modal-handle",
1148
1177
  // Prevents the handle from receiving keyboard focus when it does not cycle
1149
- tabIndex: !isHandleCycle ? -1 : 0, "aria-label": "Activate to adjust the size of the dialog overlaying the screen", onClick: isHandleCycle ? this.onHandleClick : undefined, part: "handle", ref: (el) => (this.dragHandleEl = el) })), h("slot", { key: '7de20298b61abee67a16d275c9ebd9a25ce7dd26', onSlotchange: this.onSlotChange }))));
1178
+ tabIndex: !isHandleCycle ? -1 : 0, "aria-label": "Activate to adjust the size of the dialog overlaying the screen", onClick: isHandleCycle ? this.onHandleClick : undefined, part: "handle", ref: (el) => (this.dragHandleEl = el) })), h("slot", { key: '81dc58b09cf7d7022b04cd170f53113604364d5e', onSlotchange: this.onSlotChange }))));
1150
1179
  }
1151
1180
  static get is() { return "ion-modal"; }
1152
1181
  static get encapsulation() { return "shadow"; }