shell-panel.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405
  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, Prop, Watch, h, State, forceUpdate } from "@stencil/core";
  7. import { CSS, SLOTS, TEXT } from "./resources";
  8. import { getSlotted, getElementDir } from "../../utils/dom";
  9. import { clamp } from "../../utils/math";
  10. import { connectConditionalSlotComponent, disconnectConditionalSlotComponent } from "../../utils/conditionalSlot";
  11. /**
  12. * @slot - A slot for adding content to the shell panel.
  13. * @slot action-bar - A slot for adding a `calcite-action-bar` to the panel.
  14. */
  15. export class ShellPanel {
  16. constructor() {
  17. // --------------------------------------------------------------------------
  18. //
  19. // Properties
  20. //
  21. // --------------------------------------------------------------------------
  22. /**
  23. * Hide the content panel.
  24. */
  25. this.collapsed = false;
  26. /**
  27. * This property makes the content area appear like a "floating" panel.
  28. */
  29. this.detached = false;
  30. /**
  31. * Specifies the maximum height of the contents when detached.
  32. */
  33. this.detachedHeightScale = "l";
  34. /**
  35. * This sets width of the content area.
  36. */
  37. this.widthScale = "m";
  38. /**
  39. * Accessible label for resize separator.
  40. * @default "Resize"
  41. */
  42. this.intlResize = TEXT.resize;
  43. /**
  44. * This property makes the content area resizable if the calcite-shell-panel is not 'detached'.
  45. */
  46. this.resizable = false;
  47. this.contentWidth = null;
  48. this.initialContentWidth = null;
  49. this.initialClientX = null;
  50. this.contentWidthMax = null;
  51. this.contentWidthMin = null;
  52. this.step = 1;
  53. this.stepMultiplier = 10;
  54. this.storeContentEl = (contentEl) => {
  55. this.contentEl = contentEl;
  56. };
  57. this.getKeyAdjustedWidth = (event) => {
  58. const { key } = event;
  59. const { el, step, stepMultiplier, contentWidthMin, contentWidthMax, initialContentWidth, position } = this;
  60. const multipliedStep = step * stepMultiplier;
  61. const MOVEMENT_KEYS = [
  62. "ArrowUp",
  63. "ArrowDown",
  64. "ArrowLeft",
  65. "ArrowRight",
  66. "Home",
  67. "End",
  68. "PageUp",
  69. "PageDown"
  70. ];
  71. if (MOVEMENT_KEYS.indexOf(key) > -1) {
  72. event.preventDefault();
  73. }
  74. const dir = getElementDir(el);
  75. const directionKeys = ["ArrowLeft", "ArrowRight"];
  76. const directionFactor = dir === "rtl" && directionKeys.includes(key) ? -1 : 1;
  77. const increaseKeys = key === "ArrowUp" ||
  78. (position === "end" ? key === directionKeys[0] : key === directionKeys[1]);
  79. if (increaseKeys) {
  80. const stepValue = event.shiftKey ? multipliedStep : step;
  81. return initialContentWidth + directionFactor * stepValue;
  82. }
  83. const decreaseKeys = key === "ArrowDown" ||
  84. (position === "end" ? key === directionKeys[1] : key === directionKeys[0]);
  85. if (decreaseKeys) {
  86. const stepValue = event.shiftKey ? multipliedStep : step;
  87. return initialContentWidth - directionFactor * stepValue;
  88. }
  89. if (typeof contentWidthMin === "number" && key === "Home") {
  90. return contentWidthMin;
  91. }
  92. if (typeof contentWidthMax === "number" && key === "End") {
  93. return contentWidthMax;
  94. }
  95. if (key === "PageDown") {
  96. return initialContentWidth - multipliedStep;
  97. }
  98. if (key === "PageUp") {
  99. return initialContentWidth + multipliedStep;
  100. }
  101. return null;
  102. };
  103. this.separatorKeyDown = (event) => {
  104. this.setInitialContentWidth();
  105. const width = this.getKeyAdjustedWidth(event);
  106. if (typeof width === "number") {
  107. this.setContentWidth(width);
  108. }
  109. };
  110. this.separatorPointerMove = (event) => {
  111. event.preventDefault();
  112. const { el, initialContentWidth, position, initialClientX } = this;
  113. const offset = event.clientX - initialClientX;
  114. const dir = getElementDir(el);
  115. const adjustmentDirection = dir === "rtl" ? -1 : 1;
  116. const adjustedOffset = position === "end" ? -adjustmentDirection * offset : adjustmentDirection * offset;
  117. const width = initialContentWidth + adjustedOffset;
  118. this.setContentWidth(width);
  119. };
  120. this.separatorPointerUp = (event) => {
  121. event.preventDefault();
  122. document.removeEventListener("pointerup", this.separatorPointerUp);
  123. document.removeEventListener("pointermove", this.separatorPointerMove);
  124. };
  125. this.setInitialContentWidth = () => {
  126. var _a;
  127. this.initialContentWidth = (_a = this.contentEl) === null || _a === void 0 ? void 0 : _a.getBoundingClientRect().width;
  128. };
  129. this.separatorPointerDown = (event) => {
  130. event.preventDefault();
  131. const { separatorEl } = this;
  132. separatorEl && document.activeElement !== separatorEl && separatorEl.focus();
  133. this.setInitialContentWidth();
  134. this.initialClientX = event.clientX;
  135. document.addEventListener("pointerup", this.separatorPointerUp);
  136. document.addEventListener("pointermove", this.separatorPointerMove);
  137. };
  138. this.connectSeparator = (separatorEl) => {
  139. this.disconnectSeparator();
  140. this.separatorEl = separatorEl;
  141. separatorEl.addEventListener("pointerdown", this.separatorPointerDown);
  142. };
  143. this.disconnectSeparator = () => {
  144. var _a;
  145. (_a = this.separatorEl) === null || _a === void 0 ? void 0 : _a.removeEventListener("pointerdown", this.separatorPointerDown);
  146. };
  147. }
  148. watchHandler() {
  149. this.calciteShellPanelToggle.emit();
  150. }
  151. //--------------------------------------------------------------------------
  152. //
  153. // Lifecycle
  154. //
  155. //--------------------------------------------------------------------------
  156. connectedCallback() {
  157. connectConditionalSlotComponent(this);
  158. }
  159. disconnectedCallback() {
  160. disconnectConditionalSlotComponent(this);
  161. this.disconnectSeparator();
  162. }
  163. componentDidLoad() {
  164. this.updateAriaValues();
  165. }
  166. // --------------------------------------------------------------------------
  167. //
  168. // Render Methods
  169. //
  170. // --------------------------------------------------------------------------
  171. renderHeader() {
  172. const { el } = this;
  173. const hasHeader = getSlotted(el, SLOTS.header);
  174. return hasHeader ? (h("div", { class: CSS.contentHeader, key: "header" },
  175. h("slot", { name: SLOTS.header }))) : null;
  176. }
  177. render() {
  178. const { collapsed, detached, position, initialContentWidth, contentWidth, contentWidthMax, contentWidthMin, intlResize, resizable } = this;
  179. const allowResizing = !detached && resizable;
  180. const contentNode = (h("div", { class: { [CSS.content]: true, [CSS.contentDetached]: detached }, hidden: collapsed, key: "content", ref: this.storeContentEl, style: allowResizing && contentWidth ? { width: `${contentWidth}px` } : null },
  181. this.renderHeader(),
  182. h("div", { class: CSS.contentBody },
  183. h("slot", null))));
  184. const separatorNode = allowResizing ? (h("div", { "aria-label": intlResize, "aria-orientation": "horizontal", "aria-valuemax": contentWidthMax, "aria-valuemin": contentWidthMin, "aria-valuenow": contentWidth !== null && contentWidth !== void 0 ? contentWidth : initialContentWidth, class: CSS.separator, key: "separator", onKeyDown: this.separatorKeyDown, ref: this.connectSeparator, role: "separator", tabIndex: 0, "touch-action": "none" })) : null;
  185. const actionBarNode = h("slot", { key: "action-bar", name: SLOTS.actionBar });
  186. const mainNodes = [actionBarNode, contentNode, separatorNode];
  187. if (position === "end") {
  188. mainNodes.reverse();
  189. }
  190. return h("div", { class: { [CSS.container]: true } }, mainNodes);
  191. }
  192. // --------------------------------------------------------------------------
  193. //
  194. // private Methods
  195. //
  196. // --------------------------------------------------------------------------
  197. setContentWidth(width) {
  198. const { contentWidthMax, contentWidthMin } = this;
  199. const roundedWidth = Math.round(width);
  200. this.contentWidth =
  201. typeof contentWidthMax === "number" && typeof contentWidthMin === "number"
  202. ? clamp(roundedWidth, contentWidthMin, contentWidthMax)
  203. : roundedWidth;
  204. }
  205. updateAriaValues() {
  206. const { contentEl } = this;
  207. const computedStyle = contentEl && getComputedStyle(contentEl);
  208. if (!computedStyle) {
  209. return;
  210. }
  211. const max = parseInt(computedStyle.getPropertyValue("max-width"), 10);
  212. const min = parseInt(computedStyle.getPropertyValue("min-width"), 10);
  213. const valueNow = parseInt(computedStyle.getPropertyValue("width"), 10);
  214. if (typeof valueNow === "number" && !isNaN(valueNow)) {
  215. this.initialContentWidth = valueNow;
  216. }
  217. if (typeof max === "number" && !isNaN(max)) {
  218. this.contentWidthMax = max;
  219. }
  220. if (typeof min === "number" && !isNaN(min)) {
  221. this.contentWidthMin = min;
  222. }
  223. forceUpdate(this);
  224. }
  225. static get is() { return "calcite-shell-panel"; }
  226. static get encapsulation() { return "shadow"; }
  227. static get originalStyleUrls() { return {
  228. "$": ["shell-panel.scss"]
  229. }; }
  230. static get styleUrls() { return {
  231. "$": ["shell-panel.css"]
  232. }; }
  233. static get properties() { return {
  234. "collapsed": {
  235. "type": "boolean",
  236. "mutable": false,
  237. "complexType": {
  238. "original": "boolean",
  239. "resolved": "boolean",
  240. "references": {}
  241. },
  242. "required": false,
  243. "optional": false,
  244. "docs": {
  245. "tags": [],
  246. "text": "Hide the content panel."
  247. },
  248. "attribute": "collapsed",
  249. "reflect": true,
  250. "defaultValue": "false"
  251. },
  252. "detached": {
  253. "type": "boolean",
  254. "mutable": false,
  255. "complexType": {
  256. "original": "boolean",
  257. "resolved": "boolean",
  258. "references": {}
  259. },
  260. "required": false,
  261. "optional": false,
  262. "docs": {
  263. "tags": [],
  264. "text": "This property makes the content area appear like a \"floating\" panel."
  265. },
  266. "attribute": "detached",
  267. "reflect": true,
  268. "defaultValue": "false"
  269. },
  270. "detachedHeightScale": {
  271. "type": "string",
  272. "mutable": false,
  273. "complexType": {
  274. "original": "Scale",
  275. "resolved": "\"l\" | \"m\" | \"s\"",
  276. "references": {
  277. "Scale": {
  278. "location": "import",
  279. "path": "../interfaces"
  280. }
  281. }
  282. },
  283. "required": false,
  284. "optional": false,
  285. "docs": {
  286. "tags": [],
  287. "text": "Specifies the maximum height of the contents when detached."
  288. },
  289. "attribute": "detached-height-scale",
  290. "reflect": true,
  291. "defaultValue": "\"l\""
  292. },
  293. "widthScale": {
  294. "type": "string",
  295. "mutable": false,
  296. "complexType": {
  297. "original": "Scale",
  298. "resolved": "\"l\" | \"m\" | \"s\"",
  299. "references": {
  300. "Scale": {
  301. "location": "import",
  302. "path": "../interfaces"
  303. }
  304. }
  305. },
  306. "required": false,
  307. "optional": false,
  308. "docs": {
  309. "tags": [],
  310. "text": "This sets width of the content area."
  311. },
  312. "attribute": "width-scale",
  313. "reflect": true,
  314. "defaultValue": "\"m\""
  315. },
  316. "position": {
  317. "type": "string",
  318. "mutable": false,
  319. "complexType": {
  320. "original": "Position",
  321. "resolved": "\"end\" | \"start\"",
  322. "references": {
  323. "Position": {
  324. "location": "import",
  325. "path": "../interfaces"
  326. }
  327. }
  328. },
  329. "required": false,
  330. "optional": false,
  331. "docs": {
  332. "tags": [],
  333. "text": "Arranges the component depending on the elements 'dir' property."
  334. },
  335. "attribute": "position",
  336. "reflect": true
  337. },
  338. "intlResize": {
  339. "type": "string",
  340. "mutable": false,
  341. "complexType": {
  342. "original": "string",
  343. "resolved": "string",
  344. "references": {}
  345. },
  346. "required": false,
  347. "optional": false,
  348. "docs": {
  349. "tags": [{
  350. "name": "default",
  351. "text": "\"Resize\""
  352. }],
  353. "text": "Accessible label for resize separator."
  354. },
  355. "attribute": "intl-resize",
  356. "reflect": false,
  357. "defaultValue": "TEXT.resize"
  358. },
  359. "resizable": {
  360. "type": "boolean",
  361. "mutable": false,
  362. "complexType": {
  363. "original": "boolean",
  364. "resolved": "boolean",
  365. "references": {}
  366. },
  367. "required": false,
  368. "optional": false,
  369. "docs": {
  370. "tags": [],
  371. "text": "This property makes the content area resizable if the calcite-shell-panel is not 'detached'."
  372. },
  373. "attribute": "resizable",
  374. "reflect": true,
  375. "defaultValue": "false"
  376. }
  377. }; }
  378. static get states() { return {
  379. "contentWidth": {}
  380. }; }
  381. static get events() { return [{
  382. "method": "calciteShellPanelToggle",
  383. "name": "calciteShellPanelToggle",
  384. "bubbles": true,
  385. "cancelable": true,
  386. "composed": true,
  387. "docs": {
  388. "tags": [{
  389. "name": "deprecated",
  390. "text": "use a resizeObserver on the shell-panel to listen for changes to its size."
  391. }],
  392. "text": "Emitted when collapse has been toggled."
  393. },
  394. "complexType": {
  395. "original": "any",
  396. "resolved": "any",
  397. "references": {}
  398. }
  399. }]; }
  400. static get elementRef() { return "el"; }
  401. static get watchers() { return [{
  402. "propName": "collapsed",
  403. "methodName": "watchHandler"
  404. }]; }
  405. }