radio-button.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598
  1. /*!
  2. * All material copyright ESRI, All Rights Reserved, unless otherwise specified.
  3. * See https://github.com/Esri/calcite-components/blob/master/LICENSE.md for details.
  4. * v1.0.0-beta.82
  5. */
  6. import { Component, Element, Event, h, Host, Listen, Method, Prop, Watch } from "@stencil/core";
  7. import { guid } from "../../utils/guid";
  8. import { focusElement, getElementDir, toAriaBoolean } from "../../utils/dom";
  9. import { connectLabel, disconnectLabel, getLabelText } from "../../utils/label";
  10. import { HiddenFormInputSlot, connectForm, disconnectForm } from "../../utils/form";
  11. import { CSS } from "./resources";
  12. import { getRoundRobinIndex } from "../../utils/array";
  13. import { updateHostInteraction } from "../../utils/interactive";
  14. export class RadioButton {
  15. constructor() {
  16. //--------------------------------------------------------------------------
  17. //
  18. // Properties
  19. //
  20. //--------------------------------------------------------------------------
  21. /** The checked state of the radio button. */
  22. this.checked = false;
  23. /** The disabled state of the radio button. */
  24. this.disabled = false;
  25. /**
  26. * The focused state of the radio button.
  27. * @internal
  28. */
  29. this.focused = false;
  30. /** The radio button's hidden status. When a radio button is hidden it is not focusable or checkable. */
  31. this.hidden = false;
  32. /**
  33. * The hovered state of the radio button.
  34. * @internal
  35. */
  36. this.hovered = false;
  37. /** Requires that a value is selected for the radio button group before the parent form will submit. */
  38. this.required = false;
  39. /** The scale (size) of the radio button. `scale` is passed as a property automatically from `calcite-radio-button-group`. */
  40. this.scale = "m";
  41. //--------------------------------------------------------------------------
  42. //
  43. // Private Methods
  44. //
  45. //--------------------------------------------------------------------------
  46. this.selectItem = (items, selectedIndex) => {
  47. items[selectedIndex].click();
  48. };
  49. this.queryButtons = () => {
  50. return Array.from(this.rootNode.querySelectorAll("calcite-radio-button:not([hidden])")).filter((radioButton) => radioButton.name === this.name);
  51. };
  52. this.isDefaultSelectable = () => {
  53. const radioButtons = this.queryButtons();
  54. return !radioButtons.some((radioButton) => radioButton.checked) && radioButtons[0] === this.el;
  55. };
  56. this.check = () => {
  57. if (this.disabled) {
  58. return;
  59. }
  60. this.uncheckAllRadioButtonsInGroup();
  61. this.checked = true;
  62. this.calciteRadioButtonChange.emit();
  63. this.setFocus();
  64. };
  65. this.clickHandler = () => {
  66. this.check();
  67. };
  68. this.setContainerEl = (el) => {
  69. this.containerEl = el;
  70. };
  71. this.handleKeyDown = (event) => {
  72. const keys = ["ArrowLeft", "ArrowUp", "ArrowRight", "ArrowDown", " "];
  73. const key = event.key;
  74. const { el } = this;
  75. if (keys.indexOf(key) === -1) {
  76. return;
  77. }
  78. if (key === " ") {
  79. this.check();
  80. return;
  81. }
  82. let adjustedKey = key;
  83. if (getElementDir(el) === "rtl") {
  84. if (key === "ArrowRight") {
  85. adjustedKey = "ArrowLeft";
  86. }
  87. if (key === "ArrowLeft") {
  88. adjustedKey = "ArrowRight";
  89. }
  90. }
  91. const radioButtons = Array.from(this.rootNode.querySelectorAll("calcite-radio-button:not([hidden]")).filter((radioButton) => radioButton.name === this.name);
  92. let currentIndex = 0;
  93. const radioButtonsLength = radioButtons.length;
  94. radioButtons.some((item, index) => {
  95. if (item.checked) {
  96. currentIndex = index;
  97. return true;
  98. }
  99. });
  100. switch (adjustedKey) {
  101. case "ArrowLeft":
  102. case "ArrowUp":
  103. event.preventDefault();
  104. this.selectItem(radioButtons, getRoundRobinIndex(Math.max(currentIndex - 1, -1), radioButtonsLength));
  105. return;
  106. case "ArrowRight":
  107. case "ArrowDown":
  108. event.preventDefault();
  109. this.selectItem(radioButtons, getRoundRobinIndex(currentIndex + 1, radioButtonsLength));
  110. return;
  111. default:
  112. return;
  113. }
  114. };
  115. this.onContainerBlur = () => {
  116. this.focused = false;
  117. this.calciteInternalRadioButtonBlur.emit();
  118. };
  119. this.onContainerFocus = () => {
  120. if (!this.disabled) {
  121. this.focused = true;
  122. this.calciteInternalRadioButtonFocus.emit();
  123. }
  124. };
  125. }
  126. checkedChanged(newChecked) {
  127. if (newChecked) {
  128. this.uncheckOtherRadioButtonsInGroup();
  129. }
  130. this.calciteInternalRadioButtonCheckedChange.emit(newChecked);
  131. }
  132. nameChanged() {
  133. this.checkLastRadioButton();
  134. }
  135. //--------------------------------------------------------------------------
  136. //
  137. // Public Methods
  138. //
  139. //--------------------------------------------------------------------------
  140. /** Sets focus on the component. */
  141. async setFocus() {
  142. if (!this.disabled) {
  143. focusElement(this.containerEl);
  144. }
  145. }
  146. onLabelClick(event) {
  147. if (!this.disabled && !this.hidden) {
  148. this.uncheckOtherRadioButtonsInGroup();
  149. const label = event.currentTarget;
  150. const radioButton = label.for
  151. ? this.rootNode.querySelector(`calcite-radio-button[id="${label.for}"]`)
  152. : label.querySelector(`calcite-radio-button[name="${this.name}"]`);
  153. if (radioButton) {
  154. radioButton.checked = true;
  155. radioButton.focused = true;
  156. }
  157. this.calciteRadioButtonChange.emit();
  158. this.setFocus();
  159. }
  160. }
  161. checkLastRadioButton() {
  162. const radioButtons = this.queryButtons();
  163. const checkedRadioButtons = radioButtons.filter((radioButton) => radioButton.checked);
  164. if ((checkedRadioButtons === null || checkedRadioButtons === void 0 ? void 0 : checkedRadioButtons.length) > 1) {
  165. const lastCheckedRadioButton = checkedRadioButtons[checkedRadioButtons.length - 1];
  166. checkedRadioButtons
  167. .filter((checkedRadioButton) => checkedRadioButton !== lastCheckedRadioButton)
  168. .forEach((checkedRadioButton) => {
  169. checkedRadioButton.checked = false;
  170. checkedRadioButton.emitCheckedChange();
  171. });
  172. }
  173. }
  174. /** @internal */
  175. async emitCheckedChange() {
  176. this.calciteInternalRadioButtonCheckedChange.emit();
  177. }
  178. uncheckAllRadioButtonsInGroup() {
  179. const radioButtons = this.queryButtons();
  180. radioButtons.forEach((radioButton) => {
  181. if (radioButton.checked) {
  182. radioButton.checked = false;
  183. radioButton.focused = false;
  184. }
  185. });
  186. }
  187. uncheckOtherRadioButtonsInGroup() {
  188. const radioButtons = this.queryButtons();
  189. const otherRadioButtons = radioButtons.filter((radioButton) => radioButton.guid !== this.guid);
  190. otherRadioButtons.forEach((otherRadioButton) => {
  191. if (otherRadioButton.checked) {
  192. otherRadioButton.checked = false;
  193. otherRadioButton.focused = false;
  194. }
  195. });
  196. }
  197. getTabIndex() {
  198. if (this.disabled) {
  199. return undefined;
  200. }
  201. return this.checked || this.isDefaultSelectable() ? 0 : -1;
  202. }
  203. //--------------------------------------------------------------------------
  204. //
  205. // Event Listeners
  206. //
  207. //--------------------------------------------------------------------------
  208. mouseenter() {
  209. this.hovered = true;
  210. }
  211. mouseleave() {
  212. this.hovered = false;
  213. }
  214. //--------------------------------------------------------------------------
  215. //
  216. // Lifecycle
  217. //
  218. //--------------------------------------------------------------------------
  219. connectedCallback() {
  220. this.rootNode = this.el.getRootNode();
  221. this.guid = this.el.id || `calcite-radio-button-${guid()}`;
  222. if (this.name) {
  223. this.checkLastRadioButton();
  224. }
  225. connectLabel(this);
  226. connectForm(this);
  227. }
  228. componentDidLoad() {
  229. if (this.focused && !this.disabled) {
  230. this.setFocus();
  231. }
  232. }
  233. disconnectedCallback() {
  234. disconnectLabel(this);
  235. disconnectForm(this);
  236. }
  237. componentDidRender() {
  238. updateHostInteraction(this);
  239. }
  240. // --------------------------------------------------------------------------
  241. //
  242. // Render Methods
  243. //
  244. // --------------------------------------------------------------------------
  245. render() {
  246. const tabIndex = this.getTabIndex();
  247. return (h(Host, { onClick: this.clickHandler, onKeyDown: this.handleKeyDown },
  248. h("div", { "aria-checked": toAriaBoolean(this.checked), "aria-label": getLabelText(this), class: CSS.container, onBlur: this.onContainerBlur, onFocus: this.onContainerFocus, ref: this.setContainerEl, role: "radio", tabIndex: tabIndex },
  249. h("div", { class: "radio" })),
  250. h(HiddenFormInputSlot, { component: this })));
  251. }
  252. static get is() { return "calcite-radio-button"; }
  253. static get encapsulation() { return "shadow"; }
  254. static get originalStyleUrls() { return {
  255. "$": ["radio-button.scss"]
  256. }; }
  257. static get styleUrls() { return {
  258. "$": ["radio-button.css"]
  259. }; }
  260. static get properties() { return {
  261. "checked": {
  262. "type": "boolean",
  263. "mutable": true,
  264. "complexType": {
  265. "original": "boolean",
  266. "resolved": "boolean",
  267. "references": {}
  268. },
  269. "required": false,
  270. "optional": false,
  271. "docs": {
  272. "tags": [],
  273. "text": "The checked state of the radio button."
  274. },
  275. "attribute": "checked",
  276. "reflect": true,
  277. "defaultValue": "false"
  278. },
  279. "disabled": {
  280. "type": "boolean",
  281. "mutable": false,
  282. "complexType": {
  283. "original": "boolean",
  284. "resolved": "boolean",
  285. "references": {}
  286. },
  287. "required": false,
  288. "optional": false,
  289. "docs": {
  290. "tags": [],
  291. "text": "The disabled state of the radio button."
  292. },
  293. "attribute": "disabled",
  294. "reflect": true,
  295. "defaultValue": "false"
  296. },
  297. "focused": {
  298. "type": "boolean",
  299. "mutable": true,
  300. "complexType": {
  301. "original": "boolean",
  302. "resolved": "boolean",
  303. "references": {}
  304. },
  305. "required": false,
  306. "optional": false,
  307. "docs": {
  308. "tags": [{
  309. "name": "internal",
  310. "text": undefined
  311. }],
  312. "text": "The focused state of the radio button."
  313. },
  314. "attribute": "focused",
  315. "reflect": true,
  316. "defaultValue": "false"
  317. },
  318. "guid": {
  319. "type": "string",
  320. "mutable": true,
  321. "complexType": {
  322. "original": "string",
  323. "resolved": "string",
  324. "references": {}
  325. },
  326. "required": false,
  327. "optional": false,
  328. "docs": {
  329. "tags": [],
  330. "text": "The id attribute of the radio button. When omitted, a globally unique identifier is used."
  331. },
  332. "attribute": "guid",
  333. "reflect": true
  334. },
  335. "hidden": {
  336. "type": "boolean",
  337. "mutable": false,
  338. "complexType": {
  339. "original": "boolean",
  340. "resolved": "boolean",
  341. "references": {}
  342. },
  343. "required": false,
  344. "optional": false,
  345. "docs": {
  346. "tags": [],
  347. "text": "The radio button's hidden status. When a radio button is hidden it is not focusable or checkable."
  348. },
  349. "attribute": "hidden",
  350. "reflect": true,
  351. "defaultValue": "false"
  352. },
  353. "hovered": {
  354. "type": "boolean",
  355. "mutable": true,
  356. "complexType": {
  357. "original": "boolean",
  358. "resolved": "boolean",
  359. "references": {}
  360. },
  361. "required": false,
  362. "optional": false,
  363. "docs": {
  364. "tags": [{
  365. "name": "internal",
  366. "text": undefined
  367. }],
  368. "text": "The hovered state of the radio button."
  369. },
  370. "attribute": "hovered",
  371. "reflect": true,
  372. "defaultValue": "false"
  373. },
  374. "label": {
  375. "type": "string",
  376. "mutable": false,
  377. "complexType": {
  378. "original": "string",
  379. "resolved": "string",
  380. "references": {}
  381. },
  382. "required": false,
  383. "optional": true,
  384. "docs": {
  385. "tags": [{
  386. "name": "internal",
  387. "text": undefined
  388. }],
  389. "text": "The label of the radio input"
  390. },
  391. "attribute": "label",
  392. "reflect": false
  393. },
  394. "name": {
  395. "type": "string",
  396. "mutable": false,
  397. "complexType": {
  398. "original": "string",
  399. "resolved": "string",
  400. "references": {}
  401. },
  402. "required": false,
  403. "optional": false,
  404. "docs": {
  405. "tags": [],
  406. "text": "The name of the radio button. `name` is passed as a property automatically from `calcite-radio-button-group`."
  407. },
  408. "attribute": "name",
  409. "reflect": true
  410. },
  411. "required": {
  412. "type": "boolean",
  413. "mutable": false,
  414. "complexType": {
  415. "original": "boolean",
  416. "resolved": "boolean",
  417. "references": {}
  418. },
  419. "required": false,
  420. "optional": false,
  421. "docs": {
  422. "tags": [],
  423. "text": "Requires that a value is selected for the radio button group before the parent form will submit."
  424. },
  425. "attribute": "required",
  426. "reflect": true,
  427. "defaultValue": "false"
  428. },
  429. "scale": {
  430. "type": "string",
  431. "mutable": false,
  432. "complexType": {
  433. "original": "Scale",
  434. "resolved": "\"l\" | \"m\" | \"s\"",
  435. "references": {
  436. "Scale": {
  437. "location": "import",
  438. "path": "../interfaces"
  439. }
  440. }
  441. },
  442. "required": false,
  443. "optional": false,
  444. "docs": {
  445. "tags": [],
  446. "text": "The scale (size) of the radio button. `scale` is passed as a property automatically from `calcite-radio-button-group`."
  447. },
  448. "attribute": "scale",
  449. "reflect": true,
  450. "defaultValue": "\"m\""
  451. },
  452. "value": {
  453. "type": "any",
  454. "mutable": true,
  455. "complexType": {
  456. "original": "any",
  457. "resolved": "any",
  458. "references": {}
  459. },
  460. "required": true,
  461. "optional": false,
  462. "docs": {
  463. "tags": [],
  464. "text": "The value of the radio button."
  465. },
  466. "attribute": "value",
  467. "reflect": false
  468. }
  469. }; }
  470. static get events() { return [{
  471. "method": "calciteInternalRadioButtonBlur",
  472. "name": "calciteInternalRadioButtonBlur",
  473. "bubbles": true,
  474. "cancelable": true,
  475. "composed": true,
  476. "docs": {
  477. "tags": [{
  478. "name": "internal",
  479. "text": undefined
  480. }],
  481. "text": "Fires when the radio button is blurred."
  482. },
  483. "complexType": {
  484. "original": "any",
  485. "resolved": "any",
  486. "references": {}
  487. }
  488. }, {
  489. "method": "calciteRadioButtonChange",
  490. "name": "calciteRadioButtonChange",
  491. "bubbles": true,
  492. "cancelable": true,
  493. "composed": true,
  494. "docs": {
  495. "tags": [],
  496. "text": "Fires only when the radio button is checked. This behavior is identical to the native HTML input element.\nSince this event does not fire when the radio button is unchecked, it's not recommended to attach a listener for this event\ndirectly on the element, but instead either attach it to a node that contains all of the radio buttons in the group\nor use the calciteRadioButtonGroupChange event if using this with calcite-radio-button-group."
  497. },
  498. "complexType": {
  499. "original": "any",
  500. "resolved": "any",
  501. "references": {}
  502. }
  503. }, {
  504. "method": "calciteInternalRadioButtonCheckedChange",
  505. "name": "calciteInternalRadioButtonCheckedChange",
  506. "bubbles": true,
  507. "cancelable": true,
  508. "composed": true,
  509. "docs": {
  510. "tags": [{
  511. "name": "internal",
  512. "text": undefined
  513. }],
  514. "text": "Fires when the checked property changes. This is an internal event used for styling purposes only.\nUse calciteRadioButtonChange or calciteRadioButtonGroupChange for responding to changes in the checked value for forms."
  515. },
  516. "complexType": {
  517. "original": "any",
  518. "resolved": "any",
  519. "references": {}
  520. }
  521. }, {
  522. "method": "calciteInternalRadioButtonFocus",
  523. "name": "calciteInternalRadioButtonFocus",
  524. "bubbles": true,
  525. "cancelable": true,
  526. "composed": true,
  527. "docs": {
  528. "tags": [{
  529. "name": "internal",
  530. "text": undefined
  531. }],
  532. "text": "Fires when the radio button is focused."
  533. },
  534. "complexType": {
  535. "original": "any",
  536. "resolved": "any",
  537. "references": {}
  538. }
  539. }]; }
  540. static get methods() { return {
  541. "setFocus": {
  542. "complexType": {
  543. "signature": "() => Promise<void>",
  544. "parameters": [],
  545. "references": {
  546. "Promise": {
  547. "location": "global"
  548. }
  549. },
  550. "return": "Promise<void>"
  551. },
  552. "docs": {
  553. "text": "Sets focus on the component.",
  554. "tags": []
  555. }
  556. },
  557. "emitCheckedChange": {
  558. "complexType": {
  559. "signature": "() => Promise<void>",
  560. "parameters": [],
  561. "references": {
  562. "Promise": {
  563. "location": "global"
  564. }
  565. },
  566. "return": "Promise<void>"
  567. },
  568. "docs": {
  569. "text": "",
  570. "tags": [{
  571. "name": "internal",
  572. "text": undefined
  573. }]
  574. }
  575. }
  576. }; }
  577. static get elementRef() { return "el"; }
  578. static get watchers() { return [{
  579. "propName": "checked",
  580. "methodName": "checkedChanged"
  581. }, {
  582. "propName": "name",
  583. "methodName": "nameChanged"
  584. }]; }
  585. static get listeners() { return [{
  586. "name": "mouseenter",
  587. "method": "mouseenter",
  588. "target": undefined,
  589. "capture": false,
  590. "passive": true
  591. }, {
  592. "name": "mouseleave",
  593. "method": "mouseleave",
  594. "target": undefined,
  595. "capture": false,
  596. "passive": true
  597. }]; }
  598. }