diff --git a/src/api/forms/components/FormProperties.vue b/src/api/forms/components/FormProperties.vue index 1e4bdf30bb0..efe219a803f 100644 --- a/src/api/forms/components/FormProperties.vue +++ b/src/api/forms/components/FormProperties.vue @@ -129,6 +129,7 @@ export default { } }, mounted() { + document.addEventListener('keydown', this.handleKeyDown); this.formSections = this.model.sections.map((section) => { section.id = uuid(); @@ -141,6 +142,9 @@ export default { return section; }); }, + unmounted() { + document.removeEventListener('keydown', this.handleKeyDown); + }, methods: { onChange(data) { this.invalidProperties[data.model.key] = data.invalid; @@ -152,6 +156,13 @@ export default { }, onSave() { this.$emit('on-save'); + }, + handleKeyDown({ key }) { + if (key === 'Enter' && !this.isInvalid) { + this.onSave(); + } else if (key === 'Escape') { + this.onCancel(); + } } } }; diff --git a/src/api/menu/components/SuperMenu.vue b/src/api/menu/components/SuperMenu.vue index cd8d72f7be3..754cc2f4605 100644 --- a/src/api/menu/components/SuperMenu.vue +++ b/src/api/menu/components/SuperMenu.vue @@ -26,53 +26,66 @@ :class="[options.menuClass, 'c-super-menu']" :style="styleObject" > - - + +
@@ -89,10 +102,12 @@ import popupMenuMixin from '../mixins/popupMenuMixin.js'; export default { mixins: [popupMenuMixin], - inject: ['options'], + inject: ['options', 'dismiss'], data() { return { - hoveredItem: null + hoveredItem: null, + filteredActions: [], + searchTerm: '' }; }, computed: { @@ -114,6 +129,15 @@ export default { return this.hoveredItem?.description ?? ''; } }, + mounted() { + this.filteredActions = this.options.actions; + + if (this.options.filterable) { + this.$nextTick(() => { + this.$refs.filterInput.focus(); + }); + } + }, methods: { toggleItemDescription(action = null) { const hoveredItem = { @@ -123,6 +147,42 @@ export default { }; this.hoveredItem = hoveredItem; + }, + filterItems() { + const term = this.searchTerm.toLowerCase(); + + if (!term) { + this.filteredActions = this.options.actions; + + return; + } + + if (Array.isArray(this.options.actions[0])) { + // Handle grouped actions + this.filteredActions = this.options.actions + .map((group) => group.filter((action) => action.name.toLowerCase().includes(term))) + .filter((group) => group.length > 0); + } else { + // Handle flat actions list + this.filteredActions = this.options.actions.filter((action) => + action.name.toLowerCase().includes(term) + ); + } + }, + handleKeyDown({ key }) { + if (key === 'Enter') { + // if there is only one action, select it immediately on enter + const flattenedActions = Array.isArray(this.filteredActions[0]) + ? this.filteredActions.flat() + : this.filteredActions; + + if (flattenedActions.length === 1) { + flattenedActions[0].onItemClicked(); + this.dismiss(); + } + } else if (key === 'Escape') { + this.dismiss(); + } } } }; diff --git a/src/api/menu/menu.js b/src/api/menu/menu.js index da7de0d3367..54f37d4089a 100644 --- a/src/api/menu/menu.js +++ b/src/api/menu/menu.js @@ -114,7 +114,8 @@ class Menu extends EventEmitter { return h(SuperMenuComponent); }, provide: { - options: this.options + options: this.options, + dismiss: this.dismiss } }); diff --git a/src/api/overlays/components/OverlayComponent.vue b/src/api/overlays/components/OverlayComponent.vue index 72941fd022c..ae0d9a78013 100644 --- a/src/api/overlays/components/OverlayComponent.vue +++ b/src/api/overlays/components/OverlayComponent.vue @@ -64,6 +64,7 @@ export default { }; }, mounted() { + document.addEventListener('keydown', this.handleKeyDown); const element = this.$refs.element; element.appendChild(this.element); const elementForFocus = this.getElementForFocus() || element; @@ -73,6 +74,7 @@ export default { }, methods: { destroy() { + document.removeEventListener('keydown', this.handleKeyDown); if (this.dismissible) { this.dismiss(); } @@ -100,6 +102,20 @@ export default { } return focusButton[0]; + }, + handleKeyDown({ key }) { + if (key === 'Enter') { + if (this.focusIndex >= 0 && this.focusIndex < this.buttons.length) { + this.buttonClickHandler(this.buttons[this.focusIndex].callback); + } else { + const okButton = this.buttons?.find((button) => button.label.toLowerCase() === 'ok'); + if (okButton) { + this.buttonClickHandler(okButton.callback); + } + } + } else if (key === 'Escape') { + this.destroy(); + } } } }; diff --git a/src/plugins/timeConductor/TimePopupFixed.vue b/src/plugins/timeConductor/TimePopupFixed.vue index 652df767e38..cfbaa23440f 100644 --- a/src/plugins/timeConductor/TimePopupFixed.vue +++ b/src/plugins/timeConductor/TimePopupFixed.vue @@ -182,13 +182,22 @@ export default { this.handleNewBounds = _.throttle(this.handleNewBounds, 300); }, mounted() { + document.addEventListener('keydown', this.handleKeyDown); this.setTimeSystem(JSON.parse(JSON.stringify(this.openmct.time.getTimeSystem()))); this.setViewFromBounds(this.bounds); }, beforeUnmount() { this.clearAllValidation(); + document.removeEventListener('keydown', this.handleKeyDown); }, methods: { + handleKeyDown({ key }) { + if (key === 'Enter' && !this.hasInputValidityError) { + this.handleFormSubmission(true); + } else if (key === 'Escape') { + this.dismiss(); + } + }, handleNewBounds(bounds) { this.setBounds(bounds); this.setViewFromBounds(bounds); @@ -322,8 +331,11 @@ export default { }, hide($event) { if ($event.target.className.indexOf('c-button icon-x') > -1) { - this.$emit('dismiss'); + this.dismiss(); } + }, + dismiss() { + this.$emit('dismiss'); } } }; diff --git a/src/plugins/timeConductor/TimePopupRealtime.vue b/src/plugins/timeConductor/TimePopupRealtime.vue index 326f21bd72e..71fa5d782cd 100644 --- a/src/plugins/timeConductor/TimePopupRealtime.vue +++ b/src/plugins/timeConductor/TimePopupRealtime.vue @@ -172,11 +172,20 @@ export default { mounted() { this.setOffsets(); document.addEventListener('click', this.hide); + document.addEventListener('keydown', this.handleKeyDown); }, beforeUnmount() { document.removeEventListener('click', this.hide); + document.removeEventListener('keydown', this.handleKeyDown); }, methods: { + handleKeyDown({ key }) { + if (key === 'Enter' && !this.isDisabled) { + this.submit(); + } else if (key === 'Escape') { + this.dismiss(); + } + }, format(ref) { const curVal = this[ref]; this[ref] = curVal.toString().padStart(2, '0'); @@ -218,13 +227,16 @@ export default { seconds: this.endInputSecs } }); - this.$emit('dismiss'); + this.dismiss(); }, hide($event) { if ($event.target.className.indexOf('c-button icon-x') > -1) { - this.$emit('dismiss'); + this.dismiss(); } }, + dismiss() { + this.$emit('dismiss'); + }, increment($ev, ref) { $ev.preventDefault(); const step = ref === 'startInputHrs' || ref === 'endInputHrs' ? 1 : 5; diff --git a/src/styles/_constants-darkmatter.scss b/src/styles/_constants-darkmatter.scss index c86a6530e04..90fa8305b15 100644 --- a/src/styles/_constants-darkmatter.scss +++ b/src/styles/_constants-darkmatter.scss @@ -348,7 +348,7 @@ $colorMenuElementHilite: pullForward($colorMenuBg, 10%); $shdwMenu: rgba(black, 0.8) 0 2px 10px; $shdwMenuInner: inset 0 0 0 1px rgba(white, 0.2); $shdwMenuText: none; -$menuItemPad: $interiorMargin, floor($interiorMargin * 1.25); +$menuItemPad: 4px, 6px; // Palettes and Swatches $paletteItemBorderOuterColorSelected: black; diff --git a/src/styles/_constants-espresso.scss b/src/styles/_constants-espresso.scss index 36702317a57..9c6983414f2 100644 --- a/src/styles/_constants-espresso.scss +++ b/src/styles/_constants-espresso.scss @@ -317,7 +317,7 @@ $colorMenuElementHilite: pullForward($colorMenuBg, 10%); $shdwMenu: rgba(black, 0.8) 0 2px 10px; $shdwMenuInner: inset 0 0 0 1px rgba(white, 0.2); $shdwMenuText: none; -$menuItemPad: $interiorMargin, floor($interiorMargin * 1.25); +$menuItemPad: 4px, 6px; // Palettes and Swatches $paletteItemBorderOuterColorSelected: black; diff --git a/src/styles/_constants-maelstrom.scss b/src/styles/_constants-maelstrom.scss index b0f2999174c..8e0b8bc314d 100644 --- a/src/styles/_constants-maelstrom.scss +++ b/src/styles/_constants-maelstrom.scss @@ -333,7 +333,7 @@ $colorMenuElementHilite: pullForward($colorMenuBg, 10%); $shdwMenu: rgba(black, 0.8) 0 2px 10px; $shdwMenuInner: inset 0 0 0 1px rgba(white, 0.2); $shdwMenuText: none; -$menuItemPad: $interiorMargin, floor($interiorMargin * 1.25); +$menuItemPad: 4px, 6px; // Palettes and Swatches $paletteItemBorderOuterColorSelected: black; diff --git a/src/styles/_constants-snow.scss b/src/styles/_constants-snow.scss index 4b4efdb52bf..783e0e38e3f 100644 --- a/src/styles/_constants-snow.scss +++ b/src/styles/_constants-snow.scss @@ -316,7 +316,7 @@ $colorMenuElementHilite: darken($colorMenuBg, 10%); $shdwMenu: rgba(black, 0.8) 0 2px 10px; $shdwMenuInner: none; $shdwMenuText: none; -$menuItemPad: $interiorMargin, floor($interiorMargin * 1.25); +$menuItemPad: 4px, 6px; // Palettes and Swatches $paletteItemBorderOuterColorSelected: black; diff --git a/src/styles/_controls.scss b/src/styles/_controls.scss index a25859d2c4d..7d7cf98bca8 100644 --- a/src/styles/_controls.scss +++ b/src/styles/_controls.scss @@ -58,9 +58,11 @@ @mixin menuInner() { li { @include cControl(); + align-items: baseline; justify-content: start; cursor: pointer; display: flex; + line-height: 1.2em; padding: nth($menuItemPad, 1) nth($menuItemPad, 2); white-space: nowrap; @@ -651,22 +653,21 @@ select { padding: $interiorMarginLg; flex-direction: row; - > [class*='__'] { - //flex: 1 1 50%; - //&:first-child { - // margin-right: $m; - //} + &__left-col { + display: flex; + flex-direction: column; + flex: 1 1 50%; + gap: $interiorMargin; + margin-right: $m; + } - &:last-child { - //border-left: 1px solid $colorInteriorBorder; - //padding-left: $m; - } + &__filter { + flex: 0 0 auto; } &__menu { @include menuInner(); - flex: 1 1 50%; - margin-right: $m; + flex: 1 1 auto; overflow: auto; ul { @@ -675,6 +676,7 @@ select { li { border-radius: $controlCr; + white-space: normal; // Let long names wrap } } diff --git a/src/ui/layout/CreateButton.vue b/src/ui/layout/CreateButton.vue index 5c22ce0574f..b7f227a6a66 100644 --- a/src/ui/layout/CreateButton.vue +++ b/src/ui/layout/CreateButton.vue @@ -92,7 +92,8 @@ export default { const y = elementBoundingClientRect.y + elementBoundingClientRect.height; const menuOptions = { - menuClass: 'c-create-menu' + menuClass: 'c-create-menu', + filterable: true }; this.openmct.menus.showSuperMenu(x, y, this.sortedItems, menuOptions);