fix(ui): readable date-time trigger, showcase code panel, release ui 0.8.88
- DatePicker trigger shows the value formatted in the component locale
("Oct 2, 2026 · 9:30 AM", "Jul 15, 2026"); the hidden input still submits
the ISO value.
- Showcase playground code panel: the reactive runtime read "{...}" in the
rewritten code as interpolations and blanked every object in array props.
Braces are now written as their own spans, like the server-rendered code.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1bed8e4b74
commit
9030e7d3a3
5 files changed
+42
-6
No files matched your search
@@ -749,7 +749,7 @@
|
||||
},
|
||||
"packages/ui": {
|
||||
"name": "@wrnexus/ui",
|
||||
"version": "0.8.87",
|
||||
"version": "0.8.88",
|
||||
"dependencies": {
|
||||
"@wrnexus/core": "workspace:*",
|
||||
},
|
||||
|
||||
@@ -169,11 +169,33 @@
|
||||
const mount = publicTag ? [] : [`data-component="${sourceAttribute(component)}"`];
|
||||
const lines = [...mount, ...attributes].map((attribute) => ` ${attribute}`).join("\n");
|
||||
const opening = lines ? `<${tag}\n${lines}` : `<${tag}`;
|
||||
code.textContent = hasSlot
|
||||
let source = hasSlot
|
||||
? `${opening}>\n <!-- Add slot content here -->\n</${tag}>`
|
||||
: `${opening}\n/>`;
|
||||
if (declarations.length) code.textContent = `page ${component.replace(/[^A-Za-z0-9]/g, "")}Example {\n${declarations.join("\n")}\n view {\n${code.textContent}\n }\n}`;
|
||||
if (declarations.length) source = `page ${component.replace(/[^A-Za-z0-9]/g, "")}Example {\n${declarations.join("\n")}\n view {\n${source}\n }\n}`;
|
||||
writeCode(code, source);
|
||||
}
|
||||
|
||||
/*
|
||||
* The reactive runtime treats "{expr}" inside a text node as an
|
||||
* interpolation, so a sample like { "label": "Mon" } written with
|
||||
* textContent was evaluated and rendered as an empty string. Putting each
|
||||
* brace in its own <span> means no single text node ever holds a {...}
|
||||
* pair -- the same markup the generator uses for the server-rendered code.
|
||||
*/
|
||||
function writeCode(code, source) {
|
||||
const fragment = document.createDocumentFragment();
|
||||
for (const part of source.split(/([{}])/)) {
|
||||
if (!part) continue;
|
||||
if (part === "{" || part === "}") {
|
||||
const brace = document.createElement("span");
|
||||
brace.textContent = part;
|
||||
fragment.append(brace);
|
||||
} else {
|
||||
fragment.append(document.createTextNode(part));
|
||||
}
|
||||
}
|
||||
code.replaceChildren(fragment);
|
||||
}
|
||||
|
||||
function valuesFrom(form) {
|
||||
|
||||
@@ -52,6 +52,20 @@ size: string = "default"
|
||||
output.change({ value: currentValue, name: name, sourceEvent: sourceEvent })
|
||||
}
|
||||
|
||||
// Human-readable trigger text in the component locale. The hidden input
|
||||
// keeps the ISO value, so forms still submit 2026-10-02 / 2026-10-02T09:30.
|
||||
shared function displayValue(parts, label, time, clock) {
|
||||
if (!currentValue) { return placeholder || "Select date" }
|
||||
parts = currentValue.slice(0, 10).split("-")
|
||||
if (parts.length !== 3 || !Number(parts[0]) || !Number(parts[1]) || !Number(parts[2])) { return currentValue }
|
||||
label = Intl.DateTimeFormat(locale, { dateStyle: "medium", timeZone: "UTC" }).format(Date.UTC(Number(parts[0]), Number(parts[1]) - 1, Number(parts[2])))
|
||||
if (currentValue.charAt(10) === "T") {
|
||||
time = currentValue.slice(11, 16).split(":")
|
||||
clock = Intl.DateTimeFormat(locale, { timeStyle: "short", timeZone: "UTC" }).format(Date.UTC(1970, 0, 1, Number(time[0]) || 0, Number(time[1]) || 0))
|
||||
return label + " · " + clock
|
||||
}
|
||||
return label
|
||||
}
|
||||
shared function calendarYears() {
|
||||
if (years.includes(Number(selectedYear))) { return years }
|
||||
return years.concat(Number(selectedYear))
|
||||
@@ -142,7 +156,7 @@ size: string = "default"
|
||||
view {
|
||||
<div {...attrs} class="wrn-component wrn-component--color-{color} wrn-component--size-{size} wrn-next--field wrn-next--date-picker {class}" data-variant="{variant}" data-inline="{inline}" data-invalid="{error ? 'true' : 'false'}" data-expanded="{expanded}" @keydown="calendarKeydown(event)">
|
||||
<div class="wrn-next__field-heading"><label for="{id || name}">{label}</label>{#if cornerHint}<span class="wrn-next__field-hint">{cornerHint}</span>{/if}</div>
|
||||
<div class="wrn-next__picker-control"><input id="{id || name}" class="wrn-next__sr-only wrn-next__picker-value" type="text" name="{name}" value="{currentValue}" required="{required}" readonly aria-hidden="true" tabindex="-1" @invalid="handleInvalid(event)" /><button type="button" class="wrn-next__date-trigger" aria-label="{label}" disabled="{disabled || readonly}" aria-haspopup="dialog" aria-expanded="{expanded}" @click="openCalendar(event)" @focus="handleFocus(event)" @blur="handleBlur(event)"><span>{currentValue || placeholder || "Select date"}</span><i class="icon-[lucide--calendar-days]" aria-hidden="true"></i></button>{#if currentValue && !readonly && !disabled}<button type="button" class="wrn-next__picker-clear" aria-label="Clear date" @click="clearDate(event)">×</button>{/if}</div>
|
||||
<div class="wrn-next__picker-control"><input id="{id || name}" class="wrn-next__sr-only wrn-next__picker-value" type="text" name="{name}" value="{currentValue}" required="{required}" readonly aria-hidden="true" tabindex="-1" @invalid="handleInvalid(event)" /><button type="button" class="wrn-next__date-trigger" aria-label="{label}" disabled="{disabled || readonly}" aria-haspopup="dialog" aria-expanded="{expanded}" @click="openCalendar(event)" @focus="handleFocus(event)" @blur="handleBlur(event)"><span>{displayValue()}</span><i class="icon-[lucide--calendar-days]" aria-hidden="true"></i></button>{#if currentValue && !readonly && !disabled}<button type="button" class="wrn-next__picker-clear" aria-label="Clear date" @click="clearDate(event)">×</button>{/if}</div>
|
||||
<button type="button" class="wrn-picker-dismiss" aria-label="Close calendar" data-show="expanded" @click="finishDate(event)"></button><div data-show="expanded" class="wrn-next__calendar" role="dialog" aria-label="{label}"><div class="wrn-next__date-selectors"><label>Month<select value="{selectedMonth}" @change="updateDate('month', event.currentTarget.value, event)">{#each months as month}<option value="{month.value}" selected="{month.value === selectedMonth}">{month.label}</option>{/each}</select></label><label>Year<select value="{selectedYear}" @change="updateDate('year', event.currentTarget.value, event)">{#each calendarYears() as calendarYear}<option value="{calendarYear}" selected="{String(calendarYear) === selectedYear}" data-text="calendarYear"></option>{/each}</select></label></div><div class="wrn-next__calendar-weekdays" aria-hidden="true">{#each weekdays() as weekday}<span>{weekday}</span>{/each}</div><div class="wrn-next__calendar-grid">{#each calendarDays() as day}<button type="button" data-value="{day.value}" data-today="{day.today}" style="grid-column: {day.column}" disabled="{day.disabled}" aria-label="{day.label}" aria-pressed="{day.value === currentValue.slice(0, 10)}" @click="selectDay(event)">{day.day}</button>{/each}</div>{#if type === "datetime-local"}<TimePicker label="Time" value="{selectedTime}" @change="selectedTime = payload.value; currentValue = (currentValue ? currentValue.slice(0, 10) : selectedYear + '-' + selectedMonth + '-01') + 'T' + selectedTime; output.change({ value: currentValue, name: name })" />{/if}<button type="button" class="wrn-next__date-done" disabled="{!currentValue}" @click="finishDate(event)">Done</button></div>
|
||||
{#if helperText}<small class="wrn-next__field-help">{helperText}</small>{/if}<small class="wrn-next__field-error" data-error="{name}">{error}</small>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/ui",
|
||||
"version": "0.8.87",
|
||||
"version": "0.8.88",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
|
||||
@@ -1687,7 +1687,7 @@ test("date picker renders a custom calendar trigger with accessible field state
|
||||
)[14] as HTMLButtonElement;
|
||||
fifteenth.dispatchEvent(new (dom.window as any).Event("click", { bubbles: true }));
|
||||
expect(input.value).toBe("2026-07-15");
|
||||
expect(dom.querySelector(".wrn-next__date-trigger span")?.textContent).toBe("2026-07-15");
|
||||
expect(dom.querySelector(".wrn-next__date-trigger span")?.textContent).toBe("Jul 15, 2026");
|
||||
});
|
||||
|
||||
test("time picker uses a custom reactive panel instead of the browser time control", async () => {
|
||||
|
||||
Reference in new issue
Block a user