date-picker-month-header.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421
  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, Fragment } from "@stencil/core";
  7. import { dateFromRange, nextMonth, prevMonth, getOrder } from "../../utils/date";
  8. import { Heading } from "../functional/Heading";
  9. import { BUDDHIST_CALENDAR_YEAR_OFFSET } from "./resources";
  10. import { isActivationKey } from "../../utils/key";
  11. import { numberStringFormatter } from "../../utils/locale";
  12. import { closestElementCrossShadowBoundary } from "../../utils/dom";
  13. export class DatePickerMonthHeader {
  14. constructor() {
  15. //--------------------------------------------------------------------------
  16. //
  17. // Private State/Props
  18. //
  19. //--------------------------------------------------------------------------
  20. this.globalAttributes = {};
  21. //--------------------------------------------------------------------------
  22. //
  23. // Private Methods
  24. //
  25. //--------------------------------------------------------------------------
  26. /**
  27. * Increment year on UP/DOWN keys
  28. *
  29. * @param event
  30. */
  31. this.onYearKey = (event) => {
  32. const localizedYear = this.parseCalendarYear(event.target.value);
  33. switch (event.key) {
  34. case "ArrowDown":
  35. event.preventDefault();
  36. this.setYear({ localizedYear, offset: -1 });
  37. break;
  38. case "ArrowUp":
  39. event.preventDefault();
  40. this.setYear({ localizedYear, offset: 1 });
  41. break;
  42. }
  43. };
  44. this.onYearChange = (event) => {
  45. this.setYear({
  46. localizedYear: this.parseCalendarYear(event.target.value)
  47. });
  48. };
  49. this.onYearInput = (event) => {
  50. this.setYear({
  51. localizedYear: this.parseCalendarYear(event.target.value),
  52. commit: false
  53. });
  54. };
  55. this.prevMonthClick = (event) => {
  56. this.handleArrowClick(event, this.prevMonthDate);
  57. };
  58. this.prevMonthKeydown = (event) => {
  59. if (isActivationKey(event.key)) {
  60. this.prevMonthClick(event);
  61. }
  62. };
  63. this.nextMonthClick = (event) => {
  64. this.handleArrowClick(event, this.nextMonthDate);
  65. };
  66. this.nextMonthKeydown = (event) => {
  67. if (isActivationKey(event.key)) {
  68. this.nextMonthClick(event);
  69. }
  70. };
  71. /*
  72. * Update active month on clicks of left/right arrows
  73. */
  74. this.handleArrowClick = (event, date) => {
  75. event.preventDefault();
  76. this.calciteDatePickerSelect.emit(date);
  77. };
  78. }
  79. //--------------------------------------------------------------------------
  80. //
  81. // Lifecycle
  82. //
  83. //--------------------------------------------------------------------------
  84. componentWillLoad() {
  85. this.parentDatePickerEl = closestElementCrossShadowBoundary(this.el, "calcite-date-picker");
  86. }
  87. connectedCallback() {
  88. this.setNextPrevMonthDates();
  89. }
  90. render() {
  91. return h("div", { class: "header" }, this.renderContent());
  92. }
  93. renderContent() {
  94. var _a;
  95. if (!this.activeDate || !this.localeData) {
  96. return null;
  97. }
  98. if (this.parentDatePickerEl) {
  99. const { numberingSystem, lang: locale } = this.parentDatePickerEl;
  100. numberStringFormatter.numberFormatOptions = {
  101. useGrouping: false,
  102. ...(numberingSystem && { numberingSystem }),
  103. ...(locale && { locale })
  104. };
  105. }
  106. const activeMonth = this.activeDate.getMonth();
  107. const { months, unitOrder } = this.localeData;
  108. const localizedMonth = (months.wide || months.narrow || months.abbreviated)[activeMonth];
  109. const localizedYear = this.formatCalendarYear(this.activeDate.getFullYear());
  110. const iconScale = this.scale === "l" ? "m" : "s";
  111. const order = getOrder(unitOrder);
  112. const reverse = order.indexOf("y") < order.indexOf("m");
  113. const suffix = (_a = this.localeData.year) === null || _a === void 0 ? void 0 : _a.suffix;
  114. return (h(Fragment, null, h("a", { "aria-disabled": `${this.prevMonthDate.getMonth() === activeMonth}`, "aria-label": this.intlPrevMonth, class: "chevron", href: "#", onClick: this.prevMonthClick, onKeyDown: this.prevMonthKeydown, role: "button", tabindex: this.prevMonthDate.getMonth() === activeMonth ? -1 : 0 }, h("calcite-icon", { "flip-rtl": true, icon: "chevron-left", scale: iconScale })), h("div", { class: { text: true, "text--reverse": reverse } }, h(Heading, { class: "month", level: this.headingLevel }, localizedMonth), h("span", { class: "year-wrap" }, h("input", { "aria-label": this.intlYear, class: {
  115. year: true,
  116. "year--suffix": !!suffix
  117. }, inputmode: "numeric", maxlength: "4", minlength: "1", onChange: this.onYearChange, onInput: this.onYearInput, onKeyDown: this.onYearKey, pattern: "\\d*", ref: (el) => (this.yearInput = el), type: "text", value: localizedYear }), suffix && h("span", { class: "suffix" }, suffix))), h("a", { "aria-disabled": `${this.nextMonthDate.getMonth() === activeMonth}`, "aria-label": this.intlNextMonth, class: "chevron", href: "#", onClick: this.nextMonthClick, onKeyDown: this.nextMonthKeydown, role: "button", tabindex: this.nextMonthDate.getMonth() === activeMonth ? -1 : 0 }, h("calcite-icon", { "flip-rtl": true, icon: "chevron-right", scale: iconScale }))));
  118. }
  119. setNextPrevMonthDates() {
  120. if (!this.activeDate) {
  121. return;
  122. }
  123. this.nextMonthDate = dateFromRange(nextMonth(this.activeDate), this.min, this.max);
  124. this.prevMonthDate = dateFromRange(prevMonth(this.activeDate), this.min, this.max);
  125. }
  126. formatCalendarYear(year) {
  127. const { localeData } = this;
  128. const buddhistCalendar = localeData["default-calendar"] === "buddhist";
  129. const yearOffset = buddhistCalendar ? BUDDHIST_CALENDAR_YEAR_OFFSET : 0;
  130. return numberStringFormatter.localize(`${year + yearOffset}`);
  131. }
  132. parseCalendarYear(year) {
  133. const { localeData } = this;
  134. const buddhistCalendar = localeData["default-calendar"] === "buddhist";
  135. const yearOffset = buddhistCalendar ? BUDDHIST_CALENDAR_YEAR_OFFSET : 0;
  136. const parsedYear = Number(numberStringFormatter.delocalize(year)) - yearOffset;
  137. return numberStringFormatter.localize(`${parsedYear}`);
  138. }
  139. getInRangeDate({ localizedYear, offset = 0 }) {
  140. const { min, max, activeDate } = this;
  141. const parsedYear = Number(numberStringFormatter.delocalize(localizedYear));
  142. const length = parsedYear.toString().length;
  143. const year = isNaN(parsedYear) ? false : parsedYear + offset;
  144. const inRange = year && (!min || min.getFullYear() <= year) && (!max || max.getFullYear() >= year);
  145. // if you've supplied a year and it's in range
  146. if (year && inRange && length === localizedYear.length) {
  147. const nextDate = new Date(activeDate);
  148. nextDate.setFullYear(year);
  149. return dateFromRange(nextDate, min, max);
  150. }
  151. }
  152. /**
  153. * Parse localized year string from input,
  154. * set to active if in range
  155. *
  156. * @param root0
  157. * @param root0.localizedYear
  158. * @param root0.commit
  159. * @param root0.offset
  160. */
  161. setYear({ localizedYear, commit = true, offset = 0 }) {
  162. const { yearInput, activeDate } = this;
  163. const inRangeDate = this.getInRangeDate({ localizedYear, offset });
  164. // if you've supplied a year and it's in range, update active date
  165. if (inRangeDate) {
  166. this.calciteDatePickerSelect.emit(inRangeDate);
  167. }
  168. if (commit) {
  169. yearInput.value = this.formatCalendarYear((inRangeDate || activeDate).getFullYear());
  170. }
  171. }
  172. static get is() { return "calcite-date-picker-month-header"; }
  173. static get encapsulation() { return "shadow"; }
  174. static get originalStyleUrls() {
  175. return {
  176. "$": ["date-picker-month-header.scss"]
  177. };
  178. }
  179. static get styleUrls() {
  180. return {
  181. "$": ["date-picker-month-header.css"]
  182. };
  183. }
  184. static get properties() {
  185. return {
  186. "selectedDate": {
  187. "type": "unknown",
  188. "mutable": false,
  189. "complexType": {
  190. "original": "Date",
  191. "resolved": "Date",
  192. "references": {
  193. "Date": {
  194. "location": "global"
  195. }
  196. }
  197. },
  198. "required": false,
  199. "optional": false,
  200. "docs": {
  201. "tags": [],
  202. "text": "Already selected date."
  203. }
  204. },
  205. "activeDate": {
  206. "type": "unknown",
  207. "mutable": false,
  208. "complexType": {
  209. "original": "Date",
  210. "resolved": "Date",
  211. "references": {
  212. "Date": {
  213. "location": "global"
  214. }
  215. }
  216. },
  217. "required": false,
  218. "optional": false,
  219. "docs": {
  220. "tags": [],
  221. "text": "Focused date with indicator (will become selected date if user proceeds)"
  222. }
  223. },
  224. "headingLevel": {
  225. "type": "number",
  226. "mutable": false,
  227. "complexType": {
  228. "original": "HeadingLevel",
  229. "resolved": "1 | 2 | 3 | 4 | 5 | 6",
  230. "references": {
  231. "HeadingLevel": {
  232. "location": "import",
  233. "path": "../functional/Heading"
  234. }
  235. }
  236. },
  237. "required": false,
  238. "optional": false,
  239. "docs": {
  240. "tags": [],
  241. "text": "Number at which section headings should start for this component."
  242. },
  243. "attribute": "heading-level",
  244. "reflect": false
  245. },
  246. "min": {
  247. "type": "unknown",
  248. "mutable": false,
  249. "complexType": {
  250. "original": "Date",
  251. "resolved": "Date",
  252. "references": {
  253. "Date": {
  254. "location": "global"
  255. }
  256. }
  257. },
  258. "required": false,
  259. "optional": false,
  260. "docs": {
  261. "tags": [],
  262. "text": "Minimum date of the calendar below which is disabled."
  263. }
  264. },
  265. "max": {
  266. "type": "unknown",
  267. "mutable": false,
  268. "complexType": {
  269. "original": "Date",
  270. "resolved": "Date",
  271. "references": {
  272. "Date": {
  273. "location": "global"
  274. }
  275. }
  276. },
  277. "required": false,
  278. "optional": false,
  279. "docs": {
  280. "tags": [],
  281. "text": "Maximum date of the calendar above which is disabled."
  282. }
  283. },
  284. "intlPrevMonth": {
  285. "type": "string",
  286. "mutable": false,
  287. "complexType": {
  288. "original": "string",
  289. "resolved": "string",
  290. "references": {}
  291. },
  292. "required": false,
  293. "optional": false,
  294. "docs": {
  295. "tags": [],
  296. "text": "Localized string for previous month."
  297. },
  298. "attribute": "intl-prev-month",
  299. "reflect": false
  300. },
  301. "intlNextMonth": {
  302. "type": "string",
  303. "mutable": false,
  304. "complexType": {
  305. "original": "string",
  306. "resolved": "string",
  307. "references": {}
  308. },
  309. "required": false,
  310. "optional": false,
  311. "docs": {
  312. "tags": [],
  313. "text": "Localized string for next month."
  314. },
  315. "attribute": "intl-next-month",
  316. "reflect": false
  317. },
  318. "intlYear": {
  319. "type": "string",
  320. "mutable": false,
  321. "complexType": {
  322. "original": "string",
  323. "resolved": "string",
  324. "references": {}
  325. },
  326. "required": false,
  327. "optional": false,
  328. "docs": {
  329. "tags": [],
  330. "text": "Localized string for year."
  331. },
  332. "attribute": "intl-year",
  333. "reflect": false
  334. },
  335. "scale": {
  336. "type": "string",
  337. "mutable": false,
  338. "complexType": {
  339. "original": "Scale",
  340. "resolved": "\"l\" | \"m\" | \"s\"",
  341. "references": {
  342. "Scale": {
  343. "location": "import",
  344. "path": "../interfaces"
  345. }
  346. }
  347. },
  348. "required": false,
  349. "optional": false,
  350. "docs": {
  351. "tags": [],
  352. "text": "specify the scale of the date picker"
  353. },
  354. "attribute": "scale",
  355. "reflect": true
  356. },
  357. "localeData": {
  358. "type": "unknown",
  359. "mutable": false,
  360. "complexType": {
  361. "original": "DateLocaleData",
  362. "resolved": "DateLocaleData",
  363. "references": {
  364. "DateLocaleData": {
  365. "location": "import",
  366. "path": "../date-picker/utils"
  367. }
  368. }
  369. },
  370. "required": false,
  371. "optional": false,
  372. "docs": {
  373. "tags": [],
  374. "text": "CLDR locale data for translated calendar info"
  375. }
  376. }
  377. };
  378. }
  379. static get states() {
  380. return {
  381. "globalAttributes": {},
  382. "nextMonthDate": {},
  383. "prevMonthDate": {}
  384. };
  385. }
  386. static get events() {
  387. return [{
  388. "method": "calciteDatePickerSelect",
  389. "name": "calciteDatePickerSelect",
  390. "bubbles": true,
  391. "cancelable": false,
  392. "composed": true,
  393. "docs": {
  394. "tags": [],
  395. "text": "Changes to active date"
  396. },
  397. "complexType": {
  398. "original": "Date",
  399. "resolved": "Date",
  400. "references": {
  401. "Date": {
  402. "location": "global"
  403. }
  404. }
  405. }
  406. }];
  407. }
  408. static get elementRef() { return "el"; }
  409. static get watchers() {
  410. return [{
  411. "propName": "min",
  412. "methodName": "setNextPrevMonthDates"
  413. }, {
  414. "propName": "max",
  415. "methodName": "setNextPrevMonthDates"
  416. }, {
  417. "propName": "activeDate",
  418. "methodName": "setNextPrevMonthDates"
  419. }];
  420. }
  421. }