fix(ui): honor the default font in showFontPicker on the desktop (#4340)
Backend Tests / changes (push) Waiting to run
Maintain Release Merge PR / update-release-pr (push) Waiting to run
Notify HeyPuter / notify (push) Waiting to run
release-please / release-please (push) Waiting to run

* fix(ui): honor the default font in showFontPicker on the desktop

String arguments are normalized to { defaultFont } in the IPC handler,
matching showColorPicker, instead of throwing on property assignment.
UIWindowFontPicker reads defaultValue/defaultFont/default case-
insensitively, and the SDK fallback and docs accept the same aliases.

* fix(ui): document the showFontPicker cancel result and alias order

The JSDoc gains overloads for the string and options call forms and
states that closing the picker resolves false in an app and null on a
website; the docs page and examples match. e2e coverage adds a
scroll-into-view check for the preselected font and a close-without-
selecting case.
This commit is contained in:
sabrimete authored and GitHub committed 2026-11-03 00:07:05 -07:00
1 parent d934a47c5e
commit cdb3110857
7 files changed
+187 -14

No files matched your search

+10 -1
View File
@@ -17,6 +17,13 @@ puter.ui.showFontPicker(options)
#### `defaultFont` (String)
The default font to select when the font picker is opened.
#### `options` (Object)
An object with the following properties:
- `defaultFont` (String): The font to select when the font picker is opened. The name is matched without regard to case. `defaultValue` and `default` are accepted as aliases. If more than one is given, `defaultValue` is used, then `defaultFont`, then `default`.
## Return value
A `Promise` that resolves to an object with a `fontFamily` property holding the name of the selected font. If the user closes the picker without selecting a font, the promise resolves to `false` in a Puter app and to `null` on a website.
## Examples
@@ -28,7 +35,9 @@ The default font to select when the font picker is opened.
<script>
puter.ui.showFontPicker().then((font)=>{
document.body.style.fontFamily = font.fontFamily;
if (font) {
document.body.style.fontFamily = font.fontFamily;
}
})
</script>
</body>
@@ -6,8 +6,10 @@
<script>
document.getElementById('pick-font').addEventListener('click', () => {
puter.ui.showFontPicker().then((font) => {
document.getElementById('preview').style.fontFamily = font.fontFamily;
puter.print('Selected font: ' + font.fontFamily);
if (font) {
document.getElementById('preview').style.fontFamily = font.fontFamily;
puter.print('Selected font: ' + font.fontFamily);
}
});
});
</script>
+3
View File
@@ -1630,6 +1630,9 @@ const ipc_listener = async (event, handled) => {
}
// set options
if ( typeof event.data.options === 'string' ) {
event.data.options = { defaultFont: event.data.options };
}
event.data.options = event.data.options ?? {};
// clear window_options for security reasons
+9 -5
View File
@@ -41,7 +41,7 @@ const font_list = new Set([
async function UIWindowFontPicker (options) {
// set sensible defaults
if ( arguments.length > 0 ) {
// if first argument is a string, then assume it is the default color
// if first argument is a string, then assume it is the default font
if ( window.isString(arguments[0]) ) {
options = {};
options.default = arguments[0];
@@ -49,13 +49,17 @@ async function UIWindowFontPicker (options) {
}
options = options || {};
const defaultFont = options.defaultValue ?? options.defaultFont ?? options.default;
const isDefault = (font) => window.isString(defaultFont)
&& font.toLowerCase() === defaultFont.trim().toLowerCase();
return new Promise(async (resolve) => {
let h = '';
h += '<div>';
h += '<div style="padding: 20px; border-bottom: 1px solid #ced7e1; width: 100%; box-sizing: border-box;">';
h += '<div class="font-list" style="margin-bottom: 10px; height: 200px; overflow-y: scroll; background-color: white; padding: 0 10px;">';
fontAvailable.forEach(element => {
h += `<p class="font-selector disable-user-select ${options.default === element ? 'font-selector-active' : ''}" style="font-family: '${html_encode(element)}';" data-font-family="${html_encode(element)}">${html_encode(element)}</p>`; // 👉️ one, two, three, four
h += `<p class="font-selector disable-user-select ${isDefault(element) ? 'font-selector-active' : ''}" style="font-family: '${html_encode(element)}';" data-font-family="${html_encode(element)}">${html_encode(element)}</p>`; // 👉️ one, two, three, four
});
h += '</div>';
@@ -89,10 +93,10 @@ async function UIWindowFontPicker (options) {
on_close: () => {
resolve(false);
},
onAppend: function (window) {
let active_font = $(window).find('.font-selector-active');
onAppend: function (win) {
let active_font = $(win).find('.font-selector-active');
if ( active_font.length > 0 ) {
window.scrollParentToChild($(window).find('.font-list').get(0), active_font.get(0));
window.scrollParentToChild($(win).find('.font-list').get(0), active_font.get(0));
}
},
window_class: 'window-login',
+18 -6
View File
@@ -1419,12 +1419,24 @@ export class UIModule extends EventListener {
};
/**
* Shows a font picker. Resolves to the chosen font. Accepts either a
* default font name or an options object. `default` is a legacy alias for
* `defaultFont`.
* @overload
* @param {string} [defaultFont]
* @returns {Promise<{ fontFamily: string } | false | null>}
*/
/**
* @overload
* @param {FontPickerOptions & { default?: string, defaultValue?: string }} [options]
* @returns {Promise<{ fontFamily: string } | false | null>}
*/
/**
* Shows a font picker. Resolves to the chosen font, or to `false` inside a
* Puter app and `null` on a website when the user closes the picker without
* choosing. Accepts either a default font name or an options object.
* `default` and `defaultValue` are legacy aliases for `defaultFont`;
* `defaultValue` wins over `defaultFont`, which wins over `default`.
*
* @param {string | (FontPickerOptions & { default?: string })} [options]
* @returns {Promise<{ fontFamily: string }>}
* @param {string | (FontPickerOptions & { default?: string, defaultValue?: string })} [options]
* @returns {Promise<{ fontFamily: string } | false | null>}
*/
showFontPicker (options) {
if ( this.messageTarget ) {
@@ -1436,7 +1448,7 @@ export class UIModule extends EventListener {
return new Promise((resolve) => {
const opts = typeof options === 'string' ? { defaultFont: options } : (options ?? {});
const el = document.createElement('puter-font-picker');
const defaultFont = opts.defaultFont || opts.default || 'System UI';
const defaultFont = opts.defaultValue || opts.defaultFont || opts.default || 'System UI';
el.setAttribute('default-font', defaultFont);
el.addEventListener('response', (e) => resolve(e.detail));
document.body.appendChild(el);
@@ -0,0 +1,70 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>puter-js test fixture: showFontPicker</title>
<style>
body { font-family: system-ui, sans-serif; padding: 16px; }
button { padding: 8px 12px; margin-right: 8px; font-size: 14px; }
#log { margin-top: 16px; padding: 8px; background: #f5f5f5; border: 1px solid #ddd; min-height: 60px; font-family: monospace; white-space: pre-wrap; }
.ready #status { color: green; }
</style>
</head>
<body>
<h1>puter-js showFontPicker fixture</h1>
<p id="status">Loading puter.js…</p>
<button id="font-string" disabled>Font Picker (string)</button>
<button id="font-default-font" disabled>Font Picker (defaultFont)</button>
<button id="font-default-value" disabled>Font Picker (defaultValue)</button>
<button id="font-verdana" disabled>Font Picker (Verdana)</button>
<div id="log" data-testid="log"></div>
<script>
window.PUTER_ORIGIN = 'http://puter.localhost:4100';
// Must be the real api subdomain: the SDK sends credentialed CORS
// requests, and only the api host answers with
// Access-Control-Allow-Credentials — on the GUI host they all fail.
window.PUTER_API_ORIGIN = 'http://api.puter.localhost:4100';
</script>
<script src="/dist/puter.dev.js"></script>
<script>
const logEl = document.getElementById('log');
const statusEl = document.getElementById('status');
function log(entry) {
const line = document.createElement('div');
line.textContent = entry;
line.dataset.entry = entry;
logEl.appendChild(line);
}
function ready() {
document.body.classList.add('ready');
statusEl.textContent = `Ready (env=${puter.env})`;
document.querySelectorAll('button').forEach((btn) => { btn.disabled = false; });
}
function waitForPuter() {
if (window.puter && puter.ui) ready();
else setTimeout(waitForPuter, 50);
}
waitForPuter();
async function pickFont(options) {
try {
const font = await puter.ui.showFontPicker(options);
log(font === false ? 'font:cancel' : font ? `font:${font.fontFamily}` : `font:${font}`);
} catch (e) {
log(`font:error:${e?.message ?? e}`);
}
}
document.getElementById('font-string').addEventListener('click', () => pickFont('Georgia'));
document.getElementById('font-default-font').addEventListener('click', () => pickFont({ defaultFont: 'Georgia' }));
document.getElementById('font-default-value').addEventListener('click', () => pickFont({ defaultValue: 'georgia' }));
document.getElementById('font-verdana').addEventListener('click', () => pickFont('Verdana'));
</script>
</body>
</html>
@@ -0,0 +1,73 @@
import { test, expect } from '@playwright/test';
import { registerTestApp, deleteTestApp, gotoTestApp, FIXTURE_URL } from '../helpers/testApp.js';
const FONT_FIXTURE_URL = FIXTURE_URL.replace(
'menubar-contextmenu.html',
'font-picker.html',
);
test.describe('puter.ui.showFontPicker (env=app)', () => {
for ( const [buttonId, label] of [
['font-string', 'a string argument'],
['font-default-font', '{ defaultFont }'],
['font-default-value', '{ defaultValue }'],
] ) {
test(`preselects the default font with ${label}`, async ({ page }) => {
const appName = await registerTestApp(page, { fixtureURL: FONT_FIXTURE_URL });
try {
const appFrame = await gotoTestApp(page, appName);
await appFrame.locator(`#${buttonId}`).click();
const dialog = page.locator('.window[data-app="font-picker"]');
await expect(dialog).toBeVisible({ timeout: 15_000 });
// Exactly one preselected entry, and it is the default we sent
// (defaultValue is passed lowercase on purpose: the match is
// case-insensitive).
const active = dialog.locator('.font-selector-active');
await expect(active).toHaveCount(1);
await expect(active).toHaveAttribute('data-font-family', 'Georgia');
await dialog.locator('.select-btn').click();
await expect(appFrame.locator('#log [data-entry="font:Georgia"]')).toBeVisible();
} finally {
await deleteTestApp(page, appName);
}
});
}
test('scrolls the preselected font into view', async ({ page }) => {
const appName = await registerTestApp(page, { fixtureURL: FONT_FIXTURE_URL });
try {
const appFrame = await gotoTestApp(page, appName);
await appFrame.locator('#font-verdana').click();
const dialog = page.locator('.window[data-app="font-picker"]');
await expect(dialog).toBeVisible({ timeout: 15_000 });
const active = dialog.locator('.font-selector-active');
await expect(active).toHaveCount(1);
await expect(active).toHaveAttribute('data-font-family', 'Verdana');
await expect(active).toBeInViewport();
} finally {
await deleteTestApp(page, appName);
}
});
test('resolves false when the picker is closed', async ({ page }) => {
const appName = await registerTestApp(page, { fixtureURL: FONT_FIXTURE_URL });
try {
const appFrame = await gotoTestApp(page, appName);
await appFrame.locator('#font-string').click();
const dialog = page.locator('.window[data-app="font-picker"]');
await expect(dialog).toBeVisible({ timeout: 15_000 });
await dialog.locator('.window-close-btn').click();
await expect(dialog).toBeHidden();
await expect(appFrame.locator('#log [data-entry="font:cancel"]')).toBeVisible();
} finally {
await deleteTestApp(page, appName);
}
});
});