import {
booleanAttribute,
ChangeDetectionStrategy,
Component,
computed,
ElementRef,
forwardRef,
inject,
input,
model,
signal,
viewChild,
} from '@angular/core';
import {
type ControlValueAccessor,
NG_VALUE_ACCESSOR,
} from '@angular/forms';
import { ThemeService } from '@africanies/africanies-theme';
import {
FORM_AFFIX_CLASS,
FORM_DATE_INNER_CLASS,
FORM_DISABLED_CLASS,
FORM_ERROR_CLASS,
FORM_FIELD_CLASS,
FORM_FIELD_ERROR_CLASS,
FORM_HINT_CLASS,
FORM_LABEL_CLASS,
} from '../form-field.classes';
let nextDatePickerId = 0;
/**
* Date field wrapping native `input[type=date]`, following
* shared AFRICANIES form field pattern.
*
* Value is an ISO calendar date string (`YYYY-MM-DD`) or `null` — matching the
* native control — not a `Date` object (avoids timezone surprises).
*
* Clicking anywhere on the field shell calls `HTMLInputElement.showPicker()`
* so the calendar opens without hunting for the tiny indicator glyph.
*
* Dark calendar chrome follows `color-scheme` from {@link ThemeService}
* (document + this input). Supports prefix/suffix slots.
*
* @example
* ```html
*
*
*
* ```
*/
@Component({
selector: 'africanies-date-picker',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => DatePickerComponent),
multi: true,
},
],
template: `
@if (error(); as err) {
{{ err }}
} @else if (hint(); as h) {
{{ h }}
}
`,
})
/**
* Date field wrapping native `input[type=date]`, following shared AFRICANIES form field pattern.
*/
export class DatePickerComponent implements ControlValueAccessor {
private readonly theme = inject(ThemeService, { optional: true });
protected readonly controlId = `africanies-date-picker-${++nextDatePickerId}`;
protected readonly hintId = `${this.controlId}-hint`;
protected readonly errorId = `${this.controlId}-error`;
protected readonly labelClass = FORM_LABEL_CLASS;
protected readonly affixClass = FORM_AFFIX_CLASS;
protected readonly innerClass = FORM_DATE_INNER_CLASS;
protected readonly hintClass = FORM_HINT_CLASS;
protected readonly errorClass = FORM_ERROR_CLASS;
private readonly dateInput =
viewChild>('dateInput');
/** Visible field label. */
readonly label = input('');
/** Helper copy. Hidden while {@link error} is set. */
readonly hint = input(undefined);
/** Field-level validation message. Distinct from `ErrorStateComponent`. */
readonly error = input(null);
/** Optional native `min` (`YYYY-MM-DD`). */
readonly min = input(undefined);
/** Optional native `max` (`YYYY-MM-DD`). */
readonly max = input(undefined);
/** ISO date string or `null` (`[(value)]` + CVA). */
readonly value = model(null);
/** Host disable flag (in addition to CVA). */
readonly disabledInput = input(false, {
alias: 'disabled',
transform: booleanAttribute,
});
protected readonly cvaDisabled = signal(false);
protected readonly disabled = computed(
() => this.disabledInput() || this.cvaDisabled(),
);
/** Native picker chrome — mirrors app theme when ThemeService is present. */
protected readonly colorScheme = computed(
() => this.theme?.theme() ?? 'light',
);
protected readonly shellClass = computed(() => {
let classes = FORM_FIELD_CLASS;
if (this.error()) {
classes += ` ${FORM_FIELD_ERROR_CLASS}`;
}
if (this.disabled()) {
classes += ` ${FORM_DISABLED_CLASS}`;
} else {
classes += ' cursor-pointer';
}
return classes;
});
protected readonly describedBy = computed(() => {
if (this.error()) {
return this.errorId;
}
if (this.hint()) {
return this.hintId;
}
return null;
});
private onChange: (value: string | null) => void = () => undefined;
private onTouched: () => void = () => undefined;
/** @param value - ISO date or `null`. */
writeValue(value: string | null): void {
this.value.set(value);
}
/** @param fn - User edit callback. */
registerOnChange(fn: (value: string | null) => void): void {
this.onChange = fn;
}
/** @param fn - Blur callback. */
registerOnTouched(fn: () => void): void {
this.onTouched = fn;
}
/** @param isDisabled - Blocks editing when true. */
setDisabledState(isDisabled: boolean): void {
this.cvaDisabled.set(isDisabled);
}
/**
* Opens the native calendar on input click.
* Uses `showPicker()` when available (Chromium, Safari 16+, Firefox 101+).
*
* @param event - Click from the input.
*/
protected openPicker(event?: Event): void {
if (this.disabled()) {
return;
}
event?.stopPropagation();
const el = this.dateInput()?.nativeElement;
if (!el || typeof el.showPicker !== 'function') {
return;
}
try {
el.showPicker();
} catch {
// showPicker throws if not tied to a user gesture or if the input is not
// rendered — ignore and leave native indicator behavior as fallback.
}
}
protected onInput(event: Event): void {
const raw = (event.target as HTMLInputElement).value;
const next = raw === '' ? null : raw;
this.value.set(next);
this.onChange(next);
}
protected onBlur(): void {
this.onTouched();
}
}