// Copyright (c) Meta Platforms, Inc. and affiliates.
'use client';
/**
* @file nativeDateSegments.ts
* @input Uses the DOM only
* @output Exports hasEditableDateSegments, resetDateSegmentProbe
* @position Internal helper for NativeDateField; answers "can the user type
* into this engine's ``?"
*
* `` is two different controls wearing one tag name: a
* single untypable run the OS picker drives (iOS, Android), or a row of
* editable `mm`/`dd`/`yyyy` fields (desktop engines). Hiding the first costs
* nothing; hiding the second hides what the user is editing.
*
* Neither obvious test tells them apart. The pointer is wrong on Chrome's
* touch simulator, a Windows tablet and a ChromeOS convertible, which report
* a coarse pointer while Blink still renders segments. And measured, iOS and
* Chromium BOTH accept every `-webkit-datetime-*` selector, so
* `CSS.supports('selector(...)')` only proves the parser knows the name. What
* separates them is which pseudo is a real box, so this measures that.
*
* SYNC: When modified, update these files to stay in sync:
* - /packages/core/src/DateInput/NativeDateField.tsx (the caller)
* - /packages/core/src/DateInput/NativeDateField.test.tsx (tests)
*/
/** Marks the throwaway probe element for the probe stylesheet. */
const PROBE_ATTR = 'data-astryx-date-probe';
/** How much padding the probe adds, in px. Large enough to beat rounding. */
const PROBE_PADDING = 100;
type EngineKind = 'segmented' | 'picker-only' | 'unknown';
let cachedEngine: EngineKind | null = null;
/** Whether padding this pseudo-element moves the control's box. */
function pseudoIsRealBox(input: HTMLInputElement, style: HTMLStyleElement, pseudo: string, baseWidth: number): boolean {
style.textContent = `[${PROBE_ATTR}]${pseudo}{padding-inline-start:${PROBE_PADDING}px !important}`;
// Reading geometry flushes style and layout, so this already reflects it.
return input.getBoundingClientRect().width > baseWidth + PROBE_PADDING / 2;
}
/**
* Classifies the engine's date control. Cached: the answer is a build-time
* property of the browser, not of the field or the pointer in use.
*/
function probeEngine(): EngineKind {
if (cachedEngine !== null) {
return cachedEngine;
}
if (typeof document === 'undefined' || !document.body) {
return 'unknown';
}
const input = document.createElement('input');
input.setAttribute('type', 'date');
input.setAttribute(PROBE_ATTR, '');
// Off-screen, and immune to the page's own `input` rules: the probe reads a
// width DELTA, so anything pinning the width would swallow the signal.
input.style.cssText = 'position:fixed;top:-9999px;left:0;width:auto;min-width:0;max-width:none;' + 'padding:0;border:0;font-size:16px;box-sizing:content-box;';
const style = document.createElement('style');
let kind: EngineKind = 'unknown';
try {
document.body.appendChild(input);
document.head.appendChild(style);
const base = input.getBoundingClientRect().width;
if (pseudoIsRealBox(input, style, '::-webkit-datetime-edit', base)) {
// Chromium's editable field row, and WebKit's on macOS/GTK. Probe the
// container: the individual field pseudos overflow it rather than
// expand it, so they do not move the box even in Chromium.
kind = 'segmented';
} else if (pseudoIsRealBox(input, style, '::-webkit-date-and-time-value', base)) {
// The single, untypable run iOS Safari and Chrome on Android paint.
kind = 'picker-only';
}
} catch {
// A hostile CSSOM leaves the answer unknown, resolved by the pointer.
} finally {
input.remove();
style.remove();
}
cachedEngine = kind;
return kind;
}
/**
* Whether the user can type into this engine's ``.
*
* Firefox exposes neither pseudo-element, so it — and any future engine —
* comes back `'unknown'` and the pointer breaks the tie. That is the right
* fallback precisely where the probe is blind: the pointer is wrong for
* Chromium because Blink's answer is fixed at build time and contradicts it,
* and an engine we cannot see is not in that position. It lands Firefox on
* the desktop (segmented) and on Android (picker-only) correctly.
*
* @param isTouchPointer Whether `(pointer: coarse)` matches.
* @internal Exported for tests.
*/
export function hasEditableDateSegments(isTouchPointer: boolean): boolean {
const engine = probeEngine();
return engine === 'unknown' ? !isTouchPointer : engine === 'segmented';
}
/**
* Drops the cached classification.
*
* @internal Exists for tests, which render against several engines in one
* document.
*/
export function resetDateSegmentProbe(): void {
cachedEngine = null;
}