@ionic/core 8.8.4-dev.11776178970.14763043 → 8.8.4-dev.11776186452.1cc0af05

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 (145) hide show
  1. package/components/ion-action-sheet.js +1 -1
  2. package/components/ion-alert.js +1 -1
  3. package/components/ion-infinite-scroll-content.js +1 -1
  4. package/components/ion-loading.js +1 -1
  5. package/components/ion-range.js +1 -1
  6. package/components/ion-refresher-content.js +1 -1
  7. package/components/ion-select-modal.js +1 -1
  8. package/components/ion-select-option.js +1 -1
  9. package/components/ion-select-popover.js +1 -1
  10. package/components/ion-select.js +1 -1
  11. package/components/ion-skeleton-text.js +1 -1
  12. package/components/ion-spinner.js +1 -1
  13. package/components/ion-split-pane.js +1 -1
  14. package/components/ion-tab-bar.js +1 -1
  15. package/components/ion-tab-button.js +1 -1
  16. package/components/ion-tab.js +1 -1
  17. package/components/ion-tabs.js +1 -1
  18. package/components/ion-text.js +1 -1
  19. package/components/ion-textarea.js +1 -1
  20. package/components/ion-thumbnail.js +1 -1
  21. package/components/ion-title.js +1 -1
  22. package/components/ion-toast.js +1 -1
  23. package/components/ion-toggle.js +1 -1
  24. package/components/ion-toolbar.js +1 -1
  25. package/components/{p-C-_EGKki.js → p-B0orUoaV.js} +1 -1
  26. package/components/p-BObmvbuR.js +4 -0
  27. package/components/p-C8Dne7pI.js +4 -0
  28. package/components/p-Cm7hjN9B.js +4 -0
  29. package/components/p-DQY5lHUa.js +4 -0
  30. package/components/p-D_uMZULz.js +4 -0
  31. package/components/{p-CoarhFWH.js → p-ZRp3l6Dk.js} +1 -1
  32. package/components/{p-C2cZvGcF.js → p-qhBzWoOF.js} +1 -1
  33. package/dist/cjs/ion-action-sheet.cjs.entry.js +23 -8
  34. package/dist/cjs/ion-alert.cjs.entry.js +42 -24
  35. package/dist/cjs/ion-app_8.cjs.entry.js +4 -4
  36. package/dist/cjs/ion-avatar_3.cjs.entry.js +2 -2
  37. package/dist/cjs/ion-item_8.cjs.entry.js +2 -2
  38. package/dist/cjs/ion-range.cjs.entry.js +1 -1
  39. package/dist/cjs/ion-select-modal.cjs.entry.js +59 -26
  40. package/dist/cjs/ion-select_3.cjs.entry.js +190 -47
  41. package/dist/cjs/ion-spinner.cjs.entry.js +1 -1
  42. package/dist/cjs/ion-split-pane.cjs.entry.js +2 -2
  43. package/dist/cjs/ion-tab-bar_2.cjs.entry.js +4 -4
  44. package/dist/cjs/ion-tab_2.cjs.entry.js +3 -3
  45. package/dist/cjs/ion-text.cjs.entry.js +2 -2
  46. package/dist/cjs/ion-textarea.cjs.entry.js +3 -3
  47. package/dist/cjs/ion-toast.cjs.entry.js +2 -2
  48. package/dist/cjs/ion-toggle.cjs.entry.js +3 -3
  49. package/dist/cjs/ionic.cjs.js +1 -1
  50. package/dist/cjs/loader.cjs.js +1 -1
  51. package/dist/cjs/select-option-render-qQf4xz8P.js +67 -0
  52. package/dist/collection/components/action-sheet/action-sheet.ionic.css +834 -0
  53. package/dist/collection/components/action-sheet/action-sheet.ios.css +84 -0
  54. package/dist/collection/components/action-sheet/action-sheet.js +21 -6
  55. package/dist/collection/components/action-sheet/action-sheet.md.css +84 -0
  56. package/dist/collection/components/alert/alert.ionic.css +1165 -0
  57. package/dist/collection/components/alert/alert.ios.css +83 -0
  58. package/dist/collection/components/alert/alert.js +40 -22
  59. package/dist/collection/components/alert/alert.md.css +83 -0
  60. package/dist/collection/components/range/range.ionic.css +6 -9
  61. package/dist/collection/components/select/select.ionic.css +31 -0
  62. package/dist/collection/components/select/select.ios.css +31 -0
  63. package/dist/collection/components/select/select.js +125 -14
  64. package/dist/collection/components/select/select.md.css +31 -0
  65. package/dist/collection/components/select-modal/select-modal.ionic.css +24 -0
  66. package/dist/collection/components/select-modal/select-modal.ios.css +77 -0
  67. package/dist/collection/components/select-modal/select-modal.js +56 -23
  68. package/dist/collection/components/select-modal/select-modal.md.css +80 -1
  69. package/dist/collection/components/select-option/select-option.js +21 -2
  70. package/dist/collection/components/select-popover/select-popover.ionic.css +489 -0
  71. package/dist/collection/components/select-popover/select-popover.ios.css +77 -0
  72. package/dist/collection/components/select-popover/select-popover.js +59 -26
  73. package/dist/collection/components/select-popover/select-popover.md.css +77 -0
  74. package/dist/collection/components/skeleton-text/skeleton-text.js +2 -2
  75. package/dist/collection/components/spinner/spinner.js +1 -1
  76. package/dist/collection/components/split-pane/split-pane.js +2 -2
  77. package/dist/collection/components/tab/tab.js +2 -2
  78. package/dist/collection/components/tab-bar/tab-bar.js +2 -2
  79. package/dist/collection/components/tab-button/tab-button.js +2 -2
  80. package/dist/collection/components/tabs/tabs.js +1 -1
  81. package/dist/collection/components/text/text.js +2 -2
  82. package/dist/collection/components/textarea/textarea.js +3 -3
  83. package/dist/collection/components/thumbnail/thumbnail.js +2 -2
  84. package/dist/collection/components/title/title.js +2 -2
  85. package/dist/collection/components/toast/toast.js +2 -2
  86. package/dist/collection/components/toggle/toggle.js +3 -3
  87. package/dist/collection/components/toolbar/toolbar.js +2 -2
  88. package/dist/collection/utils/select-option-render.js +62 -0
  89. package/dist/docs.json +135 -1
  90. package/dist/esm/ion-action-sheet.entry.js +23 -8
  91. package/dist/esm/ion-alert.entry.js +42 -24
  92. package/dist/esm/ion-app_8.entry.js +4 -4
  93. package/dist/esm/ion-avatar_3.entry.js +2 -2
  94. package/dist/esm/ion-item_8.entry.js +2 -2
  95. package/dist/esm/ion-range.entry.js +1 -1
  96. package/dist/esm/ion-select-modal.entry.js +59 -26
  97. package/dist/esm/ion-select_3.entry.js +191 -48
  98. package/dist/esm/ion-spinner.entry.js +1 -1
  99. package/dist/esm/ion-split-pane.entry.js +2 -2
  100. package/dist/esm/ion-tab-bar_2.entry.js +4 -4
  101. package/dist/esm/ion-tab_2.entry.js +3 -3
  102. package/dist/esm/ion-text.entry.js +2 -2
  103. package/dist/esm/ion-textarea.entry.js +3 -3
  104. package/dist/esm/ion-toast.entry.js +2 -2
  105. package/dist/esm/ion-toggle.entry.js +3 -3
  106. package/dist/esm/ionic.js +1 -1
  107. package/dist/esm/loader.js +1 -1
  108. package/dist/esm/select-option-render-BQUT9Aqa.js +65 -0
  109. package/dist/html.html-data.json +4 -0
  110. package/dist/ionic/ionic.esm.js +1 -1
  111. package/dist/ionic/p-003b40fc.entry.js +4 -0
  112. package/dist/ionic/{p-3bf01c2c.entry.js → p-26595044.entry.js} +1 -1
  113. package/dist/ionic/p-27edb91a.entry.js +4 -0
  114. package/dist/ionic/p-35aa95dc.entry.js +4 -0
  115. package/dist/ionic/{p-e9d6ce67.entry.js → p-37f895a8.entry.js} +1 -1
  116. package/dist/ionic/p-38897781.entry.js +4 -0
  117. package/dist/ionic/p-575061c0.entry.js +4 -0
  118. package/dist/ionic/{p-2bf931ae.entry.js → p-69033ad6.entry.js} +1 -1
  119. package/dist/ionic/{p-cdfbe4cc.entry.js → p-71f28573.entry.js} +1 -1
  120. package/dist/ionic/{p-9b9b1450.entry.js → p-96ec9a10.entry.js} +1 -1
  121. package/dist/ionic/{p-49799a34.entry.js → p-9e71982e.entry.js} +1 -1
  122. package/dist/ionic/p-Dr3N4o63.js +4 -0
  123. package/dist/ionic/{p-bc5713f7.entry.js → p-a1c8ba8c.entry.js} +1 -1
  124. package/dist/ionic/{p-91e242e4.entry.js → p-a94016be.entry.js} +1 -1
  125. package/dist/ionic/{p-6c8c37c2.entry.js → p-aa8c1e64.entry.js} +1 -1
  126. package/dist/ionic/p-c3335fe0.entry.js +4 -0
  127. package/dist/ionic/{p-2193e875.entry.js → p-d2fe6791.entry.js} +1 -1
  128. package/dist/types/components/select/select-interface.d.ts +21 -0
  129. package/dist/types/components/select/select.d.ts +7 -0
  130. package/dist/types/components/select-option/select-option.d.ts +4 -0
  131. package/dist/types/components.d.ts +9 -0
  132. package/dist/types/utils/select-option-render.d.ts +26 -0
  133. package/hydrate/index.js +396 -132
  134. package/hydrate/index.mjs +396 -132
  135. package/package.json +1 -1
  136. package/components/p-BGHaEUgp.js +0 -4
  137. package/components/p-BR9Yxas9.js +0 -4
  138. package/components/p-Ch9P0ikq.js +0 -4
  139. package/components/p-iwGbwewM.js +0 -4
  140. package/dist/ionic/p-227744b8.entry.js +0 -4
  141. package/dist/ionic/p-4b0f5ffd.entry.js +0 -4
  142. package/dist/ionic/p-64341e32.entry.js +0 -4
  143. package/dist/ionic/p-6be2b2d3.entry.js +0 -4
  144. package/dist/ionic/p-9acd3fd3.entry.js +0 -4
  145. package/dist/ionic/p-a283aa4d.entry.js +0 -4
@@ -3,11 +3,13 @@
3
3
  */
4
4
  import caretDownRegular from "@phosphor-icons/core/assets/regular/caret-down.svg";
5
5
  import { Build, Host, h, forceUpdate } from "@stencil/core";
6
+ import { ENABLE_HTML_CONTENT_DEFAULT } from "../../utils/config";
6
7
  import { compareOptions, createNotchController, isOptionSelected, checkInvalidState } from "../../utils/forms/index";
7
8
  import { focusVisibleElement, renderHiddenInput, inheritAttributes } from "../../utils/helpers";
8
9
  import { printIonWarning } from "../../utils/logging/index";
9
10
  import { actionSheetController, alertController, popoverController, modalController } from "../../utils/overlays";
10
11
  import { isRTL } from "../../utils/rtl/index";
12
+ import { sanitizeDOMString } from "../../utils/sanitization/index";
11
13
  import { createColorClasses, hostContext } from "../../utils/theme";
12
14
  import { watchForOptions } from "../../utils/watch-options";
13
15
  import { caretDownSharp, chevronExpand } from "ionicons/icons";
@@ -40,6 +42,7 @@ export class Select {
40
42
  this.helperTextId = `${this.inputId}-helper-text`;
41
43
  this.errorTextId = `${this.inputId}-error-text`;
42
44
  this.inheritedAttributes = {};
45
+ this.customHTMLEnabled = config.get('innerHTMLTemplatesEnabled', ENABLE_HTML_CONTENT_DEFAULT);
43
46
  this.isExpanded = false;
44
47
  /**
45
48
  * The `hasFocus` state ensures the focus class is
@@ -402,9 +405,14 @@ export class Select {
402
405
  .join(' ');
403
406
  const optClass = `${OPTION_CLASS} ${copyClasses}`;
404
407
  const isSelected = isOptionSelected(selectValue, value, this.compareWith);
408
+ const text = this.customHTMLEnabled ? getOptionContent(option) : option.textContent;
409
+ const startContent = this.customHTMLEnabled
410
+ ? getOptionContent(option, 'start')
411
+ : undefined;
412
+ const endContent = this.customHTMLEnabled ? getOptionContent(option, 'end') : undefined;
405
413
  return {
406
414
  role: isSelected ? 'selected' : '',
407
- text: option.textContent,
415
+ text: text !== null && text !== void 0 ? text : '',
408
416
  cssClass: optClass,
409
417
  handler: () => {
410
418
  this.setValue(value);
@@ -413,6 +421,9 @@ export class Select {
413
421
  'aria-checked': isSelected ? 'true' : 'false',
414
422
  role: 'radio',
415
423
  },
424
+ startContent: startContent !== null && startContent !== void 0 ? startContent : undefined,
425
+ endContent: endContent !== null && endContent !== void 0 ? endContent : undefined,
426
+ description: option.description,
416
427
  };
417
428
  });
418
429
  // Add "cancel" button
@@ -433,13 +444,21 @@ export class Select {
433
444
  .filter((cls) => cls !== 'hydrated')
434
445
  .join(' ');
435
446
  const optClass = `${OPTION_CLASS} ${copyClasses}`;
447
+ const label = this.customHTMLEnabled ? getOptionContent(option) : option.textContent;
448
+ const startContent = this.customHTMLEnabled
449
+ ? getOptionContent(option, 'start')
450
+ : undefined;
451
+ const endContent = this.customHTMLEnabled ? getOptionContent(option, 'end') : undefined;
436
452
  return {
437
453
  type: inputType,
438
454
  cssClass: optClass,
439
- label: option.textContent || '',
455
+ label: label !== null && label !== void 0 ? label : '',
440
456
  value,
441
457
  checked: isOptionSelected(selectValue, value, this.compareWith),
442
458
  disabled: option.disabled,
459
+ startContent: startContent !== null && startContent !== void 0 ? startContent : undefined,
460
+ endContent: endContent !== null && endContent !== void 0 ? endContent : undefined,
461
+ description: option.description,
443
462
  };
444
463
  });
445
464
  return alertInputs;
@@ -452,8 +471,13 @@ export class Select {
452
471
  .filter((cls) => cls !== 'hydrated')
453
472
  .join(' ');
454
473
  const optClass = `${OPTION_CLASS} ${copyClasses}`;
474
+ const text = this.customHTMLEnabled ? getOptionContent(option) : option.textContent;
475
+ const startContent = this.customHTMLEnabled
476
+ ? getOptionContent(option, 'start')
477
+ : undefined;
478
+ const endContent = this.customHTMLEnabled ? getOptionContent(option, 'end') : undefined;
455
479
  return {
456
- text: option.textContent || '',
480
+ text: text !== null && text !== void 0 ? text : '',
457
481
  cssClass: optClass,
458
482
  value,
459
483
  checked: isOptionSelected(selectValue, value, this.compareWith),
@@ -464,6 +488,9 @@ export class Select {
464
488
  this.close();
465
489
  }
466
490
  },
491
+ startContent: startContent !== null && startContent !== void 0 ? startContent : undefined,
492
+ endContent: endContent !== null && endContent !== void 0 ? endContent : undefined,
493
+ description: option.description,
467
494
  };
468
495
  });
469
496
  return popoverOptions;
@@ -631,12 +658,18 @@ export class Select {
631
658
  }
632
659
  return;
633
660
  }
634
- getText() {
661
+ /**
662
+ * Returns the text to display in the select based on the selected value.
663
+ *
664
+ * @param useHTML If `true`, the returned text will include any custom HTML content from the selected option. If `false`, the returned text will be plain text without any HTML. Defaults to `false`.
665
+ * @returns The text to display in the select, either with or without HTML based on the `useHTML` parameter.
666
+ */
667
+ getText(useHTML = false) {
635
668
  const selectedText = this.selectedText;
636
669
  if (selectedText != null && selectedText !== '') {
637
670
  return selectedText;
638
671
  }
639
- return generateText(this.childOpts, this.value, this.compareWith);
672
+ return generateText(this.childOpts, this.value, this.compareWith, useHTML);
640
673
  }
641
674
  setFocus() {
642
675
  if (this.focusEl) {
@@ -725,7 +758,7 @@ export class Select {
725
758
  */
726
759
  renderSelectText() {
727
760
  const { placeholder } = this;
728
- const displayValue = this.getText();
761
+ const displayValue = this.getText(true);
729
762
  let addPlaceholderClass = false;
730
763
  let selectText = displayValue;
731
764
  if (selectText === '' && placeholder !== undefined) {
@@ -737,6 +770,9 @@ export class Select {
737
770
  'select-placeholder': addPlaceholderClass,
738
771
  };
739
772
  const textPart = addPlaceholderClass ? 'placeholder' : 'text';
773
+ if (this.customHTMLEnabled) {
774
+ return h("div", { "aria-hidden": "true", class: selectTextClasses, part: textPart, innerHTML: selectText });
775
+ }
740
776
  return (h("div", { "aria-hidden": "true", class: selectTextClasses, part: textPart }, selectText));
741
777
  }
742
778
  /**
@@ -754,6 +790,7 @@ export class Select {
754
790
  get ariaLabel() {
755
791
  var _a;
756
792
  const { placeholder, inheritedAttributes } = this;
793
+ // Get the plain text from the selected text
757
794
  const displayValue = this.getText();
758
795
  // The aria label should be preferred over visible text if both are specified
759
796
  const definedLabel = (_a = inheritedAttributes['aria-label']) !== null && _a !== void 0 ? _a : this.labelText;
@@ -908,7 +945,7 @@ export class Select {
908
945
  * TODO(FW-5592): Remove hasStartEndSlots condition
909
946
  */
910
947
  const labelShouldFloat = labelPlacement === 'stacked' || (labelPlacement === 'floating' && (hasValue || isExpanded || hasStartEndSlots));
911
- return (h(Host, { key: '663520f7e688836f89101008efd49f38cc5e4aed', onClick: this.onClick, class: createColorClasses(this.color, {
948
+ return (h(Host, { key: '460ec72063e2f484a1fd0d7605739f4cef2d8c87', onClick: this.onClick, class: createColorClasses(this.color, {
912
949
  [theme]: true,
913
950
  'in-item': inItem,
914
951
  'in-item-color': hostContext('ion-item.ion-color', el),
@@ -927,7 +964,7 @@ export class Select {
927
964
  [`select-shape-${shape}`]: shape !== undefined,
928
965
  [`select-label-placement-${labelPlacement}`]: true,
929
966
  [`select-size-${size}`]: size !== undefined,
930
- }) }, h("label", { key: '17c2602b8e33017cbb898a332f9960482ac72c84', class: "select-wrapper", id: "select-label", onClick: this.onLabelClick, part: "wrapper" }, this.renderLabelContainer(), h("div", { key: '00b4887a9f5c6137b2c817e69e77498b3502051d', class: "select-wrapper-inner", part: "inner" },
967
+ }) }, h("label", { key: 'a553837e266ce48e17687ee7ff4027f69a2eff1e', class: "select-wrapper", id: "select-label", onClick: this.onLabelClick, part: "wrapper" }, this.renderLabelContainer(), h("div", { key: 'e5071d8ee5e4078887ac27a192aa95553a9f487d', class: "select-wrapper-inner", part: "inner" },
931
968
  /**
932
969
  * For the ionic theme, we render the outline container here
933
970
  * instead of higher up, so it can be positioned relative to
@@ -937,7 +974,7 @@ export class Select {
937
974
  * <label> element, ensuring that clicking the label text
938
975
  * focuses the select.
939
976
  */
940
- theme === 'ionic' && fill === 'outline' && h("div", { key: 'd4916d84ec0d92888139b84be64cd766150e1d83', class: "select-outline" }), h("slot", { key: 'ad9ca19118b0a4cad5995f1010bf8dc9b6c1d4d3', name: "start" }), h("div", { key: '7f36c316973409d237a135ee191bb0f39174e47c', class: "native-wrapper", ref: (el) => (this.nativeWrapperEl = el), part: "container" }, this.renderSelectText(), this.renderListbox()), h("slot", { key: '0a23c1be8afefa31cf7161dd7470decf30603ce0', name: "end" }), shouldRenderInnerIcon && this.renderSelectIcon()), shouldRenderOuterIcon && this.renderSelectIcon(), shouldRenderHighlight && h("div", { key: '0c455c157c0fb6f43f3f030758f5c583f13fceda', class: "select-highlight" })), this.renderBottomContent()));
977
+ theme === 'ionic' && fill === 'outline' && h("div", { key: '04a19d0f8deff600f354c1e2682937884e8e3673', class: "select-outline" }), h("slot", { key: '51e50dccee39355a3ac78b615e4a7a71a371a744', name: "start" }), h("div", { key: '6bf71c54ee895db920d3f98ed7bc8869d9ea3874', class: "native-wrapper", ref: (el) => (this.nativeWrapperEl = el), part: "container" }, this.renderSelectText(), this.renderListbox()), h("slot", { key: '2f9ebe8996ebfc8371116ec48ca4b750b8c42e43', name: "end" }), shouldRenderInnerIcon && this.renderSelectIcon()), shouldRenderOuterIcon && this.renderSelectIcon(), shouldRenderHighlight && h("div", { key: 'cdf1742886c636270ada0b1e5c61faff40bacdc1', class: "select-highlight" })), this.renderBottomContent()));
941
978
  }
942
979
  static get is() { return "ion-select"; }
943
980
  static get encapsulation() { return "shadow"; }
@@ -1618,25 +1655,99 @@ const parseValue = (value) => {
1618
1655
  }
1619
1656
  return value.toString();
1620
1657
  };
1621
- const generateText = (opts, value, compareWith) => {
1658
+ const generateText = (opts, value, compareWith, useHTML = false) => {
1622
1659
  if (value === undefined) {
1623
1660
  return '';
1624
1661
  }
1625
1662
  if (Array.isArray(value)) {
1626
1663
  return value
1627
- .map((v) => textForValue(opts, v, compareWith))
1664
+ .map((v) => textForValue(opts, v, compareWith, useHTML))
1628
1665
  .filter((opt) => opt !== null)
1629
1666
  .join(', ');
1630
1667
  }
1631
1668
  else {
1632
- return textForValue(opts, value, compareWith) || '';
1669
+ return textForValue(opts, value, compareWith, useHTML) || '';
1633
1670
  }
1634
1671
  };
1635
- const textForValue = (opts, value, compareWith) => {
1672
+ const textForValue = (opts, value, compareWith, useHTML = false) => {
1636
1673
  const selectOpt = opts.find((opt) => {
1637
1674
  return compareOptions(value, getOptionValue(opt), compareWith);
1638
1675
  });
1639
- return selectOpt ? selectOpt.textContent : null;
1676
+ const customHTMLEnabled = config.get('innerHTMLTemplatesEnabled', ENABLE_HTML_CONTENT_DEFAULT);
1677
+ if (!selectOpt)
1678
+ return null;
1679
+ return customHTMLEnabled && useHTML
1680
+ ? getOptionContent(selectOpt, undefined, true)
1681
+ : selectOpt.textContent;
1682
+ };
1683
+ /**
1684
+ * Trims whitespace from all text nodes within a DOM tree.
1685
+ * This prevents invisible layout shifts and unwanted gaps between
1686
+ * elements when HTML content is injected via innerHTML or cloneNode,
1687
+ * as browsers preserve whitespace (tabs, newlines, spaces) from
1688
+ * the original source markup.
1689
+ *
1690
+ * @param node The root node to start trimming text nodes from.
1691
+ */
1692
+ const trimTextNodes = (node) => {
1693
+ node.childNodes.forEach((child) => {
1694
+ var _a;
1695
+ if (child.nodeType === Node.TEXT_NODE) {
1696
+ child.textContent = ((_a = child.textContent) === null || _a === void 0 ? void 0 : _a.trim()) || '';
1697
+ }
1698
+ else if (child.nodeType === Node.ELEMENT_NODE) {
1699
+ trimTextNodes(child);
1700
+ }
1701
+ });
1702
+ };
1703
+ /**
1704
+ * Extracts and clones content from an `ion-select-option` element
1705
+ * for rendering within overlay interfaces or the select text when `customHTMLEnabled` is `true`.
1706
+ *
1707
+ * @param option - The `ion-select-option` element to extract content from.
1708
+ * @param slotName - Optional slot name to extract. If omitted, extracts the default slot content.
1709
+ * @param useHTML - If `true`, the returned string will include any custom HTML content. If `false`, the returned string will be plain text without any HTML.
1710
+ * @returns When `useHTML` is `true`, a sanitized HTML string. When `false`, a
1711
+ * div element containing cloned child nodes. Returns `null` if no matching
1712
+ * content is found.
1713
+ */
1714
+ const getOptionContent = (option, slotName, useHTML = false) => {
1715
+ let nodes;
1716
+ if (slotName) {
1717
+ // Named slot: get elements with matching slot attribute
1718
+ nodes = Array.from(option.children).filter((el) => el.getAttribute('slot') === slotName);
1719
+ }
1720
+ else {
1721
+ // Default slot: get nodes without a slot attribute
1722
+ nodes = Array.from(option.childNodes).filter((node) => {
1723
+ var _a;
1724
+ if (node.nodeType === Node.ELEMENT_NODE) {
1725
+ return !node.hasAttribute('slot');
1726
+ }
1727
+ // Exclude whitespace-only text nodes to prevent empty container returns
1728
+ return ((_a = node.textContent) === null || _a === void 0 ? void 0 : _a.trim().length) !== 0;
1729
+ });
1730
+ }
1731
+ if (nodes.length === 0) {
1732
+ return null;
1733
+ }
1734
+ // Clone each node into a temporary container
1735
+ const container = document.createElement('div');
1736
+ nodes.forEach((n) => {
1737
+ var _a;
1738
+ const clone = n.cloneNode(true);
1739
+ if (clone.nodeType === Node.TEXT_NODE) {
1740
+ clone.textContent = ((_a = clone.textContent) === null || _a === void 0 ? void 0 : _a.trim()) || '';
1741
+ }
1742
+ else {
1743
+ trimTextNodes(clone);
1744
+ }
1745
+ container.appendChild(clone);
1746
+ });
1747
+ if (useHTML) {
1748
+ return sanitizeDOMString(container.innerHTML.trim()) || null;
1749
+ }
1750
+ return container;
1640
1751
  };
1641
1752
  let selectIds = 0;
1642
1753
  const OPTION_CLASS = 'select-interface-option';
@@ -103,6 +103,13 @@
103
103
  * @prop --border-width: Width of the select border
104
104
  *
105
105
  * @prop --ripple-color: The color of the ripple effect on MD mode.
106
+ *
107
+ * @prop --select-text-media-width: The width of media (icons/images) in the select text.
108
+ * @prop --select-text-media-height: The height of media (icons/images) in the select text.
109
+ * @prop --select-text-media-border-width: The border width of media (icons/images) in the select text.
110
+ * @prop --select-text-media-border-color: The border color of media (icons/images) in the select text.
111
+ * @prop --select-text-media-border-radius: The border radius of media (icons/images) in the select text.
112
+ * @prop --select-text-media-border-style: The border style of media (icons/images) in the select text.
106
113
  */
107
114
  --padding-top: 0px;
108
115
  --padding-end: 0px;
@@ -114,6 +121,8 @@
114
121
  --highlight-color-focused: var(--ion-color-primary, #0054e9);
115
122
  --highlight-color-valid: var(--ion-color-success, #2dd55b);
116
123
  --highlight-color-invalid: var(--ion-color-danger, #c5000f);
124
+ --select-text-media-height: 1.5em;
125
+ --select-text-media-width: 1.5em;
117
126
  /**
118
127
  * This is a private API that is used to switch
119
128
  * out the highlight color based on the state
@@ -218,6 +227,19 @@ button {
218
227
  overflow: hidden;
219
228
  }
220
229
 
230
+ .select-text img,
231
+ .select-text ion-img,
232
+ .select-text ion-icon,
233
+ .select-text ion-thumbnail,
234
+ .select-text ion-avatar {
235
+ border-radius: var(--select-text-media-border-radius);
236
+ width: var(--select-text-media-width);
237
+ height: var(--select-text-media-height);
238
+ border-width: var(--select-text-media-border-width);
239
+ border-style: var(--select-text-media-border-style);
240
+ border-color: var(--select-text-media-border-color);
241
+ }
242
+
221
243
  .select-wrapper {
222
244
  display: flex;
223
245
  position: relative;
@@ -619,6 +641,15 @@ button {
619
641
  min-width: 16px;
620
642
  }
621
643
 
644
+ /**
645
+ * If the select text contains rich content, we want to add some
646
+ * spacing between the items without changing the display to prevent
647
+ * losing the ellipsis behavior.
648
+ */
649
+ .select-text > * {
650
+ margin-inline-start: 8px;
651
+ }
652
+
622
653
  .label-text-wrapper {
623
654
  /**
624
655
  * Label text should not extend
@@ -63,6 +63,15 @@
63
63
  height: 100%;
64
64
  }
65
65
 
66
+ .select-option-label {
67
+ display: flex;
68
+ align-items: center;
69
+ }
70
+
71
+ .select-option-description {
72
+ display: block;
73
+ }
74
+
66
75
  :host(.in-modal-default) ion-content::part(scroll) {
67
76
  --padding-bottom: var(--token-space-400, var(--token-scale-400, 16px));
68
77
  }
@@ -127,4 +136,19 @@ ion-content {
127
136
  }
128
137
  ion-content:has(.radio-checked) .ion-focused:not(.item-radio-checked) {
129
138
  --background-focused-opacity: 1;
139
+ }
140
+
141
+ .select-option-label {
142
+ gap: var(--token-space-300, var(--token-scale-300, 12px));
143
+ }
144
+
145
+ .select-option-description {
146
+ font-size: var(--token-font-size-350, 0.875rem);
147
+ font-weight: var(--token-font-weight-regular, 400);
148
+ letter-spacing: var(--token-font-letter-spacing-0, 0%);
149
+ line-height: var(--token-font-line-height-600, var(--token-scale-600, 24px));
150
+ text-decoration: none;
151
+ text-transform: none;
152
+ color: var(--token-text-subtle, var(--token-primitives-neutral-1000, #3b3b3b));
153
+ font-size: var(--token-font-size-350, 0.875rem);
130
154
  }
@@ -1,7 +1,84 @@
1
+ /**
2
+ * A heuristic that applies CSS to tablet
3
+ * viewports.
4
+ *
5
+ * Usage:
6
+ * @include tablet-viewport() {
7
+ * :host {
8
+ * background-color: green;
9
+ * }
10
+ * }
11
+ */
12
+ /**
13
+ * A heuristic that applies CSS to mobile
14
+ * viewports (i.e. phones, not tablets).
15
+ *
16
+ * Usage:
17
+ * @include mobile-viewport() {
18
+ * :host {
19
+ * background-color: blue;
20
+ * }
21
+ * }
22
+ */
23
+ /**
24
+ * Convert a font size to a dynamic font size.
25
+ * Fonts that participate in Dynamic Type should use
26
+ * dynamic font sizes.
27
+ * @param size - The initial font size including the unit (i.e. px or pt)
28
+ * @param unit (optional) - The unit to convert to. Use this if you want to
29
+ * convert to a unit other than $baselineUnit.
30
+ */
31
+ /**
32
+ * Convert a font size to a dynamic font size but impose
33
+ * a maximum font size.
34
+ * @param size - The initial font size including the unit (i.e. px or pt)
35
+ * @param maxScale - The maximum scale of the font (i.e. 2.5 for a maximum 250% scale).
36
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
37
+ * convert to a unit other than $baselineUnit.
38
+ */
39
+ /**
40
+ * Convert a font size to a dynamic font size but impose
41
+ * a minimum font size.
42
+ * @param size - The initial font size including the unit (i.e. px or pt)
43
+ * @param minScale - The minimum scale of the font (i.e. 0.8 for a minimum 80% scale).
44
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
45
+ * convert to a unit other than $baselineUnit.
46
+ */
47
+ /**
48
+ * Convert a font size to a dynamic font size but impose
49
+ * maximum and minimum font sizes.
50
+ * @param size - The initial font size including the unit (i.e. px or pt)
51
+ * @param minScale - The minimum scale of the font (i.e. 0.8 for a minimum 80% scale).
52
+ * @param maxScale - The maximum scale of the font (i.e. 2.5 for a maximum 250% scale).
53
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
54
+ * convert to a unit other than $baselineUnit.
55
+ */
1
56
  :host {
2
57
  height: 100%;
3
58
  }
4
59
 
60
+ .select-option-label {
61
+ display: flex;
62
+ align-items: center;
63
+ }
64
+
65
+ .select-option-description {
66
+ display: block;
67
+ }
68
+
69
+ .select-option-label {
70
+ gap: 12px;
71
+ }
72
+
73
+ .select-option-description {
74
+ padding-left: 0;
75
+ padding-right: 0;
76
+ padding-top: 5px;
77
+ padding-bottom: 0;
78
+ color: var(--ion-color-step-700, var(--ion-text-color-step-300, #4d4d4d));
79
+ font-size: 0.75rem;
80
+ }
81
+
5
82
  /**
6
83
  * Convert a pixels given value into rem
7
84
  *
@@ -4,6 +4,7 @@
4
4
  import { getIonMode } from "../../global/ionic-global";
5
5
  import { Host, forceUpdate, h } from "@stencil/core";
6
6
  import { safeCall } from "../../utils/overlays";
7
+ import { renderOptionLabel } from "../../utils/select-option-render";
7
8
  import { getClassMap, hostContext } from "../../utils/theme";
8
9
  export class SelectModal {
9
10
  constructor() {
@@ -60,33 +61,65 @@ export class SelectModal {
60
61
  }
61
62
  renderRadioOptions() {
62
63
  const checked = this.options.filter((o) => o.checked).map((o) => o.value)[0];
63
- return (h("ion-radio-group", { value: checked, onIonChange: (ev) => this.callOptionHandler(ev) }, this.options.map((option) => (h("ion-item", { lines: "none", class: Object.assign({
64
- // TODO FW-4784
65
- 'item-radio-checked': option.value === checked
66
- }, getClassMap(option.cssClass)) }, h("ion-radio", { value: option.value, disabled: option.disabled, justify: "start", labelPlacement: "end", onClick: () => this.closeModal(), onKeyUp: (ev) => {
67
- if (ev.key === ' ') {
68
- /**
69
- * Selecting a radio option with keyboard navigation,
70
- * either through the Enter or Space keys, should
71
- * dismiss the modal.
72
- */
73
- this.closeModal();
74
- }
75
- } }, option.text))))));
64
+ return (h("ion-radio-group", { value: checked, onIonChange: (ev) => this.callOptionHandler(ev) }, this.options.map((option, index) => {
65
+ /**
66
+ * Cast to `SelectOverlayOption` to access rich content
67
+ * fields (`startContent`, `endContent`, `description`)
68
+ * that are passed through from `ion-select` but not
69
+ * part of the public `SelectModalOption` interface.
70
+ */
71
+ const richOption = option;
72
+ const optionLabelOptions = {
73
+ id: `modal-option-${index}`,
74
+ label: richOption.text,
75
+ startContent: richOption.startContent,
76
+ endContent: richOption.endContent,
77
+ description: richOption.description,
78
+ };
79
+ return (h("ion-item", { lines: "none", class: Object.assign({
80
+ // TODO FW-4784
81
+ 'item-radio-checked': option.value === checked
82
+ }, getClassMap(option.cssClass)) }, h("ion-radio", { value: option.value, disabled: option.disabled, justify: "start", labelPlacement: "end", onClick: () => this.closeModal(), onKeyUp: (ev) => {
83
+ if (ev.key === ' ') {
84
+ /**
85
+ * Selecting a radio option with keyboard navigation,
86
+ * either through the Enter or Space keys, should
87
+ * dismiss the modal.
88
+ */
89
+ this.closeModal();
90
+ }
91
+ } }, renderOptionLabel(optionLabelOptions, 'select-option-label'))));
92
+ })));
76
93
  }
77
94
  renderCheckboxOptions() {
78
- return this.options.map((option) => (h("ion-item", { class: Object.assign({
79
- // TODO FW-4784
80
- 'item-checkbox-checked': option.checked
81
- }, getClassMap(option.cssClass)) }, h("ion-checkbox", { value: option.value, disabled: option.disabled, checked: option.checked, justify: "start", labelPlacement: "end", onIonChange: (ev) => {
82
- this.setChecked(ev);
83
- this.callOptionHandler(ev);
84
- // TODO FW-4784
85
- forceUpdate(this);
86
- } }, option.text))));
95
+ return this.options.map((option, index) => {
96
+ /**
97
+ * Cast to `SelectOverlayOption` to access rich content
98
+ * fields (`startContent`, `endContent`, `description`)
99
+ * that are passed through from `ion-select` but not
100
+ * part of the public `SelectModalOption` interface.
101
+ */
102
+ const richOption = option;
103
+ const optionLabelOptions = {
104
+ id: `modal-option-${index}`,
105
+ label: richOption.text,
106
+ startContent: richOption.startContent,
107
+ endContent: richOption.endContent,
108
+ description: richOption.description,
109
+ };
110
+ return (h("ion-item", { class: Object.assign({
111
+ // TODO FW-4784
112
+ 'item-checkbox-checked': option.checked
113
+ }, getClassMap(option.cssClass)) }, h("ion-checkbox", { value: option.value, disabled: option.disabled, checked: option.checked, justify: "start", labelPlacement: "end", onIonChange: (ev) => {
114
+ this.setChecked(ev);
115
+ this.callOptionHandler(ev);
116
+ // TODO FW-4784
117
+ forceUpdate(this);
118
+ } }, renderOptionLabel(optionLabelOptions, 'select-option-label'))));
119
+ });
87
120
  }
88
121
  render() {
89
- return (h(Host, { key: 'c0218aa9ebcbe9d70a6cc1ed8d005ca656e163d3', class: Object.assign({ [getIonMode(this)]: true }, this.getModalContextClasses()) }, h("ion-header", { key: '4698d7442befa17e449655b2f9439001298991a0' }, h("ion-toolbar", { key: 'd0f1d3418f8fdbfdf12fef752f6be4939e70eb79' }, this.header !== undefined && h("ion-title", { key: '2916c22b3a42635e503fa0a0c916b32909999761' }, this.header), h("ion-buttons", { key: 'e93827fc49edd8ac04100e7df8a2a5144d257c19', slot: "end" }, h("ion-button", { key: '0b46828d67d5b4864f40fcbf082ae24797099fb9', onClick: () => this.closeModal() }, this.cancelText)))), h("ion-content", { key: '8460ad9ce425ad0b25b603d0cd42c58b68ca63ee' }, h("ion-list", { key: 'c48ea90af6f36a44102a8852267bd7f6f82f9598' }, this.multiple === true ? this.renderCheckboxOptions() : this.renderRadioOptions()))));
122
+ return (h(Host, { key: '3fa77e7726ef4b7678cc2ea910e2e09b03ef7cbe', class: Object.assign({ [getIonMode(this)]: true }, this.getModalContextClasses()) }, h("ion-header", { key: '141c87f6f2c0de5e3b614fb616402d2e26276829' }, h("ion-toolbar", { key: '3a44e1c42eb4b48e313a234540ad4f71a1fe7d05' }, this.header !== undefined && h("ion-title", { key: '418d57b29f25eb88a5a1cd9ccbdf73f34f75408a' }, this.header), h("ion-buttons", { key: '7e37ad754da093d86acb758583acf47b9960fba1', slot: "end" }, h("ion-button", { key: '5ac2c7d9bbc345f575319c141f0f6d7acdc4921e', onClick: () => this.closeModal() }, this.cancelText)))), h("ion-content", { key: '4d397a2f668220839e58611da686a2eed94d4135' }, h("ion-list", { key: 'e3e870e4a060af0d475ba8b8263b0eeb4158bac8' }, this.multiple === true ? this.renderCheckboxOptions() : this.renderRadioOptions()))));
90
123
  }
91
124
  static get is() { return "ion-select-modal"; }
92
125
  static get encapsulation() { return "scoped"; }
@@ -1,6 +1,85 @@
1
+ /**
2
+ * A heuristic that applies CSS to tablet
3
+ * viewports.
4
+ *
5
+ * Usage:
6
+ * @include tablet-viewport() {
7
+ * :host {
8
+ * background-color: green;
9
+ * }
10
+ * }
11
+ */
12
+ /**
13
+ * A heuristic that applies CSS to mobile
14
+ * viewports (i.e. phones, not tablets).
15
+ *
16
+ * Usage:
17
+ * @include mobile-viewport() {
18
+ * :host {
19
+ * background-color: blue;
20
+ * }
21
+ * }
22
+ */
23
+ /**
24
+ * Convert a font size to a dynamic font size.
25
+ * Fonts that participate in Dynamic Type should use
26
+ * dynamic font sizes.
27
+ * @param size - The initial font size including the unit (i.e. px or pt)
28
+ * @param unit (optional) - The unit to convert to. Use this if you want to
29
+ * convert to a unit other than $baselineUnit.
30
+ */
31
+ /**
32
+ * Convert a font size to a dynamic font size but impose
33
+ * a maximum font size.
34
+ * @param size - The initial font size including the unit (i.e. px or pt)
35
+ * @param maxScale - The maximum scale of the font (i.e. 2.5 for a maximum 250% scale).
36
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
37
+ * convert to a unit other than $baselineUnit.
38
+ */
39
+ /**
40
+ * Convert a font size to a dynamic font size but impose
41
+ * a minimum font size.
42
+ * @param size - The initial font size including the unit (i.e. px or pt)
43
+ * @param minScale - The minimum scale of the font (i.e. 0.8 for a minimum 80% scale).
44
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
45
+ * convert to a unit other than $baselineUnit.
46
+ */
47
+ /**
48
+ * Convert a font size to a dynamic font size but impose
49
+ * maximum and minimum font sizes.
50
+ * @param size - The initial font size including the unit (i.e. px or pt)
51
+ * @param minScale - The minimum scale of the font (i.e. 0.8 for a minimum 80% scale).
52
+ * @param maxScale - The maximum scale of the font (i.e. 2.5 for a maximum 250% scale).
53
+ * @param unit (optional) - The unit to convert the initial font size to. Use this if you want to
54
+ * convert to a unit other than $baselineUnit.
55
+ */
1
56
  :host {
2
57
  height: 100%;
3
- } /**
58
+ }
59
+
60
+ .select-option-label {
61
+ display: flex;
62
+ align-items: center;
63
+ }
64
+
65
+ .select-option-description {
66
+ display: block;
67
+ }
68
+
69
+ .select-option-label {
70
+ gap: 12px;
71
+ }
72
+
73
+ .select-option-description {
74
+ padding-left: 0;
75
+ padding-right: 0;
76
+ padding-top: 5px;
77
+ padding-bottom: 0;
78
+ color: var(--ion-color-step-700, var(--ion-text-color-step-300, #4d4d4d));
79
+ font-size: 0.75rem;
80
+ }
81
+
82
+ /**
4
83
  * A heuristic that applies CSS to tablet
5
84
  * viewports.
6
85
  *