foundation.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403
  1. /**
  2. * @license
  3. * Copyright 2020 Google Inc.
  4. *
  5. * Permission is hereby granted, free of charge, to any person obtaining a copy
  6. * of this software and associated documentation files (the "Software"), to deal
  7. * in the Software without restriction, including without limitation the rights
  8. * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  9. * copies of the Software, and to permit persons to whom the Software is
  10. * furnished to do so, subject to the following conditions:
  11. *
  12. * The above copyright notice and this permission notice shall be included in
  13. * all copies or substantial portions of the Software.
  14. *
  15. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  16. * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  17. * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  18. * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  19. * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  20. * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  21. * THE SOFTWARE.
  22. */
  23. import { __assign, __extends, __values } from "tslib";
  24. import { MDCFoundation } from '@material/base/foundation';
  25. import { KEY } from '@material/dom/keyboard';
  26. import { MDCChipActionFocusBehavior, MDCChipActionType } from '../action/constants';
  27. import { MDCChipAnimation } from '../chip/constants';
  28. import { MDCChipSetAttributes, MDCChipSetEvents } from './constants';
  29. var Operator;
  30. (function (Operator) {
  31. Operator[Operator["INCREMENT"] = 0] = "INCREMENT";
  32. Operator[Operator["DECREMENT"] = 1] = "DECREMENT";
  33. })(Operator || (Operator = {}));
  34. /**
  35. * MDCChipSetFoundation provides a foundation for all chips.
  36. */
  37. var MDCChipSetFoundation = /** @class */ (function (_super) {
  38. __extends(MDCChipSetFoundation, _super);
  39. function MDCChipSetFoundation(adapter) {
  40. return _super.call(this, __assign(__assign({}, MDCChipSetFoundation.defaultAdapter), adapter)) || this;
  41. }
  42. Object.defineProperty(MDCChipSetFoundation, "defaultAdapter", {
  43. get: function () {
  44. return {
  45. announceMessage: function () { return undefined; },
  46. emitEvent: function () { return undefined; },
  47. getAttribute: function () { return null; },
  48. getChipActionsAtIndex: function () { return []; },
  49. getChipCount: function () { return 0; },
  50. getChipIdAtIndex: function () { return ''; },
  51. getChipIndexById: function () { return 0; },
  52. isChipFocusableAtIndex: function () { return false; },
  53. isChipSelectableAtIndex: function () { return false; },
  54. isChipSelectedAtIndex: function () { return false; },
  55. removeChipAtIndex: function () { },
  56. setChipFocusAtIndex: function () { return undefined; },
  57. setChipSelectedAtIndex: function () { return undefined; },
  58. startChipAnimationAtIndex: function () { return undefined; },
  59. };
  60. },
  61. enumerable: false,
  62. configurable: true
  63. });
  64. MDCChipSetFoundation.prototype.handleChipAnimation = function (_a) {
  65. var detail = _a.detail;
  66. var chipID = detail.chipID, animation = detail.animation, isComplete = detail.isComplete, addedAnnouncement = detail.addedAnnouncement, removedAnnouncement = detail.removedAnnouncement;
  67. var index = this.adapter.getChipIndexById(chipID);
  68. if (animation === MDCChipAnimation.EXIT && isComplete) {
  69. if (removedAnnouncement) {
  70. this.adapter.announceMessage(removedAnnouncement);
  71. }
  72. this.removeAfterAnimation(index, chipID);
  73. return;
  74. }
  75. if (animation === MDCChipAnimation.ENTER && isComplete &&
  76. addedAnnouncement) {
  77. this.adapter.announceMessage(addedAnnouncement);
  78. return;
  79. }
  80. };
  81. MDCChipSetFoundation.prototype.handleChipInteraction = function (_a) {
  82. var detail = _a.detail;
  83. var source = detail.source, chipID = detail.chipID, isSelectable = detail.isSelectable, isSelected = detail.isSelected, shouldRemove = detail.shouldRemove;
  84. var index = this.adapter.getChipIndexById(chipID);
  85. if (shouldRemove) {
  86. this.removeChip(index);
  87. return;
  88. }
  89. this.focusChip(index, source, MDCChipActionFocusBehavior.FOCUSABLE);
  90. this.adapter.emitEvent(MDCChipSetEvents.INTERACTION, {
  91. chipIndex: index,
  92. chipID: chipID,
  93. });
  94. if (isSelectable) {
  95. this.setSelection(index, source, !isSelected);
  96. }
  97. };
  98. MDCChipSetFoundation.prototype.handleChipNavigation = function (_a) {
  99. var detail = _a.detail;
  100. var chipID = detail.chipID, key = detail.key, isRTL = detail.isRTL, source = detail.source;
  101. var index = this.adapter.getChipIndexById(chipID);
  102. var toNextChip = (key === KEY.ARROW_RIGHT && !isRTL) ||
  103. (key === KEY.ARROW_LEFT && isRTL);
  104. if (toNextChip) {
  105. // Start from the next chip so we increment the index
  106. this.focusNextChipFrom(index + 1);
  107. return;
  108. }
  109. var toPreviousChip = (key === KEY.ARROW_LEFT && !isRTL) ||
  110. (key === KEY.ARROW_RIGHT && isRTL);
  111. if (toPreviousChip) {
  112. // Start from the previous chip so we decrement the index
  113. this.focusPrevChipFrom(index - 1);
  114. return;
  115. }
  116. if (key === KEY.ARROW_DOWN) {
  117. // Start from the next chip so we increment the index
  118. this.focusNextChipFrom(index + 1, source);
  119. return;
  120. }
  121. if (key === KEY.ARROW_UP) {
  122. // Start from the previous chip so we decrement the index
  123. this.focusPrevChipFrom(index - 1, source);
  124. return;
  125. }
  126. if (key === KEY.HOME) {
  127. this.focusNextChipFrom(0, source);
  128. return;
  129. }
  130. if (key === KEY.END) {
  131. this.focusPrevChipFrom(this.adapter.getChipCount() - 1, source);
  132. return;
  133. }
  134. };
  135. /** Returns the unique selected indexes of the chips. */
  136. MDCChipSetFoundation.prototype.getSelectedChipIndexes = function () {
  137. var e_1, _a;
  138. var selectedIndexes = new Set();
  139. var chipCount = this.adapter.getChipCount();
  140. for (var i = 0; i < chipCount; i++) {
  141. var actions = this.adapter.getChipActionsAtIndex(i);
  142. try {
  143. for (var actions_1 = (e_1 = void 0, __values(actions)), actions_1_1 = actions_1.next(); !actions_1_1.done; actions_1_1 = actions_1.next()) {
  144. var action = actions_1_1.value;
  145. if (this.adapter.isChipSelectedAtIndex(i, action)) {
  146. selectedIndexes.add(i);
  147. }
  148. }
  149. }
  150. catch (e_1_1) { e_1 = { error: e_1_1 }; }
  151. finally {
  152. try {
  153. if (actions_1_1 && !actions_1_1.done && (_a = actions_1.return)) _a.call(actions_1);
  154. }
  155. finally { if (e_1) throw e_1.error; }
  156. }
  157. }
  158. return selectedIndexes;
  159. };
  160. /** Sets the selected state of the chip at the given index and action. */
  161. MDCChipSetFoundation.prototype.setChipSelected = function (index, action, isSelected) {
  162. if (this.adapter.isChipSelectableAtIndex(index, action)) {
  163. this.setSelection(index, action, isSelected);
  164. }
  165. };
  166. /** Returns the selected state of the chip at the given index and action. */
  167. MDCChipSetFoundation.prototype.isChipSelected = function (index, action) {
  168. return this.adapter.isChipSelectedAtIndex(index, action);
  169. };
  170. /** Removes the chip at the given index. */
  171. MDCChipSetFoundation.prototype.removeChip = function (index) {
  172. // Early exit if the index is out of bounds
  173. if (index >= this.adapter.getChipCount() || index < 0)
  174. return;
  175. this.adapter.startChipAnimationAtIndex(index, MDCChipAnimation.EXIT);
  176. this.adapter.emitEvent(MDCChipSetEvents.REMOVAL, {
  177. chipID: this.adapter.getChipIdAtIndex(index),
  178. chipIndex: index,
  179. isComplete: false,
  180. });
  181. };
  182. MDCChipSetFoundation.prototype.addChip = function (index) {
  183. // Early exit if the index is out of bounds
  184. if (index >= this.adapter.getChipCount() || index < 0)
  185. return;
  186. this.adapter.startChipAnimationAtIndex(index, MDCChipAnimation.ENTER);
  187. };
  188. /**
  189. * Increments to find the first focusable chip.
  190. */
  191. MDCChipSetFoundation.prototype.focusNextChipFrom = function (startIndex, targetAction) {
  192. var chipCount = this.adapter.getChipCount();
  193. for (var i = startIndex; i < chipCount; i++) {
  194. var focusableAction = this.getFocusableAction(i, Operator.INCREMENT, targetAction);
  195. if (focusableAction) {
  196. this.focusChip(i, focusableAction, MDCChipActionFocusBehavior.FOCUSABLE_AND_FOCUSED);
  197. return;
  198. }
  199. }
  200. };
  201. /**
  202. * Decrements to find the first focusable chip. Takes an optional target
  203. * action that can be used to focus the first matching focusable action.
  204. */
  205. MDCChipSetFoundation.prototype.focusPrevChipFrom = function (startIndex, targetAction) {
  206. for (var i = startIndex; i > -1; i--) {
  207. var focusableAction = this.getFocusableAction(i, Operator.DECREMENT, targetAction);
  208. if (focusableAction) {
  209. this.focusChip(i, focusableAction, MDCChipActionFocusBehavior.FOCUSABLE_AND_FOCUSED);
  210. return;
  211. }
  212. }
  213. };
  214. /** Returns the appropriate focusable action, or null if none exist. */
  215. MDCChipSetFoundation.prototype.getFocusableAction = function (index, op, targetAction) {
  216. var actions = this.adapter.getChipActionsAtIndex(index);
  217. // Reverse the actions if decrementing
  218. if (op === Operator.DECREMENT)
  219. actions.reverse();
  220. if (targetAction) {
  221. return this.getMatchingFocusableAction(index, actions, targetAction);
  222. }
  223. return this.getFirstFocusableAction(index, actions);
  224. };
  225. /**
  226. * Returs the first focusable action, regardless of type, or null if no
  227. * focusable actions exist.
  228. */
  229. MDCChipSetFoundation.prototype.getFirstFocusableAction = function (index, actions) {
  230. var e_2, _a;
  231. try {
  232. for (var actions_2 = __values(actions), actions_2_1 = actions_2.next(); !actions_2_1.done; actions_2_1 = actions_2.next()) {
  233. var action = actions_2_1.value;
  234. if (this.adapter.isChipFocusableAtIndex(index, action)) {
  235. return action;
  236. }
  237. }
  238. }
  239. catch (e_2_1) { e_2 = { error: e_2_1 }; }
  240. finally {
  241. try {
  242. if (actions_2_1 && !actions_2_1.done && (_a = actions_2.return)) _a.call(actions_2);
  243. }
  244. finally { if (e_2) throw e_2.error; }
  245. }
  246. return null;
  247. };
  248. /**
  249. * If the actions contain a focusable action that matches the target action,
  250. * return that. Otherwise, return the first focusable action, or null if no
  251. * focusable action exists.
  252. */
  253. MDCChipSetFoundation.prototype.getMatchingFocusableAction = function (index, actions, targetAction) {
  254. var e_3, _a;
  255. var focusableAction = null;
  256. try {
  257. for (var actions_3 = __values(actions), actions_3_1 = actions_3.next(); !actions_3_1.done; actions_3_1 = actions_3.next()) {
  258. var action = actions_3_1.value;
  259. if (this.adapter.isChipFocusableAtIndex(index, action)) {
  260. focusableAction = action;
  261. }
  262. // Exit and return the focusable action if it matches the target
  263. if (focusableAction === targetAction) {
  264. return focusableAction;
  265. }
  266. }
  267. }
  268. catch (e_3_1) { e_3 = { error: e_3_1 }; }
  269. finally {
  270. try {
  271. if (actions_3_1 && !actions_3_1.done && (_a = actions_3.return)) _a.call(actions_3);
  272. }
  273. finally { if (e_3) throw e_3.error; }
  274. }
  275. return focusableAction;
  276. };
  277. MDCChipSetFoundation.prototype.focusChip = function (index, action, focus) {
  278. var e_4, _a;
  279. this.adapter.setChipFocusAtIndex(index, action, focus);
  280. var chipCount = this.adapter.getChipCount();
  281. for (var i = 0; i < chipCount; i++) {
  282. var actions = this.adapter.getChipActionsAtIndex(i);
  283. try {
  284. for (var actions_4 = (e_4 = void 0, __values(actions)), actions_4_1 = actions_4.next(); !actions_4_1.done; actions_4_1 = actions_4.next()) {
  285. var chipAction = actions_4_1.value;
  286. // Skip the action and index provided since we set it above
  287. if (chipAction === action && i === index)
  288. continue;
  289. this.adapter.setChipFocusAtIndex(i, chipAction, MDCChipActionFocusBehavior.NOT_FOCUSABLE);
  290. }
  291. }
  292. catch (e_4_1) { e_4 = { error: e_4_1 }; }
  293. finally {
  294. try {
  295. if (actions_4_1 && !actions_4_1.done && (_a = actions_4.return)) _a.call(actions_4);
  296. }
  297. finally { if (e_4) throw e_4.error; }
  298. }
  299. }
  300. };
  301. MDCChipSetFoundation.prototype.supportsMultiSelect = function () {
  302. return this.adapter.getAttribute(MDCChipSetAttributes.ARIA_MULTISELECTABLE) === 'true';
  303. };
  304. MDCChipSetFoundation.prototype.setSelection = function (index, action, isSelected) {
  305. var e_5, _a;
  306. this.adapter.setChipSelectedAtIndex(index, action, isSelected);
  307. this.adapter.emitEvent(MDCChipSetEvents.SELECTION, {
  308. chipID: this.adapter.getChipIdAtIndex(index),
  309. chipIndex: index,
  310. isSelected: isSelected,
  311. });
  312. // Early exit if we support multi-selection
  313. if (this.supportsMultiSelect()) {
  314. return;
  315. }
  316. // If we get here, we ony support single selection. This means we need to
  317. // unselect all chips
  318. var chipCount = this.adapter.getChipCount();
  319. for (var i = 0; i < chipCount; i++) {
  320. var actions = this.adapter.getChipActionsAtIndex(i);
  321. try {
  322. for (var actions_5 = (e_5 = void 0, __values(actions)), actions_5_1 = actions_5.next(); !actions_5_1.done; actions_5_1 = actions_5.next()) {
  323. var chipAction = actions_5_1.value;
  324. // Skip the action and index provided since we set it above
  325. if (chipAction === action && i === index)
  326. continue;
  327. this.adapter.setChipSelectedAtIndex(i, chipAction, false);
  328. }
  329. }
  330. catch (e_5_1) { e_5 = { error: e_5_1 }; }
  331. finally {
  332. try {
  333. if (actions_5_1 && !actions_5_1.done && (_a = actions_5.return)) _a.call(actions_5);
  334. }
  335. finally { if (e_5) throw e_5.error; }
  336. }
  337. }
  338. };
  339. MDCChipSetFoundation.prototype.removeAfterAnimation = function (index, chipID) {
  340. this.adapter.removeChipAtIndex(index);
  341. this.adapter.emitEvent(MDCChipSetEvents.REMOVAL, {
  342. chipIndex: index,
  343. isComplete: true,
  344. chipID: chipID,
  345. });
  346. var chipCount = this.adapter.getChipCount();
  347. // Early exit if we have an empty chip set
  348. if (chipCount <= 0)
  349. return;
  350. this.focusNearestFocusableAction(index);
  351. };
  352. /**
  353. * Find the first focusable action by moving bidirectionally horizontally
  354. * from the start index.
  355. *
  356. * Given chip set [A, B, C, D, E, F, G]...
  357. * Let's say we remove chip "F". We don't know where the nearest focusable
  358. * action is since any of them could be disabled. The nearest focusable
  359. * action could be E, it could be G, it could even be A. To find it, we
  360. * start from the source index (5 for "F" in this case) and move out
  361. * horizontally, checking each chip at each index.
  362. *
  363. */
  364. MDCChipSetFoundation.prototype.focusNearestFocusableAction = function (index) {
  365. var chipCount = this.adapter.getChipCount();
  366. var decrIndex = index;
  367. var incrIndex = index;
  368. while (decrIndex > -1 || incrIndex < chipCount) {
  369. var focusAction = this.getNearestFocusableAction(decrIndex, incrIndex, MDCChipActionType.TRAILING);
  370. if (focusAction) {
  371. this.focusChip(focusAction.index, focusAction.action, MDCChipActionFocusBehavior.FOCUSABLE_AND_FOCUSED);
  372. return;
  373. }
  374. decrIndex--;
  375. incrIndex++;
  376. }
  377. };
  378. MDCChipSetFoundation.prototype.getNearestFocusableAction = function (decrIndex, incrIndex, actionType) {
  379. var decrAction = this.getFocusableAction(decrIndex, Operator.DECREMENT, actionType);
  380. if (decrAction) {
  381. return {
  382. index: decrIndex,
  383. action: decrAction,
  384. };
  385. }
  386. // Early exit if the incremented and decremented indices are identical
  387. if (incrIndex === decrIndex)
  388. return null;
  389. var incrAction = this.getFocusableAction(incrIndex, Operator.INCREMENT, actionType);
  390. if (incrAction) {
  391. return {
  392. index: incrIndex,
  393. action: incrAction,
  394. };
  395. }
  396. return null;
  397. };
  398. return MDCChipSetFoundation;
  399. }(MDCFoundation));
  400. export { MDCChipSetFoundation };
  401. // tslint:disable-next-line:no-default-export Needed for backward compatibility with MDC Web v0.44.0 and earlier.
  402. export default MDCChipSetFoundation;
  403. //# sourceMappingURL=foundation.js.map