Skip to content

Commit 6aef0c7

Browse files
committed
refactor(notification): extract inline toast into reusable NotificationUI.showToastOn API
Move terminal focus hint toast implementation into NotificationUI as a generic showToastOn(container, template, options) function. Move toast CSS from Extn-Terminal.less to brackets.less as .inline-toast. Add unit tests for the new API.
1 parent 8ea0ca0 commit 6aef0c7

5 files changed

Lines changed: 226 additions & 61 deletions

File tree

src/extensionsIntegrated/Terminal/main.js

Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ define(function (require, exports, module) {
4040
const StringUtils = require("utils/StringUtils");
4141

4242
const Commands = require("command/Commands");
43+
const NotificationUI = require("widgets/NotificationUI");
4344
const TerminalInstance = require("./TerminalInstance");
4445
const ShellProfiles = require("./ShellProfiles");
4546
const panelHTML = require("text!./terminal-panel.html");
@@ -546,22 +547,10 @@ define(function (require, exports, module) {
546547

547548
const shortcutKey = '<kbd>Shift+Esc</kbd>';
548549
const message = StringUtils.format(Strings.TERMINAL_FOCUS_HINT, shortcutKey);
549-
const $toast = $('<div class="terminal-focus-toast"></div>')
550-
.html('<span class="terminal-toast-text">' + message + '</span>');
551-
$contentArea.append($toast);
552-
553-
// Fade in
554-
setTimeout(function () {
555-
$toast.addClass("visible");
556-
}, 100);
557-
558-
// Auto-dismiss after 5 seconds
559-
setTimeout(function () {
560-
$toast.removeClass("visible");
561-
setTimeout(function () {
562-
$toast.remove();
563-
}, 300);
564-
}, 5000);
550+
NotificationUI.showToastOn($contentArea[0], message, {
551+
autoCloseTimeS: 5,
552+
dismissOnClick: true
553+
});
565554
}
566555

567556
/**

src/styles/Extn-Terminal.less

Lines changed: 0 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -400,48 +400,3 @@
400400
font-size: 13px;
401401
}
402402

403-
/* Focus hint toast */
404-
.terminal-focus-toast {
405-
position: absolute;
406-
bottom: 12px;
407-
left: 50%;
408-
transform: translateX(-50%);
409-
padding: 6px 14px;
410-
border-radius: 6px;
411-
font-size: 12px;
412-
line-height: 1.4;
413-
opacity: 0;
414-
transition: opacity 0.3s ease;
415-
pointer-events: none;
416-
z-index: 10;
417-
background: rgba(0, 0, 0, 0.75);
418-
color: #e0e0e0;
419-
}
420-
421-
.terminal-focus-toast.visible {
422-
opacity: 1;
423-
}
424-
425-
.terminal-focus-toast kbd {
426-
display: inline-block;
427-
padding: 1px 5px;
428-
margin: 0 2px;
429-
border-radius: 3px;
430-
font-family: inherit;
431-
font-size: 11px;
432-
background: rgba(255, 255, 255, 0.15);
433-
border: 1px solid rgba(255, 255, 255, 0.2);
434-
color: #fff;
435-
}
436-
437-
/* Light theme toast */
438-
.terminal-panel-container .terminal-focus-toast {
439-
background: rgba(0, 0, 0, 0.7);
440-
color: #f0f0f0;
441-
}
442-
443-
.terminal-panel-container .terminal-focus-toast kbd {
444-
background: rgba(255, 255, 255, 0.2);
445-
border-color: rgba(255, 255, 255, 0.25);
446-
color: #fff;
447-
}

src/styles/brackets.less

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3381,6 +3381,40 @@ label input {
33813381
}
33823382
}
33833383

3384+
/* Inline toast: positioned inside a relative/absolute container */
3385+
.inline-toast {
3386+
position: absolute;
3387+
bottom: 12px;
3388+
left: 50%;
3389+
transform: translateX(-50%);
3390+
padding: 6px 14px;
3391+
border-radius: 6px;
3392+
font-size: 12px;
3393+
line-height: 1.4;
3394+
opacity: 0;
3395+
transition: opacity 0.3s ease;
3396+
pointer-events: none;
3397+
z-index: 10;
3398+
background: rgba(0, 0, 0, 0.75);
3399+
color: #e0e0e0;
3400+
}
3401+
3402+
.inline-toast.visible {
3403+
opacity: 1;
3404+
}
3405+
3406+
.inline-toast kbd {
3407+
display: inline-block;
3408+
padding: 1px 5px;
3409+
margin: 0 2px;
3410+
border-radius: 3px;
3411+
font-family: inherit;
3412+
font-size: 11px;
3413+
background: rgba(255, 255, 255, 0.15);
3414+
border: 1px solid rgba(255, 255, 255, 0.2);
3415+
color: #fff;
3416+
}
3417+
33843418
.github-stars-button {
33853419
.starContainer {
33863420
--fa-style-family-brands: "Font Awesome 6 Brands";

src/widgets/NotificationUI.js

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -398,8 +398,91 @@ define(function (require, exports, module) {
398398
return notification;
399399
}
400400

401+
/**
402+
* Shows a small, transient inline toast notification inside a given DOM container.
403+
* The toast is centered at the bottom of the container and auto-dismisses.
404+
*
405+
* ```js
406+
* NotificationUI.showToastOn(document.getElementById("my-panel"), "Hello!", {
407+
* autoCloseTimeS: 5,
408+
* dismissOnClick: true
409+
* });
410+
* ```
411+
*
412+
* @param {Element|string} containerOrSelector A DOM element or CSS selector for the parent container.
413+
* The container should have `position: relative` or `absolute` so the toast is positioned correctly.
414+
* @param {string|Element} template HTML string or DOM Element for the toast content.
415+
* @param {Object} [options] optional, supported options:
416+
* * `autoCloseTimeS` - Time in seconds after which the toast auto-closes. Default is 5.
417+
* * `dismissOnClick` - If true, clicking the toast dismisses it. Default is true.
418+
* @return {Notification} Object with a done handler that resolves when the toast closes.
419+
* @type {function}
420+
*/
421+
function showToastOn(containerOrSelector, template, options = {}) {
422+
const autoCloseTimeS = options.autoCloseTimeS !== undefined ? options.autoCloseTimeS : 5;
423+
const dismissOnClick = options.dismissOnClick !== undefined ? options.dismissOnClick : true;
424+
425+
const $container = $(containerOrSelector);
426+
const $toast = $('<div class="inline-toast"></div>');
427+
if (typeof template === "string") {
428+
$toast.html(template);
429+
} else {
430+
$toast.append($(template));
431+
}
432+
$container.append($toast);
433+
434+
const notification = new Notification($toast, "inlineToast");
435+
436+
// Fade in on next frame
437+
requestAnimationFrame(function () {
438+
$toast.addClass("visible");
439+
});
440+
441+
function closeToast(reason) {
442+
let cleaned = false;
443+
function cleanup() {
444+
if (cleaned) {
445+
return;
446+
}
447+
cleaned = true;
448+
$toast.remove();
449+
notification._result.resolve(reason);
450+
}
451+
$toast.removeClass("visible");
452+
$toast.one("transitionend transitioncancel", cleanup);
453+
// Safety fallback in case transition events don't fire
454+
setTimeout(cleanup, 500);
455+
}
456+
457+
notification.close = function (closeType) {
458+
if (!this.$notification) {
459+
return this;
460+
}
461+
this.$notification = null;
462+
closeToast(closeType || CLOSE_REASON.CLICK_DISMISS);
463+
return this;
464+
};
465+
466+
if (autoCloseTimeS) {
467+
setTimeout(function () {
468+
if (notification.$notification) {
469+
notification.close(CLOSE_REASON.TIMEOUT);
470+
}
471+
}, autoCloseTimeS * 1000);
472+
}
473+
474+
if (dismissOnClick) {
475+
$toast.on("click", function () {
476+
notification.close(CLOSE_REASON.CLICK_DISMISS);
477+
});
478+
}
479+
480+
return notification;
481+
}
482+
401483
exports.createFromTemplate = createFromTemplate;
402484
exports.createToastFromTemplate = createToastFromTemplate;
485+
exports.showToastOn = showToastOn;
403486
exports.CLOSE_REASON = CLOSE_REASON;
404487
exports.NOTIFICATION_STYLES_CSS_CLASS = NOTIFICATION_STYLES_CSS_CLASS;
405488
});

test/spec/NotificationUI-test.js

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,5 +114,109 @@ define(function (require, exports, module) {
114114
await verifyToast(NotificationUI.NOTIFICATION_STYLES_CSS_CLASS.DANGER);
115115
await verifyToast("custom-class-name");
116116
}, 10000);
117+
118+
describe("showToastOn", function () {
119+
let $container;
120+
121+
beforeAll(function () {
122+
$container = $(
123+
'<div id="inline-toast-test-container" style="position:relative;width:200px;height:200px;"></div>');
124+
$("body").append($container);
125+
});
126+
127+
afterAll(function () {
128+
$container.remove();
129+
});
130+
131+
it("Should show an inline toast inside a container", async function () {
132+
let notification = NotificationUI.showToastOn($container[0], "Hello inline toast");
133+
await awaitsFor(function () {
134+
return $container.find(".inline-toast").length === 1;
135+
}, "waiting for inline toast to appear");
136+
expect($container.find(".inline-toast").text()).toBe("Hello inline toast");
137+
notification.close();
138+
await awaitsFor(function () {
139+
return $container.find(".inline-toast").length === 0;
140+
}, "waiting for inline toast to close");
141+
});
142+
143+
it("Should auto-close after autoCloseTimeS", async function () {
144+
NotificationUI.showToastOn($container[0], "Auto close", {
145+
autoCloseTimeS: 1
146+
});
147+
await awaitsFor(function () {
148+
return $container.find(".inline-toast").length === 1;
149+
}, "waiting for inline toast to appear");
150+
await awaitsFor(function () {
151+
return $container.find(".inline-toast").length === 0;
152+
}, "waiting for inline toast to auto-close", 3000);
153+
});
154+
155+
it("Should dismiss on click by default", async function () {
156+
NotificationUI.showToastOn($container[0], "Click me");
157+
await awaitsFor(function () {
158+
return $container.find(".inline-toast.visible").length === 1;
159+
}, "waiting for inline toast to be visible");
160+
$container.find(".inline-toast").click();
161+
await awaitsFor(function () {
162+
return $container.find(".inline-toast").length === 0;
163+
}, "waiting for inline toast to close on click");
164+
});
165+
166+
it("Should not dismiss on click when dismissOnClick is false", async function () {
167+
let notification = NotificationUI.showToastOn($container[0], "No dismiss", {
168+
dismissOnClick: false,
169+
autoCloseTimeS: 0
170+
});
171+
await awaitsFor(function () {
172+
return $container.find(".inline-toast.visible").length === 1;
173+
}, "waiting for inline toast to be visible");
174+
$container.find(".inline-toast").click();
175+
await awaits(250);
176+
expect($container.find(".inline-toast").length).toBe(1);
177+
notification.close("manual");
178+
await awaitsFor(function () {
179+
return $container.find(".inline-toast").length === 0;
180+
}, "waiting for inline toast to close manually");
181+
});
182+
183+
it("Should accept a jQuery selector string as container", async function () {
184+
NotificationUI.showToastOn("#inline-toast-test-container", "Selector toast");
185+
await awaitsFor(function () {
186+
return $container.find(".inline-toast").length === 1;
187+
}, "waiting for inline toast via selector");
188+
$container.find(".inline-toast").click();
189+
await awaitsFor(function () {
190+
return $container.find(".inline-toast").length === 0;
191+
}, "waiting for inline toast to close");
192+
});
193+
194+
it("Should resolve done callback with close reason", async function () {
195+
let closeReason;
196+
let notification = NotificationUI.showToastOn($container[0], "Done test");
197+
notification.done(function (reason) {
198+
closeReason = reason;
199+
});
200+
await awaitsFor(function () {
201+
return $container.find(".inline-toast.visible").length === 1;
202+
}, "waiting for inline toast to be visible");
203+
notification.close("testReason");
204+
await awaitsFor(function () {
205+
return closeReason === "testReason";
206+
}, "waiting for done callback");
207+
});
208+
209+
it("Should accept HTML template with elements", async function () {
210+
NotificationUI.showToastOn($container[0], '<b>Bold</b> text');
211+
await awaitsFor(function () {
212+
return $container.find(".inline-toast").length === 1;
213+
}, "waiting for inline toast");
214+
expect($container.find(".inline-toast b").length).toBe(1);
215+
$container.find(".inline-toast").click();
216+
await awaitsFor(function () {
217+
return $container.find(".inline-toast").length === 0;
218+
}, "waiting for inline toast to close");
219+
});
220+
});
117221
});
118222
});

0 commit comments

Comments
 (0)