Skip to content

Commit f7c997d

Browse files
committed
feat: spotlight-style floating Quick Open for design mode
Quick Open's standard ModalBar mounts above #editor-holder, which is collapsed in design mode — invoking the picker there would show no UI. Add a ModalBar-compatible floating variant: when WorkspaceManager.isInDesignMode() is true, showDialog creates a centered overlay with a compact search bar instead. The picker stays non-modal: no backdrop dim or blur so the live preview stays fully visible and file switching never feels jarring. - _createFloatingQuickOpenBar implements the bits of ModalBar that QuickOpen uses (on("close", ...), close(), prepareClose, getRoot) and returns a resolved jQuery Deferred from close() so callers chain .done() safely. - Dropdown (appended to <body> by QuickSearchField) is anchored to the floating bar via the $positionEl option and bumped above the overlay's stacking so its rendering isn't dimmed. - verticalAdjust is computed as (bar.bottom - input.bottom) so the dropdown sits flush with the bar instead of floating detached. - Escape handling runs in capture phase on both the overlay and the document so one keypress dismisses both dropdown and picker at once instead of requiring two. - setSearchFieldValue guards against a torn-down searchField so re-entering the command after a stale close doesn't throw. - QuickOpen's own "exit design mode first" branch is removed — the floating picker replaces that workaround entirely.
1 parent 245427c commit f7c997d

2 files changed

Lines changed: 209 additions & 10 deletions

File tree

src/search/QuickOpen.js

Lines changed: 119 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,88 @@ define(function (require, exports, module) {
5252
var _providerRegistrationHandler = new ProviderRegistrationHandler(),
5353
_registerQuickOpenProvider = _providerRegistrationHandler.registerProvider.bind(_providerRegistrationHandler);
5454

55+
/**
56+
* Build a ModalBar-compatible stub that renders a Spotlight-style floating
57+
* picker on top of the viewport (used while in design mode, since the
58+
* standard ModalBar mounts into #editor-holder which is collapsed).
59+
* Implements the small subset of the ModalBar API that QuickOpen relies on:
60+
* `on("close", fn)`, `close()`, `prepareClose()`, and `getRoot()`.
61+
* @private
62+
*/
63+
function _createFloatingQuickOpenBar(templateHTML) {
64+
const $overlay = $("<div class='quick-open-floating-overlay'/>");
65+
const $bar = $("<div class='quick-open-floating-bar'/>").html(templateHTML);
66+
$overlay.append($bar).appendTo("body");
67+
const closeHandlers = [];
68+
let closed = false;
69+
function close() {
70+
if (closed) { return closePromise; }
71+
closed = true;
72+
// QuickNavigateDialog.close() returns this.closePromise, which is
73+
// the 3rd arg of the ModalBar "close" event payload. Pass a
74+
// resolved jQuery Deferred so callers can chain .done() safely.
75+
closePromise = $.Deferred().resolve().promise();
76+
closeHandlers.forEach(function (fn) {
77+
try {
78+
// Match ModalBar's trigger signature: event, reason, promise
79+
fn({}, undefined, closePromise);
80+
} catch (e) { /* swallow */ }
81+
});
82+
$overlay.remove();
83+
window.document.body.removeEventListener("focusin", onFocusIn, true);
84+
return closePromise;
85+
}
86+
let closePromise = null;
87+
function onFocusIn(e) {
88+
// Close when focus moves outside both the bar and the results
89+
// dropdown (which QuickSearchField appends to <body>, not into
90+
// our overlay).
91+
if ($overlay[0].contains(e.target)) { return; }
92+
if (e.target.closest && e.target.closest(".quick-search-container")) { return; }
93+
close();
94+
}
95+
const bar = {
96+
on: function (evt, fn) {
97+
if (evt === "close" && typeof fn === "function") {
98+
closeHandlers.push(fn);
99+
}
100+
},
101+
prepareClose: function () { /* no-op — floating bar doesn't reserve editor space */ },
102+
close: close,
103+
getRoot: function () { return $bar; }
104+
};
105+
// Close the whole picker on the FIRST Escape rather than just the
106+
// dropdown. Use capture so we run before QuickSearchField's keydown
107+
// handler (which does internal dropdown dismiss handling) can
108+
// consume the event.
109+
function onEscape(e) {
110+
if (e.key === "Escape" || e.keyCode === 27) {
111+
close();
112+
e.preventDefault();
113+
e.stopPropagation();
114+
if (e.stopImmediatePropagation) { e.stopImmediatePropagation(); }
115+
}
116+
}
117+
$overlay[0].addEventListener("keydown", onEscape, true);
118+
// Global safety net: while overlay is up, first Escape anywhere closes it.
119+
window.document.addEventListener("keydown", onEscape, true);
120+
closeHandlers.push(function () {
121+
window.document.removeEventListener("keydown", onEscape, true);
122+
});
123+
$overlay.on("mousedown", function (e) {
124+
if (e.target === $overlay[0]) {
125+
close();
126+
}
127+
});
128+
window.document.body.addEventListener("focusin", onFocusIn, true);
129+
// Move focus into the input once the overlay is in the DOM.
130+
window.setTimeout(function () {
131+
const $input = $bar.find("input").first();
132+
if ($input.length) { $input.focus(); }
133+
}, 0);
134+
return bar;
135+
}
136+
55137
/**
56138
* Represents the symbol kind
57139
* @type {Object}
@@ -637,6 +719,14 @@ define(function (require, exports, module) {
637719
initialString = initialString || "";
638720
initialString = prefix + initialString;
639721

722+
// If the underlying search field was already torn down (e.g. the bar
723+
// was closed via focus-loss but the isOpen flag wasn't updated in
724+
// time), fall back to closing cleanly so the caller reopens fresh.
725+
if (!this.searchField || !this.searchField.$input || !this.$searchField || !this.$searchField[0]) {
726+
this.isOpen = false;
727+
return;
728+
}
729+
640730
this.searchField.setText(initialString);
641731

642732
// Select just the text after the prefix
@@ -707,17 +797,44 @@ define(function (require, exports, module) {
707797
placeholder='${Strings.CMD_QUICK_OPEN}\u2026' style='width: 30em'>
708798
<span class='find-dialog-label'></span>
709799
</div>`;
710-
this.modalBar = new ModalBar(searchBarHTML, true);
800+
// In design mode the editor area is collapsed and the normal ModalBar
801+
// (which slides in before #editor-holder) has no room to render. Use a
802+
// Spotlight-style floating overlay instead so the picker stays usable.
803+
if (WorkspaceManager.isInDesignMode()) {
804+
this.modalBar = _createFloatingQuickOpenBar(searchBarHTML);
805+
} else {
806+
this.modalBar = new ModalBar(searchBarHTML, true);
807+
}
711808

712809
this.modalBar.on("close", this._handleCloseBar);
713810

714811
this.$searchField = $("input#quickOpenSearch");
715812
this.$indexingSpinner = $("#indexing-spinner");
716813

814+
const _floating = WorkspaceManager.isInDesignMode();
815+
let _verticalAdjust = this.modalBar.getRoot().outerHeight();
816+
if (_floating) {
817+
// QuickSearchField computes dropdownTop as
818+
// input.offset().top + input.height + verticalAdjust
819+
// For the standard ModalBar that lands flush with the bar's bottom
820+
// because the input sits at the bar's bottom edge. In the floating
821+
// bar the input is vertically centered inside, so `bar.height` as
822+
// verticalAdjust overshoots and the dropdown floats detached. Use
823+
// (bar.bottom - input.bottom) instead so the dropdown starts right
824+
// below the bar.
825+
const $root = this.modalBar.getRoot();
826+
const barBottom = $root.offset().top + $root.outerHeight();
827+
const inputBottom = this.$searchField.offset().top + this.$searchField.outerHeight();
828+
_verticalAdjust = Math.max(0, barBottom - inputBottom);
829+
}
717830
this.searchField = new QuickSearchField(this.$searchField, {
718831
maxResults: 20,
719832
firstHighlightIndex: 0,
720-
verticalAdjust: this.modalBar.getRoot().outerHeight(),
833+
verticalAdjust: _verticalAdjust,
834+
// In design mode the floating bar is the visible container for the
835+
// picker; align the results dropdown to that element instead of the
836+
// bare <input> so the two edges match.
837+
$positionEl: _floating ? this.modalBar.getRoot() : undefined,
721838
resultProvider: this._filterCallback,
722839
formatter: this._resultsFormatterCallback,
723840
onCommit: this._handleItemSelect,
@@ -780,14 +897,6 @@ define(function (require, exports, module) {
780897
}
781898

782899
function doFileSearch() {
783-
// Design mode hides the editor area where Quick Open's modal bar and
784-
// result interactions need to land. Exit design mode first so the user
785-
// sees the picker on the normal editor chrome.
786-
// TODO: allow Quick Open to float above the live preview so users can
787-
// navigate without leaving design mode.
788-
if (WorkspaceManager.isInDesignMode()) {
789-
CommandManager.execute(Commands.VIEW_TOGGLE_DESIGN_MODE);
790-
}
791900
beginSearch("", getCurrentEditorSelectedText());
792901
}
793902

src/styles/brackets.less

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2734,6 +2734,96 @@ textarea.exclusions-editor {
27342734
}
27352735
}
27362736

2737+
// Spotlight-style floating Quick Open picker used while in design mode, where
2738+
// the standard ModalBar has no editor space to mount into.
2739+
// No backdrop dim/blur — file switching is frequent and UI-wide visual
2740+
// changes feel jarring. The overlay only exists to host the centered bar
2741+
// and catch outside-clicks for dismiss.
2742+
.quick-open-floating-overlay {
2743+
position: fixed;
2744+
top: 0;
2745+
left: 0;
2746+
right: 0;
2747+
bottom: 0;
2748+
z-index: @z-index-brackets-context-menu-base + 50;
2749+
display: flex;
2750+
justify-content: center;
2751+
align-items: flex-start;
2752+
padding-top: 12vh;
2753+
background: transparent;
2754+
pointer-events: none; // passthrough; the bar re-enables pointer events below
2755+
}
2756+
.quick-open-floating-bar {
2757+
width: 640px;
2758+
max-width: 90vw;
2759+
padding: 0;
2760+
background-color: @bc-panel-bg;
2761+
border: 1px solid @bc-panel-border;
2762+
border-radius: 10px 10px 0 0;
2763+
box-shadow: 0 24px 60px rgba(0, 0, 0, 0.55);
2764+
pointer-events: auto; // re-enable inside the bar (overlay disables it for passthrough)
2765+
.dark & {
2766+
background-color: @dark-bc-panel-bg;
2767+
border-color: @dark-bc-panel-border;
2768+
}
2769+
2770+
// Input line
2771+
> div {
2772+
display: flex;
2773+
align-items: center;
2774+
gap: 10px;
2775+
padding: 4px 14px;
2776+
}
2777+
input[type="text"] {
2778+
flex: 1 1 auto;
2779+
width: auto !important;
2780+
min-width: 0;
2781+
box-sizing: border-box;
2782+
height: 48px !important;
2783+
font-size: 18px;
2784+
line-height: 1.4;
2785+
padding: 10px 4px !important;
2786+
margin: 0 !important;
2787+
border: 0 !important;
2788+
outline: none !important;
2789+
background: transparent !important;
2790+
box-shadow: none !important;
2791+
color: @bc-text;
2792+
.dark & {
2793+
color: @dark-bc-text;
2794+
}
2795+
&::placeholder {
2796+
color: @bc-text-quiet;
2797+
.dark & { color: @dark-bc-text-quiet; }
2798+
}
2799+
}
2800+
.indexing-group {
2801+
display: inline-flex;
2802+
align-items: center;
2803+
gap: 6px;
2804+
flex: 0 0 auto;
2805+
order: -1; // spinner before input
2806+
}
2807+
.find-dialog-label {
2808+
flex: 0 0 auto;
2809+
color: @bc-text-quiet;
2810+
font-size: 12px;
2811+
.dark & { color: @dark-bc-text-quiet; }
2812+
}
2813+
}
2814+
// While the floating Quick Open is up, anchor the results dropdown to the
2815+
// floating bar (it's the one styled to contain it). The dropdown is appended
2816+
// to <body> by QuickSearchField; constrain its max-height so it never
2817+
// overflows the viewport, and lift its z-index above the overlay so the
2818+
// overlay's backdrop-filter doesn't blur the dropdown too.
2819+
body:has(.quick-open-floating-overlay) .quick-search-container {
2820+
z-index: @z-index-brackets-context-menu-base + 51;
2821+
max-height: 60vh;
2822+
overflow-y: auto;
2823+
border-radius: 0 0 10px 10px;
2824+
box-shadow: 0 24px 60px rgba(0, 0, 0, 0.55);
2825+
}
2826+
27372827
.quick-search-container {
27382828
border: 1px solid @bc-panel-border;
27392829
box-sizing: border-box; // lets QuickSearchField size the width more nicely

0 commit comments

Comments
 (0)