menu.mjs 82 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856
  1. import * as i0 from '@angular/core';
  2. import { Directive, InjectionToken, Optional, SkipSelf, Inject, Injectable, inject, Injector, ViewContainerRef, EventEmitter, NgZone, ElementRef, Input, Output, ContentChildren, NgModule } from '@angular/core';
  3. import { Overlay, OverlayConfig, STANDARD_DROPDOWN_BELOW_POSITIONS, STANDARD_DROPDOWN_ADJACENT_POSITIONS, OverlayModule } from '@angular/cdk/overlay';
  4. import { UP_ARROW, hasModifierKey, DOWN_ARROW, LEFT_ARROW, RIGHT_ARROW, ENTER, SPACE, TAB, ESCAPE } from '@angular/cdk/keycodes';
  5. import { startWith, debounceTime, distinctUntilChanged, filter, takeUntil, mergeMap, mapTo, mergeAll, switchMap, skip } from 'rxjs/operators';
  6. import { UniqueSelectionDispatcher } from '@angular/cdk/collections';
  7. import { Subject, merge, fromEvent, defer, partition } from 'rxjs';
  8. import { TemplatePortal } from '@angular/cdk/portal';
  9. import { InputModalityDetector, FocusKeyManager } from '@angular/cdk/a11y';
  10. import { coerceBooleanProperty } from '@angular/cdk/coercion';
  11. import { Directionality } from '@angular/cdk/bidi';
  12. import { _getEventTarget } from '@angular/cdk/platform';
  13. /**
  14. * A grouping container for `CdkMenuItemRadio` instances, similar to a `role="radiogroup"` element.
  15. */
  16. class CdkMenuGroup {
  17. static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: CdkMenuGroup, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
  18. static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.0.0", type: CdkMenuGroup, isStandalone: true, selector: "[cdkMenuGroup]", host: { attributes: { "role": "group" }, classAttribute: "cdk-menu-group" }, providers: [{ provide: UniqueSelectionDispatcher, useClass: UniqueSelectionDispatcher }], exportAs: ["cdkMenuGroup"], ngImport: i0 }); }
  19. }
  20. i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: CdkMenuGroup, decorators: [{
  21. type: Directive,
  22. args: [{
  23. selector: '[cdkMenuGroup]',
  24. exportAs: 'cdkMenuGroup',
  25. standalone: true,
  26. host: {
  27. 'role': 'group',
  28. 'class': 'cdk-menu-group',
  29. },
  30. providers: [{ provide: UniqueSelectionDispatcher, useClass: UniqueSelectionDispatcher }],
  31. }]
  32. }] });
  33. /** Injection token used to return classes implementing the Menu interface */
  34. const CDK_MENU = new InjectionToken('cdk-menu');
  35. /** Injection token used for an implementation of MenuStack. */
  36. const MENU_STACK = new InjectionToken('cdk-menu-stack');
  37. /** Provider that provides the parent menu stack, or a new menu stack if there is no parent one. */
  38. const PARENT_OR_NEW_MENU_STACK_PROVIDER = {
  39. provide: MENU_STACK,
  40. deps: [[new Optional(), new SkipSelf(), new Inject(MENU_STACK)]],
  41. useFactory: (parentMenuStack) => parentMenuStack || new MenuStack(),
  42. };
  43. /** Provider that provides the parent menu stack, or a new inline menu stack if there is no parent one. */
  44. const PARENT_OR_NEW_INLINE_MENU_STACK_PROVIDER = (orientation) => ({
  45. provide: MENU_STACK,
  46. deps: [[new Optional(), new SkipSelf(), new Inject(MENU_STACK)]],
  47. useFactory: (parentMenuStack) => parentMenuStack || MenuStack.inline(orientation),
  48. });
  49. /** The next available menu stack ID. */
  50. let nextId$2 = 0;
  51. /**
  52. * MenuStack allows subscribers to listen for close events (when a MenuStackItem is popped off
  53. * of the stack) in order to perform closing actions. Upon the MenuStack being empty it emits
  54. * from the `empty` observable specifying the next focus action which the listener should perform
  55. * as requested by the closer.
  56. */
  57. class MenuStack {
  58. constructor() {
  59. /** The ID of this menu stack. */
  60. this.id = `${nextId$2++}`;
  61. /** All MenuStackItems tracked by this MenuStack. */
  62. this._elements = [];
  63. /** Emits the element which was popped off of the stack when requested by a closer. */
  64. this._close = new Subject();
  65. /** Emits once the MenuStack has become empty after popping off elements. */
  66. this._empty = new Subject();
  67. /** Emits whether any menu in the menu stack has focus. */
  68. this._hasFocus = new Subject();
  69. /** Observable which emits the MenuStackItem which has been requested to close. */
  70. this.closed = this._close;
  71. /** Observable which emits whether any menu in the menu stack has focus. */
  72. this.hasFocus = this._hasFocus.pipe(startWith(false), debounceTime(0), distinctUntilChanged());
  73. /**
  74. * Observable which emits when the MenuStack is empty after popping off the last element. It
  75. * emits a FocusNext event which specifies the action the closer has requested the listener
  76. * perform.
  77. */
  78. this.emptied = this._empty;
  79. /**
  80. * Whether the inline menu associated with this menu stack is vertical or horizontal.
  81. * `null` indicates there is no inline menu associated with this menu stack.
  82. */
  83. this._inlineMenuOrientation = null;
  84. }
  85. /** Creates a menu stack that originates from an inline menu. */
  86. static inline(orientation) {
  87. const stack = new MenuStack();
  88. stack._inlineMenuOrientation = orientation;
  89. return stack;
  90. }
  91. /**
  92. * Adds an item to the menu stack.
  93. * @param menu the MenuStackItem to put on the stack.
  94. */
  95. push(menu) {
  96. this._elements.push(menu);
  97. }
  98. /**
  99. * Pop items off of the stack up to and including `lastItem` and emit each on the close
  100. * observable. If the stack is empty or `lastItem` is not on the stack it does nothing.
  101. * @param lastItem the last item to pop off the stack.
  102. * @param options Options that configure behavior on close.
  103. */
  104. close(lastItem, options) {
  105. const { focusNextOnEmpty, focusParentTrigger } = { ...options };
  106. if (this._elements.indexOf(lastItem) >= 0) {
  107. let poppedElement;
  108. do {
  109. poppedElement = this._elements.pop();
  110. this._close.next({ item: poppedElement, focusParentTrigger });
  111. } while (poppedElement !== lastItem);
  112. if (this.isEmpty()) {
  113. this._empty.next(focusNextOnEmpty);
  114. }
  115. }
  116. }
  117. /**
  118. * Pop items off of the stack up to but excluding `lastItem` and emit each on the close
  119. * observable. If the stack is empty or `lastItem` is not on the stack it does nothing.
  120. * @param lastItem the element which should be left on the stack
  121. * @return whether or not an item was removed from the stack
  122. */
  123. closeSubMenuOf(lastItem) {
  124. let removed = false;
  125. if (this._elements.indexOf(lastItem) >= 0) {
  126. removed = this.peek() !== lastItem;
  127. while (this.peek() !== lastItem) {
  128. this._close.next({ item: this._elements.pop() });
  129. }
  130. }
  131. return removed;
  132. }
  133. /**
  134. * Pop off all MenuStackItems and emit each one on the `close` observable one by one.
  135. * @param options Options that configure behavior on close.
  136. */
  137. closeAll(options) {
  138. const { focusNextOnEmpty, focusParentTrigger } = { ...options };
  139. if (!this.isEmpty()) {
  140. while (!this.isEmpty()) {
  141. const menuStackItem = this._elements.pop();
  142. if (menuStackItem) {
  143. this._close.next({ item: menuStackItem, focusParentTrigger });
  144. }
  145. }
  146. this._empty.next(focusNextOnEmpty);
  147. }
  148. }
  149. /** Return true if this stack is empty. */
  150. isEmpty() {
  151. return !this._elements.length;
  152. }
  153. /** Return the length of the stack. */
  154. length() {
  155. return this._elements.length;
  156. }
  157. /** Get the top most element on the stack. */
  158. peek() {
  159. return this._elements[this._elements.length - 1];
  160. }
  161. /** Whether the menu stack is associated with an inline menu. */
  162. hasInlineMenu() {
  163. return this._inlineMenuOrientation != null;
  164. }
  165. /** The orientation of the associated inline menu. */
  166. inlineMenuOrientation() {
  167. return this._inlineMenuOrientation;
  168. }
  169. /** Sets whether the menu stack contains the focused element. */
  170. setHasFocus(hasFocus) {
  171. this._hasFocus.next(hasFocus);
  172. }
  173. static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: MenuStack, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
  174. static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: MenuStack }); }
  175. }
  176. i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: MenuStack, decorators: [{
  177. type: Injectable
  178. }] });
  179. /** Injection token used for an implementation of MenuStack. */
  180. const MENU_TRIGGER = new InjectionToken('cdk-menu-trigger');
  181. /**
  182. * Abstract directive that implements shared logic common to all menu triggers.
  183. * This class can be extended to create custom menu trigger types.
  184. */
  185. class CdkMenuTriggerBase {
  186. constructor() {
  187. /** The DI injector for this component. */
  188. this.injector = inject(Injector);
  189. /** The view container ref for this component */
  190. this.viewContainerRef = inject(ViewContainerRef);
  191. /** The menu stack in which this menu resides. */
  192. this.menuStack = inject(MENU_STACK);
  193. /** Emits when the attached menu is requested to open */
  194. this.opened = new EventEmitter();
  195. /** Emits when the attached menu is requested to close */
  196. this.closed = new EventEmitter();
  197. /** A reference to the overlay which manages the triggered menu */
  198. this.overlayRef = null;
  199. /** Emits when this trigger is destroyed. */
  200. this.destroyed = new Subject();
  201. /** Emits when the outside pointer events listener on the overlay should be stopped. */
  202. this.stopOutsideClicksListener = merge(this.closed, this.destroyed);
  203. }
  204. ngOnDestroy() {
  205. this._destroyOverlay();
  206. this.destroyed.next();
  207. this.destroyed.complete();
  208. }
  209. /** Whether the attached menu is open. */
  210. isOpen() {
  211. return !!this.overlayRef?.hasAttached();
  212. }
  213. /** Registers a child menu as having been opened by this trigger. */
  214. registerChildMenu(child) {
  215. this.childMenu = child;
  216. }
  217. /**
  218. * Get the portal to be attached to the overlay which contains the menu. Allows for the menu
  219. * content to change dynamically and be reflected in the application.
  220. */
  221. getMenuContentPortal() {
  222. const hasMenuContentChanged = this.menuTemplateRef !== this._menuPortal?.templateRef;
  223. if (this.menuTemplateRef && (!this._menuPortal || hasMenuContentChanged)) {
  224. this._menuPortal = new TemplatePortal(this.menuTemplateRef, this.viewContainerRef, this.menuData, this._getChildMenuInjector());
  225. }
  226. return this._menuPortal;
  227. }
  228. /**
  229. * Whether the given element is inside the scope of this trigger's menu stack.
  230. * @param element The element to check.
  231. * @return Whether the element is inside the scope of this trigger's menu stack.
  232. */
  233. isElementInsideMenuStack(element) {
  234. for (let el = element; el; el = el?.parentElement ?? null) {
  235. if (el.getAttribute('data-cdk-menu-stack-id') === this.menuStack.id) {
  236. return true;
  237. }
  238. }
  239. return false;
  240. }
  241. /** Destroy and unset the overlay reference it if exists */
  242. _destroyOverlay() {
  243. if (this.overlayRef) {
  244. this.overlayRef.dispose();
  245. this.overlayRef = null;
  246. }
  247. }
  248. /** Gets the injector to use when creating a child menu. */
  249. _getChildMenuInjector() {
  250. this._childMenuInjector =
  251. this._childMenuInjector ||
  252. Injector.create({
  253. providers: [
  254. { provide: MENU_TRIGGER, useValue: this },
  255. { provide: MENU_STACK, useValue: this.menuStack },
  256. ],
  257. parent: this.injector,
  258. });
  259. return this._childMenuInjector;
  260. }
  261. static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: CdkMenuTriggerBase, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
  262. static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.0.0", type: CdkMenuTriggerBase, host: { properties: { "attr.aria-controls": "childMenu?.id", "attr.data-cdk-menu-stack-id": "menuStack.id" } }, ngImport: i0 }); }
  263. }
  264. i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: CdkMenuTriggerBase, decorators: [{
  265. type: Directive,
  266. args: [{
  267. host: {
  268. '[attr.aria-controls]': 'childMenu?.id',
  269. '[attr.data-cdk-menu-stack-id]': 'menuStack.id',
  270. },
  271. }]
  272. }] });
  273. /**
  274. * Throws an exception when an instance of the PointerFocusTracker is not provided.
  275. * @docs-private
  276. */
  277. function throwMissingPointerFocusTracker() {
  278. throw Error('expected an instance of PointerFocusTracker to be provided');
  279. }
  280. /**
  281. * Throws an exception when a reference to the parent menu is not provided.
  282. * @docs-private
  283. */
  284. function throwMissingMenuReference() {
  285. throw Error('expected a reference to the parent menu');
  286. }
  287. /** Injection token used for an implementation of MenuAim. */
  288. const MENU_AIM = new InjectionToken('cdk-menu-aim');
  289. /** Capture every nth mouse move event. */
  290. const MOUSE_MOVE_SAMPLE_FREQUENCY = 3;
  291. /** The number of mouse move events to track. */
  292. const NUM_POINTS = 5;
  293. /**
  294. * How long to wait before closing a sibling menu if a user stops short of the submenu they were
  295. * predicted to go into.
  296. */
  297. const CLOSE_DELAY = 300;
  298. /** Calculate the slope between point a and b. */
  299. function getSlope(a, b) {
  300. return (b.y - a.y) / (b.x - a.x);
  301. }
  302. /** Calculate the y intercept for the given point and slope. */
  303. function getYIntercept(point, slope) {
  304. return point.y - slope * point.x;
  305. }
  306. /**
  307. * Whether the given mouse trajectory line defined by the slope and y intercept falls within the
  308. * submenu as defined by `submenuPoints`
  309. * @param submenuPoints the submenu DOMRect points.
  310. * @param m the slope of the trajectory line.
  311. * @param b the y intercept of the trajectory line.
  312. * @return true if any point on the line falls within the submenu.
  313. */
  314. function isWithinSubmenu(submenuPoints, m, b) {
  315. const { left, right, top, bottom } = submenuPoints;
  316. // Check for intersection with each edge of the submenu (left, right, top, bottom)
  317. // by fixing one coordinate to that edge's coordinate (either x or y) and checking if the
  318. // other coordinate is within bounds.
  319. return ((m * left + b >= top && m * left + b <= bottom) ||
  320. (m * right + b >= top && m * right + b <= bottom) ||
  321. ((top - b) / m >= left && (top - b) / m <= right) ||
  322. ((bottom - b) / m >= left && (bottom - b) / m <= right));
  323. }
  324. /**
  325. * TargetMenuAim predicts if a user is moving into a submenu. It calculates the
  326. * trajectory of the user's mouse movement in the current menu to determine if the
  327. * mouse is moving towards an open submenu.
  328. *
  329. * The determination is made by calculating the slope of the users last NUM_POINTS moves where each
  330. * pair of points determines if the trajectory line points into the submenu. It uses consensus
  331. * approach by checking if at least NUM_POINTS / 2 pairs determine that the user is moving towards
  332. * to submenu.
  333. */
  334. class TargetMenuAim {
  335. constructor() {
  336. /** The Angular zone. */
  337. this._ngZone = inject(NgZone);
  338. /** The last NUM_POINTS mouse move events. */
  339. this._points = [];
  340. /** Emits when this service is destroyed. */
  341. this._destroyed = new Subject();
  342. }
  343. ngOnDestroy() {
  344. this._destroyed.next();
  345. this._destroyed.complete();
  346. }
  347. /**
  348. * Set the Menu and its PointerFocusTracker.
  349. * @param menu The menu that this menu aim service controls.
  350. * @param pointerTracker The `PointerFocusTracker` for the given menu.
  351. */
  352. initialize(menu, pointerTracker) {
  353. this._menu = menu;
  354. this._pointerTracker = pointerTracker;
  355. this._subscribeToMouseMoves();
  356. }
  357. /**
  358. * Calls the `doToggle` callback when it is deemed that the user is not moving towards
  359. * the submenu.
  360. * @param doToggle the function called when the user is not moving towards the submenu.
  361. */
  362. toggle(doToggle) {
  363. // If the menu is horizontal the sub-menus open below and there is no risk of premature
  364. // closing of any sub-menus therefore we automatically resolve the callback.
  365. if (this._menu.orientation === 'horizontal') {
  366. doToggle();
  367. }
  368. this._checkConfigured();
  369. const siblingItemIsWaiting = !!this._timeoutId;
  370. const hasPoints = this._points.length > 1;
  371. if (hasPoints && !siblingItemIsWaiting) {
  372. if (this._isMovingToSubmenu()) {
  373. this._startTimeout(doToggle);
  374. }
  375. else {
  376. doToggle();
  377. }
  378. }
  379. else if (!siblingItemIsWaiting) {
  380. doToggle();
  381. }
  382. }
  383. /**
  384. * Start the delayed toggle handler if one isn't running already.
  385. *
  386. * The delayed toggle handler executes the `doToggle` callback after some period of time iff the
  387. * users mouse is on an item in the current menu.
  388. *
  389. * @param doToggle the function called when the user is not moving towards the submenu.
  390. */
  391. _startTimeout(doToggle) {
  392. // If the users mouse is moving towards a submenu we don't want to immediately resolve.
  393. // Wait for some period of time before determining if the previous menu should close in
  394. // cases where the user may have moved towards the submenu but stopped on a sibling menu
  395. // item intentionally.
  396. const timeoutId = setTimeout(() => {
  397. // Resolve if the user is currently moused over some element in the root menu
  398. if (this._pointerTracker.activeElement && timeoutId === this._timeoutId) {
  399. doToggle();
  400. }
  401. this._timeoutId = null;
  402. }, CLOSE_DELAY);
  403. this._timeoutId = timeoutId;
  404. }
  405. /** Whether the user is heading towards the open submenu. */
  406. _isMovingToSubmenu() {
  407. const submenuPoints = this._getSubmenuBounds();
  408. if (!submenuPoints) {
  409. return false;
  410. }
  411. let numMoving = 0;
  412. const currPoint = this._points[this._points.length - 1];
  413. // start from the second last point and calculate the slope between each point and the last
  414. // point.
  415. for (let i = this._points.length - 2; i >= 0; i--) {
  416. const previous = this._points[i];
  417. const slope = getSlope(currPoint, previous);
  418. if (isWithinSubmenu(submenuPoints, slope, getYIntercept(currPoint, slope))) {
  419. numMoving++;
  420. }
  421. }
  422. return numMoving >= Math.floor(NUM_POINTS / 2);
  423. }
  424. /** Get the bounding DOMRect for the open submenu. */
  425. _getSubmenuBounds() {
  426. return this._pointerTracker?.previousElement?.getMenu()?.nativeElement.getBoundingClientRect();
  427. }
  428. /**
  429. * Check if a reference to the PointerFocusTracker and menu element is provided.
  430. * @throws an error if neither reference is provided.
  431. */
  432. _checkConfigured() {
  433. if (typeof ngDevMode === 'undefined' || ngDevMode) {
  434. if (!this._pointerTracker) {
  435. throwMissingPointerFocusTracker();
  436. }
  437. if (!this._menu) {
  438. throwMissingMenuReference();
  439. }
  440. }
  441. }
  442. /** Subscribe to the root menus mouse move events and update the tracked mouse points. */
  443. _subscribeToMouseMoves() {
  444. this._ngZone.runOutsideAngular(() => {
  445. fromEvent(this._menu.nativeElement, 'mousemove')
  446. .pipe(filter((_, index) => index % MOUSE_MOVE_SAMPLE_FREQUENCY === 0), takeUntil(this._destroyed))
  447. .subscribe((event) => {
  448. this._points.push({ x: event.clientX, y: event.clientY });
  449. if (this._points.length > NUM_POINTS) {
  450. this._points.shift();
  451. }
  452. });
  453. });
  454. }
  455. static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: TargetMenuAim, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
  456. static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: TargetMenuAim }); }
  457. }
  458. i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: TargetMenuAim, decorators: [{
  459. type: Injectable
  460. }] });
  461. /**
  462. * CdkTargetMenuAim is a provider for the TargetMenuAim service. It can be added to an
  463. * element with either the `cdkMenu` or `cdkMenuBar` directive and child menu items.
  464. */
  465. class CdkTargetMenuAim {
  466. static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: CdkTargetMenuAim, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
  467. static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.0.0", type: CdkTargetMenuAim, isStandalone: true, selector: "[cdkTargetMenuAim]", providers: [{ provide: MENU_AIM, useClass: TargetMenuAim }], exportAs: ["cdkTargetMenuAim"], ngImport: i0 }); }
  468. }
  469. i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: CdkTargetMenuAim, decorators: [{
  470. type: Directive,
  471. args: [{
  472. selector: '[cdkTargetMenuAim]',
  473. exportAs: 'cdkTargetMenuAim',
  474. standalone: true,
  475. providers: [{ provide: MENU_AIM, useClass: TargetMenuAim }],
  476. }]
  477. }] });
  478. /**
  479. * A directive that turns its host element into a trigger for a popup menu.
  480. * It can be combined with cdkMenuItem to create sub-menus. If the element is in a top level
  481. * MenuBar it will open the menu on click, or if a sibling is already opened it will open on hover.
  482. * If it is inside of a Menu it will open the attached Submenu on hover regardless of its sibling
  483. * state.
  484. */
  485. class CdkMenuTrigger extends CdkMenuTriggerBase {
  486. constructor() {
  487. super();
  488. this._elementRef = inject(ElementRef);
  489. this._overlay = inject(Overlay);
  490. this._ngZone = inject(NgZone);
  491. this._directionality = inject(Directionality, { optional: true });
  492. this._inputModalityDetector = inject(InputModalityDetector);
  493. /** The parent menu this trigger belongs to. */
  494. this._parentMenu = inject(CDK_MENU, { optional: true });
  495. /** The menu aim service used by this menu. */
  496. this._menuAim = inject(MENU_AIM, { optional: true });
  497. this._setRole();
  498. this._registerCloseHandler();
  499. this._subscribeToMenuStackClosed();
  500. this._subscribeToMouseEnter();
  501. this._subscribeToMenuStackHasFocus();
  502. this._setType();
  503. }
  504. /** Toggle the attached menu. */
  505. toggle() {
  506. this.isOpen() ? this.close() : this.open();
  507. }
  508. /** Open the attached menu. */
  509. open() {
  510. if (!this.isOpen() && this.menuTemplateRef != null) {
  511. this.opened.next();
  512. this.overlayRef = this.overlayRef || this._overlay.create(this._getOverlayConfig());
  513. this.overlayRef.attach(this.getMenuContentPortal());
  514. this._subscribeToOutsideClicks();
  515. }
  516. }
  517. /** Close the opened menu. */
  518. close() {
  519. if (this.isOpen()) {
  520. this.closed.next();
  521. this.overlayRef.detach();
  522. }
  523. this._closeSiblingTriggers();
  524. }
  525. /**
  526. * Get a reference to the rendered Menu if the Menu is open and rendered in the DOM.
  527. */
  528. getMenu() {
  529. return this.childMenu;
  530. }
  531. /**
  532. * Handles keyboard events for the menu item.
  533. * @param event The keyboard event to handle
  534. */
  535. _toggleOnKeydown(event) {
  536. const isParentVertical = this._parentMenu?.orientation === 'vertical';
  537. switch (event.keyCode) {
  538. case SPACE:
  539. case ENTER:
  540. if (!hasModifierKey(event)) {
  541. this.toggle();
  542. this.childMenu?.focusFirstItem('keyboard');
  543. }
  544. break;
  545. case RIGHT_ARROW:
  546. if (!hasModifierKey(event)) {
  547. if (this._parentMenu && isParentVertical && this._directionality?.value !== 'rtl') {
  548. event.preventDefault();
  549. this.open();
  550. this.childMenu?.focusFirstItem('keyboard');
  551. }
  552. }
  553. break;
  554. case LEFT_ARROW:
  555. if (!hasModifierKey(event)) {
  556. if (this._parentMenu && isParentVertical && this._directionality?.value === 'rtl') {
  557. event.preventDefault();
  558. this.open();
  559. this.childMenu?.focusFirstItem('keyboard');
  560. }
  561. }
  562. break;
  563. case DOWN_ARROW:
  564. case UP_ARROW:
  565. if (!hasModifierKey(event)) {
  566. if (!isParentVertical) {
  567. event.preventDefault();
  568. this.open();
  569. event.keyCode === DOWN_ARROW
  570. ? this.childMenu?.focusFirstItem('keyboard')
  571. : this.childMenu?.focusLastItem('keyboard');
  572. }
  573. }
  574. break;
  575. }
  576. }
  577. /** Handles clicks on the menu trigger. */
  578. _handleClick() {
  579. // Don't handle clicks originating from the keyboard since we
  580. // already do the same on `keydown` events for enter and space.
  581. if (this._inputModalityDetector.mostRecentModality !== 'keyboard') {
  582. this.toggle();
  583. this.childMenu?.focusFirstItem('mouse');
  584. }
  585. }
  586. /**
  587. * Sets whether the trigger's menu stack has focus.
  588. * @param hasFocus Whether the menu stack has focus.
  589. */
  590. _setHasFocus(hasFocus) {
  591. if (!this._parentMenu) {
  592. this.menuStack.setHasFocus(hasFocus);
  593. }
  594. }
  595. /**
  596. * Subscribe to the mouseenter events and close any sibling menu items if this element is moused
  597. * into.
  598. */
  599. _subscribeToMouseEnter() {
  600. this._ngZone.runOutsideAngular(() => {
  601. fromEvent(this._elementRef.nativeElement, 'mouseenter')
  602. .pipe(filter(() => !this.menuStack.isEmpty() && !this.isOpen()), takeUntil(this.destroyed))
  603. .subscribe(() => {
  604. // Closes any sibling menu items and opens the menu associated with this trigger.
  605. const toggleMenus = () => this._ngZone.run(() => {
  606. this._closeSiblingTriggers();
  607. this.open();
  608. });
  609. if (this._menuAim) {
  610. this._menuAim.toggle(toggleMenus);
  611. }
  612. else {
  613. toggleMenus();
  614. }
  615. });
  616. });
  617. }
  618. /** Close out any sibling menu trigger menus. */
  619. _closeSiblingTriggers() {
  620. if (this._parentMenu) {
  621. // If nothing was removed from the stack and the last element is not the parent item
  622. // that means that the parent menu is a menu bar since we don't put the menu bar on the
  623. // stack
  624. const isParentMenuBar = !this.menuStack.closeSubMenuOf(this._parentMenu) &&
  625. this.menuStack.peek() !== this._parentMenu;
  626. if (isParentMenuBar) {
  627. this.menuStack.closeAll();
  628. }
  629. }
  630. else {
  631. this.menuStack.closeAll();
  632. }
  633. }
  634. /** Get the configuration object used to create the overlay. */
  635. _getOverlayConfig() {
  636. return new OverlayConfig({
  637. positionStrategy: this._getOverlayPositionStrategy(),
  638. scrollStrategy: this._overlay.scrollStrategies.reposition(),
  639. direction: this._directionality || undefined,
  640. });
  641. }
  642. /** Build the position strategy for the overlay which specifies where to place the menu. */
  643. _getOverlayPositionStrategy() {
  644. return this._overlay
  645. .position()
  646. .flexibleConnectedTo(this._elementRef)
  647. .withLockedPosition()
  648. .withGrowAfterOpen()
  649. .withPositions(this._getOverlayPositions());
  650. }
  651. /** Get the preferred positions for the opened menu relative to the menu item. */
  652. _getOverlayPositions() {
  653. return (this.menuPosition ??
  654. (!this._parentMenu || this._parentMenu.orientation === 'horizontal'
  655. ? STANDARD_DROPDOWN_BELOW_POSITIONS
  656. : STANDARD_DROPDOWN_ADJACENT_POSITIONS));
  657. }
  658. /**
  659. * Subscribe to the MenuStack close events if this is a standalone trigger and close out the menu
  660. * this triggers when requested.
  661. */
  662. _registerCloseHandler() {
  663. if (!this._parentMenu) {
  664. this.menuStack.closed.pipe(takeUntil(this.destroyed)).subscribe(({ item }) => {
  665. if (item === this.childMenu) {
  666. this.close();
  667. }
  668. });
  669. }
  670. }
  671. /**
  672. * Subscribe to the overlays outside pointer events stream and handle closing out the stack if a
  673. * click occurs outside the menus.
  674. */
  675. _subscribeToOutsideClicks() {
  676. if (this.overlayRef) {
  677. this.overlayRef
  678. .outsidePointerEvents()
  679. .pipe(takeUntil(this.stopOutsideClicksListener))
  680. .subscribe(event => {
  681. const target = _getEventTarget(event);
  682. const element = this._elementRef.nativeElement;
  683. if (target !== element && !element.contains(target)) {
  684. if (!this.isElementInsideMenuStack(target)) {
  685. this.menuStack.closeAll();
  686. }
  687. else {
  688. this._closeSiblingTriggers();
  689. }
  690. }
  691. });
  692. }
  693. }
  694. /** Subscribe to the MenuStack hasFocus events. */
  695. _subscribeToMenuStackHasFocus() {
  696. if (!this._parentMenu) {
  697. this.menuStack.hasFocus.pipe(takeUntil(this.destroyed)).subscribe(hasFocus => {
  698. if (!hasFocus) {
  699. this.menuStack.closeAll();
  700. }
  701. });
  702. }
  703. }
  704. /** Subscribe to the MenuStack closed events. */
  705. _subscribeToMenuStackClosed() {
  706. if (!this._parentMenu) {
  707. this.menuStack.closed.subscribe(({ focusParentTrigger }) => {
  708. if (focusParentTrigger && !this.menuStack.length()) {
  709. this._elementRef.nativeElement.focus();
  710. }
  711. });
  712. }
  713. }
  714. /** Sets the role attribute for this trigger if needed. */
  715. _setRole() {
  716. // If this trigger is part of another menu, the cdkMenuItem directive will handle setting the
  717. // role, otherwise this is a standalone trigger, and we should ensure it has role="button".
  718. if (!this._parentMenu) {
  719. this._elementRef.nativeElement.setAttribute('role', 'button');
  720. }
  721. }
  722. /** Sets thte `type` attribute of the trigger. */
  723. _setType() {
  724. const element = this._elementRef.nativeElement;
  725. if (element.nodeName === 'BUTTON' && !element.getAttribute('type')) {
  726. // Prevents form submissions.
  727. element.setAttribute('type', 'button');
  728. }
  729. }
  730. static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: CdkMenuTrigger, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
  731. static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.0.0", type: CdkMenuTrigger, isStandalone: true, selector: "[cdkMenuTriggerFor]", inputs: { menuTemplateRef: ["cdkMenuTriggerFor", "menuTemplateRef"], menuPosition: ["cdkMenuPosition", "menuPosition"], menuData: ["cdkMenuTriggerData", "menuData"] }, outputs: { opened: "cdkMenuOpened", closed: "cdkMenuClosed" }, host: { listeners: { "focusin": "_setHasFocus(true)", "focusout": "_setHasFocus(false)", "keydown": "_toggleOnKeydown($event)", "click": "_handleClick()" }, properties: { "attr.aria-haspopup": "menuTemplateRef ? \"menu\" : null", "attr.aria-expanded": "menuTemplateRef == null ? null : isOpen()" }, classAttribute: "cdk-menu-trigger" }, providers: [
  732. { provide: MENU_TRIGGER, useExisting: CdkMenuTrigger },
  733. PARENT_OR_NEW_MENU_STACK_PROVIDER,
  734. ], exportAs: ["cdkMenuTriggerFor"], usesInheritance: true, ngImport: i0 }); }
  735. }
  736. i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: CdkMenuTrigger, decorators: [{
  737. type: Directive,
  738. args: [{
  739. selector: '[cdkMenuTriggerFor]',
  740. exportAs: 'cdkMenuTriggerFor',
  741. standalone: true,
  742. host: {
  743. 'class': 'cdk-menu-trigger',
  744. '[attr.aria-haspopup]': 'menuTemplateRef ? "menu" : null',
  745. '[attr.aria-expanded]': 'menuTemplateRef == null ? null : isOpen()',
  746. '(focusin)': '_setHasFocus(true)',
  747. '(focusout)': '_setHasFocus(false)',
  748. '(keydown)': '_toggleOnKeydown($event)',
  749. '(click)': '_handleClick()',
  750. },
  751. inputs: [
  752. 'menuTemplateRef: cdkMenuTriggerFor',
  753. 'menuPosition: cdkMenuPosition',
  754. 'menuData: cdkMenuTriggerData',
  755. ],
  756. outputs: ['opened: cdkMenuOpened', 'closed: cdkMenuClosed'],
  757. providers: [
  758. { provide: MENU_TRIGGER, useExisting: CdkMenuTrigger },
  759. PARENT_OR_NEW_MENU_STACK_PROVIDER,
  760. ],
  761. }]
  762. }], ctorParameters: function () { return []; } });
  763. /**
  764. * Directive which provides the ability for an element to be focused and navigated to using the
  765. * keyboard when residing in a CdkMenu, CdkMenuBar, or CdkMenuGroup. It performs user defined
  766. * behavior when clicked.
  767. */
  768. class CdkMenuItem {
  769. /** Whether the CdkMenuItem is disabled - defaults to false */
  770. get disabled() {
  771. return this._disabled;
  772. }
  773. set disabled(value) {
  774. this._disabled = coerceBooleanProperty(value);
  775. }
  776. /** Whether the menu item opens a menu. */
  777. get hasMenu() {
  778. return this._menuTrigger?.menuTemplateRef != null;
  779. }
  780. constructor() {
  781. this._dir = inject(Directionality, { optional: true });
  782. this._inputModalityDetector = inject(InputModalityDetector);
  783. this._elementRef = inject(ElementRef);
  784. this._ngZone = inject(NgZone);
  785. /** The menu aim service used by this menu. */
  786. this._menuAim = inject(MENU_AIM, { optional: true });
  787. /** The stack of menus this menu belongs to. */
  788. this._menuStack = inject(MENU_STACK);
  789. /** The parent menu in which this menuitem resides. */
  790. this._parentMenu = inject(CDK_MENU, { optional: true });
  791. /** Reference to the CdkMenuItemTrigger directive if one is added to the same element */
  792. this._menuTrigger = inject(CdkMenuTrigger, { optional: true, self: true });
  793. this._disabled = false;
  794. /**
  795. * If this MenuItem is a regular MenuItem, outputs when it is triggered by a keyboard or mouse
  796. * event.
  797. */
  798. this.triggered = new EventEmitter();
  799. /**
  800. * The tabindex for this menu item managed internally and used for implementing roving a
  801. * tab index.
  802. */
  803. this._tabindex = -1;
  804. /** Whether the item should close the menu if triggered by the spacebar. */
  805. this.closeOnSpacebarTrigger = true;
  806. /** Emits when the menu item is destroyed. */
  807. this.destroyed = new Subject();
  808. this._setupMouseEnter();
  809. this._setType();
  810. if (this._isStandaloneItem()) {
  811. this._tabindex = 0;
  812. }
  813. }
  814. ngOnDestroy() {
  815. this.destroyed.next();
  816. this.destroyed.complete();
  817. }
  818. /** Place focus on the element. */
  819. focus() {
  820. this._elementRef.nativeElement.focus();
  821. }
  822. /**
  823. * If the menu item is not disabled and the element does not have a menu trigger attached, emit
  824. * on the cdkMenuItemTriggered emitter and close all open menus.
  825. * @param options Options the configure how the item is triggered
  826. * - keepOpen: specifies that the menu should be kept open after triggering the item.
  827. */
  828. trigger(options) {
  829. const { keepOpen } = { ...options };
  830. if (!this.disabled && !this.hasMenu) {
  831. this.triggered.next();
  832. if (!keepOpen) {
  833. this._menuStack.closeAll({ focusParentTrigger: true });
  834. }
  835. }
  836. }
  837. /** Return true if this MenuItem has an attached menu and it is open. */
  838. isMenuOpen() {
  839. return !!this._menuTrigger?.isOpen();
  840. }
  841. /**
  842. * Get a reference to the rendered Menu if the Menu is open and it is visible in the DOM.
  843. * @return the menu if it is open, otherwise undefined.
  844. */
  845. getMenu() {
  846. return this._menuTrigger?.getMenu();
  847. }
  848. /** Get the CdkMenuTrigger associated with this element. */
  849. getMenuTrigger() {
  850. return this._menuTrigger;
  851. }
  852. /** Get the label for this element which is required by the FocusableOption interface. */
  853. getLabel() {
  854. return this.typeaheadLabel || this._elementRef.nativeElement.textContent?.trim() || '';
  855. }
  856. /** Reset the tabindex to -1. */
  857. _resetTabIndex() {
  858. if (!this._isStandaloneItem()) {
  859. this._tabindex = -1;
  860. }
  861. }
  862. /**
  863. * Set the tab index to 0 if not disabled and it's a focus event, or a mouse enter if this element
  864. * is not in a menu bar.
  865. */
  866. _setTabIndex(event) {
  867. if (this.disabled) {
  868. return;
  869. }
  870. // don't set the tabindex if there are no open sibling or parent menus
  871. if (!event || !this._menuStack.isEmpty()) {
  872. this._tabindex = 0;
  873. }
  874. }
  875. /**
  876. * Handles keyboard events for the menu item, specifically either triggering the user defined
  877. * callback or opening/closing the current menu based on whether the left or right arrow key was
  878. * pressed.
  879. * @param event the keyboard event to handle
  880. */
  881. _onKeydown(event) {
  882. switch (event.keyCode) {
  883. case SPACE:
  884. case ENTER:
  885. if (!hasModifierKey(event)) {
  886. this.trigger({ keepOpen: event.keyCode === SPACE && !this.closeOnSpacebarTrigger });
  887. }
  888. break;
  889. case RIGHT_ARROW:
  890. if (!hasModifierKey(event)) {
  891. if (this._parentMenu && this._isParentVertical()) {
  892. if (this._dir?.value !== 'rtl') {
  893. this._forwardArrowPressed(event);
  894. }
  895. else {
  896. this._backArrowPressed(event);
  897. }
  898. }
  899. }
  900. break;
  901. case LEFT_ARROW:
  902. if (!hasModifierKey(event)) {
  903. if (this._parentMenu && this._isParentVertical()) {
  904. if (this._dir?.value !== 'rtl') {
  905. this._backArrowPressed(event);
  906. }
  907. else {
  908. this._forwardArrowPressed(event);
  909. }
  910. }
  911. }
  912. break;
  913. }
  914. }
  915. /** Handles clicks on the menu item. */
  916. _handleClick() {
  917. // Don't handle clicks originating from the keyboard since we
  918. // already do the same on `keydown` events for enter and space.
  919. if (this._inputModalityDetector.mostRecentModality !== 'keyboard') {
  920. this.trigger();
  921. }
  922. }
  923. /** Whether this menu item is standalone or within a menu or menu bar. */
  924. _isStandaloneItem() {
  925. return !this._parentMenu;
  926. }
  927. /**
  928. * Handles the user pressing the back arrow key.
  929. * @param event The keyboard event.
  930. */
  931. _backArrowPressed(event) {
  932. const parentMenu = this._parentMenu;
  933. if (this._menuStack.hasInlineMenu() || this._menuStack.length() > 1) {
  934. event.preventDefault();
  935. this._menuStack.close(parentMenu, {
  936. focusNextOnEmpty: this._menuStack.inlineMenuOrientation() === 'horizontal'
  937. ? 1 /* FocusNext.previousItem */
  938. : 2 /* FocusNext.currentItem */,
  939. focusParentTrigger: true,
  940. });
  941. }
  942. }
  943. /**
  944. * Handles the user pressing the forward arrow key.
  945. * @param event The keyboard event.
  946. */
  947. _forwardArrowPressed(event) {
  948. if (!this.hasMenu && this._menuStack.inlineMenuOrientation() === 'horizontal') {
  949. event.preventDefault();
  950. this._menuStack.closeAll({
  951. focusNextOnEmpty: 0 /* FocusNext.nextItem */,
  952. focusParentTrigger: true,
  953. });
  954. }
  955. }
  956. /**
  957. * Subscribe to the mouseenter events and close any sibling menu items if this element is moused
  958. * into.
  959. */
  960. _setupMouseEnter() {
  961. if (!this._isStandaloneItem()) {
  962. const closeOpenSiblings = () => this._ngZone.run(() => this._menuStack.closeSubMenuOf(this._parentMenu));
  963. this._ngZone.runOutsideAngular(() => fromEvent(this._elementRef.nativeElement, 'mouseenter')
  964. .pipe(filter(() => !this._menuStack.isEmpty() && !this.hasMenu), takeUntil(this.destroyed))
  965. .subscribe(() => {
  966. if (this._menuAim) {
  967. this._menuAim.toggle(closeOpenSiblings);
  968. }
  969. else {
  970. closeOpenSiblings();
  971. }
  972. }));
  973. }
  974. }
  975. /**
  976. * Return true if the enclosing parent menu is configured in a horizontal orientation, false
  977. * otherwise or if no parent.
  978. */
  979. _isParentVertical() {
  980. return this._parentMenu?.orientation === 'vertical';
  981. }
  982. /** Sets the `type` attribute of the menu item. */
  983. _setType() {
  984. const element = this._elementRef.nativeElement;
  985. if (element.nodeName === 'BUTTON' && !element.getAttribute('type')) {
  986. // Prevent form submissions.
  987. element.setAttribute('type', 'button');
  988. }
  989. }
  990. static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: CdkMenuItem, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
  991. static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.0.0", type: CdkMenuItem, isStandalone: true, selector: "[cdkMenuItem]", inputs: { disabled: ["cdkMenuItemDisabled", "disabled"], typeaheadLabel: ["cdkMenuitemTypeaheadLabel", "typeaheadLabel"] }, outputs: { triggered: "cdkMenuItemTriggered" }, host: { attributes: { "role": "menuitem" }, listeners: { "blur": "_resetTabIndex()", "focus": "_setTabIndex()", "click": "_handleClick()", "keydown": "_onKeydown($event)" }, properties: { "tabindex": "_tabindex", "attr.aria-disabled": "disabled || null" }, classAttribute: "cdk-menu-item" }, exportAs: ["cdkMenuItem"], ngImport: i0 }); }
  992. }
  993. i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: CdkMenuItem, decorators: [{
  994. type: Directive,
  995. args: [{
  996. selector: '[cdkMenuItem]',
  997. exportAs: 'cdkMenuItem',
  998. standalone: true,
  999. host: {
  1000. 'role': 'menuitem',
  1001. 'class': 'cdk-menu-item',
  1002. '[tabindex]': '_tabindex',
  1003. '[attr.aria-disabled]': 'disabled || null',
  1004. '(blur)': '_resetTabIndex()',
  1005. '(focus)': '_setTabIndex()',
  1006. '(click)': '_handleClick()',
  1007. '(keydown)': '_onKeydown($event)',
  1008. },
  1009. }]
  1010. }], ctorParameters: function () { return []; }, propDecorators: { disabled: [{
  1011. type: Input,
  1012. args: ['cdkMenuItemDisabled']
  1013. }], typeaheadLabel: [{
  1014. type: Input,
  1015. args: ['cdkMenuitemTypeaheadLabel']
  1016. }], triggered: [{
  1017. type: Output,
  1018. args: ['cdkMenuItemTriggered']
  1019. }] } });
  1020. /**
  1021. * PointerFocusTracker keeps track of the currently active item under mouse focus. It also has
  1022. * observables which emit when the users mouse enters and leaves a tracked element.
  1023. */
  1024. class PointerFocusTracker {
  1025. constructor(
  1026. /** The list of items being tracked. */
  1027. _items) {
  1028. this._items = _items;
  1029. /** Emits when an element is moused into. */
  1030. this.entered = this._getItemPointerEntries();
  1031. /** Emits when an element is moused out. */
  1032. this.exited = this._getItemPointerExits();
  1033. /** Emits when this is destroyed. */
  1034. this._destroyed = new Subject();
  1035. this.entered.subscribe(element => (this.activeElement = element));
  1036. this.exited.subscribe(() => {
  1037. this.previousElement = this.activeElement;
  1038. this.activeElement = undefined;
  1039. });
  1040. }
  1041. /** Stop the managers listeners. */
  1042. destroy() {
  1043. this._destroyed.next();
  1044. this._destroyed.complete();
  1045. }
  1046. /**
  1047. * Gets a stream of pointer (mouse) entries into the given items.
  1048. * This should typically run outside the Angular zone.
  1049. */
  1050. _getItemPointerEntries() {
  1051. return defer(() => this._items.changes.pipe(startWith(this._items), mergeMap((list) => list.map(element => fromEvent(element._elementRef.nativeElement, 'mouseenter').pipe(mapTo(element), takeUntil(this._items.changes)))), mergeAll()));
  1052. }
  1053. /**
  1054. * Gets a stream of pointer (mouse) exits out of the given items.
  1055. * This should typically run outside the Angular zone.
  1056. */
  1057. _getItemPointerExits() {
  1058. return defer(() => this._items.changes.pipe(startWith(this._items), mergeMap((list) => list.map(element => fromEvent(element._elementRef.nativeElement, 'mouseout').pipe(mapTo(element), takeUntil(this._items.changes)))), mergeAll()));
  1059. }
  1060. }
  1061. /** Counter used to create unique IDs for menus. */
  1062. let nextId$1 = 0;
  1063. /**
  1064. * Abstract directive that implements shared logic common to all menus.
  1065. * This class can be extended to create custom menu types.
  1066. */
  1067. class CdkMenuBase extends CdkMenuGroup {
  1068. constructor() {
  1069. super(...arguments);
  1070. /** The menu's native DOM host element. */
  1071. this.nativeElement = inject(ElementRef).nativeElement;
  1072. /** The Angular zone. */
  1073. this.ngZone = inject(NgZone);
  1074. /** The stack of menus this menu belongs to. */
  1075. this.menuStack = inject(MENU_STACK);
  1076. /** The menu aim service used by this menu. */
  1077. this.menuAim = inject(MENU_AIM, { optional: true, self: true });
  1078. /** The directionality (text direction) of the current page. */
  1079. this.dir = inject(Directionality, { optional: true });
  1080. /** The id of the menu's host element. */
  1081. this.id = `cdk-menu-${nextId$1++}`;
  1082. /** The direction items in the menu flow. */
  1083. this.orientation = 'vertical';
  1084. /**
  1085. * Whether the menu is displayed inline (i.e. always present vs a conditional popup that the
  1086. * user triggers with a trigger element).
  1087. */
  1088. this.isInline = false;
  1089. /** Emits when the MenuBar is destroyed. */
  1090. this.destroyed = new Subject();
  1091. /** Whether this menu's menu stack has focus. */
  1092. this._menuStackHasFocus = false;
  1093. }
  1094. ngAfterContentInit() {
  1095. if (!this.isInline) {
  1096. this.menuStack.push(this);
  1097. }
  1098. this._setKeyManager();
  1099. this._subscribeToMenuStackHasFocus();
  1100. this._subscribeToMenuOpen();
  1101. this._subscribeToMenuStackClosed();
  1102. this._setUpPointerTracker();
  1103. }
  1104. ngOnDestroy() {
  1105. this.keyManager?.destroy();
  1106. this.destroyed.next();
  1107. this.destroyed.complete();
  1108. this.pointerTracker?.destroy();
  1109. }
  1110. /**
  1111. * Place focus on the first MenuItem in the menu and set the focus origin.
  1112. * @param focusOrigin The origin input mode of the focus event.
  1113. */
  1114. focusFirstItem(focusOrigin = 'program') {
  1115. this.keyManager.setFocusOrigin(focusOrigin);
  1116. this.keyManager.setFirstItemActive();
  1117. }
  1118. /**
  1119. * Place focus on the last MenuItem in the menu and set the focus origin.
  1120. * @param focusOrigin The origin input mode of the focus event.
  1121. */
  1122. focusLastItem(focusOrigin = 'program') {
  1123. this.keyManager.setFocusOrigin(focusOrigin);
  1124. this.keyManager.setLastItemActive();
  1125. }
  1126. /** Gets the tabindex for this menu. */
  1127. _getTabIndex() {
  1128. const tabindexIfInline = this._menuStackHasFocus ? -1 : 0;
  1129. return this.isInline ? tabindexIfInline : null;
  1130. }
  1131. /**
  1132. * Close the open menu if the current active item opened the requested MenuStackItem.
  1133. * @param menu The menu requested to be closed.
  1134. * @param options Options to configure the behavior on close.
  1135. * - `focusParentTrigger` Whether to focus the parent trigger after closing the menu.
  1136. */
  1137. closeOpenMenu(menu, options) {
  1138. const { focusParentTrigger } = { ...options };
  1139. const keyManager = this.keyManager;
  1140. const trigger = this.triggerItem;
  1141. if (menu === trigger?.getMenuTrigger()?.getMenu()) {
  1142. trigger?.getMenuTrigger()?.close();
  1143. // If the user has moused over a sibling item we want to focus the element under mouse focus
  1144. // not the trigger which previously opened the now closed menu.
  1145. if (focusParentTrigger) {
  1146. if (trigger) {
  1147. keyManager.setActiveItem(trigger);
  1148. }
  1149. else {
  1150. keyManager.setFirstItemActive();
  1151. }
  1152. }
  1153. }
  1154. }
  1155. /** Setup the FocusKeyManager with the correct orientation for the menu. */
  1156. _setKeyManager() {
  1157. this.keyManager = new FocusKeyManager(this.items).withWrap().withTypeAhead().withHomeAndEnd();
  1158. if (this.orientation === 'horizontal') {
  1159. this.keyManager.withHorizontalOrientation(this.dir?.value || 'ltr');
  1160. }
  1161. else {
  1162. this.keyManager.withVerticalOrientation();
  1163. }
  1164. }
  1165. /**
  1166. * Subscribe to the menu trigger's open events in order to track the trigger which opened the menu
  1167. * and stop tracking it when the menu is closed.
  1168. */
  1169. _subscribeToMenuOpen() {
  1170. const exitCondition = merge(this.items.changes, this.destroyed);
  1171. this.items.changes
  1172. .pipe(startWith(this.items), mergeMap((list) => list
  1173. .filter(item => item.hasMenu)
  1174. .map(item => item.getMenuTrigger().opened.pipe(mapTo(item), takeUntil(exitCondition)))), mergeAll(), switchMap((item) => {
  1175. this.triggerItem = item;
  1176. return item.getMenuTrigger().closed;
  1177. }), takeUntil(this.destroyed))
  1178. .subscribe(() => (this.triggerItem = undefined));
  1179. }
  1180. /** Subscribe to the MenuStack close events. */
  1181. _subscribeToMenuStackClosed() {
  1182. this.menuStack.closed
  1183. .pipe(takeUntil(this.destroyed))
  1184. .subscribe(({ item, focusParentTrigger }) => this.closeOpenMenu(item, { focusParentTrigger }));
  1185. }
  1186. /** Subscribe to the MenuStack hasFocus events. */
  1187. _subscribeToMenuStackHasFocus() {
  1188. if (this.isInline) {
  1189. this.menuStack.hasFocus.pipe(takeUntil(this.destroyed)).subscribe(hasFocus => {
  1190. this._menuStackHasFocus = hasFocus;
  1191. });
  1192. }
  1193. }
  1194. /**
  1195. * Set the PointerFocusTracker and ensure that when mouse focus changes the key manager is updated
  1196. * with the latest menu item under mouse focus.
  1197. */
  1198. _setUpPointerTracker() {
  1199. if (this.menuAim) {
  1200. this.ngZone.runOutsideAngular(() => {
  1201. this.pointerTracker = new PointerFocusTracker(this.items);
  1202. });
  1203. this.menuAim.initialize(this, this.pointerTracker);
  1204. }
  1205. }
  1206. static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: CdkMenuBase, deps: null, target: i0.ɵɵFactoryTarget.Directive }); }
  1207. static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.0.0", type: CdkMenuBase, inputs: { id: "id" }, host: { attributes: { "role": "menu" }, listeners: { "focus": "focusFirstItem()", "focusin": "menuStack.setHasFocus(true)", "focusout": "menuStack.setHasFocus(false)" }, properties: { "tabindex": "_getTabIndex()", "id": "id", "attr.aria-orientation": "orientation", "attr.data-cdk-menu-stack-id": "menuStack.id" } }, queries: [{ propertyName: "items", predicate: CdkMenuItem, descendants: true }], usesInheritance: true, ngImport: i0 }); }
  1208. }
  1209. i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: CdkMenuBase, decorators: [{
  1210. type: Directive,
  1211. args: [{
  1212. host: {
  1213. 'role': 'menu',
  1214. 'class': '',
  1215. '[tabindex]': '_getTabIndex()',
  1216. '[id]': 'id',
  1217. '[attr.aria-orientation]': 'orientation',
  1218. '[attr.data-cdk-menu-stack-id]': 'menuStack.id',
  1219. '(focus)': 'focusFirstItem()',
  1220. '(focusin)': 'menuStack.setHasFocus(true)',
  1221. '(focusout)': 'menuStack.setHasFocus(false)',
  1222. },
  1223. }]
  1224. }], propDecorators: { id: [{
  1225. type: Input
  1226. }], items: [{
  1227. type: ContentChildren,
  1228. args: [CdkMenuItem, { descendants: true }]
  1229. }] } });
  1230. /**
  1231. * Directive which configures the element as a Menu which should contain child elements marked as
  1232. * CdkMenuItem or CdkMenuGroup. Sets the appropriate role and aria-attributes for a menu and
  1233. * contains accessible keyboard and mouse handling logic.
  1234. *
  1235. * It also acts as a RadioGroup for elements marked with role `menuitemradio`.
  1236. */
  1237. class CdkMenu extends CdkMenuBase {
  1238. constructor() {
  1239. super();
  1240. this._parentTrigger = inject(MENU_TRIGGER, { optional: true });
  1241. /** Event emitted when the menu is closed. */
  1242. this.closed = new EventEmitter();
  1243. /** The direction items in the menu flow. */
  1244. this.orientation = 'vertical';
  1245. /** Whether the menu is displayed inline (i.e. always present vs a conditional popup that the user triggers with a trigger element). */
  1246. this.isInline = !this._parentTrigger;
  1247. this.destroyed.subscribe(this.closed);
  1248. this._parentTrigger?.registerChildMenu(this);
  1249. }
  1250. ngAfterContentInit() {
  1251. super.ngAfterContentInit();
  1252. this._subscribeToMenuStackEmptied();
  1253. }
  1254. ngOnDestroy() {
  1255. super.ngOnDestroy();
  1256. this.closed.complete();
  1257. }
  1258. /**
  1259. * Handle keyboard events for the Menu.
  1260. * @param event The keyboard event to be handled.
  1261. */
  1262. _handleKeyEvent(event) {
  1263. const keyManager = this.keyManager;
  1264. switch (event.keyCode) {
  1265. case LEFT_ARROW:
  1266. case RIGHT_ARROW:
  1267. if (!hasModifierKey(event)) {
  1268. event.preventDefault();
  1269. keyManager.setFocusOrigin('keyboard');
  1270. keyManager.onKeydown(event);
  1271. }
  1272. break;
  1273. case ESCAPE:
  1274. if (!hasModifierKey(event)) {
  1275. event.preventDefault();
  1276. this.menuStack.close(this, {
  1277. focusNextOnEmpty: 2 /* FocusNext.currentItem */,
  1278. focusParentTrigger: true,
  1279. });
  1280. }
  1281. break;
  1282. case TAB:
  1283. if (!hasModifierKey(event, 'altKey', 'metaKey', 'ctrlKey')) {
  1284. this.menuStack.closeAll({ focusParentTrigger: true });
  1285. }
  1286. break;
  1287. default:
  1288. keyManager.onKeydown(event);
  1289. }
  1290. }
  1291. /**
  1292. * Set focus the either the current, previous or next item based on the FocusNext event.
  1293. * @param focusNext The element to focus.
  1294. */
  1295. _toggleMenuFocus(focusNext) {
  1296. const keyManager = this.keyManager;
  1297. switch (focusNext) {
  1298. case 0 /* FocusNext.nextItem */:
  1299. keyManager.setFocusOrigin('keyboard');
  1300. keyManager.setNextItemActive();
  1301. break;
  1302. case 1 /* FocusNext.previousItem */:
  1303. keyManager.setFocusOrigin('keyboard');
  1304. keyManager.setPreviousItemActive();
  1305. break;
  1306. case 2 /* FocusNext.currentItem */:
  1307. if (keyManager.activeItem) {
  1308. keyManager.setFocusOrigin('keyboard');
  1309. keyManager.setActiveItem(keyManager.activeItem);
  1310. }
  1311. break;
  1312. }
  1313. }
  1314. /** Subscribe to the MenuStack emptied events. */
  1315. _subscribeToMenuStackEmptied() {
  1316. this.menuStack.emptied
  1317. .pipe(takeUntil(this.destroyed))
  1318. .subscribe(event => this._toggleMenuFocus(event));
  1319. }
  1320. static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: CdkMenu, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
  1321. static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.0.0", type: CdkMenu, isStandalone: true, selector: "[cdkMenu]", outputs: { closed: "closed" }, host: { attributes: { "role": "menu" }, listeners: { "keydown": "_handleKeyEvent($event)" }, properties: { "class.cdk-menu-inline": "isInline" }, classAttribute: "cdk-menu" }, providers: [
  1322. { provide: CdkMenuGroup, useExisting: CdkMenu },
  1323. { provide: CDK_MENU, useExisting: CdkMenu },
  1324. PARENT_OR_NEW_INLINE_MENU_STACK_PROVIDER('vertical'),
  1325. ], exportAs: ["cdkMenu"], usesInheritance: true, ngImport: i0 }); }
  1326. }
  1327. i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: CdkMenu, decorators: [{
  1328. type: Directive,
  1329. args: [{
  1330. selector: '[cdkMenu]',
  1331. exportAs: 'cdkMenu',
  1332. standalone: true,
  1333. host: {
  1334. 'role': 'menu',
  1335. 'class': 'cdk-menu',
  1336. '[class.cdk-menu-inline]': 'isInline',
  1337. '(keydown)': '_handleKeyEvent($event)',
  1338. },
  1339. providers: [
  1340. { provide: CdkMenuGroup, useExisting: CdkMenu },
  1341. { provide: CDK_MENU, useExisting: CdkMenu },
  1342. PARENT_OR_NEW_INLINE_MENU_STACK_PROVIDER('vertical'),
  1343. ],
  1344. }]
  1345. }], ctorParameters: function () { return []; }, propDecorators: { closed: [{
  1346. type: Output
  1347. }] } });
  1348. /**
  1349. * Directive applied to an element which configures it as a MenuBar by setting the appropriate
  1350. * role, aria attributes, and accessible keyboard and mouse handling logic. The component that
  1351. * this directive is applied to should contain components marked with CdkMenuItem.
  1352. *
  1353. */
  1354. class CdkMenuBar extends CdkMenuBase {
  1355. constructor() {
  1356. super(...arguments);
  1357. /** The direction items in the menu flow. */
  1358. this.orientation = 'horizontal';
  1359. /** Whether the menu is displayed inline (i.e. always present vs a conditional popup that the user triggers with a trigger element). */
  1360. this.isInline = true;
  1361. }
  1362. ngAfterContentInit() {
  1363. super.ngAfterContentInit();
  1364. this._subscribeToMenuStackEmptied();
  1365. }
  1366. /**
  1367. * Handle keyboard events for the Menu.
  1368. * @param event The keyboard event to be handled.
  1369. */
  1370. _handleKeyEvent(event) {
  1371. const keyManager = this.keyManager;
  1372. switch (event.keyCode) {
  1373. case UP_ARROW:
  1374. case DOWN_ARROW:
  1375. case LEFT_ARROW:
  1376. case RIGHT_ARROW:
  1377. if (!hasModifierKey(event)) {
  1378. const horizontalArrows = event.keyCode === LEFT_ARROW || event.keyCode === RIGHT_ARROW;
  1379. // For a horizontal menu if the left/right keys were clicked, or a vertical menu if the
  1380. // up/down keys were clicked: if the current menu is open, close it then focus and open the
  1381. // next menu.
  1382. if (horizontalArrows) {
  1383. event.preventDefault();
  1384. const prevIsOpen = keyManager.activeItem?.isMenuOpen();
  1385. keyManager.activeItem?.getMenuTrigger()?.close();
  1386. keyManager.setFocusOrigin('keyboard');
  1387. keyManager.onKeydown(event);
  1388. if (prevIsOpen) {
  1389. keyManager.activeItem?.getMenuTrigger()?.open();
  1390. }
  1391. }
  1392. }
  1393. break;
  1394. case ESCAPE:
  1395. if (!hasModifierKey(event)) {
  1396. event.preventDefault();
  1397. keyManager.activeItem?.getMenuTrigger()?.close();
  1398. }
  1399. break;
  1400. case TAB:
  1401. if (!hasModifierKey(event, 'altKey', 'metaKey', 'ctrlKey')) {
  1402. keyManager.activeItem?.getMenuTrigger()?.close();
  1403. }
  1404. break;
  1405. default:
  1406. keyManager.onKeydown(event);
  1407. }
  1408. }
  1409. /**
  1410. * Set focus to either the current, previous or next item based on the FocusNext event, then
  1411. * open the previous or next item.
  1412. * @param focusNext The element to focus.
  1413. */
  1414. _toggleOpenMenu(focusNext) {
  1415. const keyManager = this.keyManager;
  1416. switch (focusNext) {
  1417. case 0 /* FocusNext.nextItem */:
  1418. keyManager.setFocusOrigin('keyboard');
  1419. keyManager.setNextItemActive();
  1420. keyManager.activeItem?.getMenuTrigger()?.open();
  1421. break;
  1422. case 1 /* FocusNext.previousItem */:
  1423. keyManager.setFocusOrigin('keyboard');
  1424. keyManager.setPreviousItemActive();
  1425. keyManager.activeItem?.getMenuTrigger()?.open();
  1426. break;
  1427. case 2 /* FocusNext.currentItem */:
  1428. if (keyManager.activeItem) {
  1429. keyManager.setFocusOrigin('keyboard');
  1430. keyManager.setActiveItem(keyManager.activeItem);
  1431. }
  1432. break;
  1433. }
  1434. }
  1435. /** Subscribe to the MenuStack emptied events. */
  1436. _subscribeToMenuStackEmptied() {
  1437. this.menuStack?.emptied
  1438. .pipe(takeUntil(this.destroyed))
  1439. .subscribe(event => this._toggleOpenMenu(event));
  1440. }
  1441. static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: CdkMenuBar, deps: null, target: i0.ɵɵFactoryTarget.Directive }); }
  1442. static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.0.0", type: CdkMenuBar, isStandalone: true, selector: "[cdkMenuBar]", host: { attributes: { "role": "menubar" }, listeners: { "keydown": "_handleKeyEvent($event)" }, classAttribute: "cdk-menu-bar" }, providers: [
  1443. { provide: CdkMenuGroup, useExisting: CdkMenuBar },
  1444. { provide: CDK_MENU, useExisting: CdkMenuBar },
  1445. { provide: MENU_STACK, useFactory: () => MenuStack.inline('horizontal') },
  1446. ], exportAs: ["cdkMenuBar"], usesInheritance: true, ngImport: i0 }); }
  1447. }
  1448. i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: CdkMenuBar, decorators: [{
  1449. type: Directive,
  1450. args: [{
  1451. selector: '[cdkMenuBar]',
  1452. exportAs: 'cdkMenuBar',
  1453. standalone: true,
  1454. host: {
  1455. 'role': 'menubar',
  1456. 'class': 'cdk-menu-bar',
  1457. '(keydown)': '_handleKeyEvent($event)',
  1458. },
  1459. providers: [
  1460. { provide: CdkMenuGroup, useExisting: CdkMenuBar },
  1461. { provide: CDK_MENU, useExisting: CdkMenuBar },
  1462. { provide: MENU_STACK, useFactory: () => MenuStack.inline('horizontal') },
  1463. ],
  1464. }]
  1465. }] });
  1466. /** Base class providing checked state for selectable MenuItems. */
  1467. class CdkMenuItemSelectable extends CdkMenuItem {
  1468. constructor() {
  1469. super(...arguments);
  1470. this._checked = false;
  1471. /** Whether the item should close the menu if triggered by the spacebar. */
  1472. this.closeOnSpacebarTrigger = false;
  1473. }
  1474. /** Whether the element is checked */
  1475. get checked() {
  1476. return this._checked;
  1477. }
  1478. set checked(value) {
  1479. this._checked = coerceBooleanProperty(value);
  1480. }
  1481. static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: CdkMenuItemSelectable, deps: null, target: i0.ɵɵFactoryTarget.Directive }); }
  1482. static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.0.0", type: CdkMenuItemSelectable, inputs: { checked: ["cdkMenuItemChecked", "checked"] }, host: { properties: { "attr.aria-checked": "!!checked", "attr.aria-disabled": "disabled || null" } }, usesInheritance: true, ngImport: i0 }); }
  1483. }
  1484. i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: CdkMenuItemSelectable, decorators: [{
  1485. type: Directive,
  1486. args: [{
  1487. host: {
  1488. '[attr.aria-checked]': '!!checked',
  1489. '[attr.aria-disabled]': 'disabled || null',
  1490. },
  1491. }]
  1492. }], propDecorators: { checked: [{
  1493. type: Input,
  1494. args: ['cdkMenuItemChecked']
  1495. }] } });
  1496. /** Counter used to set a unique id and name for a selectable item */
  1497. let nextId = 0;
  1498. /**
  1499. * A directive providing behavior for the "menuitemradio" ARIA role, which behaves similarly to
  1500. * a conventional radio-button. Any sibling `CdkMenuItemRadio` instances within the same `CdkMenu`
  1501. * or `CdkMenuGroup` comprise a radio group with unique selection enforced.
  1502. */
  1503. class CdkMenuItemRadio extends CdkMenuItemSelectable {
  1504. constructor() {
  1505. super();
  1506. /** The unique selection dispatcher for this radio's `CdkMenuGroup`. */
  1507. this._selectionDispatcher = inject(UniqueSelectionDispatcher);
  1508. /** An ID to identify this radio item to the `UniqueSelectionDispatcher`. */
  1509. this._id = `${nextId++}`;
  1510. this._registerDispatcherListener();
  1511. }
  1512. ngOnDestroy() {
  1513. super.ngOnDestroy();
  1514. this._removeDispatcherListener();
  1515. }
  1516. /**
  1517. * Toggles the checked state of the radio-button.
  1518. * @param options Options the configure how the item is triggered
  1519. * - keepOpen: specifies that the menu should be kept open after triggering the item.
  1520. */
  1521. trigger(options) {
  1522. super.trigger(options);
  1523. if (!this.disabled) {
  1524. this._selectionDispatcher.notify(this._id, '');
  1525. }
  1526. }
  1527. /** Configure the unique selection dispatcher listener in order to toggle the checked state */
  1528. _registerDispatcherListener() {
  1529. this._removeDispatcherListener = this._selectionDispatcher.listen((id) => {
  1530. this.checked = this._id === id;
  1531. });
  1532. }
  1533. static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: CdkMenuItemRadio, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
  1534. static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.0.0", type: CdkMenuItemRadio, isStandalone: true, selector: "[cdkMenuItemRadio]", host: { attributes: { "role": "menuitemradio" }, properties: { "class.cdk-menu-item-radio": "true" } }, providers: [
  1535. { provide: CdkMenuItemSelectable, useExisting: CdkMenuItemRadio },
  1536. { provide: CdkMenuItem, useExisting: CdkMenuItemSelectable },
  1537. ], exportAs: ["cdkMenuItemRadio"], usesInheritance: true, ngImport: i0 }); }
  1538. }
  1539. i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: CdkMenuItemRadio, decorators: [{
  1540. type: Directive,
  1541. args: [{
  1542. selector: '[cdkMenuItemRadio]',
  1543. exportAs: 'cdkMenuItemRadio',
  1544. standalone: true,
  1545. host: {
  1546. 'role': 'menuitemradio',
  1547. '[class.cdk-menu-item-radio]': 'true',
  1548. },
  1549. providers: [
  1550. { provide: CdkMenuItemSelectable, useExisting: CdkMenuItemRadio },
  1551. { provide: CdkMenuItem, useExisting: CdkMenuItemSelectable },
  1552. ],
  1553. }]
  1554. }], ctorParameters: function () { return []; } });
  1555. /**
  1556. * A directive providing behavior for the "menuitemcheckbox" ARIA role, which behaves similarly to a
  1557. * conventional checkbox.
  1558. */
  1559. class CdkMenuItemCheckbox extends CdkMenuItemSelectable {
  1560. /**
  1561. * Toggle the checked state of the checkbox.
  1562. * @param options Options the configure how the item is triggered
  1563. * - keepOpen: specifies that the menu should be kept open after triggering the item.
  1564. */
  1565. trigger(options) {
  1566. super.trigger(options);
  1567. if (!this.disabled) {
  1568. this.checked = !this.checked;
  1569. }
  1570. }
  1571. static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: CdkMenuItemCheckbox, deps: null, target: i0.ɵɵFactoryTarget.Directive }); }
  1572. static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.0.0", type: CdkMenuItemCheckbox, isStandalone: true, selector: "[cdkMenuItemCheckbox]", host: { attributes: { "role": "menuitemcheckbox" }, properties: { "class.cdk-menu-item-checkbox": "true" } }, providers: [
  1573. { provide: CdkMenuItemSelectable, useExisting: CdkMenuItemCheckbox },
  1574. { provide: CdkMenuItem, useExisting: CdkMenuItemSelectable },
  1575. ], exportAs: ["cdkMenuItemCheckbox"], usesInheritance: true, ngImport: i0 }); }
  1576. }
  1577. i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: CdkMenuItemCheckbox, decorators: [{
  1578. type: Directive,
  1579. args: [{
  1580. selector: '[cdkMenuItemCheckbox]',
  1581. exportAs: 'cdkMenuItemCheckbox',
  1582. standalone: true,
  1583. host: {
  1584. 'role': 'menuitemcheckbox',
  1585. '[class.cdk-menu-item-checkbox]': 'true',
  1586. },
  1587. providers: [
  1588. { provide: CdkMenuItemSelectable, useExisting: CdkMenuItemCheckbox },
  1589. { provide: CdkMenuItem, useExisting: CdkMenuItemSelectable },
  1590. ],
  1591. }]
  1592. }] });
  1593. /** The preferred menu positions for the context menu. */
  1594. const CONTEXT_MENU_POSITIONS = STANDARD_DROPDOWN_BELOW_POSITIONS.map(position => {
  1595. // In cases where the first menu item in the context menu is a trigger the submenu opens on a
  1596. // hover event. We offset the context menu 2px by default to prevent this from occurring.
  1597. const offsetX = position.overlayX === 'start' ? 2 : -2;
  1598. const offsetY = position.overlayY === 'top' ? 2 : -2;
  1599. return { ...position, offsetX, offsetY };
  1600. });
  1601. /** Tracks the last open context menu trigger across the entire application. */
  1602. class ContextMenuTracker {
  1603. /**
  1604. * Close the previous open context menu and set the given one as being open.
  1605. * @param trigger The trigger for the currently open Context Menu.
  1606. */
  1607. update(trigger) {
  1608. if (ContextMenuTracker._openContextMenuTrigger !== trigger) {
  1609. ContextMenuTracker._openContextMenuTrigger?.close();
  1610. ContextMenuTracker._openContextMenuTrigger = trigger;
  1611. }
  1612. }
  1613. static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: ContextMenuTracker, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
  1614. static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: ContextMenuTracker, providedIn: 'root' }); }
  1615. }
  1616. i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: ContextMenuTracker, decorators: [{
  1617. type: Injectable,
  1618. args: [{ providedIn: 'root' }]
  1619. }] });
  1620. /**
  1621. * A directive that opens a menu when a user right-clicks within its host element.
  1622. * It is aware of nested context menus and will trigger only the lowest level non-disabled context menu.
  1623. */
  1624. class CdkContextMenuTrigger extends CdkMenuTriggerBase {
  1625. /** Whether the context menu is disabled. */
  1626. get disabled() {
  1627. return this._disabled;
  1628. }
  1629. set disabled(value) {
  1630. this._disabled = coerceBooleanProperty(value);
  1631. }
  1632. constructor() {
  1633. super();
  1634. /** The CDK overlay service. */
  1635. this._overlay = inject(Overlay);
  1636. /** The directionality of the page. */
  1637. this._directionality = inject(Directionality, { optional: true });
  1638. /** The app's context menu tracking registry */
  1639. this._contextMenuTracker = inject(ContextMenuTracker);
  1640. this._disabled = false;
  1641. this._setMenuStackCloseListener();
  1642. }
  1643. /**
  1644. * Open the attached menu at the specified location.
  1645. * @param coordinates where to open the context menu
  1646. */
  1647. open(coordinates) {
  1648. this._open(coordinates, false);
  1649. }
  1650. /** Close the currently opened context menu. */
  1651. close() {
  1652. this.menuStack.closeAll();
  1653. }
  1654. /**
  1655. * Open the context menu and closes any previously open menus.
  1656. * @param event the mouse event which opens the context menu.
  1657. */
  1658. _openOnContextMenu(event) {
  1659. if (!this.disabled) {
  1660. // Prevent the native context menu from opening because we're opening a custom one.
  1661. event.preventDefault();
  1662. // Stop event propagation to ensure that only the closest enabled context menu opens.
  1663. // Otherwise, any context menus attached to containing elements would *also* open,
  1664. // resulting in multiple stacked context menus being displayed.
  1665. event.stopPropagation();
  1666. this._contextMenuTracker.update(this);
  1667. this._open({ x: event.clientX, y: event.clientY }, true);
  1668. // A context menu can be triggered via a mouse right click or a keyboard shortcut.
  1669. if (event.button === 2) {
  1670. this.childMenu?.focusFirstItem('mouse');
  1671. }
  1672. else if (event.button === 0) {
  1673. this.childMenu?.focusFirstItem('keyboard');
  1674. }
  1675. else {
  1676. this.childMenu?.focusFirstItem('program');
  1677. }
  1678. }
  1679. }
  1680. /**
  1681. * Get the configuration object used to create the overlay.
  1682. * @param coordinates the location to place the opened menu
  1683. */
  1684. _getOverlayConfig(coordinates) {
  1685. return new OverlayConfig({
  1686. positionStrategy: this._getOverlayPositionStrategy(coordinates),
  1687. scrollStrategy: this._overlay.scrollStrategies.reposition(),
  1688. direction: this._directionality || undefined,
  1689. });
  1690. }
  1691. /**
  1692. * Get the position strategy for the overlay which specifies where to place the menu.
  1693. * @param coordinates the location to place the opened menu
  1694. */
  1695. _getOverlayPositionStrategy(coordinates) {
  1696. return this._overlay
  1697. .position()
  1698. .flexibleConnectedTo(coordinates)
  1699. .withLockedPosition()
  1700. .withGrowAfterOpen()
  1701. .withPositions(this.menuPosition ?? CONTEXT_MENU_POSITIONS);
  1702. }
  1703. /** Subscribe to the menu stack close events and close this menu when requested. */
  1704. _setMenuStackCloseListener() {
  1705. this.menuStack.closed.pipe(takeUntil(this.destroyed)).subscribe(({ item }) => {
  1706. if (item === this.childMenu && this.isOpen()) {
  1707. this.closed.next();
  1708. this.overlayRef.detach();
  1709. }
  1710. });
  1711. }
  1712. /**
  1713. * Subscribe to the overlays outside pointer events stream and handle closing out the stack if a
  1714. * click occurs outside the menus.
  1715. * @param ignoreFirstAuxClick Whether to ignore the first auxclick event outside the menu.
  1716. */
  1717. _subscribeToOutsideClicks(ignoreFirstAuxClick) {
  1718. if (this.overlayRef) {
  1719. let outsideClicks = this.overlayRef.outsidePointerEvents();
  1720. // If the menu was triggered by the `contextmenu` event, skip the first `auxclick` event
  1721. // because it fires when the mouse is released on the same click that opened the menu.
  1722. if (ignoreFirstAuxClick) {
  1723. const [auxClicks, nonAuxClicks] = partition(outsideClicks, ({ type }) => type === 'auxclick');
  1724. outsideClicks = merge(nonAuxClicks, auxClicks.pipe(skip(1)));
  1725. }
  1726. outsideClicks.pipe(takeUntil(this.stopOutsideClicksListener)).subscribe(event => {
  1727. if (!this.isElementInsideMenuStack(_getEventTarget(event))) {
  1728. this.menuStack.closeAll();
  1729. }
  1730. });
  1731. }
  1732. }
  1733. /**
  1734. * Open the attached menu at the specified location.
  1735. * @param coordinates where to open the context menu
  1736. * @param ignoreFirstOutsideAuxClick Whether to ignore the first auxclick outside the menu after opening.
  1737. */
  1738. _open(coordinates, ignoreFirstOutsideAuxClick) {
  1739. if (this.disabled) {
  1740. return;
  1741. }
  1742. if (this.isOpen()) {
  1743. // since we're moving this menu we need to close any submenus first otherwise they end up
  1744. // disconnected from this one.
  1745. this.menuStack.closeSubMenuOf(this.childMenu);
  1746. this.overlayRef.getConfig().positionStrategy.setOrigin(coordinates);
  1747. this.overlayRef.updatePosition();
  1748. }
  1749. else {
  1750. this.opened.next();
  1751. if (this.overlayRef) {
  1752. this.overlayRef.getConfig().positionStrategy.setOrigin(coordinates);
  1753. this.overlayRef.updatePosition();
  1754. }
  1755. else {
  1756. this.overlayRef = this._overlay.create(this._getOverlayConfig(coordinates));
  1757. }
  1758. this.overlayRef.attach(this.getMenuContentPortal());
  1759. this._subscribeToOutsideClicks(ignoreFirstOutsideAuxClick);
  1760. }
  1761. }
  1762. static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: CdkContextMenuTrigger, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
  1763. static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.0.0", type: CdkContextMenuTrigger, isStandalone: true, selector: "[cdkContextMenuTriggerFor]", inputs: { menuTemplateRef: ["cdkContextMenuTriggerFor", "menuTemplateRef"], menuPosition: ["cdkContextMenuPosition", "menuPosition"], menuData: ["cdkContextMenuTriggerData", "menuData"], disabled: ["cdkContextMenuDisabled", "disabled"] }, outputs: { opened: "cdkContextMenuOpened", closed: "cdkContextMenuClosed" }, host: { listeners: { "contextmenu": "_openOnContextMenu($event)" }, properties: { "attr.data-cdk-menu-stack-id": "null" } }, providers: [
  1764. { provide: MENU_TRIGGER, useExisting: CdkContextMenuTrigger },
  1765. { provide: MENU_STACK, useClass: MenuStack },
  1766. ], exportAs: ["cdkContextMenuTriggerFor"], usesInheritance: true, ngImport: i0 }); }
  1767. }
  1768. i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: CdkContextMenuTrigger, decorators: [{
  1769. type: Directive,
  1770. args: [{
  1771. selector: '[cdkContextMenuTriggerFor]',
  1772. exportAs: 'cdkContextMenuTriggerFor',
  1773. standalone: true,
  1774. host: {
  1775. '[attr.data-cdk-menu-stack-id]': 'null',
  1776. '(contextmenu)': '_openOnContextMenu($event)',
  1777. },
  1778. inputs: [
  1779. 'menuTemplateRef: cdkContextMenuTriggerFor',
  1780. 'menuPosition: cdkContextMenuPosition',
  1781. 'menuData: cdkContextMenuTriggerData',
  1782. ],
  1783. outputs: ['opened: cdkContextMenuOpened', 'closed: cdkContextMenuClosed'],
  1784. providers: [
  1785. { provide: MENU_TRIGGER, useExisting: CdkContextMenuTrigger },
  1786. { provide: MENU_STACK, useClass: MenuStack },
  1787. ],
  1788. }]
  1789. }], ctorParameters: function () { return []; }, propDecorators: { disabled: [{
  1790. type: Input,
  1791. args: ['cdkContextMenuDisabled']
  1792. }] } });
  1793. const MENU_DIRECTIVES = [
  1794. CdkMenuBar,
  1795. CdkMenu,
  1796. CdkMenuItem,
  1797. CdkMenuItemRadio,
  1798. CdkMenuItemCheckbox,
  1799. CdkMenuTrigger,
  1800. CdkMenuGroup,
  1801. CdkContextMenuTrigger,
  1802. CdkTargetMenuAim,
  1803. ];
  1804. /** Module that declares components and directives for the CDK menu. */
  1805. class CdkMenuModule {
  1806. static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: CdkMenuModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
  1807. static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "16.0.0", ngImport: i0, type: CdkMenuModule, imports: [OverlayModule, CdkMenuBar,
  1808. CdkMenu,
  1809. CdkMenuItem,
  1810. CdkMenuItemRadio,
  1811. CdkMenuItemCheckbox,
  1812. CdkMenuTrigger,
  1813. CdkMenuGroup,
  1814. CdkContextMenuTrigger,
  1815. CdkTargetMenuAim], exports: [CdkMenuBar,
  1816. CdkMenu,
  1817. CdkMenuItem,
  1818. CdkMenuItemRadio,
  1819. CdkMenuItemCheckbox,
  1820. CdkMenuTrigger,
  1821. CdkMenuGroup,
  1822. CdkContextMenuTrigger,
  1823. CdkTargetMenuAim] }); }
  1824. static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: CdkMenuModule, imports: [OverlayModule] }); }
  1825. }
  1826. i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: CdkMenuModule, decorators: [{
  1827. type: NgModule,
  1828. args: [{
  1829. imports: [OverlayModule, ...MENU_DIRECTIVES],
  1830. exports: MENU_DIRECTIVES,
  1831. }]
  1832. }] });
  1833. /**
  1834. * Generated bundle index. Do not edit.
  1835. */
  1836. export { CDK_MENU, CdkContextMenuTrigger, CdkMenu, CdkMenuBar, CdkMenuBase, CdkMenuGroup, CdkMenuItem, CdkMenuItemCheckbox, CdkMenuItemRadio, CdkMenuItemSelectable, CdkMenuModule, CdkMenuTrigger, CdkMenuTriggerBase, CdkTargetMenuAim, ContextMenuTracker, MENU_AIM, MENU_STACK, MENU_TRIGGER, MenuStack, PARENT_OR_NEW_INLINE_MENU_STACK_PROVIDER, PARENT_OR_NEW_MENU_STACK_PROVIDER, PointerFocusTracker, TargetMenuAim };
  1837. //# sourceMappingURL=menu.mjs.map