radio-group.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  1. /*!
  2. * (C) Ionic http://ionicframework.com - MIT License
  3. */
  4. import { proxyCustomElement, HTMLElement, createEvent, h, Host } from '@stencil/core/internal/client';
  5. import { e as renderHiddenInput } from './helpers.js';
  6. import { b as getIonMode } from './ionic-global.js';
  7. const radioGroupIosCss = "ion-radio-group{vertical-align:top}.radio-group-wrapper{display:inline}.radio-group-top{line-height:1.5}.radio-group-top .error-text{display:none;color:var(--ion-color-danger, #c5000f)}.radio-group-top .helper-text{display:block;color:var(--ion-color-step-700, var(--ion-text-color-step-300, #4d4d4d))}.ion-touched.ion-invalid .radio-group-top .error-text{display:block}.ion-touched.ion-invalid .radio-group-top .helper-text{display:none}ion-list .radio-group-top{-webkit-padding-start:16px;padding-inline-start:16px;-webkit-padding-end:16px;padding-inline-end:16px}";
  8. const IonRadioGroupIosStyle0 = radioGroupIosCss;
  9. const radioGroupMdCss = "ion-radio-group{vertical-align:top}.radio-group-wrapper{display:inline}.radio-group-top{line-height:1.5}.radio-group-top .error-text{display:none;color:var(--ion-color-danger, #c5000f)}.radio-group-top .helper-text{display:block;color:var(--ion-color-step-700, var(--ion-text-color-step-300, #4d4d4d))}.ion-touched.ion-invalid .radio-group-top .error-text{display:block}.ion-touched.ion-invalid .radio-group-top .helper-text{display:none}ion-list .radio-group-top{-webkit-padding-start:16px;padding-inline-start:16px;-webkit-padding-end:16px;padding-inline-end:16px}";
  10. const IonRadioGroupMdStyle0 = radioGroupMdCss;
  11. const RadioGroup = /*@__PURE__*/ proxyCustomElement(class RadioGroup extends HTMLElement {
  12. constructor() {
  13. super();
  14. this.__registerHost();
  15. this.ionChange = createEvent(this, "ionChange", 7);
  16. this.ionValueChange = createEvent(this, "ionValueChange", 7);
  17. this.inputId = `ion-rg-${radioGroupIds++}`;
  18. this.helperTextId = `${this.inputId}-helper-text`;
  19. this.errorTextId = `${this.inputId}-error-text`;
  20. this.labelId = `${this.inputId}-lbl`;
  21. this.setRadioTabindex = (value) => {
  22. const radios = this.getRadios();
  23. // Get the first radio that is not disabled and the checked one
  24. const first = radios.find((radio) => !radio.disabled);
  25. const checked = radios.find((radio) => radio.value === value && !radio.disabled);
  26. if (!first && !checked) {
  27. return;
  28. }
  29. // If an enabled checked radio exists, set it to be the focusable radio
  30. // otherwise we default to focus the first radio
  31. const focusable = checked || first;
  32. for (const radio of radios) {
  33. const tabindex = radio === focusable ? 0 : -1;
  34. radio.setButtonTabindex(tabindex);
  35. }
  36. };
  37. this.onClick = (ev) => {
  38. ev.preventDefault();
  39. /**
  40. * The Radio Group component mandates that only one radio button
  41. * within the group can be selected at any given time. Since `ion-radio`
  42. * is a shadow DOM component, it cannot natively perform this behavior
  43. * using the `name` attribute.
  44. */
  45. const selectedRadio = ev.target && ev.target.closest('ion-radio');
  46. /**
  47. * Our current disabled prop definition causes Stencil to mark it
  48. * as optional. While this is not desired, fixing this behavior
  49. * in Stencil is a significant breaking change, so this effort is
  50. * being de-risked in STENCIL-917. Until then, we compromise
  51. * here by checking for falsy `disabled` values instead of strictly
  52. * checking `disabled === false`.
  53. */
  54. if (selectedRadio && !selectedRadio.disabled) {
  55. const currentValue = this.value;
  56. const newValue = selectedRadio.value;
  57. if (newValue !== currentValue) {
  58. this.value = newValue;
  59. this.emitValueChange(ev);
  60. }
  61. else if (this.allowEmptySelection) {
  62. this.value = undefined;
  63. this.emitValueChange(ev);
  64. }
  65. }
  66. };
  67. this.allowEmptySelection = false;
  68. this.compareWith = undefined;
  69. this.name = this.inputId;
  70. this.value = undefined;
  71. this.helperText = undefined;
  72. this.errorText = undefined;
  73. }
  74. valueChanged(value) {
  75. this.setRadioTabindex(value);
  76. this.ionValueChange.emit({ value });
  77. }
  78. componentDidLoad() {
  79. /**
  80. * There's an issue when assigning a value to the radio group
  81. * within the Angular primary content (rendering within the
  82. * app component template). When the template is isolated to a route,
  83. * the value is assigned correctly.
  84. * To address this issue, we need to ensure that the watcher is
  85. * called after the component has finished loading,
  86. * allowing the emit to be dispatched correctly.
  87. */
  88. this.valueChanged(this.value);
  89. }
  90. async connectedCallback() {
  91. // Get the list header if it exists and set the id
  92. // this is used to set aria-labelledby
  93. const header = this.el.querySelector('ion-list-header') || this.el.querySelector('ion-item-divider');
  94. if (header) {
  95. const label = (this.label = header.querySelector('ion-label'));
  96. if (label) {
  97. this.labelId = label.id = this.name + '-lbl';
  98. }
  99. }
  100. }
  101. getRadios() {
  102. return Array.from(this.el.querySelectorAll('ion-radio'));
  103. }
  104. /**
  105. * Emits an `ionChange` event.
  106. *
  107. * This API should be called for user committed changes.
  108. * This API should not be used for external value changes.
  109. */
  110. emitValueChange(event) {
  111. const { value } = this;
  112. this.ionChange.emit({ value, event });
  113. }
  114. onKeydown(ev) {
  115. // We don't want the value to automatically change/emit when the radio group is part of a select interface
  116. // as this will cause the interface to close when navigating through the radio group options
  117. const inSelectInterface = !!this.el.closest('ion-select-popover') || !!this.el.closest('ion-select-modal');
  118. if (ev.target && !this.el.contains(ev.target)) {
  119. return;
  120. }
  121. // Get all radios inside of the radio group and then
  122. // filter out disabled radios since we need to skip those
  123. const radios = this.getRadios().filter((radio) => !radio.disabled);
  124. // Only move the radio if the current focus is in the radio group
  125. if (ev.target && radios.includes(ev.target)) {
  126. const index = radios.findIndex((radio) => radio === ev.target);
  127. const current = radios[index];
  128. let next;
  129. // If hitting arrow down or arrow right, move to the next radio
  130. // If we're on the last radio, move to the first radio
  131. if (['ArrowDown', 'ArrowRight'].includes(ev.key)) {
  132. next = index === radios.length - 1 ? radios[0] : radios[index + 1];
  133. }
  134. // If hitting arrow up or arrow left, move to the previous radio
  135. // If we're on the first radio, move to the last radio
  136. if (['ArrowUp', 'ArrowLeft'].includes(ev.key)) {
  137. next = index === 0 ? radios[radios.length - 1] : radios[index - 1];
  138. }
  139. if (next && radios.includes(next)) {
  140. next.setFocus(ev);
  141. if (!inSelectInterface) {
  142. this.value = next.value;
  143. this.emitValueChange(ev);
  144. }
  145. }
  146. // Update the radio group value when a user presses the
  147. // space bar on top of a selected radio
  148. if ([' '].includes(ev.key)) {
  149. const previousValue = this.value;
  150. this.value = this.allowEmptySelection && this.value !== undefined ? undefined : current.value;
  151. if (previousValue !== this.value || this.allowEmptySelection) {
  152. /**
  153. * Value change should only be emitted if the value is different,
  154. * such as selecting a new radio with the space bar or if
  155. * the radio group allows for empty selection and the user
  156. * is deselecting a checked radio.
  157. */
  158. this.emitValueChange(ev);
  159. }
  160. // Prevent browsers from jumping
  161. // to the bottom of the screen
  162. ev.preventDefault();
  163. }
  164. }
  165. }
  166. /** @internal */
  167. async setFocus() {
  168. const radioToFocus = this.getRadios().find((r) => r.tabIndex !== -1);
  169. radioToFocus === null || radioToFocus === void 0 ? void 0 : radioToFocus.setFocus();
  170. }
  171. /**
  172. * Renders the helper text or error text values
  173. */
  174. renderHintText() {
  175. const { helperText, errorText, helperTextId, errorTextId } = this;
  176. const hasHintText = !!helperText || !!errorText;
  177. if (!hasHintText) {
  178. return;
  179. }
  180. return (h("div", { class: "radio-group-top" }, h("div", { id: helperTextId, class: "helper-text" }, helperText), h("div", { id: errorTextId, class: "error-text" }, errorText)));
  181. }
  182. getHintTextID() {
  183. const { el, helperText, errorText, helperTextId, errorTextId } = this;
  184. if (el.classList.contains('ion-touched') && el.classList.contains('ion-invalid') && errorText) {
  185. return errorTextId;
  186. }
  187. if (helperText) {
  188. return helperTextId;
  189. }
  190. return undefined;
  191. }
  192. render() {
  193. const { label, labelId, el, name, value } = this;
  194. const mode = getIonMode(this);
  195. renderHiddenInput(true, el, name, value, false);
  196. return (h(Host, { key: 'cac92777297029d7fd1b6af264d92850e35dfbba', role: "radiogroup", "aria-labelledby": label ? labelId : null, "aria-describedby": this.getHintTextID(), "aria-invalid": this.getHintTextID() === this.errorTextId, onClick: this.onClick, class: mode }, this.renderHintText(), h("div", { key: '6b5c634dba30d54eedc031b077863f3d6a9d9e9b', class: "radio-group-wrapper" }, h("slot", { key: '443edb3ff6f4c59d4c4324c8a19f2d6def47a322' }))));
  197. }
  198. get el() { return this; }
  199. static get watchers() { return {
  200. "value": ["valueChanged"]
  201. }; }
  202. static get style() { return {
  203. ios: IonRadioGroupIosStyle0,
  204. md: IonRadioGroupMdStyle0
  205. }; }
  206. }, [36, "ion-radio-group", {
  207. "allowEmptySelection": [4, "allow-empty-selection"],
  208. "compareWith": [1, "compare-with"],
  209. "name": [1],
  210. "value": [1032],
  211. "helperText": [1, "helper-text"],
  212. "errorText": [1, "error-text"],
  213. "setFocus": [64]
  214. }, [[4, "keydown", "onKeydown"]], {
  215. "value": ["valueChanged"]
  216. }]);
  217. let radioGroupIds = 0;
  218. function defineCustomElement() {
  219. if (typeof customElements === "undefined") {
  220. return;
  221. }
  222. const components = ["ion-radio-group"];
  223. components.forEach(tagName => { switch (tagName) {
  224. case "ion-radio-group":
  225. if (!customElements.get(tagName)) {
  226. customElements.define(tagName, RadioGroup);
  227. }
  228. break;
  229. } });
  230. }
  231. export { RadioGroup as R, defineCustomElement as d };