foundation.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463
  1. /**
  2. * @license
  3. * Copyright 2016 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, __read, __spreadArray } from "tslib";
  24. import { MDCFoundation } from '@material/base/foundation';
  25. import { KEY, normalizeKey } from '@material/dom/keyboard';
  26. import { Corner } from '@material/menu-surface/constants';
  27. import { cssClasses, numbers, strings } from './constants';
  28. /** MDC Select Foundation */
  29. var MDCSelectFoundation = /** @class */ (function (_super) {
  30. __extends(MDCSelectFoundation, _super);
  31. /* istanbul ignore next: optional argument is not a branch statement */
  32. /**
  33. * @param adapter
  34. * @param foundationMap Map from subcomponent names to their subfoundations.
  35. */
  36. function MDCSelectFoundation(adapter, foundationMap) {
  37. if (foundationMap === void 0) { foundationMap = {}; }
  38. var _a, _b;
  39. var _this = _super.call(this, __assign(__assign({}, MDCSelectFoundation.defaultAdapter), adapter)) || this;
  40. // Disabled state
  41. _this.disabled = false;
  42. // isMenuOpen is used to track the state of the menu by listening to the
  43. // MDCMenuSurface:closed event For reference, menu.open will return false if
  44. // the menu is still closing, but isMenuOpen returns false only after the menu
  45. // has closed
  46. _this.isMenuOpen = false;
  47. // By default, select is invalid if it is required but no value is selected.
  48. _this.useDefaultValidation = true;
  49. _this.customValidity = true;
  50. _this.lastSelectedIndex = numbers.UNSET_INDEX;
  51. _this.clickDebounceTimeout = 0;
  52. _this.recentlyClicked = false;
  53. _this.leadingIcon = foundationMap.leadingIcon;
  54. _this.helperText = foundationMap.helperText;
  55. _this.ariaDescribedbyIds =
  56. ((_b = (_a = _this.adapter.getSelectAnchorAttr(strings.ARIA_DESCRIBEDBY)) === null || _a === void 0 ? void 0 : _a.split(' ')) === null || _b === void 0 ? void 0 : _b.filter(function (id) { var _a; return id !== ((_a = _this.helperText) === null || _a === void 0 ? void 0 : _a.getId()) && id !== ''; })) ||
  57. [];
  58. return _this;
  59. }
  60. Object.defineProperty(MDCSelectFoundation, "cssClasses", {
  61. get: function () {
  62. return cssClasses;
  63. },
  64. enumerable: false,
  65. configurable: true
  66. });
  67. Object.defineProperty(MDCSelectFoundation, "numbers", {
  68. get: function () {
  69. return numbers;
  70. },
  71. enumerable: false,
  72. configurable: true
  73. });
  74. Object.defineProperty(MDCSelectFoundation, "strings", {
  75. get: function () {
  76. return strings;
  77. },
  78. enumerable: false,
  79. configurable: true
  80. });
  81. Object.defineProperty(MDCSelectFoundation, "defaultAdapter", {
  82. /**
  83. * See {@link MDCSelectAdapter} for typing information on parameters and
  84. * return types.
  85. */
  86. get: function () {
  87. // tslint:disable:object-literal-sort-keys Methods should be in the same order as the adapter interface.
  88. return {
  89. addClass: function () { return undefined; },
  90. removeClass: function () { return undefined; },
  91. hasClass: function () { return false; },
  92. activateBottomLine: function () { return undefined; },
  93. deactivateBottomLine: function () { return undefined; },
  94. getSelectedIndex: function () { return -1; },
  95. setSelectedIndex: function () { return undefined; },
  96. hasLabel: function () { return false; },
  97. floatLabel: function () { return undefined; },
  98. getLabelWidth: function () { return 0; },
  99. setLabelRequired: function () { return undefined; },
  100. hasOutline: function () { return false; },
  101. notchOutline: function () { return undefined; },
  102. closeOutline: function () { return undefined; },
  103. setRippleCenter: function () { return undefined; },
  104. notifyChange: function () { return undefined; },
  105. setSelectedText: function () { return undefined; },
  106. isSelectAnchorFocused: function () { return false; },
  107. getSelectAnchorAttr: function () { return ''; },
  108. setSelectAnchorAttr: function () { return undefined; },
  109. removeSelectAnchorAttr: function () { return undefined; },
  110. addMenuClass: function () { return undefined; },
  111. removeMenuClass: function () { return undefined; },
  112. openMenu: function () { return undefined; },
  113. closeMenu: function () { return undefined; },
  114. getAnchorElement: function () { return null; },
  115. setMenuAnchorElement: function () { return undefined; },
  116. setMenuAnchorCorner: function () { return undefined; },
  117. setMenuWrapFocus: function () { return undefined; },
  118. focusMenuItemAtIndex: function () { return undefined; },
  119. getMenuItemCount: function () { return 0; },
  120. getMenuItemValues: function () { return []; },
  121. getMenuItemTextAtIndex: function () { return ''; },
  122. isTypeaheadInProgress: function () { return false; },
  123. typeaheadMatchItem: function () { return -1; },
  124. };
  125. // tslint:enable:object-literal-sort-keys
  126. },
  127. enumerable: false,
  128. configurable: true
  129. });
  130. /** Returns the index of the currently selected menu item, or -1 if none. */
  131. MDCSelectFoundation.prototype.getSelectedIndex = function () {
  132. return this.adapter.getSelectedIndex();
  133. };
  134. MDCSelectFoundation.prototype.setSelectedIndex = function (index, closeMenu, skipNotify) {
  135. if (closeMenu === void 0) { closeMenu = false; }
  136. if (skipNotify === void 0) { skipNotify = false; }
  137. if (index >= this.adapter.getMenuItemCount()) {
  138. return;
  139. }
  140. this.adapter.setSelectedIndex(index);
  141. if (index === numbers.UNSET_INDEX) {
  142. this.adapter.setSelectedText('');
  143. }
  144. else {
  145. this.adapter.setSelectedText(this.adapter.getMenuItemTextAtIndex(index).trim());
  146. }
  147. if (closeMenu) {
  148. this.adapter.closeMenu();
  149. }
  150. if (!skipNotify && this.lastSelectedIndex !== index) {
  151. this.handleChange();
  152. }
  153. this.lastSelectedIndex = index;
  154. };
  155. MDCSelectFoundation.prototype.setValue = function (value, skipNotify) {
  156. if (skipNotify === void 0) { skipNotify = false; }
  157. var index = this.adapter.getMenuItemValues().indexOf(value);
  158. this.setSelectedIndex(index, /** closeMenu */ false, skipNotify);
  159. };
  160. MDCSelectFoundation.prototype.getValue = function () {
  161. var index = this.adapter.getSelectedIndex();
  162. var menuItemValues = this.adapter.getMenuItemValues();
  163. return index !== numbers.UNSET_INDEX ? menuItemValues[index] : '';
  164. };
  165. MDCSelectFoundation.prototype.getDisabled = function () {
  166. return this.disabled;
  167. };
  168. MDCSelectFoundation.prototype.setDisabled = function (isDisabled) {
  169. this.disabled = isDisabled;
  170. if (this.disabled) {
  171. this.adapter.addClass(cssClasses.DISABLED);
  172. this.adapter.closeMenu();
  173. }
  174. else {
  175. this.adapter.removeClass(cssClasses.DISABLED);
  176. }
  177. if (this.leadingIcon) {
  178. this.leadingIcon.setDisabled(this.disabled);
  179. }
  180. if (this.disabled) {
  181. // Prevent click events from focusing select. Simply pointer-events: none
  182. // is not enough since screenreader clicks may bypass this.
  183. this.adapter.removeSelectAnchorAttr('tabindex');
  184. }
  185. else {
  186. this.adapter.setSelectAnchorAttr('tabindex', '0');
  187. }
  188. this.adapter.setSelectAnchorAttr('aria-disabled', this.disabled.toString());
  189. };
  190. /** Opens the menu. */
  191. MDCSelectFoundation.prototype.openMenu = function () {
  192. this.adapter.addClass(cssClasses.ACTIVATED);
  193. this.adapter.openMenu();
  194. this.isMenuOpen = true;
  195. this.adapter.setSelectAnchorAttr('aria-expanded', 'true');
  196. };
  197. /**
  198. * @param content Sets the content of the helper text.
  199. */
  200. MDCSelectFoundation.prototype.setHelperTextContent = function (content) {
  201. if (this.helperText) {
  202. this.helperText.setContent(content);
  203. }
  204. };
  205. /**
  206. * Re-calculates if the notched outline should be notched and if the label
  207. * should float.
  208. */
  209. MDCSelectFoundation.prototype.layout = function () {
  210. if (this.adapter.hasLabel()) {
  211. var optionHasValue = this.getValue().length > 0;
  212. var isFocused = this.adapter.hasClass(cssClasses.FOCUSED);
  213. var shouldFloatAndNotch = optionHasValue || isFocused;
  214. var isRequired = this.adapter.hasClass(cssClasses.REQUIRED);
  215. this.notchOutline(shouldFloatAndNotch);
  216. this.adapter.floatLabel(shouldFloatAndNotch);
  217. this.adapter.setLabelRequired(isRequired);
  218. }
  219. };
  220. /**
  221. * Synchronizes the list of options with the state of the foundation. Call
  222. * this whenever menu options are dynamically updated.
  223. */
  224. MDCSelectFoundation.prototype.layoutOptions = function () {
  225. var menuItemValues = this.adapter.getMenuItemValues();
  226. var selectedIndex = menuItemValues.indexOf(this.getValue());
  227. this.setSelectedIndex(selectedIndex, /** closeMenu */ false, /** skipNotify */ true);
  228. };
  229. MDCSelectFoundation.prototype.handleMenuOpened = function () {
  230. if (this.adapter.getMenuItemValues().length === 0) {
  231. return;
  232. }
  233. // Menu should open to the last selected element, should open to first menu
  234. // item otherwise.
  235. var selectedIndex = this.getSelectedIndex();
  236. var focusItemIndex = selectedIndex >= 0 ? selectedIndex : 0;
  237. this.adapter.focusMenuItemAtIndex(focusItemIndex);
  238. };
  239. MDCSelectFoundation.prototype.handleMenuClosing = function () {
  240. this.adapter.setSelectAnchorAttr('aria-expanded', 'false');
  241. };
  242. MDCSelectFoundation.prototype.handleMenuClosed = function () {
  243. this.adapter.removeClass(cssClasses.ACTIVATED);
  244. this.isMenuOpen = false;
  245. // Unfocus the select if menu is closed without a selection
  246. if (!this.adapter.isSelectAnchorFocused()) {
  247. this.blur();
  248. }
  249. };
  250. /**
  251. * Handles value changes, via change event or programmatic updates.
  252. */
  253. MDCSelectFoundation.prototype.handleChange = function () {
  254. this.layout();
  255. this.adapter.notifyChange(this.getValue());
  256. var isRequired = this.adapter.hasClass(cssClasses.REQUIRED);
  257. if (isRequired && this.useDefaultValidation) {
  258. this.setValid(this.isValid());
  259. }
  260. };
  261. MDCSelectFoundation.prototype.handleMenuItemAction = function (index) {
  262. this.setSelectedIndex(index, /** closeMenu */ true);
  263. };
  264. /**
  265. * Handles focus events from select element.
  266. */
  267. MDCSelectFoundation.prototype.handleFocus = function () {
  268. this.adapter.addClass(cssClasses.FOCUSED);
  269. this.layout();
  270. this.adapter.activateBottomLine();
  271. };
  272. /**
  273. * Handles blur events from select element.
  274. */
  275. MDCSelectFoundation.prototype.handleBlur = function () {
  276. if (this.isMenuOpen) {
  277. return;
  278. }
  279. this.blur();
  280. };
  281. MDCSelectFoundation.prototype.handleClick = function (normalizedX) {
  282. if (this.disabled || this.recentlyClicked) {
  283. return;
  284. }
  285. this.setClickDebounceTimeout();
  286. if (this.isMenuOpen) {
  287. this.adapter.closeMenu();
  288. return;
  289. }
  290. this.adapter.setRippleCenter(normalizedX);
  291. this.openMenu();
  292. };
  293. /**
  294. * Handles keydown events on select element. Depending on the type of
  295. * character typed, does typeahead matching or opens menu.
  296. */
  297. MDCSelectFoundation.prototype.handleKeydown = function (event) {
  298. if (this.isMenuOpen || !this.adapter.hasClass(cssClasses.FOCUSED)) {
  299. return;
  300. }
  301. var isEnter = normalizeKey(event) === KEY.ENTER;
  302. var isSpace = normalizeKey(event) === KEY.SPACEBAR;
  303. var arrowUp = normalizeKey(event) === KEY.ARROW_UP;
  304. var arrowDown = normalizeKey(event) === KEY.ARROW_DOWN;
  305. var isModifier = event.ctrlKey || event.metaKey;
  306. // Typeahead
  307. if (!isModifier &&
  308. (!isSpace && event.key && event.key.length === 1 ||
  309. isSpace && this.adapter.isTypeaheadInProgress())) {
  310. var key = isSpace ? ' ' : event.key;
  311. var typeaheadNextIndex = this.adapter.typeaheadMatchItem(key, this.getSelectedIndex());
  312. if (typeaheadNextIndex >= 0) {
  313. this.setSelectedIndex(typeaheadNextIndex);
  314. }
  315. event.preventDefault();
  316. return;
  317. }
  318. if (!isEnter && !isSpace && !arrowUp && !arrowDown) {
  319. return;
  320. }
  321. this.openMenu();
  322. event.preventDefault();
  323. };
  324. /**
  325. * Opens/closes the notched outline.
  326. */
  327. MDCSelectFoundation.prototype.notchOutline = function (openNotch) {
  328. if (!this.adapter.hasOutline()) {
  329. return;
  330. }
  331. var isFocused = this.adapter.hasClass(cssClasses.FOCUSED);
  332. if (openNotch) {
  333. var labelScale = numbers.LABEL_SCALE;
  334. var labelWidth = this.adapter.getLabelWidth() * labelScale;
  335. this.adapter.notchOutline(labelWidth);
  336. }
  337. else if (!isFocused) {
  338. this.adapter.closeOutline();
  339. }
  340. };
  341. /**
  342. * Sets the aria label of the leading icon.
  343. */
  344. MDCSelectFoundation.prototype.setLeadingIconAriaLabel = function (label) {
  345. if (this.leadingIcon) {
  346. this.leadingIcon.setAriaLabel(label);
  347. }
  348. };
  349. /**
  350. * Sets the text content of the leading icon.
  351. */
  352. MDCSelectFoundation.prototype.setLeadingIconContent = function (content) {
  353. if (this.leadingIcon) {
  354. this.leadingIcon.setContent(content);
  355. }
  356. };
  357. MDCSelectFoundation.prototype.getUseDefaultValidation = function () {
  358. return this.useDefaultValidation;
  359. };
  360. MDCSelectFoundation.prototype.setUseDefaultValidation = function (useDefaultValidation) {
  361. this.useDefaultValidation = useDefaultValidation;
  362. };
  363. MDCSelectFoundation.prototype.setValid = function (isValid) {
  364. if (!this.useDefaultValidation) {
  365. this.customValidity = isValid;
  366. }
  367. this.adapter.setSelectAnchorAttr('aria-invalid', (!isValid).toString());
  368. if (isValid) {
  369. this.adapter.removeClass(cssClasses.INVALID);
  370. this.adapter.removeMenuClass(cssClasses.MENU_INVALID);
  371. }
  372. else {
  373. this.adapter.addClass(cssClasses.INVALID);
  374. this.adapter.addMenuClass(cssClasses.MENU_INVALID);
  375. }
  376. this.syncHelperTextValidity(isValid);
  377. };
  378. MDCSelectFoundation.prototype.isValid = function () {
  379. if (this.useDefaultValidation &&
  380. this.adapter.hasClass(cssClasses.REQUIRED) &&
  381. !this.adapter.hasClass(cssClasses.DISABLED)) {
  382. // See notes for required attribute under
  383. // https://www.w3.org/TR/html52/sec-forms.html#the-select-element TL;DR:
  384. // Invalid if no index is selected, or if the first index is selected and
  385. // has an empty value.
  386. return this.getSelectedIndex() !== numbers.UNSET_INDEX &&
  387. (this.getSelectedIndex() !== 0 || Boolean(this.getValue()));
  388. }
  389. return this.customValidity;
  390. };
  391. MDCSelectFoundation.prototype.setRequired = function (isRequired) {
  392. if (isRequired) {
  393. this.adapter.addClass(cssClasses.REQUIRED);
  394. }
  395. else {
  396. this.adapter.removeClass(cssClasses.REQUIRED);
  397. }
  398. this.adapter.setSelectAnchorAttr('aria-required', isRequired.toString());
  399. this.adapter.setLabelRequired(isRequired);
  400. };
  401. MDCSelectFoundation.prototype.getRequired = function () {
  402. return this.adapter.getSelectAnchorAttr('aria-required') === 'true';
  403. };
  404. MDCSelectFoundation.prototype.init = function () {
  405. var anchorEl = this.adapter.getAnchorElement();
  406. if (anchorEl) {
  407. this.adapter.setMenuAnchorElement(anchorEl);
  408. this.adapter.setMenuAnchorCorner(Corner.BOTTOM_START);
  409. }
  410. this.adapter.setMenuWrapFocus(false);
  411. this.setDisabled(this.adapter.hasClass(cssClasses.DISABLED));
  412. this.syncHelperTextValidity(!this.adapter.hasClass(cssClasses.INVALID));
  413. this.layout();
  414. this.layoutOptions();
  415. };
  416. /**
  417. * Unfocuses the select component.
  418. */
  419. MDCSelectFoundation.prototype.blur = function () {
  420. this.adapter.removeClass(cssClasses.FOCUSED);
  421. this.layout();
  422. this.adapter.deactivateBottomLine();
  423. var isRequired = this.adapter.hasClass(cssClasses.REQUIRED);
  424. if (isRequired && this.useDefaultValidation) {
  425. this.setValid(this.isValid());
  426. }
  427. };
  428. MDCSelectFoundation.prototype.syncHelperTextValidity = function (isValid) {
  429. if (!this.helperText) {
  430. return;
  431. }
  432. this.helperText.setValidity(isValid);
  433. var helperTextVisible = this.helperText.isVisible();
  434. var helperTextId = this.helperText.getId();
  435. if (helperTextVisible && helperTextId) {
  436. this.adapter.setSelectAnchorAttr(strings.ARIA_DESCRIBEDBY, __spreadArray(__spreadArray([], __read(this.ariaDescribedbyIds)), [helperTextId]).join(' '));
  437. }
  438. else {
  439. // Remove helptext from list of describedby ids. Needed because
  440. // screenreaders will read labels pointed to by `aria-describedby` even if
  441. // they are `aria-hidden`.
  442. if (this.ariaDescribedbyIds.length > 0) {
  443. this.adapter.setSelectAnchorAttr(strings.ARIA_DESCRIBEDBY, this.ariaDescribedbyIds.join(' '));
  444. }
  445. else { // helper text is the only describedby element
  446. this.adapter.removeSelectAnchorAttr(strings.ARIA_DESCRIBEDBY);
  447. }
  448. }
  449. };
  450. MDCSelectFoundation.prototype.setClickDebounceTimeout = function () {
  451. var _this = this;
  452. clearTimeout(this.clickDebounceTimeout);
  453. this.clickDebounceTimeout = setTimeout(function () {
  454. _this.recentlyClicked = false;
  455. }, numbers.CLICK_DEBOUNCE_TIMEOUT_MS);
  456. this.recentlyClicked = true;
  457. };
  458. return MDCSelectFoundation;
  459. }(MDCFoundation));
  460. export { MDCSelectFoundation };
  461. // tslint:disable-next-line:no-default-export Needed for backward compatibility with MDC Web v0.44.0 and earlier.
  462. export default MDCSelectFoundation;
  463. //# sourceMappingURL=foundation.js.map