rating.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399
  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, Fragment, h, Listen, Method, Prop, State } from "@stencil/core";
  7. import { guid } from "../../utils/guid";
  8. import { connectLabel, disconnectLabel } from "../../utils/label";
  9. import { connectForm, disconnectForm, HiddenFormInputSlot } from "../../utils/form";
  10. import { TEXT } from "./resources";
  11. import { updateHostInteraction } from "../../utils/interactive";
  12. export class Rating {
  13. constructor() {
  14. // --------------------------------------------------------------------------
  15. //
  16. // Properties
  17. //
  18. // --------------------------------------------------------------------------
  19. /** specify the scale of the component, defaults to m */
  20. this.scale = "m";
  21. /** the value of the rating component */
  22. this.value = 0;
  23. /** is the rating component in a selectable mode */
  24. this.readOnly = false;
  25. /** is the rating component in a selectable mode */
  26. this.disabled = false;
  27. /** Show average and count data summary chip (if available) */
  28. this.showChip = false;
  29. /** Localized string for "Rating" (used for aria label)
  30. * @default "Rating"
  31. */
  32. this.intlRating = TEXT.rating;
  33. /** Localized string for labelling each star, `${num}` in the string will be replaced by the number
  34. * @default "Stars: ${num}"
  35. */
  36. this.intlStars = TEXT.stars;
  37. /**
  38. * When true, makes the component required for form-submission.
  39. *
  40. * @internal
  41. */
  42. this.required = false;
  43. this.guid = `calcite-ratings-${guid()}`;
  44. }
  45. //--------------------------------------------------------------------------
  46. //
  47. // Lifecycle
  48. //
  49. //--------------------------------------------------------------------------
  50. connectedCallback() {
  51. connectLabel(this);
  52. connectForm(this);
  53. }
  54. disconnectedCallback() {
  55. disconnectLabel(this);
  56. disconnectForm(this);
  57. }
  58. componentDidRender() {
  59. updateHostInteraction(this);
  60. }
  61. //--------------------------------------------------------------------------
  62. //
  63. // Event Listeners
  64. //
  65. //--------------------------------------------------------------------------
  66. blurHandler() {
  67. this.hasFocus = false;
  68. }
  69. // --------------------------------------------------------------------------
  70. //
  71. // Render Methods
  72. //
  73. // --------------------------------------------------------------------------
  74. renderStars() {
  75. return [1, 2, 3, 4, 5].map((i) => {
  76. const selected = this.value >= i;
  77. const average = this.average && !this.value && i <= this.average;
  78. const hovered = i <= this.hoverValue;
  79. const fraction = this.average && this.average + 1 - i;
  80. const partial = !this.value && !hovered && fraction > 0 && fraction < 1;
  81. const focused = this.hasFocus && this.focusValue === i;
  82. return (h("span", { class: { wrapper: true } },
  83. h("label", { class: { star: true, focused, selected, average, hovered, partial }, htmlFor: `${this.guid}-${i}`, onMouseOver: () => {
  84. this.hoverValue = i;
  85. } },
  86. h("calcite-icon", { "aria-hidden": "true", class: "icon", icon: selected || average || this.readOnly ? "star-f" : "star", scale: this.scale }),
  87. partial && (h("div", { class: "fraction", style: { width: `${fraction * 100}%` } },
  88. h("calcite-icon", { icon: "star-f", scale: this.scale }))),
  89. h("span", { class: "visually-hidden" }, this.intlStars.replace("${num}", `${i}`))),
  90. h("input", { checked: i === this.value, class: "visually-hidden", disabled: this.disabled || this.readOnly, id: `${this.guid}-${i}`, name: this.guid, onChange: () => this.updateValue(i), onClick: (event) =>
  91. // click is fired from the the component's label, so we treat this as an internal event
  92. event.stopPropagation(), onFocus: () => {
  93. this.hasFocus = true;
  94. this.focusValue = i;
  95. }, ref: (el) => (i === 1 || i === this.value) && (this.inputFocusRef = el), type: "radio", value: i })));
  96. });
  97. }
  98. render() {
  99. const { disabled, intlRating, showChip, scale, count, average } = this;
  100. return (h(Fragment, null,
  101. h("fieldset", { class: "fieldset", disabled: disabled, onBlur: () => (this.hoverValue = null), onMouseLeave: () => (this.hoverValue = null), onTouchEnd: () => (this.hoverValue = null) },
  102. h("legend", { class: "visually-hidden" }, intlRating),
  103. this.renderStars()),
  104. (count || average) && showChip ? (h("calcite-chip", { scale: scale, value: count === null || count === void 0 ? void 0 : count.toString() },
  105. !!average && h("span", { class: "number--average" }, average.toString()),
  106. !!count && h("span", { class: "number--count" },
  107. "(", count === null || count === void 0 ? void 0 :
  108. count.toString(),
  109. ")"))) : null,
  110. h(HiddenFormInputSlot, { component: this })));
  111. }
  112. //--------------------------------------------------------------------------
  113. //
  114. // Private Methods
  115. //
  116. //--------------------------------------------------------------------------
  117. onLabelClick() {
  118. this.setFocus();
  119. }
  120. updateValue(value) {
  121. this.value = value;
  122. this.calciteRatingChange.emit({ value });
  123. }
  124. //--------------------------------------------------------------------------
  125. //
  126. // Public Methods
  127. //
  128. //--------------------------------------------------------------------------
  129. /** Sets focus on the component. */
  130. async setFocus() {
  131. this.inputFocusRef.focus();
  132. }
  133. static get is() { return "calcite-rating"; }
  134. static get encapsulation() { return "shadow"; }
  135. static get originalStyleUrls() { return {
  136. "$": ["rating.scss"]
  137. }; }
  138. static get styleUrls() { return {
  139. "$": ["rating.css"]
  140. }; }
  141. static get properties() { return {
  142. "scale": {
  143. "type": "string",
  144. "mutable": false,
  145. "complexType": {
  146. "original": "Scale",
  147. "resolved": "\"l\" | \"m\" | \"s\"",
  148. "references": {
  149. "Scale": {
  150. "location": "import",
  151. "path": "../interfaces"
  152. }
  153. }
  154. },
  155. "required": false,
  156. "optional": false,
  157. "docs": {
  158. "tags": [],
  159. "text": "specify the scale of the component, defaults to m"
  160. },
  161. "attribute": "scale",
  162. "reflect": true,
  163. "defaultValue": "\"m\""
  164. },
  165. "value": {
  166. "type": "number",
  167. "mutable": true,
  168. "complexType": {
  169. "original": "number",
  170. "resolved": "number",
  171. "references": {}
  172. },
  173. "required": false,
  174. "optional": false,
  175. "docs": {
  176. "tags": [],
  177. "text": "the value of the rating component"
  178. },
  179. "attribute": "value",
  180. "reflect": true,
  181. "defaultValue": "0"
  182. },
  183. "readOnly": {
  184. "type": "boolean",
  185. "mutable": false,
  186. "complexType": {
  187. "original": "boolean",
  188. "resolved": "boolean",
  189. "references": {}
  190. },
  191. "required": false,
  192. "optional": false,
  193. "docs": {
  194. "tags": [],
  195. "text": "is the rating component in a selectable mode"
  196. },
  197. "attribute": "read-only",
  198. "reflect": true,
  199. "defaultValue": "false"
  200. },
  201. "disabled": {
  202. "type": "boolean",
  203. "mutable": false,
  204. "complexType": {
  205. "original": "boolean",
  206. "resolved": "boolean",
  207. "references": {}
  208. },
  209. "required": false,
  210. "optional": false,
  211. "docs": {
  212. "tags": [],
  213. "text": "is the rating component in a selectable mode"
  214. },
  215. "attribute": "disabled",
  216. "reflect": true,
  217. "defaultValue": "false"
  218. },
  219. "showChip": {
  220. "type": "boolean",
  221. "mutable": false,
  222. "complexType": {
  223. "original": "boolean",
  224. "resolved": "boolean",
  225. "references": {}
  226. },
  227. "required": false,
  228. "optional": false,
  229. "docs": {
  230. "tags": [],
  231. "text": "Show average and count data summary chip (if available)"
  232. },
  233. "attribute": "show-chip",
  234. "reflect": true,
  235. "defaultValue": "false"
  236. },
  237. "count": {
  238. "type": "number",
  239. "mutable": false,
  240. "complexType": {
  241. "original": "number",
  242. "resolved": "number",
  243. "references": {}
  244. },
  245. "required": false,
  246. "optional": true,
  247. "docs": {
  248. "tags": [],
  249. "text": "optionally pass a number of previous ratings to display"
  250. },
  251. "attribute": "count",
  252. "reflect": true
  253. },
  254. "average": {
  255. "type": "number",
  256. "mutable": false,
  257. "complexType": {
  258. "original": "number",
  259. "resolved": "number",
  260. "references": {}
  261. },
  262. "required": false,
  263. "optional": true,
  264. "docs": {
  265. "tags": [],
  266. "text": "optionally pass a cumulative average rating to display"
  267. },
  268. "attribute": "average",
  269. "reflect": true
  270. },
  271. "name": {
  272. "type": "string",
  273. "mutable": false,
  274. "complexType": {
  275. "original": "string",
  276. "resolved": "string",
  277. "references": {}
  278. },
  279. "required": false,
  280. "optional": false,
  281. "docs": {
  282. "tags": [],
  283. "text": "The name of the rating"
  284. },
  285. "attribute": "name",
  286. "reflect": true
  287. },
  288. "intlRating": {
  289. "type": "string",
  290. "mutable": false,
  291. "complexType": {
  292. "original": "string",
  293. "resolved": "string",
  294. "references": {}
  295. },
  296. "required": false,
  297. "optional": true,
  298. "docs": {
  299. "tags": [{
  300. "name": "default",
  301. "text": "\"Rating\""
  302. }],
  303. "text": "Localized string for \"Rating\" (used for aria label)"
  304. },
  305. "attribute": "intl-rating",
  306. "reflect": false,
  307. "defaultValue": "TEXT.rating"
  308. },
  309. "intlStars": {
  310. "type": "string",
  311. "mutable": false,
  312. "complexType": {
  313. "original": "string",
  314. "resolved": "string",
  315. "references": {}
  316. },
  317. "required": false,
  318. "optional": true,
  319. "docs": {
  320. "tags": [{
  321. "name": "default",
  322. "text": "\"Stars: ${num}\""
  323. }],
  324. "text": "Localized string for labelling each star, `${num}` in the string will be replaced by the number"
  325. },
  326. "attribute": "intl-stars",
  327. "reflect": false,
  328. "defaultValue": "TEXT.stars"
  329. },
  330. "required": {
  331. "type": "boolean",
  332. "mutable": false,
  333. "complexType": {
  334. "original": "boolean",
  335. "resolved": "boolean",
  336. "references": {}
  337. },
  338. "required": false,
  339. "optional": false,
  340. "docs": {
  341. "tags": [{
  342. "name": "internal",
  343. "text": undefined
  344. }],
  345. "text": "When true, makes the component required for form-submission."
  346. },
  347. "attribute": "required",
  348. "reflect": true,
  349. "defaultValue": "false"
  350. }
  351. }; }
  352. static get states() { return {
  353. "hoverValue": {},
  354. "focusValue": {},
  355. "hasFocus": {}
  356. }; }
  357. static get events() { return [{
  358. "method": "calciteRatingChange",
  359. "name": "calciteRatingChange",
  360. "bubbles": true,
  361. "cancelable": true,
  362. "composed": true,
  363. "docs": {
  364. "tags": [],
  365. "text": "Fires when the rating value has changed."
  366. },
  367. "complexType": {
  368. "original": "{ value: number }",
  369. "resolved": "{ value: number; }",
  370. "references": {}
  371. }
  372. }]; }
  373. static get methods() { return {
  374. "setFocus": {
  375. "complexType": {
  376. "signature": "() => Promise<void>",
  377. "parameters": [],
  378. "references": {
  379. "Promise": {
  380. "location": "global"
  381. }
  382. },
  383. "return": "Promise<void>"
  384. },
  385. "docs": {
  386. "text": "Sets focus on the component.",
  387. "tags": []
  388. }
  389. }
  390. }; }
  391. static get elementRef() { return "el"; }
  392. static get listeners() { return [{
  393. "name": "blur",
  394. "method": "blurHandler",
  395. "target": undefined,
  396. "capture": false,
  397. "passive": false
  398. }]; }
  399. }