@api-client/ui 0.4.5 → 0.5.1

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 (31) hide show
  1. package/build/src/elements/highlight/MarkedHighlight.d.ts.map +1 -1
  2. package/build/src/elements/highlight/MarkedHighlight.js +2 -1
  3. package/build/src/elements/highlight/MarkedHighlight.js.map +1 -1
  4. package/build/src/md/button/internals/base.js +1 -1
  5. package/build/src/md/button/internals/base.js.map +1 -1
  6. package/build/src/md/dialog/internals/Dialog.d.ts +18 -0
  7. package/build/src/md/dialog/internals/Dialog.d.ts.map +1 -1
  8. package/build/src/md/dialog/internals/Dialog.js +60 -2
  9. package/build/src/md/dialog/internals/Dialog.js.map +1 -1
  10. package/build/src/md/menu/internal/Menu.d.ts +1 -0
  11. package/build/src/md/menu/internal/Menu.d.ts.map +1 -1
  12. package/build/src/md/menu/internal/Menu.js +41 -1
  13. package/build/src/md/menu/internal/Menu.js.map +1 -1
  14. package/build/src/md/menu/internal/Menu.styles.d.ts.map +1 -1
  15. package/build/src/md/menu/internal/Menu.styles.js +4 -0
  16. package/build/src/md/menu/internal/Menu.styles.js.map +1 -1
  17. package/demo/md/dialog/dialog.ts +135 -1
  18. package/demo/md/menu/index.ts +216 -1
  19. package/package.json +2 -2
  20. package/src/elements/highlight/MarkedHighlight.ts +2 -1
  21. package/src/md/button/internals/base.ts +1 -1
  22. package/src/md/dialog/internals/Dialog.ts +54 -1
  23. package/src/md/menu/internal/Menu.styles.ts +4 -0
  24. package/src/md/menu/internal/Menu.ts +43 -1
  25. package/test/README.md +372 -0
  26. package/test/dom-assertions.test.ts +182 -0
  27. package/test/helpers/TestUtils.ts +243 -0
  28. package/test/helpers/UiMock.ts +83 -13
  29. package/test/md/dialog/UiDialog.test.ts +169 -0
  30. package/test/setup.test.ts +217 -0
  31. package/test/setup.ts +117 -0
@@ -104,10 +104,12 @@ let Menu = (() => {
104
104
  this.open = !this.open;
105
105
  this.ariaExpanded = String(this.open);
106
106
  this.tabIndex = this.open ? 0 : -1;
107
+ const result = super.togglePopover(force);
107
108
  if (this.open) {
109
+ this.positionMenu();
108
110
  this.focus();
109
111
  }
110
- return super.togglePopover(force);
112
+ return result;
111
113
  }
112
114
  /**
113
115
  * Shows the menu
@@ -117,6 +119,7 @@ let Menu = (() => {
117
119
  this.ariaExpanded = 'true';
118
120
  this.showPopover();
119
121
  this.open = true;
122
+ this.positionMenu();
120
123
  this.focus();
121
124
  this.dispatchEvent(new CustomEvent('open', { bubbles: false, composed: true }));
122
125
  }
@@ -131,6 +134,43 @@ let Menu = (() => {
131
134
  this.closeSubMenu();
132
135
  this.dispatchEvent(new CustomEvent('close', { bubbles: false, composed: true }));
133
136
  }
137
+ positionMenu() {
138
+ // when there's more space above the anchor, position the menu above it
139
+ const box = this.getBoundingClientRect();
140
+ // Now, we determine, whether to position the menu above or below the anchor
141
+ // in a way, that if we have enough space below the anchor, we position it below,
142
+ // otherwise we position it above the anchor.
143
+ // our starting point is the anchor being positioned below the anchor
144
+ const menuBottom = box.top + box.height;
145
+ if (menuBottom <= innerHeight) {
146
+ // if the menu fits below the anchor, we leave it as is.
147
+ return;
148
+ }
149
+ // we do not make association from the menu to the anchor, so we make an assumption
150
+ // that the anchor is 40px high, which is the default height of a button.
151
+ // it can be different, but this is a good starting point.
152
+ const anchorHeight = 40;
153
+ const anchorBottom = box.top;
154
+ const anchorTop = anchorBottom - anchorHeight;
155
+ const menuHeight = box.height;
156
+ const spaceBelow = innerHeight - anchorBottom;
157
+ const spaceAbove = anchorTop;
158
+ const diffBelow = spaceBelow - menuHeight;
159
+ const diffAbove = spaceAbove - menuHeight;
160
+ // The initial check ensures the menu does not fit below. Now, check if it fits above.
161
+ if (diffAbove >= 0) {
162
+ this.style.setProperty('position-area', 'top span-right');
163
+ }
164
+ else if (diffAbove > diffBelow) {
165
+ // It doesn't fit in either direction. Choose the one with less overflow (larger, i.e., less negative, diff).
166
+ this.style.setProperty('position-area', 'top span-right');
167
+ this.style.maxHeight = `${spaceAbove}px`;
168
+ }
169
+ else {
170
+ this.style.setProperty('position-area', 'bottom span-right');
171
+ this.style.maxHeight = `${spaceBelow}px`;
172
+ }
173
+ }
134
174
  /**
135
175
  * Handles beforetoggle event from popover
136
176
  */
@@ -1 +1 @@
1
- {"version":3,"file":"Menu.js","sourceRoot":"","sources":["../../../../../src/md/menu/internal/Menu.ts"],"names":[],"mappings":"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","sourcesContent":["import { html, PropertyValues, TemplateResult } from 'lit'\nimport { property, state, queryAssignedElements } from 'lit/decorators.js'\nimport { classMap } from 'lit/directives/class-map.js'\nimport { nanoid } from 'nanoid'\nimport UiList from '../../list/internals/List.js'\nimport UiMenuItem from './MenuItem.js'\nimport UiSubMenu from './SubMenu.js'\nimport { setDisabled } from '../../../lib/disabled.js'\nimport UiListItem from '../../list/internals/ListItem.js'\nimport { bound } from '../../../decorators/bound.js'\n\n/**\n * Material Design 3 Menu component with sub-menu support.\n * Uses Popover API and Anchor Positioning API for modern positioning.\n *\n * @fires select - Dispatched when a menu item is selected\n * @fires close - Dispatched when the menu is closed\n */\nexport default class Menu extends UiList {\n /**\n * Whether the menu is currently open\n * @attribute\n */\n @property({ type: Boolean, reflect: true }) accessor open = false\n\n /**\n * Whether the menu is disabled\n * @attribute\n */\n @property({ type: Boolean, reflect: true }) accessor disabled = false\n\n /**\n * Currently active sub-menu\n */\n @state() accessor activeSubMenu: UiSubMenu | null = null\n\n /**\n * Assigned menu items from light DOM\n */\n @queryAssignedElements({ selector: 'ui-menu-item' }) protected accessor assignedMenuItems!: UiMenuItem[]\n\n constructor() {\n super()\n this.selector = 'ui-menu-item'\n this.ariaExpanded = 'false'\n this.addEventListener('beforetoggle', this.handleBeforeToggle.bind(this))\n }\n\n override connectedCallback(): void {\n super.connectedCallback()\n this.setAttribute('role', 'menu')\n this.setAttribute('tabindex', '-1')\n if (!this.hasAttribute('popover')) {\n this.setAttribute('popover', 'auto')\n }\n if (!this.id) {\n this.id = nanoid()\n }\n }\n\n protected override updated(changedProperties: PropertyValues<this>): void {\n super.updated(changedProperties)\n\n if (changedProperties.has('disabled')) {\n setDisabled(this, this.disabled)\n }\n }\n\n override togglePopover(force?: boolean): boolean {\n this.open = !this.open\n this.ariaExpanded = String(this.open)\n this.tabIndex = this.open ? 0 : -1\n if (this.open) {\n this.focus()\n }\n return super.togglePopover(force)\n }\n\n /**\n * Shows the menu\n */\n show(): void {\n this.tabIndex = 0 // Make menu focusable\n this.ariaExpanded = 'true'\n this.showPopover()\n this.open = true\n this.focus()\n this.dispatchEvent(new CustomEvent('open', { bubbles: false, composed: true }))\n }\n\n /**\n * Hides the menu\n */\n hide(): void {\n this.tabIndex = -1\n this.ariaExpanded = 'false'\n this.hidePopover()\n this.open = false\n this.closeSubMenu()\n this.dispatchEvent(new CustomEvent('close', { bubbles: false, composed: true }))\n }\n\n /**\n * Handles beforetoggle event from popover\n */\n protected handleBeforeToggle(e: Event): void {\n const toggleEvent = e as ToggleEvent\n if (toggleEvent.newState === 'closed') {\n this.open = false\n this.closeSubMenu()\n }\n }\n\n /**\n * Handles keyboard navigation for the menu\n */\n override handleKeydown(e: KeyboardEvent): void {\n if (!this.open || e.defaultPrevented) return\n\n switch (e.key) {\n case 'Escape':\n e.preventDefault()\n this.hide()\n break\n case 'ArrowRight':\n e.preventDefault()\n this.openSubMenu()\n break\n case 'ArrowLeft':\n e.preventDefault()\n this.closeSubMenu()\n break\n default:\n // Let the parent UiList handle other keys\n super.handleKeydown(e)\n }\n }\n\n @bound\n handleSubMenuSelect(e: CustomEvent): void {\n super.notifySelect(e.detail.item, e.detail.index)\n }\n\n /**\n * Opens the sub-menu for the currently active item\n */\n protected openSubMenu(): void {\n const activeItem = this.activeListItem as UiMenuItem\n if (activeItem?.hasSubMenu) {\n activeItem.openSubMenu()\n }\n }\n\n /**\n * Closes the currently open sub-menu\n */\n closeSubMenu(): void {\n if (this.activeSubMenu) {\n this.activeSubMenu.removeEventListener('select', this.handleSubMenuSelect as EventListener)\n this.activeSubMenu.hide()\n this.activeSubMenu = null\n }\n }\n\n /**\n * Sets the active sub-menu\n */\n setActiveSubMenu(subMenu: UiSubMenu | null): void {\n this.activeSubMenu = subMenu\n subMenu?.addEventListener('select', this.handleSubMenuSelect as EventListener)\n }\n\n override notifySelect(item: UiListItem, index?: number): boolean {\n this.hide()\n return super.notifySelect(item, index)\n }\n\n /**\n * Handles sub-menu opening\n */\n protected handleSubMenuOpen(e: CustomEvent): void {\n const subMenu = e.detail.subMenu\n this.setActiveSubMenu(subMenu)\n }\n\n /**\n * Handles slot changes to update menu items\n */\n protected handleSlotChange(): void {\n // Update the items list when slot content changes\n this.updateItems()\n }\n\n override render(): TemplateResult {\n const classes = classMap({\n 'menu-container': true,\n })\n\n return html`\n <div class=${classes}>\n <slot @slotchange=${this.handleSlotChange}></slot>\n </div>\n `\n }\n}\n"]}
1
+ {"version":3,"file":"Menu.js","sourceRoot":"","sources":["../../../../../src/md/menu/internal/Menu.ts"],"names":[],"mappings":"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","sourcesContent":["import { html, PropertyValues, TemplateResult } from 'lit'\nimport { property, state, queryAssignedElements } from 'lit/decorators.js'\nimport { classMap } from 'lit/directives/class-map.js'\nimport { nanoid } from 'nanoid'\nimport UiList from '../../list/internals/List.js'\nimport UiMenuItem from './MenuItem.js'\nimport UiSubMenu from './SubMenu.js'\nimport { setDisabled } from '../../../lib/disabled.js'\nimport UiListItem from '../../list/internals/ListItem.js'\nimport { bound } from '../../../decorators/bound.js'\n\n/**\n * Material Design 3 Menu component with sub-menu support.\n * Uses Popover API and Anchor Positioning API for modern positioning.\n *\n * @fires select - Dispatched when a menu item is selected\n * @fires close - Dispatched when the menu is closed\n */\nexport default class Menu extends UiList {\n /**\n * Whether the menu is currently open\n * @attribute\n */\n @property({ type: Boolean, reflect: true }) accessor open = false\n\n /**\n * Whether the menu is disabled\n * @attribute\n */\n @property({ type: Boolean, reflect: true }) accessor disabled = false\n\n /**\n * Currently active sub-menu\n */\n @state() accessor activeSubMenu: UiSubMenu | null = null\n\n /**\n * Assigned menu items from light DOM\n */\n @queryAssignedElements({ selector: 'ui-menu-item' }) protected accessor assignedMenuItems!: UiMenuItem[]\n\n constructor() {\n super()\n this.selector = 'ui-menu-item'\n this.ariaExpanded = 'false'\n this.addEventListener('beforetoggle', this.handleBeforeToggle.bind(this))\n }\n\n override connectedCallback(): void {\n super.connectedCallback()\n this.setAttribute('role', 'menu')\n this.setAttribute('tabindex', '-1')\n if (!this.hasAttribute('popover')) {\n this.setAttribute('popover', 'auto')\n }\n if (!this.id) {\n this.id = nanoid()\n }\n }\n\n protected override updated(changedProperties: PropertyValues<this>): void {\n super.updated(changedProperties)\n\n if (changedProperties.has('disabled')) {\n setDisabled(this, this.disabled)\n }\n }\n\n override togglePopover(force?: boolean): boolean {\n this.open = !this.open\n this.ariaExpanded = String(this.open)\n this.tabIndex = this.open ? 0 : -1\n const result = super.togglePopover(force)\n if (this.open) {\n this.positionMenu()\n this.focus()\n }\n return result\n }\n\n /**\n * Shows the menu\n */\n show(): void {\n this.tabIndex = 0 // Make menu focusable\n this.ariaExpanded = 'true'\n this.showPopover()\n this.open = true\n this.positionMenu()\n this.focus()\n this.dispatchEvent(new CustomEvent('open', { bubbles: false, composed: true }))\n }\n\n /**\n * Hides the menu\n */\n hide(): void {\n this.tabIndex = -1\n this.ariaExpanded = 'false'\n this.hidePopover()\n this.open = false\n this.closeSubMenu()\n this.dispatchEvent(new CustomEvent('close', { bubbles: false, composed: true }))\n }\n\n positionMenu(): void {\n // when there's more space above the anchor, position the menu above it\n const box = this.getBoundingClientRect()\n // Now, we determine, whether to position the menu above or below the anchor\n // in a way, that if we have enough space below the anchor, we position it below,\n // otherwise we position it above the anchor.\n\n // our starting point is the anchor being positioned below the anchor\n const menuBottom = box.top + box.height\n if (menuBottom <= innerHeight) {\n // if the menu fits below the anchor, we leave it as is.\n return\n }\n // we do not make association from the menu to the anchor, so we make an assumption\n // that the anchor is 40px high, which is the default height of a button.\n // it can be different, but this is a good starting point.\n const anchorHeight = 40\n const anchorBottom = box.top\n const anchorTop = anchorBottom - anchorHeight\n const menuHeight = box.height\n\n const spaceBelow = innerHeight - anchorBottom\n const spaceAbove = anchorTop\n\n const diffBelow = spaceBelow - menuHeight\n const diffAbove = spaceAbove - menuHeight\n // The initial check ensures the menu does not fit below. Now, check if it fits above.\n if (diffAbove >= 0) {\n this.style.setProperty('position-area', 'top span-right')\n } else if (diffAbove > diffBelow) {\n // It doesn't fit in either direction. Choose the one with less overflow (larger, i.e., less negative, diff).\n this.style.setProperty('position-area', 'top span-right')\n this.style.maxHeight = `${spaceAbove}px`\n } else {\n this.style.setProperty('position-area', 'bottom span-right')\n this.style.maxHeight = `${spaceBelow}px`\n }\n }\n\n /**\n * Handles beforetoggle event from popover\n */\n protected handleBeforeToggle(e: Event): void {\n const toggleEvent = e as ToggleEvent\n if (toggleEvent.newState === 'closed') {\n this.open = false\n this.closeSubMenu()\n }\n }\n\n /**\n * Handles keyboard navigation for the menu\n */\n override handleKeydown(e: KeyboardEvent): void {\n if (!this.open || e.defaultPrevented) return\n\n switch (e.key) {\n case 'Escape':\n e.preventDefault()\n this.hide()\n break\n case 'ArrowRight':\n e.preventDefault()\n this.openSubMenu()\n break\n case 'ArrowLeft':\n e.preventDefault()\n this.closeSubMenu()\n break\n default:\n // Let the parent UiList handle other keys\n super.handleKeydown(e)\n }\n }\n\n @bound\n handleSubMenuSelect(e: CustomEvent): void {\n super.notifySelect(e.detail.item, e.detail.index)\n }\n\n /**\n * Opens the sub-menu for the currently active item\n */\n protected openSubMenu(): void {\n const activeItem = this.activeListItem as UiMenuItem\n if (activeItem?.hasSubMenu) {\n activeItem.openSubMenu()\n }\n }\n\n /**\n * Closes the currently open sub-menu\n */\n closeSubMenu(): void {\n if (this.activeSubMenu) {\n this.activeSubMenu.removeEventListener('select', this.handleSubMenuSelect as EventListener)\n this.activeSubMenu.hide()\n this.activeSubMenu = null\n }\n }\n\n /**\n * Sets the active sub-menu\n */\n setActiveSubMenu(subMenu: UiSubMenu | null): void {\n this.activeSubMenu = subMenu\n subMenu?.addEventListener('select', this.handleSubMenuSelect as EventListener)\n }\n\n override notifySelect(item: UiListItem, index?: number): boolean {\n this.hide()\n return super.notifySelect(item, index)\n }\n\n /**\n * Handles sub-menu opening\n */\n protected handleSubMenuOpen(e: CustomEvent): void {\n const subMenu = e.detail.subMenu\n this.setActiveSubMenu(subMenu)\n }\n\n /**\n * Handles slot changes to update menu items\n */\n protected handleSlotChange(): void {\n // Update the items list when slot content changes\n this.updateItems()\n }\n\n override render(): TemplateResult {\n const classes = classMap({\n 'menu-container': true,\n })\n\n return html`\n <div class=${classes}>\n <slot @slotchange=${this.handleSlotChange}></slot>\n </div>\n `\n }\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"Menu.styles.d.ts","sourceRoot":"","sources":["../../../../../src/md/menu/internal/Menu.styles.ts"],"names":[],"mappings":";AAEA,wBAsLC"}
1
+ {"version":3,"file":"Menu.styles.d.ts","sourceRoot":"","sources":["../../../../../src/md/menu/internal/Menu.styles.ts"],"names":[],"mappings":";AAEA,wBA0LC"}
@@ -8,6 +8,10 @@ export default css `
8
8
  margin: 0;
9
9
  padding: 0;
10
10
  border: none;
11
+ overflow: hidden;
12
+ /* in most cases the max-height won't matter as this assumes the whole screen to be available, which is rarely the truth. */
13
+ max-height: 90vh;
14
+ overflow: auto;
11
15
  }
12
16
 
13
17
  :host(:popover-open) {
@@ -1 +1 @@
1
- {"version":3,"file":"Menu.styles.js","sourceRoot":"","sources":["../../../../../src/md/menu/internal/Menu.styles.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,KAAK,CAAA;AAEzB,eAAe,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAsLjB,CAAA","sourcesContent":["import { css } from 'lit'\n\nexport default css`\n :host {\n display: none;\n position-area: bottom span-right;\n position-try: normal flip-block;\n position: absolute;\n margin: 0;\n padding: 0;\n border: none;\n }\n\n :host(:popover-open) {\n display: block;\n background-color: var(--md-sys-color-surface);\n border-radius: var(--md-sys-shape-corner-extra-small);\n box-shadow: var(--md-sys-elevation-3);\n }\n\n .menu-container {\n min-width: 200px;\n padding: 8px 0;\n outline: none;\n }\n\n .menu-divider {\n height: 1px;\n background-color: var(--md-sys-color-outline-variant);\n margin: 8px 0;\n }\n\n /* Menu Item Styles */\n .menu-item {\n position: relative;\n display: flex;\n align-items: center;\n min-height: 48px;\n padding: 0 16px;\n cursor: pointer;\n outline: none;\n transition: background-color 0.2s ease;\n }\n\n .menu-item:hover {\n background-color: var(--md-sys-color-surface-variant);\n }\n\n .menu-item:focus {\n background-color: var(--md-sys-color-surface-variant);\n }\n\n .menu-item[disabled] {\n opacity: 0.38;\n cursor: not-allowed;\n pointer-events: none;\n }\n\n .menu-item-content {\n display: flex;\n align-items: center;\n width: 100%;\n gap: 12px;\n }\n\n .menu-item-icon {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 24px;\n height: 24px;\n color: var(--md-sys-color-on-surface);\n font-size: 20px;\n }\n\n .menu-item-label {\n flex: 1;\n color: var(--md-sys-color-on-surface);\n font-family: var(--md-sys-typescale-label-large-font-family-name);\n font-size: var(--md-sys-typescale-label-large-font-size);\n font-weight: var(--md-sys-typescale-label-large-font-weight);\n line-height: var(--md-sys-typescale-label-large-line-height);\n letter-spacing: var(--md-sys-typescale-label-large-letter-spacing);\n }\n\n .menu-item-arrow {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 24px;\n height: 24px;\n color: var(--md-sys-color-on-surface);\n font-size: 18px;\n font-weight: 500;\n }\n\n .menu-item-with-submenu {\n position: relative;\n }\n\n .menu-item-with-submenu:hover .menu-item-arrow {\n color: var(--md-sys-color-primary);\n }\n\n /* Sub-menu Styles */\n .submenu-container {\n min-width: 200px;\n max-width: 320px;\n background-color: var(--md-sys-color-surface);\n border-radius: var(--md-sys-shape-corner-extra-small);\n box-shadow: var(--md-sys-elevation-level3);\n padding: 8px 0;\n }\n\n /* Submenu positioning with Anchor API */\n ui-sub-menu {\n display: none;\n }\n\n ui-sub-menu:popover-open {\n display: block;\n background-color: var(--md-sys-color-surface);\n border-radius: var(--md-sys-shape-corner-extra-small);\n box-shadow: var(--md-sys-elevation-level3);\n min-width: 200px;\n max-width: 320px;\n padding: 8px 0;\n z-index: 1000;\n }\n\n /* Fallback positioning for browsers without anchor positioning */\n @supports not (anchor-name: --test) {\n ui-sub-menu:popover-open {\n position: fixed;\n transform: translateX(200px);\n }\n }\n\n /* Focus Ring */\n md-focus-ring {\n --md-focus-ring-color: var(--md-sys-color-primary);\n --md-focus-ring-width: 2px;\n }\n\n /* Ripple Effect */\n ui-ripple {\n --md-ripple-color: var(--md-sys-color-primary);\n --md-ripple-opacity: 0.12;\n }\n\n /* Responsive Design */\n @media (max-width: 600px) {\n .menu-container {\n min-width: 180px;\n max-width: 280px;\n }\n\n .submenu-container {\n min-width: 180px;\n max-width: 280px;\n }\n }\n\n /* High Contrast Mode */\n @media (prefers-contrast: high) {\n .menu-container {\n border: 1px solid var(--md-sys-color-outline);\n }\n\n .submenu-container {\n border: 1px solid var(--md-sys-color-outline);\n }\n\n .menu-divider {\n background-color: var(--md-sys-color-outline);\n }\n }\n\n /* Reduced Motion */\n @media (prefers-reduced-motion: reduce) {\n .menu-item {\n transition: none;\n }\n }\n`\n"]}
1
+ {"version":3,"file":"Menu.styles.js","sourceRoot":"","sources":["../../../../../src/md/menu/internal/Menu.styles.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,KAAK,CAAA;AAEzB,eAAe,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA0LjB,CAAA","sourcesContent":["import { css } from 'lit'\n\nexport default css`\n :host {\n display: none;\n position-area: bottom span-right;\n position-try: normal flip-block;\n position: absolute;\n margin: 0;\n padding: 0;\n border: none;\n overflow: hidden;\n /* in most cases the max-height won't matter as this assumes the whole screen to be available, which is rarely the truth. */\n max-height: 90vh;\n overflow: auto;\n }\n\n :host(:popover-open) {\n display: block;\n background-color: var(--md-sys-color-surface);\n border-radius: var(--md-sys-shape-corner-extra-small);\n box-shadow: var(--md-sys-elevation-3);\n }\n\n .menu-container {\n min-width: 200px;\n padding: 8px 0;\n outline: none;\n }\n\n .menu-divider {\n height: 1px;\n background-color: var(--md-sys-color-outline-variant);\n margin: 8px 0;\n }\n\n /* Menu Item Styles */\n .menu-item {\n position: relative;\n display: flex;\n align-items: center;\n min-height: 48px;\n padding: 0 16px;\n cursor: pointer;\n outline: none;\n transition: background-color 0.2s ease;\n }\n\n .menu-item:hover {\n background-color: var(--md-sys-color-surface-variant);\n }\n\n .menu-item:focus {\n background-color: var(--md-sys-color-surface-variant);\n }\n\n .menu-item[disabled] {\n opacity: 0.38;\n cursor: not-allowed;\n pointer-events: none;\n }\n\n .menu-item-content {\n display: flex;\n align-items: center;\n width: 100%;\n gap: 12px;\n }\n\n .menu-item-icon {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 24px;\n height: 24px;\n color: var(--md-sys-color-on-surface);\n font-size: 20px;\n }\n\n .menu-item-label {\n flex: 1;\n color: var(--md-sys-color-on-surface);\n font-family: var(--md-sys-typescale-label-large-font-family-name);\n font-size: var(--md-sys-typescale-label-large-font-size);\n font-weight: var(--md-sys-typescale-label-large-font-weight);\n line-height: var(--md-sys-typescale-label-large-line-height);\n letter-spacing: var(--md-sys-typescale-label-large-letter-spacing);\n }\n\n .menu-item-arrow {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 24px;\n height: 24px;\n color: var(--md-sys-color-on-surface);\n font-size: 18px;\n font-weight: 500;\n }\n\n .menu-item-with-submenu {\n position: relative;\n }\n\n .menu-item-with-submenu:hover .menu-item-arrow {\n color: var(--md-sys-color-primary);\n }\n\n /* Sub-menu Styles */\n .submenu-container {\n min-width: 200px;\n max-width: 320px;\n background-color: var(--md-sys-color-surface);\n border-radius: var(--md-sys-shape-corner-extra-small);\n box-shadow: var(--md-sys-elevation-level3);\n padding: 8px 0;\n }\n\n /* Submenu positioning with Anchor API */\n ui-sub-menu {\n display: none;\n }\n\n ui-sub-menu:popover-open {\n display: block;\n background-color: var(--md-sys-color-surface);\n border-radius: var(--md-sys-shape-corner-extra-small);\n box-shadow: var(--md-sys-elevation-level3);\n min-width: 200px;\n max-width: 320px;\n padding: 8px 0;\n z-index: 1000;\n }\n\n /* Fallback positioning for browsers without anchor positioning */\n @supports not (anchor-name: --test) {\n ui-sub-menu:popover-open {\n position: fixed;\n transform: translateX(200px);\n }\n }\n\n /* Focus Ring */\n md-focus-ring {\n --md-focus-ring-color: var(--md-sys-color-primary);\n --md-focus-ring-width: 2px;\n }\n\n /* Ripple Effect */\n ui-ripple {\n --md-ripple-color: var(--md-sys-color-primary);\n --md-ripple-opacity: 0.12;\n }\n\n /* Responsive Design */\n @media (max-width: 600px) {\n .menu-container {\n min-width: 180px;\n max-width: 280px;\n }\n\n .submenu-container {\n min-width: 180px;\n max-width: 280px;\n }\n }\n\n /* High Contrast Mode */\n @media (prefers-contrast: high) {\n .menu-container {\n border: 1px solid var(--md-sys-color-outline);\n }\n\n .submenu-container {\n border: 1px solid var(--md-sys-color-outline);\n }\n\n .menu-divider {\n background-color: var(--md-sys-color-outline);\n }\n }\n\n /* Reduced Motion */\n @media (prefers-reduced-motion: reduce) {\n .menu-item {\n transition: none;\n }\n }\n`\n"]}
@@ -18,6 +18,8 @@ class ComponentDemoPage extends DemoPage {
18
18
 
19
19
  @reactive() accessor overflowOpened = false
20
20
 
21
+ @reactive() accessor formOpened = false
22
+
21
23
  protected openSimple(): void {
22
24
  this.simpleOpened = true
23
25
  }
@@ -45,6 +47,15 @@ class ComponentDemoPage extends DemoPage {
45
47
  this.reportClosingReason(e.detail)
46
48
  }
47
49
 
50
+ protected openForm(): void {
51
+ this.formOpened = true
52
+ }
53
+
54
+ protected formClosed(e: CustomEvent<UiDialogClosingReason>): void {
55
+ this.formOpened = false
56
+ this.reportClosingReason(e.detail)
57
+ }
58
+
48
59
  imperativeDialog: UiDialog | null = null
49
60
 
50
61
  protected openImperative(): void {
@@ -81,7 +92,8 @@ class ComponentDemoPage extends DemoPage {
81
92
  contentTemplate(): TemplateResult {
82
93
  return html`
83
94
  <a href="../">Back</a>
84
- ${this.simpleDialog()} ${this.fullDialog()} ${this.overflowDialog()} ${this.renderImperativeDialog()}
95
+ ${this.simpleDialog()} ${this.fullDialog()} ${this.overflowDialog()} ${this.formDialog()}
96
+ ${this.renderImperativeDialog()}
85
97
  `
86
98
  }
87
99
 
@@ -244,6 +256,128 @@ class ComponentDemoPage extends DemoPage {
244
256
  `
245
257
  }
246
258
 
259
+ formDialog(): TemplateResult {
260
+ return html`
261
+ <section class="demo-section">
262
+ <h2 class="title-large">Form dialog</h2>
263
+ <p>This dialog contains a form with validation and submit handling.</p>
264
+ <ui-button color="filled" @click="${this.openForm}">Open Form Dialog</ui-button>
265
+ <form @submit="${this.handleFormSubmit}" style="display: contents;">
266
+ <ui-dialog
267
+ ?open="${this.formOpened}"
268
+ @close="${this.formClosed}"
269
+ modal
270
+ style="--ui-dialog-max-width: 500px;"
271
+ submitClose
272
+ >
273
+ <ui-icon slot="icon" icon="info"></ui-icon>
274
+ <span slot="title">User Registration</span>
275
+
276
+ <div style="display: flex; flex-direction: column; gap: 16px;">
277
+ <label style="display: flex; flex-direction: column; gap: 4px;">
278
+ <span style="font-weight: 500; color: var(--md-sys-color-on-surface);">Full Name *</span>
279
+ <input
280
+ type="text"
281
+ name="fullName"
282
+ required
283
+ style="padding: 12px; border: 1px solid var(--md-sys-color-outline); border-radius: 4px; font-size: 14px;"
284
+ placeholder="Enter your full name"
285
+ />
286
+ </label>
287
+
288
+ <label style="display: flex; flex-direction: column; gap: 4px;">
289
+ <span style="font-weight: 500; color: var(--md-sys-color-on-surface);">Email *</span>
290
+ <input
291
+ type="email"
292
+ name="email"
293
+ required
294
+ style="padding: 12px; border: 1px solid var(--md-sys-color-outline); border-radius: 4px; font-size: 14px;"
295
+ placeholder="Enter your email address"
296
+ />
297
+ </label>
298
+
299
+ <label style="display: flex; flex-direction: column; gap: 4px;">
300
+ <span style="font-weight: 500; color: var(--md-sys-color-on-surface);">Phone Number</span>
301
+ <input
302
+ type="tel"
303
+ name="phone"
304
+ style="padding: 12px; border: 1px solid var(--md-sys-color-outline); border-radius: 4px; font-size: 14px;"
305
+ placeholder="Enter your phone number (optional)"
306
+ />
307
+ </label>
308
+
309
+ <label style="display: flex; flex-direction: column; gap: 4px;">
310
+ <span style="font-weight: 500; color: var(--md-sys-color-on-surface);">Department</span>
311
+ <select
312
+ name="department"
313
+ style="padding: 12px; border: 1px solid var(--md-sys-color-outline); border-radius: 4px; font-size: 14px; background: white;"
314
+ >
315
+ <option value="">Select a department</option>
316
+ <option value="engineering">Engineering</option>
317
+ <option value="design">Design</option>
318
+ <option value="marketing">Marketing</option>
319
+ <option value="sales">Sales</option>
320
+ <option value="support">Support</option>
321
+ </select>
322
+ </label>
323
+
324
+ <label style="display: flex; align-items: center; gap: 8px; margin-top: 8px;">
325
+ <input type="checkbox" name="newsletter" style="margin: 0;" />
326
+ <span style="font-size: 14px; color: var(--md-sys-color-on-surface);">
327
+ Subscribe to our newsletter
328
+ </span>
329
+ </label>
330
+
331
+ <label style="display: flex; align-items: center; gap: 8px;">
332
+ <input type="checkbox" name="terms" required style="margin: 0;" />
333
+ <span style="font-size: 14px; color: var(--md-sys-color-on-surface);">
334
+ I agree to the <a href="#" style="color: var(--md-sys-color-primary);">Terms and Conditions</a> *
335
+ </span>
336
+ </label>
337
+ </div>
338
+
339
+ <ui-button color="text" slot="button" value="dismiss">Cancel</ui-button>
340
+ <ui-button color="filled" slot="button" value="confirm" type="submit">Register</ui-button>
341
+ </ui-dialog>
342
+ </form>
343
+ </section>
344
+ `
345
+ }
346
+
347
+ protected handleFormSubmit(e: CustomEvent): void {
348
+ e.preventDefault()
349
+
350
+ // Get form data from the event
351
+ const form = e.target as HTMLFormElement
352
+ const formData = new FormData(form)
353
+
354
+ // Convert to regular object for easier handling
355
+ const data: Record<string, string | boolean> = {}
356
+ for (const [key, value] of formData.entries()) {
357
+ if (key === 'newsletter' || key === 'terms') {
358
+ data[key] = value === 'on'
359
+ } else {
360
+ data[key] = value as string
361
+ }
362
+ }
363
+
364
+ console.log('Form submitted with data:', data)
365
+
366
+ // Simulate form validation
367
+ if (!data.fullName || !data.email) {
368
+ console.error('Required fields are missing')
369
+ return
370
+ }
371
+
372
+ if (!data.terms) {
373
+ console.error('Terms and conditions must be accepted')
374
+ return
375
+ }
376
+
377
+ // If validation passes, the dialog will close automatically
378
+ console.log('Registration successful!', data)
379
+ }
380
+
247
381
  renderImperativeDialog(): TemplateResult {
248
382
  return html`
249
383
  <section class="demo-section">
@@ -15,6 +15,8 @@ class ComponentDemoPage extends DemoPage {
15
15
  @reactive() accessor basicCount = 0
16
16
  @reactive() accessor nestedMenuOutput = ''
17
17
  @reactive() accessor nestedCount = 0
18
+ @reactive() accessor overflowMenuOutput = ''
19
+ @reactive() accessor overflowCount = 0
18
20
 
19
21
  handleBasicMenuSelect(e: CustomEvent): void {
20
22
  const item = e.detail.item as HTMLElement
@@ -30,8 +32,15 @@ class ComponentDemoPage extends DemoPage {
30
32
  this.nestedCount++
31
33
  }
32
34
 
35
+ handleOverflowMenuSelect(e: CustomEvent): void {
36
+ const item = e.detail.item as HTMLElement
37
+ const index = e.detail.index
38
+ this.overflowMenuOutput = `Selected item: ${item.textContent}, Index: ${index}`
39
+ this.overflowCount++
40
+ }
41
+
33
42
  contentTemplate(): TemplateResult {
34
- const { basicMenuOutput, nestedMenuOutput, nestedCount, basicCount } = this
43
+ const { basicMenuOutput, nestedMenuOutput, nestedCount, basicCount, overflowMenuOutput, overflowCount } = this
35
44
  return html`
36
45
  <a href="../">Back</a>
37
46
 
@@ -68,6 +77,212 @@ class ComponentDemoPage extends DemoPage {
68
77
  : ''}
69
78
  </section>
70
79
 
80
+ <section class="demo-section">
81
+ <h2 class="title-large">Overflow Menu</h2>
82
+ <p>A menu with many items that will overflow and require scrolling:</p>
83
+ <ui-button id="overflow-menu-trigger" color="filled" popovertarget="overflow-menu"
84
+ >Open Overflow Menu</ui-button
85
+ >
86
+ <ui-menu id="overflow-menu" popover="auto" @select="${this.handleOverflowMenuSelect}">
87
+ <ui-menu-item>
88
+ <span slot="start"><ui-icon>home</ui-icon></span>
89
+ <span>Home</span>
90
+ </ui-menu-item>
91
+ <ui-menu-item>
92
+ <span slot="start"><ui-icon>account_circle</ui-icon></span>
93
+ <span>Profile</span>
94
+ </ui-menu-item>
95
+ <ui-menu-item>
96
+ <span slot="start"><ui-icon>settings</ui-icon></span>
97
+ <span>Settings</span>
98
+ </ui-menu-item>
99
+ <ui-menu-item>
100
+ <span slot="start"><ui-icon>notifications</ui-icon></span>
101
+ <span>Notifications</span>
102
+ </ui-menu-item>
103
+ <ui-menu-item>
104
+ <span slot="start"><ui-icon>mail</ui-icon></span>
105
+ <span>Messages</span>
106
+ </ui-menu-item>
107
+ <ui-menu-item>
108
+ <span slot="start"><ui-icon>favorite</ui-icon></span>
109
+ <span>Favorites</span>
110
+ </ui-menu-item>
111
+ <ui-menu-item>
112
+ <span slot="start"><ui-icon>bookmark</ui-icon></span>
113
+ <span>Bookmarks</span>
114
+ </ui-menu-item>
115
+ <ui-menu-item>
116
+ <span slot="start"><ui-icon>history</ui-icon></span>
117
+ <span>History</span>
118
+ </ui-menu-item>
119
+ <ui-menu-item>
120
+ <span slot="start"><ui-icon>download</ui-icon></span>
121
+ <span>Downloads</span>
122
+ </ui-menu-item>
123
+ <ui-menu-item>
124
+ <span slot="start"><ui-icon>cloud</ui-icon></span>
125
+ <span>Cloud Storage</span>
126
+ </ui-menu-item>
127
+ <ui-menu-item>
128
+ <span slot="start"><ui-icon>share</ui-icon></span>
129
+ <span>Share</span>
130
+ </ui-menu-item>
131
+ <ui-menu-item>
132
+ <span slot="start"><ui-icon>security</ui-icon></span>
133
+ <span>Security</span>
134
+ </ui-menu-item>
135
+ <ui-menu-item>
136
+ <span slot="start"><ui-icon>backup</ui-icon></span>
137
+ <span>Backup</span>
138
+ </ui-menu-item>
139
+ <ui-menu-item>
140
+ <span slot="start"><ui-icon>sync</ui-icon></span>
141
+ <span>Sync</span>
142
+ </ui-menu-item>
143
+ <ui-menu-item>
144
+ <span slot="start"><ui-icon>account_box</ui-icon></span>
145
+ <span>Account Management</span>
146
+ </ui-menu-item>
147
+ <ui-menu-item>
148
+ <span slot="start"><ui-icon>payment</ui-icon></span>
149
+ <span>Payment Methods</span>
150
+ </ui-menu-item>
151
+ <ui-menu-item>
152
+ <span slot="start"><ui-icon>credit_card</ui-icon></span>
153
+ <span>Billing</span>
154
+ </ui-menu-item>
155
+ <ui-menu-item>
156
+ <span slot="start"><ui-icon>receipt</ui-icon></span>
157
+ <span>Receipts</span>
158
+ </ui-menu-item>
159
+ <ui-menu-item>
160
+ <span slot="start"><ui-icon>analytics</ui-icon></span>
161
+ <span>Analytics</span>
162
+ </ui-menu-item>
163
+ <ui-menu-item>
164
+ <span slot="start"><ui-icon>insights</ui-icon></span>
165
+ <span>Insights</span>
166
+ </ui-menu-item>
167
+ <ui-menu-item>
168
+ <span slot="start"><ui-icon>trending_up</ui-icon></span>
169
+ <span>Trends</span>
170
+ </ui-menu-item>
171
+ <ui-menu-item>
172
+ <span slot="start"><ui-icon>dashboard</ui-icon></span>
173
+ <span>Dashboard</span>
174
+ </ui-menu-item>
175
+ <ui-menu-item>
176
+ <span slot="start"><ui-icon>widgets</ui-icon></span>
177
+ <span>Widgets</span>
178
+ </ui-menu-item>
179
+ <ui-menu-item>
180
+ <span slot="start"><ui-icon>extension</ui-icon></span>
181
+ <span>Extensions</span>
182
+ </ui-menu-item>
183
+ <ui-menu-item>
184
+ <span slot="start"><ui-icon>apps</ui-icon></span>
185
+ <span>Applications</span>
186
+ </ui-menu-item>
187
+ <ui-menu-item>
188
+ <span slot="start"><ui-icon>devices</ui-icon></span>
189
+ <span>Devices</span>
190
+ </ui-menu-item>
191
+ <ui-menu-item>
192
+ <span slot="start"><ui-icon>network_check</ui-icon></span>
193
+ <span>Network Status</span>
194
+ </ui-menu-item>
195
+ <ui-menu-item>
196
+ <span slot="start"><ui-icon>wifi</ui-icon></span>
197
+ <span>Wi-Fi Settings</span>
198
+ </ui-menu-item>
199
+ <ui-menu-item>
200
+ <span slot="start"><ui-icon>bluetooth</ui-icon></span>
201
+ <span>Bluetooth</span>
202
+ </ui-menu-item>
203
+ <ui-menu-item>
204
+ <span slot="start"><ui-icon>location_on</ui-icon></span>
205
+ <span>Location Services</span>
206
+ </ui-menu-item>
207
+ <ui-menu-item>
208
+ <span slot="start"><ui-icon>language</ui-icon></span>
209
+ <span>Language</span>
210
+ </ui-menu-item>
211
+ <ui-menu-item>
212
+ <span slot="start"><ui-icon>accessibility</ui-icon></span>
213
+ <span>Accessibility</span>
214
+ </ui-menu-item>
215
+ <ui-menu-item>
216
+ <span slot="start"><ui-icon>brightness_6</ui-icon></span>
217
+ <span>Display</span>
218
+ </ui-menu-item>
219
+ <ui-menu-item>
220
+ <span slot="start"><ui-icon>volume_up</ui-icon></span>
221
+ <span>Sound</span>
222
+ </ui-menu-item>
223
+ <ui-menu-item>
224
+ <span slot="start"><ui-icon>battery_std</ui-icon></span>
225
+ <span>Battery</span>
226
+ </ui-menu-item>
227
+ <ui-menu-item>
228
+ <span slot="start"><ui-icon>storage</ui-icon></span>
229
+ <span>Storage</span>
230
+ </ui-menu-item>
231
+ <ui-menu-item>
232
+ <span slot="start"><ui-icon>memory</ui-icon></span>
233
+ <span>Memory</span>
234
+ </ui-menu-item>
235
+ <ui-menu-item>
236
+ <span slot="start"><ui-icon>update</ui-icon></span>
237
+ <span>Software Update</span>
238
+ </ui-menu-item>
239
+ <ui-menu-item>
240
+ <span slot="start"><ui-icon>info</ui-icon></span>
241
+ <span>About</span>
242
+ </ui-menu-item>
243
+ <ui-menu-item>
244
+ <span slot="start"><ui-icon>help</ui-icon></span>
245
+ <span>Help & Support</span>
246
+ </ui-menu-item>
247
+ <ui-menu-item>
248
+ <span slot="start"><ui-icon>feedback</ui-icon></span>
249
+ <span>Send Feedback</span>
250
+ </ui-menu-item>
251
+ <ui-menu-item>
252
+ <span slot="start"><ui-icon>bug_report</ui-icon></span>
253
+ <span>Report Bug</span>
254
+ </ui-menu-item>
255
+ <ui-menu-item>
256
+ <span slot="start"><ui-icon>contact_support</ui-icon></span>
257
+ <span>Contact Support</span>
258
+ </ui-menu-item>
259
+ <ui-menu-item>
260
+ <span slot="start"><ui-icon>forum</ui-icon></span>
261
+ <span>Community Forum</span>
262
+ </ui-menu-item>
263
+ <ui-menu-item>
264
+ <span slot="start"><ui-icon>school</ui-icon></span>
265
+ <span>Learning Center</span>
266
+ </ui-menu-item>
267
+ <ui-menu-item>
268
+ <span slot="start"><ui-icon>library_books</ui-icon></span>
269
+ <span>Documentation</span>
270
+ </ui-menu-item>
271
+ <ui-menu-item>
272
+ <span slot="start"><ui-icon>video_library</ui-icon></span>
273
+ <span>Video Tutorials</span>
274
+ </ui-menu-item>
275
+ <ui-menu-item>
276
+ <span slot="start"><ui-icon>logout</ui-icon></span>
277
+ <span>Sign Out</span>
278
+ </ui-menu-item>
279
+ </ui-menu>
280
+ ${overflowMenuOutput
281
+ ? html`<p>${overflowMenuOutput}</p>
282
+ <p>Count: ${overflowCount}</p>`
283
+ : ''}
284
+ </section>
285
+
71
286
  <section class="demo-section">
72
287
  <h2 class="title-large">Submenus</h2>
73
288
  <p>A menu with submenus:</p>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@api-client/ui",
3
- "version": "0.4.5",
3
+ "version": "0.5.1",
4
4
  "description": "Internal UI component library for the API Client ecosystem.",
5
5
  "license": "UNLICENSED",
6
6
  "main": "build/src/index.js",
@@ -182,7 +182,7 @@
182
182
  "dompurify": "^3.2.5",
183
183
  "idb-keyval": "^6.1.0",
184
184
  "lit": "^3.2.1",
185
- "marked": "^15.0.7",
185
+ "marked": "^16.0.0",
186
186
  "monaco-editor": "^0.52.2",
187
187
  "nanoid": "^5.1.5",
188
188
  "prismjs": "^1.28.0",
@@ -247,7 +247,8 @@ export default class MarkedHighlight extends LitElement {
247
247
  pedantic: this.pedantic,
248
248
  gfm: true,
249
249
  }
250
- let out = marked(data, opts) as string
250
+ // eslint-disable-next-line no-misleading-character-class
251
+ let out = marked(data.replace(/^[\u200B\u200C\u200D\u200E\u200F\uFEFF]/, ''), opts) as string
251
252
  if (this.sanitize) {
252
253
  if (this.sanitizer) {
253
254
  out = this.sanitizer(out)
@@ -240,7 +240,7 @@ export default class BaseButton extends UiElement {
240
240
  }
241
241
 
242
242
  override handleClick(e: MouseEvent): void {
243
- super.handleClick(e)
243
+ // Do not call super.handleClick() here, as it would call `endPress` again.
244
244
  if (this.disabled) {
245
245
  e.preventDefault()
246
246
  e.stopPropagation()