radio-button.js 18 KB

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