@momentum-design/components 0.134.21 → 0.135.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/dist/browser/index.js +582 -477
  2. package/dist/browser/index.js.map +4 -4
  3. package/dist/components/datepicker/datepicker.component.d.ts +5 -0
  4. package/dist/components/datepicker/datepicker.component.js +96 -26
  5. package/dist/components/formfieldwrapper/formfieldwrapper.component.js +7 -3
  6. package/dist/components/formfieldwrapper/formfieldwrapper.constants.js +2 -7
  7. package/dist/components/formfieldwrapper/formfieldwrapper.styles.js +10 -0
  8. package/dist/components/formfieldwrapper/formfieldwrapper.types.d.ts +2 -2
  9. package/dist/components/formfieldwrapper/formfieldwrapper.utils.js +2 -11
  10. package/dist/components/formfieldwrapper/index.d.ts +1 -0
  11. package/dist/components/formfieldwrapper/index.js +1 -0
  12. package/dist/components/menubar/menubar.component.js +2 -1
  13. package/dist/components/menupopover/menupopover.component.d.ts +1 -1
  14. package/dist/components/menupopover/menupopover.component.js +4 -4
  15. package/dist/components/navmenuitem/navmenuitem.component.d.ts +32 -2
  16. package/dist/components/navmenuitem/navmenuitem.component.js +146 -9
  17. package/dist/components/navmenuitem/navmenuitem.constants.d.ts +1 -0
  18. package/dist/components/navmenuitem/navmenuitem.constants.js +1 -0
  19. package/dist/components/navmenuitem/navmenuitem.styles.js +16 -5
  20. package/dist/components/sidenavigation/sidenavigation.component.d.ts +43 -3
  21. package/dist/components/sidenavigation/sidenavigation.component.js +240 -8
  22. package/dist/components/sidenavigation/sidenavigation.constants.d.ts +6 -1
  23. package/dist/components/sidenavigation/sidenavigation.constants.js +6 -1
  24. package/dist/components/sidenavigation/sidenavigation.context.d.ts +6 -1
  25. package/dist/components/sidenavigation/sidenavigation.context.js +36 -11
  26. package/dist/components/sidenavigation/sidenavigation.types.d.ts +3 -2
  27. package/dist/components/sidenavigation/sidenavigationbase.d.ts +8 -0
  28. package/dist/components/sidenavigation/sidenavigationbase.js +7 -0
  29. package/dist/components/statusmessage/index.d.ts +9 -0
  30. package/dist/components/statusmessage/index.js +8 -0
  31. package/dist/components/statusmessage/statusmessage.component.d.ts +54 -0
  32. package/dist/components/statusmessage/statusmessage.component.js +80 -0
  33. package/dist/components/statusmessage/statusmessage.constants.d.ts +28 -0
  34. package/dist/components/statusmessage/statusmessage.constants.js +31 -0
  35. package/dist/components/statusmessage/statusmessage.styles.d.ts +2 -0
  36. package/dist/components/statusmessage/statusmessage.styles.js +66 -0
  37. package/dist/components/statusmessage/statusmessage.types.d.ts +5 -0
  38. package/dist/components/statusmessage/statusmessage.types.js +2 -0
  39. package/dist/custom-elements.json +283 -10
  40. package/dist/index.d.ts +4 -2
  41. package/dist/index.js +2 -1
  42. package/dist/models/provider/provider.component.d.ts +2 -2
  43. package/dist/models/provider/provider.component.js +13 -0
  44. package/dist/react/index.d.ts +1 -0
  45. package/dist/react/index.js +1 -0
  46. package/dist/react/navmenuitem/index.d.ts +4 -2
  47. package/dist/react/navmenuitem/index.js +3 -1
  48. package/dist/react/statusmessage/index.d.ts +37 -0
  49. package/dist/react/statusmessage/index.js +32 -0
  50. package/package.json +1 -1
@@ -175,6 +175,11 @@ declare class DatePicker extends DatePicker_base implements AssociatedFormContro
175
175
  protected willUpdate(changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>): void;
176
176
  private parseValueToInternal;
177
177
  private internalToValue;
178
+ private getParsedMin;
179
+ private getParsedMax;
180
+ private clampValueToRange;
181
+ private getFieldRange;
182
+ private getNextSpinbuttonValue;
178
183
  private syncFormValue;
179
184
  private commitValue;
180
185
  private notifyValueChange;
@@ -179,8 +179,8 @@ class DatePicker extends FormInternalsMixin(DataAriaLabelMixin(FormfieldWrapper)
179
179
  this.syncFormValue();
180
180
  }
181
181
  }
182
- parseValueToInternal() {
183
- const parts = parseISODate(this.value);
182
+ parseValueToInternal(value = this.value) {
183
+ const parts = parseISODate(value);
184
184
  if (parts) {
185
185
  this.internalMonth = parts.month;
186
186
  this.internalDay = parts.day;
@@ -195,6 +195,86 @@ class DatePicker extends FormInternalsMixin(DataAriaLabelMixin(FormfieldWrapper)
195
195
  internalToValue() {
196
196
  return buildISODate(this.internalMonth, this.internalDay, this.internalYear);
197
197
  }
198
+ // AI-Assisted: keep input spinbuttons aligned with valid min/max date boundaries.
199
+ getParsedMin() {
200
+ var _a;
201
+ return this.min ? (_a = parseISO(this.min)) !== null && _a !== void 0 ? _a : undefined : undefined;
202
+ }
203
+ getParsedMax() {
204
+ var _a;
205
+ return this.max ? (_a = parseISO(this.max)) !== null && _a !== void 0 ? _a : undefined : undefined;
206
+ }
207
+ clampValueToRange(value) {
208
+ var _a, _b;
209
+ const dt = parseISO(value);
210
+ if (!dt)
211
+ return value;
212
+ const minDt = this.getParsedMin();
213
+ const maxDt = this.getParsedMax();
214
+ if (minDt && isBefore(dt, minDt))
215
+ return (_a = this.min) !== null && _a !== void 0 ? _a : value;
216
+ if (maxDt && isAfter(dt, maxDt))
217
+ return (_b = this.max) !== null && _b !== void 0 ? _b : value;
218
+ return value;
219
+ }
220
+ getFieldRange(field) {
221
+ const currentMonth = parseInt(this.internalMonth, 10) || undefined;
222
+ const currentYear = parseInt(this.internalYear, 10) || undefined;
223
+ const baseMin = getFieldMin(field);
224
+ const baseMax = getFieldMax(field, currentMonth, currentYear);
225
+ let min = baseMin;
226
+ let max = baseMax;
227
+ const minDt = this.getParsedMin();
228
+ const maxDt = this.getParsedMax();
229
+ if (field === 'year') {
230
+ if (minDt)
231
+ min = Math.max(min, minDt.getFullYear());
232
+ if (maxDt)
233
+ max = Math.min(max, maxDt.getFullYear());
234
+ }
235
+ else if (field === 'month' && currentYear) {
236
+ if (minDt && currentYear === minDt.getFullYear())
237
+ min = Math.max(min, minDt.getMonth() + 1);
238
+ if (maxDt && currentYear === maxDt.getFullYear())
239
+ max = Math.min(max, maxDt.getMonth() + 1);
240
+ }
241
+ else if (field === 'day' && currentMonth && currentYear) {
242
+ if (minDt &&
243
+ currentYear === minDt.getFullYear() &&
244
+ currentMonth === minDt.getMonth() + 1) {
245
+ min = Math.max(min, minDt.getDate());
246
+ }
247
+ if (maxDt &&
248
+ currentYear === maxDt.getFullYear() &&
249
+ currentMonth === maxDt.getMonth() + 1) {
250
+ max = Math.min(max, maxDt.getDate());
251
+ }
252
+ }
253
+ if (min > max) {
254
+ return { min: baseMin, max: baseMax, isMinConstrained: false, isMaxConstrained: false };
255
+ }
256
+ return {
257
+ min,
258
+ max,
259
+ isMinConstrained: min !== baseMin,
260
+ isMaxConstrained: max !== baseMax,
261
+ };
262
+ }
263
+ getNextSpinbuttonValue(currentValue, range, increment) {
264
+ if (increment) {
265
+ if (currentValue < range.min)
266
+ return range.min;
267
+ if (currentValue >= range.max)
268
+ return range.isMaxConstrained ? range.max : range.min;
269
+ return currentValue + 1;
270
+ }
271
+ if (currentValue > range.max)
272
+ return range.max;
273
+ if (currentValue <= range.min)
274
+ return range.isMinConstrained ? range.min : range.max;
275
+ return currentValue - 1;
276
+ }
277
+ // End AI-Assisted
198
278
  syncFormValue() {
199
279
  const val = this.value || this.internalToValue();
200
280
  if (this.internals) {
@@ -211,17 +291,13 @@ class DatePicker extends FormInternalsMixin(DataAriaLabelMixin(FormfieldWrapper)
211
291
  }
212
292
  commitValue() {
213
293
  const newVal = this.internalToValue();
214
- if (newVal && newVal !== this.value) {
215
- const dt = parseISO(newVal);
216
- if (dt) {
217
- const minDt = this.min ? parseISO(this.min) : undefined;
218
- const maxDt = this.max ? parseISO(this.max) : undefined;
219
- if (minDt && isBefore(dt, minDt))
220
- return;
221
- if (maxDt && isAfter(dt, maxDt))
222
- return;
223
- }
224
- this.value = newVal;
294
+ if (newVal) {
295
+ const clampedVal = this.clampValueToRange(newVal);
296
+ if (clampedVal !== newVal)
297
+ this.parseValueToInternal(clampedVal);
298
+ if (clampedVal === this.value)
299
+ return;
300
+ this.value = clampedVal;
225
301
  this.syncFormValue();
226
302
  this.notifyValueChange();
227
303
  }
@@ -364,22 +440,19 @@ class DatePicker extends FormInternalsMixin(DataAriaLabelMixin(FormfieldWrapper)
364
440
  handleSpinbuttonKeydown(event, field) {
365
441
  if (this.readonly)
366
442
  return;
367
- const currentMonth = parseInt(this.internalMonth, 10) || undefined;
368
- const currentYear = parseInt(this.internalYear, 10) || undefined;
369
- const minVal = getFieldMin(field);
370
- const maxVal = getFieldMax(field, currentMonth, currentYear);
443
+ const range = this.getFieldRange(field);
371
444
  const currentVal = parseInt(this.getFieldValue(field), 10) || 0;
372
445
  switch (event.key) {
373
446
  case KEYS.ARROW_UP: {
374
447
  event.preventDefault();
375
- const newVal = currentVal >= maxVal ? minVal : currentVal + 1;
448
+ const newVal = this.getNextSpinbuttonValue(currentVal, range, true);
376
449
  this.setFieldValue(field, newVal);
377
450
  this.commitValue();
378
451
  break;
379
452
  }
380
453
  case KEYS.ARROW_DOWN: {
381
454
  event.preventDefault();
382
- const newVal = currentVal <= minVal ? maxVal : currentVal - 1;
455
+ const newVal = this.getNextSpinbuttonValue(currentVal, range, false);
383
456
  this.setFieldValue(field, newVal);
384
457
  this.commitValue();
385
458
  break;
@@ -399,7 +472,7 @@ class DatePicker extends FormInternalsMixin(DataAriaLabelMixin(FormfieldWrapper)
399
472
  default: {
400
473
  if (/^\d$/.test(event.key)) {
401
474
  event.preventDefault();
402
- this.handleDigitInput(event.key, field, minVal, maxVal);
475
+ this.handleDigitInput(event.key, field, range.min, range.max);
403
476
  }
404
477
  else {
405
478
  event.preventDefault();
@@ -549,10 +622,7 @@ class DatePicker extends FormInternalsMixin(DataAriaLabelMixin(FormfieldWrapper)
549
622
  // -- Render methods --
550
623
  renderSpinbutton(field) {
551
624
  const value = this.getFieldValue(field);
552
- const currentMonth = parseInt(this.internalMonth, 10) || undefined;
553
- const currentYear = parseInt(this.internalYear, 10) || undefined;
554
- const minVal = getFieldMin(field);
555
- const maxVal = getFieldMax(field, currentMonth, currentYear);
625
+ const range = this.getFieldRange(field);
556
626
  const labelMap = {
557
627
  month: this.localeMonthLabel,
558
628
  day: this.localeDayLabel,
@@ -569,8 +639,8 @@ class DatePicker extends FormInternalsMixin(DataAriaLabelMixin(FormfieldWrapper)
569
639
  part="spinbutton spinbutton-${field}"
570
640
  role="spinbutton"
571
641
  aria-label="${labelMap[field]}"
572
- aria-valuemin="${minVal}"
573
- aria-valuemax="${maxVal}"
642
+ aria-valuemin="${range.min}"
643
+ aria-valuemax="${range.max}"
574
644
  aria-valuenow="${ifDefined(value ? parseInt(value, 10) : undefined)}"
575
645
  aria-description="${this.localeSpinbuttonDescription}"
576
646
  .value="${value}"
@@ -193,10 +193,14 @@ class FormfieldWrapper extends DisabledMixin(Component) {
193
193
  if (!this.helpText) {
194
194
  return nothing;
195
195
  }
196
- return html `<div part="help-text-container">
197
- <slot name="help-icon">${this.renderHelpTextIcon()}</slot>
196
+ return html `<mdc-statusmessage
197
+ part="help-text-container"
198
+ severity="${DEFAULTS.VALIDATION}"
199
+ exportparts="container: help-text-container, icon: helper-icon, text: help-text"
200
+ >
201
+ <slot name="help-icon" slot="icon">${this.renderHelpTextIcon()}</slot>
198
202
  <slot name="help-text">${this.renderHelpText()}</slot>
199
- </div>`;
203
+ </mdc-statusmessage>`;
200
204
  }
201
205
  }
202
206
  FormfieldWrapper.styles = [...Component.styles, ...styles];
@@ -1,14 +1,9 @@
1
1
  import utils from '../../utils/tag-name';
2
2
  import { POPOVER_PLACEMENT, STRATEGY } from '../popover/popover.constants';
3
3
  import { TYPE, VALID_TEXT_TAGS } from '../text/text.constants';
4
+ import { STATUSMESSAGE_SEVERITY } from '../statusmessage/statusmessage.constants';
4
5
  const TAG_NAME = utils.constructTagName('formfieldwrapper');
5
- const VALIDATION = {
6
- DEFAULT: 'default',
7
- ERROR: 'error',
8
- PRIORITY: 'priority',
9
- SUCCESS: 'success',
10
- WARNING: 'warning',
11
- };
6
+ const VALIDATION = STATUSMESSAGE_SEVERITY;
12
7
  const DEFAULTS = {
13
8
  VALIDATION: VALIDATION.DEFAULT,
14
9
  HELPER_TEXT_ID: 'helper-text-id',
@@ -59,6 +59,16 @@ const styles = css `
59
59
  color: var(--mdc-help-text-color);
60
60
  }
61
61
 
62
+ mdc-statusmessage[part='help-text-container'] {
63
+ --mdc-statusmessage-color: var(--mdc-help-text-color);
64
+ --mdc-statusmessage-font-size: var(--mdc-help-text-font-size);
65
+ --mdc-statusmessage-font-weight: var(--mdc-help-text-font-weight);
66
+ --mdc-statusmessage-line-height: var(--mdc-help-text-line-height);
67
+ --mdc-statusmessage-gap: 0.5rem;
68
+
69
+ width: 100%;
70
+ }
71
+
62
72
  :host::part(info-icon-btn) {
63
73
  align-self: flex-start;
64
74
  }
@@ -1,6 +1,6 @@
1
1
  import type { ValueOf } from '../../utils/types';
2
- import type { IconNames } from '../icon/icon.types';
2
+ import type { StatusMessageIcon } from '../statusmessage/statusmessage.types';
3
3
  import { VALIDATION } from './formfieldwrapper.constants';
4
4
  type ValidationType = ValueOf<typeof VALIDATION>;
5
- type HelperIconsList = Extract<IconNames, 'error-legacy-badge-filled' | 'warning-badge-filled' | 'check-circle-badge-filled' | 'priority-badge-filled'> | '';
5
+ type HelperIconsList = StatusMessageIcon;
6
6
  export type { HelperIconsList, ValidationType };
@@ -1,12 +1,3 @@
1
- import { VALIDATION } from './formfieldwrapper.constants';
2
- const getHelperIcon = (type) => {
3
- const helperIconSizeMap = {
4
- [VALIDATION.ERROR]: 'error-legacy-badge-filled',
5
- [VALIDATION.WARNING]: 'warning-badge-filled',
6
- [VALIDATION.SUCCESS]: 'check-circle-badge-filled',
7
- [VALIDATION.PRIORITY]: 'priority-badge-filled',
8
- [VALIDATION.DEFAULT]: '',
9
- };
10
- return helperIconSizeMap[type] || '';
11
- };
1
+ import { STATUSMESSAGE_ICON_NAME_BY_SEVERITY } from '../statusmessage/statusmessage.constants';
2
+ const getHelperIcon = (type) => STATUSMESSAGE_ICON_NAME_BY_SEVERITY[type] || '';
12
3
  export { getHelperIcon };
@@ -1,5 +1,6 @@
1
1
  import '../button';
2
2
  import '../icon';
3
+ import '../statusmessage';
3
4
  import '../text';
4
5
  import '../toggletip';
5
6
  import FormfieldWrapper from './formfieldwrapper.component';
@@ -1,5 +1,6 @@
1
1
  import '../button';
2
2
  import '../icon';
3
+ import '../statusmessage';
3
4
  import '../text';
4
5
  import '../toggletip';
5
6
  import FormfieldWrapper from './formfieldwrapper.component';
@@ -235,7 +235,8 @@ class MenuBar extends KeyDownHandledMixin(KeyToActionMixin(Component)) {
235
235
  return !!element.closest(MENUPOPOVER_TAGNAME) && element.role === ROLE.MENUITEM;
236
236
  }
237
237
  async closeAllMenuPopovers() {
238
- const popovers = this.depthManager.popUntil(item => this.contains(item));
238
+ const allPopovers = this.getAllPopovers();
239
+ const popovers = this.depthManager.popUntil(item => allPopovers.includes(item));
239
240
  await Promise.all(popovers.map(popover => popover.updateComplete));
240
241
  }
241
242
  async crossMenubarNavigationOnLeft(element) {
@@ -102,7 +102,7 @@ declare class MenuPopover extends Popover {
102
102
  onOutsidePopoverClick: (event: MouseEvent) => void;
103
103
  /**
104
104
  * Toggles the visibility of the popover.
105
- * This method checks if the trigger element has the `soft-disabled` attribute.
105
+ * This method checks if the trigger element has the `disabled` or `soft-disabled` attribute.
106
106
  * If it does, the popover will not be toggled.
107
107
  * If the popover is currently visible, it hides the popover; otherwise, it shows the popover.
108
108
  * @returns - This method does not return anything.
@@ -78,14 +78,14 @@ class MenuPopover extends Popover {
78
78
  };
79
79
  /**
80
80
  * Toggles the visibility of the popover.
81
- * This method checks if the trigger element has the `soft-disabled` attribute.
81
+ * This method checks if the trigger element has the `disabled` or `soft-disabled` attribute.
82
82
  * If it does, the popover will not be toggled.
83
83
  * If the popover is currently visible, it hides the popover; otherwise, it shows the popover.
84
84
  * @returns - This method does not return anything.
85
85
  */
86
86
  this.togglePopoverVisible = (event) => {
87
- var _a;
88
- if ((_a = this.triggerElement) === null || _a === void 0 ? void 0 : _a.hasAttribute('soft-disabled'))
87
+ var _a, _b;
88
+ if (((_a = this.triggerElement) === null || _a === void 0 ? void 0 : _a.hasAttribute('disabled')) || ((_b = this.triggerElement) === null || _b === void 0 ? void 0 : _b.hasAttribute('soft-disabled')))
89
89
  return;
90
90
  // Handle mouse click in the parent menupopover to hide open sibling submenus
91
91
  if (event.composedPath().find(el => el.tagName === this.tagName) === this) {
@@ -179,7 +179,7 @@ class MenuPopover extends Popover {
179
179
  case ACTIONS.RIGHT: {
180
180
  // If there is a submenu, open it.
181
181
  const subMenu = this.getSubMenuPopoverOfTarget(target);
182
- if (subMenu) {
182
+ if (subMenu && !target.hasAttribute('soft-disabled')) {
183
183
  subMenu.show();
184
184
  isKeyHandled = true;
185
185
  }
@@ -5,7 +5,7 @@ import type { PopoverPlacement } from '../popover/popover.types';
5
5
  import type { TooltipType } from '../tooltip/tooltip.types';
6
6
  import type { BadgeType } from './navmenuitem.types';
7
7
  /**
8
- * `mdc-navmenuitem` is a menu item styled to work as a navigation tab inside `mdc-sidenavigation`. It renders a leading icon, an optional badge, and a dynamic label that appears when the surrounding side navigation is expanded. The item can be in an active or inactive state to indicate the current page, and a tooltip can be attached for collapsed states or for parent items whose nested child is active.
8
+ * `mdc-navmenuitem` is a `menuitem`-role component styled as a navigation tab, supporting a leading icon, optional badge, and dynamic text rendering. It can operate as a simple item, as a parent with a flyout submenu (`mdc-menupopover`), or as a parent with an inline dropdown submenu, and a tooltip can be attached for collapsed states or when a nested child is active.
9
9
  *
10
10
  * **When to use**
11
11
  *
@@ -37,6 +37,7 @@ import type { BadgeType } from './navmenuitem.types';
37
37
  * @cssproperty --mdc-navmenuitem-in-sidenav-collapsed-margin-left - Left margin of the navmenuitem, when collapsed.
38
38
  * @cssproperty --mdc-navmenuitem-in-sidenav-collapsed-margin-right - Right margin of the navmenuitem, when collapsed.
39
39
  * @cssproperty --mdc-navmenuitem-color - Text color of the navmenuitem in its normal state.
40
+ * @cssproperty --mdc-navmenuitem-notch-color - Notch color of the navmenuitem (if applicable) when active.
40
41
  * @cssproperty --mdc-navmenuitem-disabled-color - Text color of the navmenuitem when disabled.
41
42
  * @cssproperty --mdc-navmenuitem-rest-active-background-color - Background color of the active nav item in its rest state.
42
43
  * @cssproperty --mdc-navmenuitem-hover-background-color - Background color of the navmenuitem when hovered.
@@ -50,7 +51,8 @@ import type { BadgeType } from './navmenuitem.types';
50
51
  * @csspart badge - The badge of the navmenuitem.
51
52
  * @csspart icon-container - The container of the icon.
52
53
  * @csspart text-container - The container of the text.
53
- * @csspart trailing-arrow - The trailing arrow of the navmenuitem.
54
+ * @csspart trailing-arrow - The trailing arrow of the navmenuitem when it has a flyout menu.
55
+ * @csspart trailing-arrow-dropdown - The trailing arrow of the navmenuitem when it has a dropdown menu.
54
56
  */
55
57
  declare class NavMenuItem extends MenuItem {
56
58
  /**
@@ -167,6 +169,11 @@ declare class NavMenuItem extends MenuItem {
167
169
  * @internal
168
170
  */
169
171
  hasActiveChild: boolean;
172
+ /**
173
+ * Tracks whether the dropdown submenu controlled by this navmenuitem is currently open.
174
+ * @internal
175
+ */
176
+ private dropdownOpen;
170
177
  /**
171
178
  * @internal
172
179
  */
@@ -184,6 +191,11 @@ declare class NavMenuItem extends MenuItem {
184
191
  * This method assumes nesting implies deeper levels of nav hierarchy.
185
192
  */
186
193
  private isNested;
194
+ /**
195
+ * Check whether the navmenuitem is inside a dropdown container (div[data-trigger]).
196
+ * @internal
197
+ */
198
+ private isInsideDropdownContainer;
187
199
  /**
188
200
  * Dispatch the activechange event.
189
201
  * @internal
@@ -191,6 +203,24 @@ declare class NavMenuItem extends MenuItem {
191
203
  */
192
204
  private emitNavMenuItemActiveChange;
193
205
  private handleClickEvent;
206
+ /**
207
+ * Toggles the visibility of the dropdown container associated with this navmenuitem.
208
+ * @internal
209
+ */
210
+ private toggleDropdown;
211
+ /**
212
+ * Opens the dropdown if it is closed.
213
+ */
214
+ openDropdown(): void;
215
+ /**
216
+ * Closes the dropdown if it is open.
217
+ */
218
+ closeDropdown(): void;
219
+ /**
220
+ * Returns the sibling div[data-trigger] element associated with this navmenuitem.
221
+ * @internal
222
+ */
223
+ private getDropdownContainer;
194
224
  private getFilledIconName;
195
225
  private renderBadge;
196
226
  render(): TemplateResult<1>;
@@ -13,12 +13,12 @@ import { ifDefined } from 'lit/directives/if-defined.js';
13
13
  import { v4 } from 'uuid';
14
14
  import { TYPE, VALID_TEXT_TAGS } from '../text/text.constants';
15
15
  import { TAG_NAME as MENUPOPOVER_TAGNAME } from '../menupopover/menupopover.constants';
16
+ import { TAG_NAME as TOOLTIP_TAG_NAME } from '../tooltip/tooltip.constants';
16
17
  import MenuItem from '../menuitem/menuitem.component';
17
18
  import providerUtils from '../../utils/provider';
18
19
  import SideNavigation from '../sidenavigation/sidenavigation.component';
19
- import { TAG_NAME as TOOLTIP_TAG_NAME } from '../tooltip/tooltip.constants';
20
20
  import { getIconNameWithoutStyle } from '../button/button.utils';
21
- import { ALLOWED_BADGE_TYPES, DEFAULTS, ICON_NAME } from './navmenuitem.constants';
21
+ import { ALLOWED_BADGE_TYPES, DEFAULTS, ICON_NAME, TAG_NAME as NAVMENUITEM_TAGNAME } from './navmenuitem.constants';
22
22
  import styles from './navmenuitem.styles';
23
23
  /**
24
24
  * @tagname mdc-navmenuitem
@@ -41,6 +41,7 @@ import styles from './navmenuitem.styles';
41
41
  * @cssproperty --mdc-navmenuitem-in-sidenav-collapsed-margin-left - Left margin of the navmenuitem, when collapsed.
42
42
  * @cssproperty --mdc-navmenuitem-in-sidenav-collapsed-margin-right - Right margin of the navmenuitem, when collapsed.
43
43
  * @cssproperty --mdc-navmenuitem-color - Text color of the navmenuitem in its normal state.
44
+ * @cssproperty --mdc-navmenuitem-notch-color - Notch color of the navmenuitem (if applicable) when active.
44
45
  * @cssproperty --mdc-navmenuitem-disabled-color - Text color of the navmenuitem when disabled.
45
46
  * @cssproperty --mdc-navmenuitem-rest-active-background-color - Background color of the active nav item in its rest state.
46
47
  * @cssproperty --mdc-navmenuitem-hover-background-color - Background color of the navmenuitem when hovered.
@@ -54,7 +55,8 @@ import styles from './navmenuitem.styles';
54
55
  * @csspart badge - The badge of the navmenuitem.
55
56
  * @csspart icon-container - The container of the icon.
56
57
  * @csspart text-container - The container of the text.
57
- * @csspart trailing-arrow - The trailing arrow of the navmenuitem.
58
+ * @csspart trailing-arrow - The trailing arrow of the navmenuitem when it has a flyout menu.
59
+ * @csspart trailing-arrow-dropdown - The trailing arrow of the navmenuitem when it has a dropdown menu.
58
60
  */
59
61
  class NavMenuItem extends MenuItem {
60
62
  constructor() {
@@ -96,6 +98,11 @@ class NavMenuItem extends MenuItem {
96
98
  * @internal
97
99
  */
98
100
  this.hasActiveChild = false;
101
+ /**
102
+ * Tracks whether the dropdown submenu controlled by this navmenuitem is currently open.
103
+ * @internal
104
+ */
105
+ this.dropdownOpen = false;
99
106
  /**
100
107
  * @internal
101
108
  */
@@ -122,6 +129,8 @@ class NavMenuItem extends MenuItem {
122
129
  }
123
130
  // Set in-menupopover attribute if nested
124
131
  this.toggleAttribute('in-menupopover', this.isNested());
132
+ // Set in-dropdown-container attribute if inside a dropdown div
133
+ this.toggleAttribute('in-dropdown-container', this.isInsideDropdownContainer());
125
134
  }
126
135
  disconnectedCallback() {
127
136
  super.disconnectedCallback();
@@ -139,7 +148,8 @@ class NavMenuItem extends MenuItem {
139
148
  super.updated(changedProperties);
140
149
  if (changedProperties.has('tooltipText') ||
141
150
  changedProperties.has('showLabel') ||
142
- changedProperties.has('hasActiveChild')) {
151
+ changedProperties.has('hasActiveChild') ||
152
+ changedProperties.has('dropdownOpen')) {
143
153
  this.renderDynamicTooltip();
144
154
  }
145
155
  if (changedProperties.has('showLabel')) {
@@ -152,11 +162,36 @@ class NavMenuItem extends MenuItem {
152
162
  if (!context)
153
163
  return;
154
164
  // Determine expansion state
155
- this.showLabel = this.isNested() ? true : context.expanded;
165
+ this.showLabel = this.isNested() || this.isInsideDropdownContainer() ? true : context.expanded;
166
+ // When sidenavigation collapses, close any open dropdown
167
+ if (!context.expanded && this.dropdownOpen) {
168
+ this.closeDropdown();
169
+ }
170
+ // When dropdown submenu type is turned off, close any open dropdown
171
+ if (!context.isDropdownSubmenuType && this.dropdownOpen) {
172
+ this.closeDropdown();
173
+ }
174
+ // In dropdown mode, manage parent active styling based on expanded state:
175
+ // Expanded + dropdown open: only the child should appear active, not the parent.
176
+ // Expanded + dropdown closed: parent should appear active when it has an active child.
177
+ // Collapsed (icon-only): parent should appear active when it has an active child.
178
+ if (context.isDropdownSubmenuType && this.hasActiveChild && context.isDropDownParent(this)) {
179
+ if (context.expanded && this.dropdownOpen) {
180
+ this.removeAttribute('active');
181
+ this.active = false;
182
+ }
183
+ else {
184
+ this.setAttribute('active', '');
185
+ this.active = true;
186
+ }
187
+ }
156
188
  }
157
189
  renderDynamicTooltip() {
158
190
  var _a;
159
191
  this.removeTooltip();
192
+ if (this.disabled || this.dropdownOpen) {
193
+ return;
194
+ }
160
195
  if (this.hasActiveChild && !this.isActiveParentTooltipText) {
161
196
  return;
162
197
  }
@@ -206,11 +241,90 @@ class NavMenuItem extends MenuItem {
206
241
  }
207
242
  return false;
208
243
  }
244
+ /**
245
+ * Check whether the navmenuitem is inside a dropdown container (div[data-trigger]).
246
+ * @internal
247
+ */
248
+ isInsideDropdownContainer() {
249
+ let parent = this.parentElement;
250
+ while (parent) {
251
+ if (parent.matches('div[data-trigger]')) {
252
+ return true;
253
+ }
254
+ parent = parent.parentElement;
255
+ }
256
+ return false;
257
+ }
209
258
  handleClickEvent() {
210
- if (this.disabled || this.cannotActivate)
259
+ var _a;
260
+ if (this.disabled || this.cannotActivate || this.softDisabled)
211
261
  return;
262
+ const context = (_a = this.sideNavigationContext) === null || _a === void 0 ? void 0 : _a.value;
263
+ if ((context === null || context === void 0 ? void 0 : context.isDropdownSubmenuType) && (context === null || context === void 0 ? void 0 : context.expanded) && (context === null || context === void 0 ? void 0 : context.isDropDownParent(this))) {
264
+ this.toggleDropdown();
265
+ // move the focus to the first navmenuitem in the dropdown when opening the dropdown
266
+ if (this.dropdownOpen) {
267
+ const dropdownContainer = this.getDropdownContainer();
268
+ const firstNavMenuItem = dropdownContainer === null || dropdownContainer === void 0 ? void 0 : dropdownContainer.querySelector(NAVMENUITEM_TAGNAME);
269
+ firstNavMenuItem === null || firstNavMenuItem === void 0 ? void 0 : firstNavMenuItem.focus();
270
+ }
271
+ return;
272
+ }
212
273
  this.emitNavMenuItemActiveChange(this.active);
213
274
  }
275
+ /**
276
+ * Toggles the visibility of the dropdown container associated with this navmenuitem.
277
+ * @internal
278
+ */
279
+ toggleDropdown() {
280
+ const dropdownContainer = this.getDropdownContainer();
281
+ if (!dropdownContainer)
282
+ return;
283
+ this.dropdownOpen = !this.dropdownOpen;
284
+ dropdownContainer.style.display = this.dropdownOpen ? 'flex' : 'none';
285
+ if (this.dropdownOpen) {
286
+ this.setAttribute('aria-expanded', 'true');
287
+ }
288
+ else {
289
+ this.removeAttribute('aria-expanded');
290
+ }
291
+ }
292
+ /**
293
+ * Opens the dropdown if it is closed.
294
+ */
295
+ openDropdown() {
296
+ if (this.dropdownOpen)
297
+ return;
298
+ const dropdownContainer = this.getDropdownContainer();
299
+ if (dropdownContainer) {
300
+ dropdownContainer.style.display = 'flex';
301
+ }
302
+ this.dropdownOpen = true;
303
+ this.setAttribute('aria-expanded', 'true');
304
+ }
305
+ /**
306
+ * Closes the dropdown if it is open.
307
+ */
308
+ closeDropdown() {
309
+ if (!this.dropdownOpen)
310
+ return;
311
+ const dropdownContainer = this.getDropdownContainer();
312
+ if (dropdownContainer) {
313
+ dropdownContainer.style.display = 'none';
314
+ }
315
+ this.dropdownOpen = false;
316
+ this.removeAttribute('aria-expanded');
317
+ }
318
+ /**
319
+ * Returns the sibling div[data-trigger] element associated with this navmenuitem.
320
+ * @internal
321
+ */
322
+ getDropdownContainer() {
323
+ var _a;
324
+ if (!this.id)
325
+ return null;
326
+ return (_a = this.parentElement) === null || _a === void 0 ? void 0 : _a.querySelector(`div[data-trigger="${this.id}"]`);
327
+ }
214
328
  getFilledIconName() {
215
329
  if (!this.iconName) {
216
330
  return undefined;
@@ -241,10 +355,20 @@ class NavMenuItem extends MenuItem {
241
355
  render() {
242
356
  var _a;
243
357
  const context = (_a = this.sideNavigationContext) === null || _a === void 0 ? void 0 : _a.value;
358
+ const isDropdownMode = (context === null || context === void 0 ? void 0 : context.isDropdownSubmenuType) && (context === null || context === void 0 ? void 0 : context.expanded);
359
+ const isDropDownParent = context === null || context === void 0 ? void 0 : context.isDropDownParent(this);
360
+ const hasFlyoutSibling = context === null || context === void 0 ? void 0 : context.hasSiblingWithTriggerId(this);
244
361
  return html `
245
362
  <div part="icon-container">
246
- <mdc-icon name="${this.iconName}" size="1.5" length-unit="rem" part="regular-icon"></mdc-icon>
247
- ${!this.cannotActivate
363
+ ${this.iconName
364
+ ? html `<mdc-icon
365
+ name="${this.iconName}"
366
+ size="1.5"
367
+ length-unit="rem"
368
+ part="regular-icon"
369
+ ></mdc-icon>`
370
+ : nothing}
371
+ ${!this.cannotActivate && this.iconName
248
372
  ? html `<mdc-icon
249
373
  name="${this.getFilledIconName()}"
250
374
  size="1.5"
@@ -266,7 +390,16 @@ class NavMenuItem extends MenuItem {
266
390
  ${this.renderBadge(this.showLabel)}
267
391
  `
268
392
  : nothing}
269
- ${(context === null || context === void 0 ? void 0 : context.hasSiblingWithTriggerId(this))
393
+ ${isDropdownMode && isDropDownParent
394
+ ? html ` <mdc-icon
395
+ name=${ICON_NAME.DOWN_ARROW}
396
+ length-unit="rem"
397
+ part="trailing-arrow-dropdown"
398
+ class="${this.dropdownOpen ? 'arrow-rotated' : ''}"
399
+ >
400
+ </mdc-icon>`
401
+ : nothing}
402
+ ${hasFlyoutSibling
270
403
  ? html ` <mdc-icon name=${ICON_NAME.RIGHT_ARROW} length-unit="rem" part="trailing-arrow"> </mdc-icon>`
271
404
  : nothing}
272
405
  `;
@@ -341,4 +474,8 @@ __decorate([
341
474
  state(),
342
475
  __metadata("design:type", Boolean)
343
476
  ], NavMenuItem.prototype, "hasActiveChild", void 0);
477
+ __decorate([
478
+ state(),
479
+ __metadata("design:type", Boolean)
480
+ ], NavMenuItem.prototype, "dropdownOpen", void 0);
344
481
  export default NavMenuItem;
@@ -6,6 +6,7 @@ declare const ALLOWED_BADGE_TYPES: {
6
6
  };
7
7
  declare const ICON_NAME: {
8
8
  readonly RIGHT_ARROW: Extract<IconNames, "arrow-right-bold">;
9
+ readonly DOWN_ARROW: Extract<IconNames, "arrow-down-bold">;
9
10
  };
10
11
  declare const DEFAULTS: {
11
12
  readonly MAX_COUNTER: 99;