• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mseemann / angular2-mdl / 278e5d97-cfb8-4d4d-ac72-b2489d434250

pending completion
278e5d97-cfb8-4d4d-ac72-b2489d434250

push

circleci

Michael Seemann
update packages and readme for angular version 15

569 of 689 branches covered (82.58%)

1283 of 1301 relevant lines covered (98.62%)

13.21 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

92.9
/projects/core/src/lib/menu/mdl-menu.component.ts
1
import {
2
  AfterViewInit,
3
  Component,
4
  ElementRef,
5
  Injectable,
6
  Input,
7
  OnDestroy,
8
  OnInit,
9
  Renderer2,
10
  ViewChild,
11
  ViewEncapsulation,
12
} from "@angular/core";
13
import { MdlError } from "../common/mdl-error";
14
import { MdlButtonComponent } from "../button/mdl-button.component";
15

16
const BOTTOM_LEFT = "bottom-left";
1✔
17
const BOTTOM_RIGHT = "bottom-right";
1✔
18
const TOP_LEFT = "top-left";
1✔
19
const TOP_RIGHT = "top-right";
1✔
20
const UNALIGNED = "unaligned";
1✔
21

22
// Total duration of the menu animation.
23
const TRANSITION_DURATION_SECONDS = 0.3;
1✔
24
// The fraction of the total duration we want to use for menu item animations.
25
const TRANSITION_DURATION_FRACTION = 0.8;
1✔
26
// How long the menu stays open after choosing an option (so the user can see
27
// the ripple).
28
const CLOSE_TIMEOUT = 175;
1✔
29

30
const CSS_ALIGN_MAP: { [key: string]: string } = {};
1✔
31
CSS_ALIGN_MAP[BOTTOM_LEFT] = "mdl-menu--bottom-left";
1✔
32
CSS_ALIGN_MAP[BOTTOM_RIGHT] = "mdl-menu--bottom-right";
1✔
33
CSS_ALIGN_MAP[TOP_LEFT] = "mdl-menu--top-left";
1✔
34
CSS_ALIGN_MAP[TOP_RIGHT] = "mdl-menu--top-right";
1✔
35
CSS_ALIGN_MAP[UNALIGNED] = "mdl-menu--unaligned";
1✔
36

37
export class MdlMenuError extends MdlError {}
38

39
@Injectable({
40
  providedIn: "root",
41
})
42
export class MdlMenuRegisty {
1✔
43
  menuComponents: MdlMenuComponent[] = [];
17✔
44

45
  add(menuComponent: MdlMenuComponent): void {
46
    this.menuComponents.push(menuComponent);
18✔
47
  }
48

49
  remove(menuComponent: MdlMenuComponent): void {
50
    const fromIndex = this.menuComponents.indexOf(menuComponent);
18✔
51
    this.menuComponents.splice(fromIndex, 1);
18✔
52
  }
53

54
  hideAllExcept(menuComponent: MdlMenuComponent): void {
55
    this.menuComponents.forEach((component) => {
8✔
56
      if (component !== menuComponent) {
10✔
57
        component.hide();
2✔
58
      }
59
    });
60
  }
61
}
62

63
@Component({
64
  selector: "mdl-menu",
65
  exportAs: "mdlMenu",
66
  template: `
67
    <div #container class="mdl-menu__container is-upgraded">
68
      <div #outline class="mdl-menu__outline" [ngClass]="cssPosition"></div>
69
      <div class="mdl-menu" #menuElement>
70
        <ng-content></ng-content>
71
      </div>
72
    </div>
73
  `,
74
  encapsulation: ViewEncapsulation.None,
75
})
76
export class MdlMenuComponent implements OnInit, AfterViewInit, OnDestroy {
1✔
77
  // eslint-disable-next-line
78
  @Input("mdl-menu-position")
79
  position: string | undefined;
80

81
  @ViewChild("container", { static: true })
82
  containerChild: ElementRef | undefined;
83
  @ViewChild("menuElement", { static: true })
84
  menuElementChild: ElementRef | undefined;
85
  @ViewChild("outline", { static: true })
86
  outlineChild: ElementRef | undefined;
87

88
  public cssPosition = "mdl-menu--bottom-left";
17✔
89
  private container: HTMLElement | undefined;
90
  private menuElement: HTMLElement | undefined;
91
  private outline: HTMLElement | undefined;
92
  private isVisible = false;
17✔
93

94
  constructor(
95
    private renderer: Renderer2,
17!
96
    private menuRegistry: MdlMenuRegisty
17!
97
  ) {
98
    this.menuRegistry.add(this);
17✔
99
  }
100

101
  ngOnInit(): void {
102
    this.cssPosition = CSS_ALIGN_MAP[this.position ?? BOTTOM_LEFT];
17✔
103
  }
104

105
  ngAfterViewInit(): void {
106
    this.container = this.containerChild?.nativeElement;
17✔
107
    this.menuElement = this.menuElementChild?.nativeElement;
17✔
108
    this.outline = this.outlineChild?.nativeElement;
17✔
109

110
    // Add a click listener to the document, to close the menu.
111
    const callback = () => {
17✔
112
      if (this.isVisible) {
289✔
113
        this.hide();
7✔
114
      }
115
      return true;
289✔
116
    };
117
    this.renderer.listen("window", "click", callback);
17✔
118
    this.renderer.listen("window", "touchstart", callback);
17✔
119
  }
120

121
  toggle(event: Event, mdlButton: MdlButtonComponent): void {
122
    if (!mdlButton) {
11✔
123
      throw new MdlMenuError(`MdlButtonComponent is required`);
1✔
124
    }
125
    if (this.isVisible) {
10✔
126
      this.hide();
2✔
127
    } else {
128
      this.show(event, mdlButton);
8✔
129
    }
130
  }
131

132
  hideOnItemClicked(): void {
133
    // Wait some time before closing menu, so the user can see the ripple.
134
    setTimeout(() => {
2✔
135
      this.hide();
×
136
    }, CLOSE_TIMEOUT);
137
  }
138

139
  hide(): void {
140
    // Remove all transition delays; menu items fade out concurrently.
141
    document.querySelectorAll("mdl-menu-item").forEach((el) => {
10✔
142
      (el as HTMLElement).style.removeProperty("transition-delay");
9✔
143
    });
144
    // this.menuItemComponents.toArray().forEach(mi => {
145
    //   mi.element.style.removeProperty('transition-delay');
146
    // });
147

148
    // Measure the inner element.
149
    const rect = this.menuElement?.getBoundingClientRect();
10✔
150
    const height = rect?.height;
10✔
151
    const width = rect?.width;
10✔
152

153
    // Turn on animation, and apply the final clip. Also make invisible.
154
    // This triggers the transitions.
155
    this.renderer.addClass(this.menuElement, "is-animating");
10✔
156
    this.applyClip(height, width);
10✔
157
    this.renderer.removeClass(this.container, "is-visible");
10✔
158

159
    // Clean up after the animation is complete.
160
    this.addAnimationEndListener();
10✔
161

162
    this.isVisible = false;
10✔
163
  }
164

165
  show(event: Event, mdlButton: MdlButtonComponent): void {
166
    this.menuRegistry.hideAllExcept(this);
8✔
167

168
    event.stopPropagation();
8✔
169

170
    const forElement = mdlButton.element;
8✔
171
    const rect = forElement.getBoundingClientRect();
8✔
172
    const forRect = forElement?.parentElement?.getBoundingClientRect();
8✔
173

174
    if (!this.container || !forRect) {
8!
175
      return;
×
176
    }
177
    if (this.position === UNALIGNED) {
8✔
178
      // Do not position the menu automatically. Requires the developer to
179
      // manually specify position.
180
    } else if (this.position === BOTTOM_RIGHT) {
7✔
181
      // Position below the "for" element, aligned to its right.
182
      this.container.style.right = forRect.right - rect.right + "px";
1✔
183
      this.container.style.top =
1✔
184
        forElement.offsetTop + forElement.offsetHeight + "px";
185
    } else if (this.position === TOP_LEFT) {
6✔
186
      // Position above the "for" element, aligned to its left.
187
      this.container.style.left = forElement.offsetLeft + "px";
1✔
188
      this.container.style.bottom = forRect.bottom - rect.top + "px";
1✔
189
    } else if (this.position === TOP_RIGHT) {
5✔
190
      // Position above the "for" element, aligned to its right.
191
      this.container.style.right = forRect.right - rect.right + "px";
1✔
192
      this.container.style.bottom = forRect.bottom - rect.top + "px";
1✔
193
    } else {
194
      // Default: position below the "for" element, aligned to its left.
195
      this.container.style.left = forElement.offsetLeft + "px";
4✔
196
      this.container.style.top =
4✔
197
        forElement.offsetTop + forElement.offsetHeight + "px";
198
    }
199

200
    // Measure the inner element.
201
    const height = this.menuElement?.getBoundingClientRect().height ?? 0;
8!
202
    const width = this.menuElement?.getBoundingClientRect().width ?? 0;
8!
203

204
    this.container.style.width = width + "px";
8✔
205
    this.container.style.height = height + "px";
8✔
206
    if (this.outline) {
8✔
207
      this.outline.style.width = width + "px";
8✔
208
      this.outline.style.height = height + "px";
8✔
209
    }
210

211
    const transitionDuration =
212
      TRANSITION_DURATION_SECONDS * TRANSITION_DURATION_FRACTION;
8✔
213
    document.querySelectorAll("mdl-menu-item").forEach((el) => {
8✔
214
      const mi = el as HTMLElement;
10✔
215
      let itemDelay;
216
      if (this.position === TOP_LEFT || this.position === TOP_RIGHT) {
10✔
217
        itemDelay =
2✔
218
          ((height - mi.offsetTop - mi.offsetHeight) / height) *
219
            transitionDuration +
220
          "s";
221
      } else {
222
        itemDelay = (mi.offsetTop / height) * transitionDuration + "s";
8✔
223
      }
224
      mi.style.transitionDelay = itemDelay;
10✔
225
    });
226

227
    // Apply the initial clip to the text before we start animating.
228
    this.applyClip(height, width);
8✔
229

230
    this.renderer.addClass(this.container, "is-visible");
8✔
231
    if (this.menuElement) {
8✔
232
      this.menuElement.style.clip =
8✔
233
        "rect(0 " + width + "px " + height + "px 0)";
234
      this.renderer.addClass(this.menuElement, "is-animating");
8✔
235
    }
236

237
    this.addAnimationEndListener();
8✔
238

239
    this.isVisible = true;
8✔
240
  }
241

242
  ngOnDestroy(): void {
243
    this.menuRegistry.remove(this);
17✔
244
  }
245

246
  private addAnimationEndListener() {
247
    this.renderer.listen(this.menuElement, "transitionend", () => {
18✔
248
      this.renderer.removeClass(this.menuElement, "is-animating");
×
249
      return true;
×
250
    });
251
  }
252

253
  private applyClip(height: number | undefined, width: number | undefined) {
254
    if (!this.menuElement) {
18!
255
      return;
×
256
    }
257
    if (this.position === UNALIGNED) {
18✔
258
      // Do not clip.
259
      this.menuElement.style.clip = "";
2✔
260
    } else if (this.position === BOTTOM_RIGHT) {
16✔
261
      // Clip to the top right corner of the menu.
262
      this.menuElement.style.clip =
2✔
263
        "rect(0 " + width + "px " + "0 " + width + "px)";
264
    } else if (this.position === TOP_LEFT) {
14✔
265
      // Clip to the bottom left corner of the menu.
266
      this.menuElement.style.clip =
2✔
267
        "rect(" + height + "px 0 " + height + "px 0)";
268
    } else if (this.position === TOP_RIGHT) {
12✔
269
      // Clip to the bottom right corner of the menu.
270
      this.menuElement.style.clip =
2✔
271
        "rect(" +
272
        height +
273
        "px " +
274
        width +
275
        "px " +
276
        height +
277
        "px " +
278
        width +
279
        "px)";
280
    } else {
281
      // Default: do not clip (same as clipping to the top left corner).
282
      this.menuElement.style.clip = "";
10✔
283
    }
284
  }
285
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc