This commit is contained in:
@@ -3,13 +3,13 @@ import {addDelegatedEventListener, generateElemId, isDocumentFragmentOrElementNo
|
||||
import octiconKebabHorizontal from '../../../public/assets/img/svg/octicon-kebab-horizontal.svg';
|
||||
|
||||
window.customElements.define('overflow-menu', class extends HTMLElement {
|
||||
popup: HTMLDivElement;
|
||||
overflowItems: Array<HTMLElement>;
|
||||
button: HTMLButtonElement | null;
|
||||
menuItemsEl: HTMLElement;
|
||||
resizeObserver: ResizeObserver;
|
||||
mutationObserver: MutationObserver;
|
||||
lastWidth: number;
|
||||
popup!: HTMLDivElement;
|
||||
overflowItems: Array<HTMLElement> = [];
|
||||
button: HTMLButtonElement | null = null;
|
||||
menuItemsEl!: HTMLElement;
|
||||
resizeObserver!: ResizeObserver;
|
||||
mutationObserver!: MutationObserver;
|
||||
lastWidth!: number;
|
||||
|
||||
updateButtonActivationState() {
|
||||
if (!this.button || !this.popup) return;
|
||||
@@ -21,7 +21,7 @@ window.customElements.define('overflow-menu', class extends HTMLElement {
|
||||
this.popup.style.display = '';
|
||||
this.button!.setAttribute('aria-expanded', 'true');
|
||||
setTimeout(() => this.popup.focus(), 0);
|
||||
document.addEventListener('click', this.onClickOutside, true);
|
||||
document.addEventListener('click', this.onClickOutside, {capture: true});
|
||||
}
|
||||
|
||||
hidePopup() {
|
||||
@@ -100,7 +100,7 @@ window.customElements.define('overflow-menu', class extends HTMLElement {
|
||||
const itemOverFlowMenuButton = this.querySelector<HTMLButtonElement>('.overflow-menu-button');
|
||||
|
||||
// move items in popup back into the menu items for subsequent measurement
|
||||
for (const item of this.overflowItems || []) {
|
||||
for (const item of this.overflowItems) {
|
||||
if (!itemFlexSpace || item.getAttribute('data-after-flex-space')) {
|
||||
this.menuItemsEl.append(item);
|
||||
} else {
|
||||
@@ -125,7 +125,7 @@ window.customElements.define('overflow-menu', class extends HTMLElement {
|
||||
const itemRight = item.offsetLeft + item.offsetWidth;
|
||||
if (menuRight - itemRight < 38) { // roughly the width of .overflow-menu-button with some extra space
|
||||
const onlyLastItem = idx === menuItems.length - 1 && this.overflowItems.length === 0;
|
||||
const lastItemFit = onlyLastItem && menuRight - itemRight > 0;
|
||||
const lastItemFit = onlyLastItem && menuRight > itemRight;
|
||||
const moveToPopup = !onlyLastItem || !lastItemFit;
|
||||
if (moveToPopup) this.overflowItems.push(item);
|
||||
}
|
||||
|
||||
@@ -54,6 +54,26 @@ test('switches to datetime format after default threshold', async () => {
|
||||
expect(getText(el)).toMatch(/on [A-Z][a-z]{2} \d{1,2}/);
|
||||
});
|
||||
|
||||
test('accepts unix seconds as integer string', async () => {
|
||||
const el = createRelativeTime(String(Math.floor(Date.now() / 1000) - 3 * 60));
|
||||
await Promise.resolve();
|
||||
expect(getText(el)).toBe('3 minutes ago');
|
||||
});
|
||||
|
||||
test('ignores fractional unix seconds', async () => {
|
||||
const el = createRelativeTime('1700000000.5');
|
||||
el.shadowRoot!.textContent = 'fallback';
|
||||
await Promise.resolve();
|
||||
expect(getText(el)).toBe('fallback');
|
||||
});
|
||||
|
||||
test('ignores negative unix seconds', async () => {
|
||||
const el = createRelativeTime('-86400');
|
||||
el.shadowRoot!.textContent = 'fallback';
|
||||
await Promise.resolve();
|
||||
expect(getText(el)).toBe('fallback');
|
||||
});
|
||||
|
||||
test('ignores invalid datetime', async () => {
|
||||
const el = createRelativeTime('bogus');
|
||||
el.shadowRoot!.textContent = 'fallback';
|
||||
@@ -61,6 +81,13 @@ test('ignores invalid datetime', async () => {
|
||||
expect(getText(el)).toBe('fallback');
|
||||
});
|
||||
|
||||
test('ignores partial numeric datetime', async () => {
|
||||
const el = createRelativeTime('123abc');
|
||||
el.shadowRoot!.textContent = 'fallback';
|
||||
await Promise.resolve();
|
||||
expect(getText(el)).toBe('fallback');
|
||||
});
|
||||
|
||||
test('handles empty datetime', async () => {
|
||||
const el = createRelativeTime('');
|
||||
el.shadowRoot!.textContent = 'fallback';
|
||||
@@ -109,6 +136,18 @@ test('respects lang from parent element', async () => {
|
||||
expect(getText(el)).toBe('vor 3 Tagen');
|
||||
});
|
||||
|
||||
test('falls back when navigator.language is invalid', async () => {
|
||||
vi.spyOn(navigator, 'language', 'get').mockReturnValue('undefined');
|
||||
try {
|
||||
const el = document.createElement('relative-time');
|
||||
el.setAttribute('datetime', new Date(Date.now() - 3 * 60 * 1000).toISOString());
|
||||
await Promise.resolve();
|
||||
expect(getText(el)).toBe('3 minutes ago');
|
||||
} finally {
|
||||
vi.restoreAllMocks();
|
||||
}
|
||||
});
|
||||
|
||||
test('switches to datetime with P1D threshold', async () => {
|
||||
const el = createRelativeTime(new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString(), {
|
||||
lang: 'en-US',
|
||||
|
||||
@@ -28,6 +28,7 @@ type FormatStyle = 'long' | 'short' | 'narrow';
|
||||
const unitNames = ['year', 'month', 'week', 'day', 'hour', 'minute', 'second'] as const;
|
||||
|
||||
const durationRe = /^[-+]?P(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)W)?(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?$/;
|
||||
const unixSecondsRe = /^\d+$/;
|
||||
|
||||
function parseDurationMs(str: string): number {
|
||||
const m = durationRe.exec(str);
|
||||
@@ -258,13 +259,13 @@ class RelativeTime extends HTMLElement {
|
||||
}
|
||||
|
||||
get #lang(): string {
|
||||
const lang = this.closest('[lang]')?.getAttribute('lang');
|
||||
if (lang) {
|
||||
for (const candidate of [this.closest('[lang]')?.getAttribute('lang'), navigator.language]) {
|
||||
if (!candidate) continue;
|
||||
try {
|
||||
return new Intl.Locale(lang).toString();
|
||||
} catch { /* invalid locale, fall through */ }
|
||||
return String(new Intl.Locale(candidate));
|
||||
} catch {}
|
||||
}
|
||||
return navigator.language ?? 'en';
|
||||
return 'en';
|
||||
}
|
||||
|
||||
get second(): 'numeric' | '2-digit' | undefined {
|
||||
@@ -364,7 +365,8 @@ class RelativeTime extends HTMLElement {
|
||||
}
|
||||
|
||||
get date(): Date | null {
|
||||
const parsed = Date.parse(this.datetime);
|
||||
const dt = this.datetime;
|
||||
const parsed = unixSecondsRe.test(dt) ? Number(dt) * 1000 : Date.parse(dt);
|
||||
return Number.isNaN(parsed) ? null : new Date(parsed);
|
||||
}
|
||||
|
||||
@@ -432,7 +434,7 @@ class RelativeTime extends HTMLElement {
|
||||
const value = d[`${unit}s` as keyof Duration] as number;
|
||||
if (value || (duration.blank && unit === 'second')) {
|
||||
try {
|
||||
parts.push(new Intl.NumberFormat(locale, {style: 'unit', unit, unitDisplay: style} as Intl.NumberFormatOptions).format(value));
|
||||
parts.push(new Intl.NumberFormat(locale, {style: 'unit', unit, unitDisplay: style}).format(value));
|
||||
} catch { // PaleMoon lacks Intl.NumberFormat unit style support
|
||||
parts.push(`${value} ${value === 1 ? unit : `${unit}s`}`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user